Advertisement
Guest User

sopel patch

a guest
Jun 2nd, 2016
230
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 189.33 KB | None | 0 0
  1. From 7022da1f72428e8b90a861a6f4bcbeb16cb5e45c Mon Sep 17 00:00:00 2001
  2. From: S00ng <???>
  3. Date: Sun, 1 May 2016 01:48:56 +0200
  4. Subject: [PATCH 1/4] iniz machine learning ready
  5.  
  6. ---
  7. sopel/modules/__init__.py | 2 -
  8. sopel/modules/admin.py | 229 ----------------------
  9. sopel/modules/adminchannel.py | 276 --------------------------
  10. sopel/modules/announce.py | 23 ---
  11. sopel/modules/bugzilla.py | 96 ----------
  12. sopel/modules/calc.py | 70 -------
  13. sopel/modules/clock.py | 277 ---------------------------
  14. sopel/modules/countdown.py | 39 ----
  15. sopel/modules/currency.py | 105 ----------
  16. sopel/modules/dice.py | 259 -------------------------
  17. sopel/modules/etymology.py | 97 ----------
  18. sopel/modules/find.py | 139 --------------
  19. sopel/modules/find_updates.py | 58 ------
  20. sopel/modules/help.py | 72 -------
  21. sopel/modules/ip.py | 139 --------------
  22. sopel/modules/ipython.py | 78 --------
  23. sopel/modules/isup.py | 36 ----
  24. sopel/modules/lmgtfy.py | 19 --
  25. sopel/modules/meetbot.py | 436 ------------------------------------------
  26. sopel/modules/movie.py | 48 -----
  27. sopel/modules/ping.py | 29 ---
  28. sopel/modules/rand.py | 49 -----
  29. sopel/modules/reddit.py | 191 ------------------
  30. sopel/modules/reload.py | 139 --------------
  31. sopel/modules/remind.py | 228 ----------------------
  32. sopel/modules/safety.py | 197 -------------------
  33. sopel/modules/search.py | 128 -------------
  34. sopel/modules/seen.py | 59 ------
  35. sopel/modules/spellcheck.py | 54 ------
  36. sopel/modules/tell.py | 183 ------------------
  37. sopel/modules/tld.py | 69 -------
  38. sopel/modules/translate.py | 203 --------------------
  39. sopel/modules/unicode_info.py | 49 -----
  40. sopel/modules/units.py | 186 ------------------
  41. sopel/modules/uptime.py | 27 ---
  42. sopel/modules/url.py | 237 -----------------------
  43. sopel/modules/version.py | 81 --------
  44. sopel/modules/weather.py | 183 ------------------
  45. sopel/modules/wikipedia.py | 133 -------------
  46. sopel/modules/wiktionary.py | 101 ----------
  47. sopel/modules/xkcd.py | 105 ----------
  48. sopel/trigger.py | 5 +-
  49. 42 files changed, 3 insertions(+), 5131 deletions(-)
  50. delete mode 100644 sopel/modules/__init__.py
  51. delete mode 100644 sopel/modules/admin.py
  52. delete mode 100644 sopel/modules/adminchannel.py
  53. delete mode 100644 sopel/modules/announce.py
  54. delete mode 100644 sopel/modules/bugzilla.py
  55. delete mode 100644 sopel/modules/calc.py
  56. delete mode 100644 sopel/modules/clock.py
  57. delete mode 100644 sopel/modules/countdown.py
  58. delete mode 100644 sopel/modules/currency.py
  59. delete mode 100644 sopel/modules/dice.py
  60. delete mode 100644 sopel/modules/etymology.py
  61. delete mode 100644 sopel/modules/find.py
  62. delete mode 100644 sopel/modules/find_updates.py
  63. delete mode 100644 sopel/modules/help.py
  64. delete mode 100644 sopel/modules/ip.py
  65. delete mode 100644 sopel/modules/ipython.py
  66. delete mode 100644 sopel/modules/isup.py
  67. delete mode 100644 sopel/modules/lmgtfy.py
  68. delete mode 100644 sopel/modules/meetbot.py
  69. delete mode 100644 sopel/modules/movie.py
  70. delete mode 100644 sopel/modules/ping.py
  71. delete mode 100644 sopel/modules/rand.py
  72. delete mode 100644 sopel/modules/reddit.py
  73. delete mode 100644 sopel/modules/reload.py
  74. delete mode 100644 sopel/modules/remind.py
  75. delete mode 100644 sopel/modules/safety.py
  76. delete mode 100644 sopel/modules/search.py
  77. delete mode 100644 sopel/modules/seen.py
  78. delete mode 100644 sopel/modules/spellcheck.py
  79. delete mode 100644 sopel/modules/tell.py
  80. delete mode 100644 sopel/modules/tld.py
  81. delete mode 100644 sopel/modules/translate.py
  82. delete mode 100644 sopel/modules/unicode_info.py
  83. delete mode 100644 sopel/modules/units.py
  84. delete mode 100644 sopel/modules/uptime.py
  85. delete mode 100644 sopel/modules/url.py
  86. delete mode 100644 sopel/modules/version.py
  87. delete mode 100644 sopel/modules/weather.py
  88. delete mode 100644 sopel/modules/wikipedia.py
  89. delete mode 100644 sopel/modules/wiktionary.py
  90. delete mode 100644 sopel/modules/xkcd.py
  91.  
  92. diff --git a/sopel/modules/__init__.py b/sopel/modules/__init__.py
  93. deleted file mode 100644
  94. index b6f85b6..0000000
  95. --- a/sopel/modules/__init__.py
  96. +++ /dev/null
  97. @@ -1,2 +0,0 @@
  98. -# coding=utf-8
  99. -from __future__ import unicode_literals, absolute_import, print_function, division
  100. diff --git a/sopel/modules/admin.py b/sopel/modules/admin.py
  101. deleted file mode 100644
  102. index 14e84aa..0000000
  103. --- a/sopel/modules/admin.py
  104. +++ /dev/null
  105. @@ -1,229 +0,0 @@
  106. -# coding=utf-8
  107. -"""
  108. -admin.py - Sopel Admin Module
  109. -Copyright 2010-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich
  110. -(yanovich.net)
  111. -Copyright © 2012, Elad Alfassa, <elad@fedoraproject.org>
  112. -Copyright 2013, Ari Koivula <ari@koivu.la>
  113. -
  114. -Licensed under the Eiffel Forum License 2.
  115. -
  116. -http://sopel.chat
  117. -"""
  118. -from __future__ import unicode_literals, absolute_import, print_function, division
  119. -
  120. -from sopel.config.types import (
  121. - StaticSection, ValidatedAttribute, FilenameAttribute
  122. -)
  123. -import sopel.module
  124. -
  125. -
  126. -class AdminSection(StaticSection):
  127. - hold_ground = ValidatedAttribute('hold_ground', bool, default=False)
  128. - """Auto re-join on kick"""
  129. - auto_accept_invite = ValidatedAttribute('auto_accept_invite', bool,
  130. - default=True)
  131. -
  132. -
  133. -def configure(config):
  134. - config.define_section('admin', AdminSection)
  135. - config.admin.configure_setting('hold_ground',
  136. - "Automatically re-join after being kicked?")
  137. - config.admin.configure_setting('auto_accept_invite',
  138. - 'Automatically join channels when invited?')
  139. -
  140. -
  141. -def setup(bot):
  142. - bot.config.define_section('admin', AdminSection)
  143. -
  144. -
  145. -@sopel.module.require_privmsg
  146. -@sopel.module.require_admin
  147. -@sopel.module.commands('join')
  148. -@sopel.module.priority('low')
  149. -@sopel.module.example('.join #example or .join #example key')
  150. -def join(bot, trigger):
  151. - """Join the specified channel. This is an admin-only command."""
  152. - channel, key = trigger.group(3), trigger.group(4)
  153. - if not channel:
  154. - return
  155. - elif not key:
  156. - bot.join(channel)
  157. - else:
  158. - bot.join(channel, key)
  159. -
  160. -
  161. -@sopel.module.require_privmsg
  162. -@sopel.module.require_admin
  163. -@sopel.module.commands('part')
  164. -@sopel.module.priority('low')
  165. -@sopel.module.example('.part #example')
  166. -def part(bot, trigger):
  167. - """Part the specified channel. This is an admin-only command."""
  168. - channel, _sep, part_msg = trigger.group(2).partition(' ')
  169. - if part_msg:
  170. - bot.part(channel, part_msg)
  171. - else:
  172. - bot.part(channel)
  173. -
  174. -
  175. -@sopel.module.require_privmsg
  176. -@sopel.module.require_owner
  177. -@sopel.module.commands('quit')
  178. -@sopel.module.priority('low')
  179. -def quit(bot, trigger):
  180. - """Quit from the server. This is an owner-only command."""
  181. - quit_message = trigger.group(2)
  182. - if not quit_message:
  183. - quit_message = 'Quitting on command from %s' % trigger.nick
  184. -
  185. - bot.quit(quit_message)
  186. -
  187. -
  188. -@sopel.module.require_privmsg
  189. -@sopel.module.require_admin
  190. -@sopel.module.commands('msg')
  191. -@sopel.module.priority('low')
  192. -@sopel.module.example('.msg #YourPants Does anyone else smell neurotoxin?')
  193. -def msg(bot, trigger):
  194. - """
  195. - Send a message to a given channel or nick. Can only be done in privmsg by an
  196. - admin.
  197. - """
  198. - if trigger.group(2) is None:
  199. - return
  200. -
  201. - channel, _sep, message = trigger.group(2).partition(' ')
  202. - message = message.strip()
  203. - if not channel or not message:
  204. - return
  205. -
  206. - bot.msg(channel, message)
  207. -
  208. -
  209. -@sopel.module.require_privmsg
  210. -@sopel.module.require_admin
  211. -@sopel.module.commands('me')
  212. -@sopel.module.priority('low')
  213. -def me(bot, trigger):
  214. - """
  215. - Send an ACTION (/me) to a given channel or nick. Can only be done in privmsg
  216. - by an admin.
  217. - """
  218. - if trigger.group(2) is None:
  219. - return
  220. -
  221. - channel, _sep, action = trigger.group(2).partition(' ')
  222. - action = action.strip()
  223. - if not channel or not action:
  224. - return
  225. -
  226. - msg = '\x01ACTION %s\x01' % action
  227. - bot.msg(channel, msg)
  228. -
  229. -
  230. -@sopel.module.event('INVITE')
  231. -@sopel.module.rule('.*')
  232. -@sopel.module.priority('low')
  233. -def invite_join(bot, trigger):
  234. - """
  235. - Join a channel sopel is invited to, if the inviter is an admin.
  236. - """
  237. - if trigger.admin or bot.config.admin.auto_accept_invite:
  238. - bot.join(trigger.args[1])
  239. - return
  240. -
  241. -
  242. -@sopel.module.event('KICK')
  243. -@sopel.module.rule(r'.*')
  244. -@sopel.module.priority('low')
  245. -def hold_ground(bot, trigger):
  246. - """
  247. - This function monitors all kicks across all channels sopel is in. If it
  248. - detects that it is the one kicked it'll automatically join that channel.
  249. -
  250. - WARNING: This may not be needed and could cause problems if sopel becomes
  251. - annoying. Please use this with caution.
  252. - """
  253. - if bot.config.admin.hold_ground:
  254. - channel = trigger.sender
  255. - if trigger.args[1] == bot.nick:
  256. - bot.join(channel)
  257. -
  258. -
  259. -@sopel.module.require_privmsg
  260. -@sopel.module.require_admin
  261. -@sopel.module.commands('mode')
  262. -@sopel.module.priority('low')
  263. -def mode(bot, trigger):
  264. - """Set a user mode on Sopel. Can only be done in privmsg by an admin."""
  265. - mode = trigger.group(3)
  266. - bot.write(('MODE ', bot.nick + ' ' + mode))
  267. -
  268. -
  269. -@sopel.module.require_privmsg("This command only works as a private message.")
  270. -@sopel.module.require_admin("This command requires admin privileges.")
  271. -@sopel.module.commands('set')
  272. -@sopel.module.example('.set core.owner Me')
  273. -def set_config(bot, trigger):
  274. - """See and modify values of sopels config object.
  275. -
  276. - Trigger args:
  277. - arg1 - section and option, in the form "section.option"
  278. - arg2 - value
  279. -
  280. - If there is no section, section will default to "core".
  281. - If value is None, the option will be deleted.
  282. - """
  283. - # Get section and option from first argument.
  284. - arg1 = trigger.group(3).split('.')
  285. - if len(arg1) == 1:
  286. - section_name, option = "core", arg1[0]
  287. - elif len(arg1) == 2:
  288. - section_name, option = arg1
  289. - else:
  290. - bot.reply("Usage: .set section.option value")
  291. - return
  292. - section = getattr(bot.config, section_name)
  293. - static_sec = isinstance(section, StaticSection)
  294. -
  295. - if static_sec and not hasattr(section, option):
  296. - bot.say('[{}] section has no option {}.'.format(section_name, option))
  297. - return
  298. -
  299. - # Display current value if no value is given.
  300. - value = trigger.group(4)
  301. - if not value:
  302. - if not static_sec and bot.config.parser.has_option(section, option):
  303. - bot.reply("Option %s.%s does not exist." % (section_name, option))
  304. - return
  305. - # Except if the option looks like a password. Censor those to stop them
  306. - # from being put on log files.
  307. - if option.endswith("password") or option.endswith("pass"):
  308. - value = "(password censored)"
  309. - else:
  310. - value = getattr(section, option)
  311. - bot.reply("%s.%s = %s" % (section_name, option, value))
  312. - return
  313. -
  314. - # Otherwise, set the value to one given as argument 2.
  315. - if static_sec:
  316. - descriptor = getattr(section.__class__, option)
  317. - try:
  318. - if isinstance(descriptor, FilenameAttribute):
  319. - value = descriptor.parse(bot.config, descriptor, value)
  320. - else:
  321. - value = descriptor.parse(value)
  322. - except ValueError as exc:
  323. - bot.say("Can't set attribute: " + str(exc))
  324. - return
  325. - setattr(section, option, value)
  326. -
  327. -
  328. -@sopel.module.require_privmsg
  329. -@sopel.module.require_admin
  330. -@sopel.module.commands('save')
  331. -@sopel.module.example('.save')
  332. -def save_config(bot, trigger):
  333. - """Save state of sopels config object to the configuration file."""
  334. - bot.config.save()
  335. diff --git a/sopel/modules/adminchannel.py b/sopel/modules/adminchannel.py
  336. deleted file mode 100644
  337. index e3fe10f..0000000
  338. --- a/sopel/modules/adminchannel.py
  339. +++ /dev/null
  340. @@ -1,276 +0,0 @@
  341. -# coding=utf-8
  342. -# Copyright 2010-2011, Michael Yanovich, Alek Rollyson, and Elsie Powell
  343. -# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
  344. -# Licensed under the Eiffel Forum License 2.
  345. -from __future__ import unicode_literals, absolute_import, print_function, division
  346. -
  347. -import re
  348. -from sopel import formatting
  349. -from sopel.module import commands, priority, OP, HALFOP, require_privilege, require_chanmsg
  350. -from sopel.tools import Identifier
  351. -
  352. -
  353. -def default_mask(trigger):
  354. - welcome = formatting.color('Welcome to:', formatting.colors.PURPLE)
  355. - chan = formatting.color(trigger.sender, formatting.colors.TEAL)
  356. - topic_ = formatting.bold('Topic:')
  357. - topic_ = formatting.color('| ' + topic_, formatting.colors.PURPLE)
  358. - arg = formatting.color('{}', formatting.colors.GREEN)
  359. - return '{} {} {} {}'.format(welcome, chan, topic_, arg)
  360. -
  361. -
  362. -@require_chanmsg
  363. -@require_privilege(OP, 'You are not a channel operator.')
  364. -@commands('kick')
  365. -@priority('high')
  366. -def kick(bot, trigger):
  367. - """
  368. - Kick a user from the channel.
  369. - """
  370. - if bot.privileges[trigger.sender][bot.nick] < HALFOP:
  371. - return bot.reply("I'm not a channel operator!")
  372. - text = trigger.group().split()
  373. - argc = len(text)
  374. - if argc < 2:
  375. - return
  376. - opt = Identifier(text[1])
  377. - nick = opt
  378. - channel = trigger.sender
  379. - reasonidx = 2
  380. - if not opt.is_nick():
  381. - if argc < 3:
  382. - return
  383. - nick = text[2]
  384. - channel = opt
  385. - reasonidx = 3
  386. - reason = ' '.join(text[reasonidx:])
  387. - if nick != bot.config.core.nick:
  388. - bot.write(['KICK', channel, nick], reason)
  389. -
  390. -
  391. -def configureHostMask(mask):
  392. - if mask == '*!*@*':
  393. - return mask
  394. - if re.match('^[^.@!/]+$', mask) is not None:
  395. - return '%s!*@*' % mask
  396. - if re.match('^[^@!]+$', mask) is not None:
  397. - return '*!*@%s' % mask
  398. -
  399. - m = re.match('^([^!@]+)@$', mask)
  400. - if m is not None:
  401. - return '*!%s@*' % m.group(1)
  402. -
  403. - m = re.match('^([^!@]+)@([^@!]+)$', mask)
  404. - if m is not None:
  405. - return '*!%s@%s' % (m.group(1), m.group(2))
  406. -
  407. - m = re.match('^([^!@]+)!(^[!@]+)@?$', mask)
  408. - if m is not None:
  409. - return '%s!%s@*' % (m.group(1), m.group(2))
  410. - return ''
  411. -
  412. -
  413. -@require_chanmsg
  414. -@require_privilege(OP, 'You are not a channel operator.')
  415. -@commands('ban')
  416. -@priority('high')
  417. -def ban(bot, trigger):
  418. - """
  419. - This give admins the ability to ban a user.
  420. - The bot must be a Channel Operator for this command to work.
  421. - """
  422. - if bot.privileges[trigger.sender][bot.nick] < HALFOP:
  423. - return bot.reply("I'm not a channel operator!")
  424. - text = trigger.group().split()
  425. - argc = len(text)
  426. - if argc < 2:
  427. - return
  428. - opt = Identifier(text[1])
  429. - banmask = opt
  430. - channel = trigger.sender
  431. - if not opt.is_nick():
  432. - if argc < 3:
  433. - return
  434. - channel = opt
  435. - banmask = text[2]
  436. - banmask = configureHostMask(banmask)
  437. - if banmask == '':
  438. - return
  439. - bot.write(['MODE', channel, '+b', banmask])
  440. -
  441. -
  442. -@require_chanmsg
  443. -@require_privilege(OP, 'You are not a channel operator.')
  444. -@commands('unban')
  445. -def unban(bot, trigger):
  446. - """
  447. - This give admins the ability to unban a user.
  448. - The bot must be a Channel Operator for this command to work.
  449. - """
  450. - if bot.privileges[trigger.sender][bot.nick] < HALFOP:
  451. - return bot.reply("I'm not a channel operator!")
  452. - text = trigger.group().split()
  453. - argc = len(text)
  454. - if argc < 2:
  455. - return
  456. - opt = Identifier(text[1])
  457. - banmask = opt
  458. - channel = trigger.sender
  459. - if not opt.is_nick():
  460. - if argc < 3:
  461. - return
  462. - channel = opt
  463. - banmask = text[2]
  464. - banmask = configureHostMask(banmask)
  465. - if banmask == '':
  466. - return
  467. - bot.write(['MODE', channel, '-b', banmask])
  468. -
  469. -
  470. -@require_chanmsg
  471. -@require_privilege(OP, 'You are not a channel operator.')
  472. -@commands('quiet')
  473. -def quiet(bot, trigger):
  474. - """
  475. - This gives admins the ability to quiet a user.
  476. - The bot must be a Channel Operator for this command to work.
  477. - """
  478. - if bot.privileges[trigger.sender][bot.nick] < OP:
  479. - return bot.reply("I'm not a channel operator!")
  480. - text = trigger.group().split()
  481. - argc = len(text)
  482. - if argc < 2:
  483. - return
  484. - opt = Identifier(text[1])
  485. - quietmask = opt
  486. - channel = trigger.sender
  487. - if not opt.is_nick():
  488. - if argc < 3:
  489. - return
  490. - quietmask = text[2]
  491. - channel = opt
  492. - quietmask = configureHostMask(quietmask)
  493. - if quietmask == '':
  494. - return
  495. - bot.write(['MODE', channel, '+q', quietmask])
  496. -
  497. -
  498. -@require_chanmsg
  499. -@require_privilege(OP, 'You are not a channel operator.')
  500. -@commands('unquiet')
  501. -def unquiet(bot, trigger):
  502. - """
  503. - This gives admins the ability to unquiet a user.
  504. - The bot must be a Channel Operator for this command to work.
  505. - """
  506. - if bot.privileges[trigger.sender][bot.nick] < OP:
  507. - return bot.reply("I'm not a channel operator!")
  508. - text = trigger.group().split()
  509. - argc = len(text)
  510. - if argc < 2:
  511. - return
  512. - opt = Identifier(text[1])
  513. - quietmask = opt
  514. - channel = trigger.sender
  515. - if not opt.is_nick():
  516. - if argc < 3:
  517. - return
  518. - quietmask = text[2]
  519. - channel = opt
  520. - quietmask = configureHostMask(quietmask)
  521. - if quietmask == '':
  522. - return
  523. - bot.write(['MODE', channel, '-q', quietmask])
  524. -
  525. -
  526. -@require_chanmsg
  527. -@require_privilege(OP, 'You are not a channel operator.')
  528. -@commands('kickban', 'kb')
  529. -@priority('high')
  530. -def kickban(bot, trigger):
  531. - """
  532. - This gives admins the ability to kickban a user.
  533. - The bot must be a Channel Operator for this command to work.
  534. - .kickban [#chan] user1 user!*@* get out of here
  535. - """
  536. - if bot.privileges[trigger.sender][bot.nick] < HALFOP:
  537. - return bot.reply("I'm not a channel operator!")
  538. - text = trigger.group().split()
  539. - argc = len(text)
  540. - if argc < 4:
  541. - return
  542. - opt = Identifier(text[1])
  543. - nick = opt
  544. - mask = text[2]
  545. - channel = trigger.sender
  546. - reasonidx = 3
  547. - if not opt.is_nick():
  548. - if argc < 5:
  549. - return
  550. - channel = opt
  551. - nick = text[2]
  552. - mask = text[3]
  553. - reasonidx = 4
  554. - reason = ' '.join(text[reasonidx:])
  555. - mask = configureHostMask(mask)
  556. - if mask == '':
  557. - return
  558. - bot.write(['MODE', channel, '+b', mask])
  559. - bot.write(['KICK', channel, nick], reason)
  560. -
  561. -
  562. -@require_chanmsg
  563. -@require_privilege(OP, 'You are not a channel operator.')
  564. -@commands('topic')
  565. -def topic(bot, trigger):
  566. - """
  567. - This gives ops the ability to change the topic.
  568. - The bot must be a Channel Operator for this command to work.
  569. - """
  570. - if bot.privileges[trigger.sender][bot.nick] < HALFOP:
  571. - return bot.reply("I'm not a channel operator!")
  572. - if not trigger.group(2):
  573. - return
  574. - channel = trigger.sender.lower()
  575. -
  576. - narg = 1
  577. - mask = None
  578. - mask = bot.db.get_channel_value(channel, 'topic_mask')
  579. - mask = mask or default_mask(trigger)
  580. - mask = mask.replace('%s', '{}')
  581. - narg = len(re.findall('{}', mask))
  582. -
  583. - top = trigger.group(2)
  584. - args = []
  585. - if top:
  586. - args = top.split('~', narg)
  587. -
  588. - if len(args) != narg:
  589. - message = "Not enough arguments. You gave {}, it requires {}.".format(
  590. - len(args), narg)
  591. - return bot.say(message)
  592. - topic = mask.format(*args)
  593. -
  594. - bot.write(('TOPIC', channel + ' :' + topic))
  595. -
  596. -
  597. -@require_chanmsg
  598. -@require_privilege(OP, 'You are not a channel operator.')
  599. -@commands('tmask')
  600. -def set_mask(bot, trigger):
  601. - """
  602. - Set the mask to use for .topic in the current channel. {} is used to allow
  603. - substituting in chunks of text.
  604. - """
  605. - bot.db.set_channel_value(trigger.sender, 'topic_mask', trigger.group(2))
  606. - bot.say("Gotcha, " + trigger.nick)
  607. -
  608. -
  609. -@require_chanmsg
  610. -@require_privilege(OP, 'You are not a channel operator.')
  611. -@commands('showmask')
  612. -def show_mask(bot, trigger):
  613. - """Show the topic mask for the current channel."""
  614. - mask = bot.db.get_channel_value(trigger.sender, 'topic_mask')
  615. - mask = mask or default_mask(trigger)
  616. - bot.say(mask)
  617. diff --git a/sopel/modules/announce.py b/sopel/modules/announce.py
  618. deleted file mode 100644
  619. index 2b3df4b..0000000
  620. --- a/sopel/modules/announce.py
  621. +++ /dev/null
  622. @@ -1,23 +0,0 @@
  623. -# coding=utf-8
  624. -"""
  625. -announce.py - Send a message to all channels
  626. -Copyright © 2013, Elad Alfassa, <elad@fedoraproject.org>
  627. -Licensed under the Eiffel Forum License 2.
  628. -
  629. -"""
  630. -from __future__ import unicode_literals, absolute_import, print_function, division
  631. -
  632. -from sopel.module import commands, example
  633. -
  634. -
  635. -@commands('announce')
  636. -@example('.announce Some important message here')
  637. -def announce(bot, trigger):
  638. - """
  639. - Send an announcement to all channels the bot is in
  640. - """
  641. - if not trigger.admin:
  642. - bot.reply('Sorry, I can\'t let you do that')
  643. - return
  644. - for channel in bot.channels:
  645. - bot.msg(channel, '[ANNOUNCEMENT] %s' % trigger.group(2))
  646. diff --git a/sopel/modules/bugzilla.py b/sopel/modules/bugzilla.py
  647. deleted file mode 100644
  648. index 9167128..0000000
  649. --- a/sopel/modules/bugzilla.py
  650. +++ /dev/null
  651. @@ -1,96 +0,0 @@
  652. -# coding=utf-8
  653. -"""Bugzilla issue reporting module
  654. -
  655. -Copyright 2013-2015, Embolalia, embolalia.com
  656. -Licensed under the Eiffel Forum License 2.
  657. -"""
  658. -from __future__ import unicode_literals, absolute_import, print_function, division
  659. -
  660. -import re
  661. -
  662. -import xmltodict
  663. -
  664. -from sopel import web, tools
  665. -from sopel.config.types import StaticSection, ListAttribute
  666. -from sopel.logger import get_logger
  667. -from sopel.module import rule
  668. -
  669. -
  670. -regex = None
  671. -LOGGER = get_logger(__name__)
  672. -
  673. -
  674. -class BugzillaSection(StaticSection):
  675. - domains = ListAttribute('domains')
  676. - """The domains of the Bugzilla instances from which to get information."""
  677. -
  678. -
  679. -def configure(config):
  680. - config.define_section('bugzilla', BugzillaSection)
  681. - config.bugzilla.configure_setting(
  682. - 'domains',
  683. - 'Enter the domains of the Bugzillas you want extra information '
  684. - 'from (e.g. bugzilla.gnome.org)'
  685. - )
  686. -
  687. -
  688. -def setup(bot):
  689. - global regex
  690. - bot.config.define_section('bugzilla', BugzillaSection)
  691. -
  692. - if not bot.config.bugzilla.domains:
  693. - return
  694. - if not bot.memory.contains('url_callbacks'):
  695. - bot.memory['url_callbacks'] = tools.SopelMemory()
  696. -
  697. - domains = '|'.join(bot.config.bugzilla.domains)
  698. - regex = re.compile((r'https?://(%s)'
  699. - '(/show_bug.cgi\?\S*?)'
  700. - '(id=\d+)')
  701. - % domains)
  702. - bot.memory['url_callbacks'][regex] = show_bug
  703. -
  704. -
  705. -def shutdown(bot):
  706. - del bot.memory['url_callbacks'][regex]
  707. -
  708. -
  709. -@rule(r'.*https?://(\S+?)'
  710. - '(/show_bug.cgi\?\S*?)'
  711. - '(id=\d+).*')
  712. -def show_bug(bot, trigger, match=None):
  713. - """Show information about a Bugzilla bug."""
  714. - match = match or trigger
  715. - domain = match.group(1)
  716. - if domain not in bot.config.bugzilla.domains:
  717. - return
  718. - url = 'https://%s%sctype=xml&%s' % match.groups()
  719. - data = web.get(url, dont_decode=True)
  720. - bug = xmltodict.parse(data).get('bugzilla').get('bug')
  721. - error = bug.get('@error', None) # error="NotPermitted"
  722. -
  723. - if error:
  724. - LOGGER.warning('Bugzilla error: %s', error)
  725. - return
  726. -
  727. - message = ('[BUGZILLA] %s | Product: %s | Component: %s | Version: %s | ' +
  728. - 'Importance: %s | Status: %s | Assigned to: %s | ' +
  729. - 'Reported: %s | Modified: %s')
  730. -
  731. - resolution = bug.get('resolution')
  732. - if resolution is not None:
  733. - status = bug.get('bug_status') + ' ' + resolution
  734. - else:
  735. - status = bug.get('bug_status')
  736. -
  737. - assigned_to = bug.get('assigned_to')
  738. - if isinstance(assigned_to, dict):
  739. - assigned_to = assigned_to.get('@name')
  740. -
  741. - message = message % (
  742. - bug.get('short_desc'), bug.get('product'),
  743. - bug.get('component'), bug.get('version'),
  744. - (bug.get('priority') + ' ' + bug.get('bug_severity')),
  745. - status, assigned_to, bug.get('creation_ts'),
  746. - bug.get('delta_ts'))
  747. - bot.say(message)
  748. diff --git a/sopel/modules/calc.py b/sopel/modules/calc.py
  749. deleted file mode 100644
  750. index 6497b74..0000000
  751. --- a/sopel/modules/calc.py
  752. +++ /dev/null
  753. @@ -1,70 +0,0 @@
  754. -# coding=utf-8
  755. -"""
  756. -calc.py - Sopel Calculator Module
  757. -Copyright 2008, Sean B. Palmer, inamidst.com
  758. -Licensed under the Eiffel Forum License 2.
  759. -
  760. -http://sopel.chat
  761. -"""
  762. -from __future__ import unicode_literals, absolute_import, print_function, division
  763. -
  764. -import re
  765. -from sopel import web
  766. -from sopel.module import commands, example
  767. -from sopel.tools.calculation import eval_equation
  768. -from socket import timeout
  769. -import sys
  770. -if sys.version_info.major < 3:
  771. - import HTMLParser
  772. -else:
  773. - unichr = chr
  774. - import html.parser as HTMLParser
  775. -
  776. -
  777. -BASE_TUMBOLIA_URI = 'https://tumbolia-two.appspot.com/'
  778. -
  779. -
  780. -@commands('c', 'calc')
  781. -@example('.c 5 + 3', '8')
  782. -@example('.c 0.9*10', '9')
  783. -@example('.c 10*0.9', '9')
  784. -@example('.c 2*(1+2)*3', '18')
  785. -@example('.c 2**10', '1024')
  786. -@example('.c 5 // 2', '2')
  787. -@example('.c 5 / 2', '2.5')
  788. -def c(bot, trigger):
  789. - """Evaluate some calculation."""
  790. - if not trigger.group(2):
  791. - return bot.reply("Nothing to calculate.")
  792. - # Account for the silly non-Anglophones and their silly radix point.
  793. - eqn = trigger.group(2).replace(',', '.')
  794. - try:
  795. - result = eval_equation(eqn)
  796. - result = "{:.10g}".format(result)
  797. - except ZeroDivisionError:
  798. - result = "Division by zero is not supported in this universe."
  799. - except Exception as e:
  800. - result = "{error}: {msg}".format(error=type(e), msg=e)
  801. - bot.reply(result)
  802. -
  803. -
  804. -@commands('py')
  805. -@example('.py len([1,2,3])', '3')
  806. -def py(bot, trigger):
  807. - """Evaluate a Python expression."""
  808. - if not trigger.group(2):
  809. - return bot.say("Need an expression to evaluate")
  810. -
  811. - query = trigger.group(2)
  812. - uri = BASE_TUMBOLIA_URI + 'py/'
  813. - answer = web.get(uri + web.quote(query))
  814. - if answer:
  815. - #bot.say can potentially lead to 3rd party commands triggering.
  816. - bot.reply(answer)
  817. - else:
  818. - bot.reply('Sorry, no result.')
  819. -
  820. -
  821. -if __name__ == "__main__":
  822. - from sopel.test_tools import run_example_tests
  823. - run_example_tests(__file__)
  824. diff --git a/sopel/modules/clock.py b/sopel/modules/clock.py
  825. deleted file mode 100644
  826. index d7fa8bb..0000000
  827. --- a/sopel/modules/clock.py
  828. +++ /dev/null
  829. @@ -1,277 +0,0 @@
  830. -# coding=utf-8
  831. -# Copyright 2008-9, Sean B. Palmer, inamidst.com
  832. -# Copyright 2012, Elsie Powell, embolalia.com
  833. -# Licensed under the Eiffel Forum License 2.
  834. -from __future__ import unicode_literals, absolute_import, print_function, division
  835. -
  836. -try:
  837. - import pytz
  838. -except ImportError:
  839. - pytz = None
  840. -
  841. -from sopel.module import commands, example, OP
  842. -from sopel.tools.time import (
  843. - get_timezone, format_time, validate_format, validate_timezone
  844. -)
  845. -from sopel.config.types import StaticSection, ValidatedAttribute
  846. -
  847. -
  848. -class TimeSection(StaticSection):
  849. - tz = ValidatedAttribute(
  850. - 'tz',
  851. - parse=validate_timezone,
  852. - serialize=validate_timezone,
  853. - default='UTC'
  854. - )
  855. - """Default time zone (see http://sopel.chat/tz)"""
  856. - time_format = ValidatedAttribute(
  857. - 'time_format',
  858. - parse=validate_format,
  859. - default='%Y-%m-%d - %T%Z'
  860. - )
  861. - """Default time format (see http://strftime.net)"""
  862. -
  863. -
  864. -def configure(config):
  865. - config.define_section('clock', TimeSection)
  866. - config.clock.configure_setting(
  867. - 'tz', 'Preferred time zone (http://sopel.chat/tz)')
  868. - config.clock.configure_setting(
  869. - 'time_format', 'Preferred time format (http://strftime.net)')
  870. -
  871. -
  872. -def setup(bot):
  873. - bot.config.define_section('clock', TimeSection)
  874. -
  875. -
  876. -@commands('t', 'time')
  877. -@example('.t America/New_York')
  878. -def f_time(bot, trigger):
  879. - """Returns the current time."""
  880. - if trigger.group(2):
  881. - zone = get_timezone(bot.db, bot.config, trigger.group(2).strip(), None, None)
  882. - if not zone:
  883. - bot.say('Could not find timezone %s.' % trigger.group(2).strip())
  884. - return
  885. - else:
  886. - zone = get_timezone(bot.db, bot.config, None, trigger.nick,
  887. - trigger.sender)
  888. - time = format_time(bot.db, bot.config, zone, trigger.nick, trigger.sender)
  889. - bot.say(time)
  890. -
  891. -
  892. -@commands('settz', 'settimezone')
  893. -@example('.settz America/New_York')
  894. -def update_user(bot, trigger):
  895. - """
  896. - Set your preferred time zone. Most timezones will work, but it's best to
  897. - use one from http://sopel.chat/tz
  898. - """
  899. - if not pytz:
  900. - bot.reply("Sorry, I don't have timezone support installed.")
  901. - else:
  902. - tz = trigger.group(2)
  903. - if not tz:
  904. - bot.reply("What timezone do you want to set? Try one from "
  905. - "http://sopel.chat/tz")
  906. - return
  907. - if tz not in pytz.all_timezones:
  908. - bot.reply("I don't know that time zone. Try one from "
  909. - "http://sopel.chat/tz")
  910. - return
  911. -
  912. - bot.db.set_nick_value(trigger.nick, 'timezone', tz)
  913. - if len(tz) < 7:
  914. - bot.say("Okay, {}, but you should use one from http://sopel.chat/tz "
  915. - "if you use DST.".format(trigger.nick))
  916. - else:
  917. - bot.reply('I now have you in the %s time zone.' % tz)
  918. -
  919. -
  920. -@commands('gettz', 'gettimezone')
  921. -@example('.gettz [nick]')
  922. -def get_user_tz(bot, trigger):
  923. - """
  924. - Gets a user's preferred time zone, will show yours if no user specified
  925. - """
  926. - if not pytz:
  927. - bot.reply("Sorry, I don't have timezone support installed.")
  928. - else:
  929. - nick = trigger.group(2)
  930. - if not nick:
  931. - nick = trigger.nick
  932. -
  933. - nick = nick.strip()
  934. -
  935. - tz = bot.db.get_nick_value(nick, 'timezone')
  936. - if tz:
  937. - bot.say('%s\'s time zone is %s.' % (nick, tz))
  938. - else:
  939. - bot.say('%s has not set their time zone' % nick)
  940. -
  941. -
  942. -@commands('settimeformat', 'settf')
  943. -@example('.settf %Y-%m-%dT%T%z')
  944. -def update_user_format(bot, trigger):
  945. - """
  946. - Sets your preferred format for time. Uses the standard strftime format. You
  947. - can use http://strftime.net or your favorite search engine to learn more.
  948. - """
  949. - tformat = trigger.group(2)
  950. - if not tformat:
  951. - bot.reply("What format do you want me to use? Try using"
  952. - " http://strftime.net to make one.")
  953. - return
  954. -
  955. - tz = get_timezone(bot.db, bot.config, None, trigger.nick, trigger.sender)
  956. -
  957. - # Get old format as back-up
  958. - old_format = bot.db.get_nick_value(trigger.nick, 'time_format')
  959. -
  960. - # Save the new format in the database so we can test it.
  961. - bot.db.set_nick_value(trigger.nick, 'time_format', tformat)
  962. -
  963. - try:
  964. - timef = format_time(db=bot.db, zone=tz, nick=trigger.nick)
  965. - except:
  966. - bot.reply("That format doesn't work. Try using"
  967. - " http://strftime.net to make one.")
  968. - # New format doesn't work. Revert save in database.
  969. - bot.db.set_nick_value(trigger.nick, 'time_format', old_format)
  970. - return
  971. - bot.reply("Got it. Your time will now appear as %s. (If the "
  972. - "timezone is wrong, you might try the settz command)"
  973. - % timef)
  974. -
  975. -
  976. -@commands('gettimeformat', 'gettf')
  977. -@example('.gettf [nick]')
  978. -def get_user_format(bot, trigger):
  979. - """
  980. - Gets a user's preferred time format, will show yours if no user specified
  981. - """
  982. - nick = trigger.group(2)
  983. - if not nick:
  984. - nick = trigger.nick
  985. -
  986. - nick = nick.strip()
  987. -
  988. - # Get old format as back-up
  989. - format = bot.db.get_nick_value(nick, 'time_format')
  990. -
  991. - if format:
  992. - bot.say("%s's time format: %s." % (nick, format))
  993. - else:
  994. - bot.say("%s hasn't set a custom time format" % nick)
  995. -
  996. -
  997. -@commands('setchanneltz', 'setctz')
  998. -@example('.setctz America/New_York')
  999. -def update_channel(bot, trigger):
  1000. - """
  1001. - Set the preferred time zone for the channel.
  1002. - """
  1003. - if bot.privileges[trigger.sender][trigger.nick] < OP:
  1004. - return
  1005. - elif not pytz:
  1006. - bot.reply("Sorry, I don't have timezone support installed.")
  1007. - else:
  1008. - tz = trigger.group(2)
  1009. - if not tz:
  1010. - bot.reply("What timezone do you want to set? Try one from "
  1011. - "http://sopel.chat/tz")
  1012. - return
  1013. - if tz not in pytz.all_timezones:
  1014. - bot.reply("I don't know that time zone. Try one from "
  1015. - "http://sopel.chat/tz")
  1016. - return
  1017. -
  1018. - bot.db.set_channel_value(trigger.sender, 'timezone', tz)
  1019. - if len(tz) < 7:
  1020. - bot.say("Okay, {}, but you should use one from http://sopel.chat/tz "
  1021. - "if you use DST.".format(trigger.nick))
  1022. - else:
  1023. - bot.reply(
  1024. - 'I now have {} in the {} time zone.'.format(trigger.sender, tz))
  1025. -
  1026. -
  1027. -@commands('getchanneltz', 'getctz')
  1028. -@example('.getctz [channel]')
  1029. -def get_channel_tz(bot, trigger):
  1030. - """
  1031. - Gets the preferred channel timezone, or the current channel timezone if no
  1032. - channel given.
  1033. - """
  1034. - if not pytz:
  1035. - bot.reply("Sorry, I don't have timezone support installed.")
  1036. - else:
  1037. - channel = trigger.group(2)
  1038. - if not channel:
  1039. - channel = trigger.sender
  1040. -
  1041. - channel = channel.strip()
  1042. -
  1043. - timezone = bot.db.get_channel_value(channel, 'timezone')
  1044. - if timezone:
  1045. - bot.say('%s\'s timezone: %s' % (channel, timezone))
  1046. - else:
  1047. - bot.say('%s has no preferred timezone' % channel)
  1048. -
  1049. -
  1050. -@commands('setchanneltimeformat', 'setctf')
  1051. -@example('.setctf %Y-%m-%dT%T%z')
  1052. -def update_channel_format(bot, trigger):
  1053. - """
  1054. - Sets your preferred format for time. Uses the standard strftime format. You
  1055. - can use http://strftime.net or your favorite search engine to learn more.
  1056. - """
  1057. - if bot.privileges[trigger.sender][trigger.nick] < OP:
  1058. - return
  1059. -
  1060. - tformat = trigger.group(2)
  1061. - if not tformat:
  1062. - bot.reply("What format do you want me to use? Try using"
  1063. - " http://strftime.net to make one.")
  1064. -
  1065. - tz = get_timezone(bot.db, bot.config, None, None, trigger.sender)
  1066. -
  1067. - # Get old format as back-up
  1068. - old_format = bot.db.get_channel_value(trigger.sender, 'time_format')
  1069. -
  1070. - # Save the new format in the database so we can test it.
  1071. - bot.db.set_channel_value(trigger.sender, 'time_format', tformat)
  1072. -
  1073. - try:
  1074. - timef = format_time(db=bot.db, zone=tz, channel=trigger.sender)
  1075. - except:
  1076. - bot.reply("That format doesn't work. Try using"
  1077. - " http://strftime.net to make one.")
  1078. - # New format doesn't work. Revert save in database.
  1079. - bot.db.set_channel_value(trigger.sender, 'time_format', old_format)
  1080. - return
  1081. - bot.db.set_channel_value(trigger.sender, 'time_format', tformat)
  1082. - bot.reply("Got it. Times in this channel will now appear as %s "
  1083. - "unless a user has their own format set. (If the timezone"
  1084. - " is wrong, you might try the settz and channeltz "
  1085. - "commands)" % timef)
  1086. -
  1087. -
  1088. -@commands('getchanneltimeformat', 'getctf')
  1089. -@example('.getctf [channel]')
  1090. -def get_channel_format(bot, trigger):
  1091. - """
  1092. - Gets the channel's preferred time format, will return current channel's if
  1093. - no channel name is given
  1094. - """
  1095. -
  1096. - channel = trigger.group(2)
  1097. - if not channel:
  1098. - channel = trigger.sender
  1099. -
  1100. - channel = channel.strip()
  1101. -
  1102. - tformat = bot.db.get_channel_value(channel, 'time_format')
  1103. - if tformat:
  1104. - bot.say('%s\'s time format: %s' % (channel, tformat))
  1105. - else:
  1106. - bot.say('%s has no preferred time format' % channel)
  1107. diff --git a/sopel/modules/countdown.py b/sopel/modules/countdown.py
  1108. deleted file mode 100644
  1109. index 23a6df8..0000000
  1110. --- a/sopel/modules/countdown.py
  1111. +++ /dev/null
  1112. @@ -1,39 +0,0 @@
  1113. -# coding=utf-8
  1114. -"""
  1115. -countdown.py - Sopel Countdown Module
  1116. -Copyright 2011, Michael Yanovich, yanovich.net
  1117. -Licensed under the Eiffel Forum License 2.
  1118. -
  1119. -http://sopel.chat
  1120. -"""
  1121. -from __future__ import unicode_literals, absolute_import, print_function, division
  1122. -from sopel.module import commands, NOLIMIT
  1123. -import datetime
  1124. -
  1125. -
  1126. -@commands('countdown')
  1127. -def generic_countdown(bot, trigger):
  1128. - """
  1129. - .countdown <year> <month> <day> - displays a countdown to a given date.
  1130. - """
  1131. - text = trigger.group(2)
  1132. - if not text:
  1133. - bot.say("Please use correct format: .countdown 2012 12 21")
  1134. - return NOLIMIT
  1135. - text = trigger.group(2).split()
  1136. - if text and (len(text) == 3 and text[0].isdigit() and text[1].isdigit()
  1137. - and text[2].isdigit()):
  1138. - try:
  1139. - diff = (datetime.datetime(int(text[0]), int(text[1]), int(text[2]))
  1140. - - datetime.datetime.today())
  1141. - except:
  1142. - bot.say("Please use correct format: .countdown 2012 12 21")
  1143. - return NOLIMIT
  1144. - bot.say(str(diff.days) + " days, " + str(diff.seconds // 3600)
  1145. - + " hours and "
  1146. - + str(diff.seconds % 3600 // 60)
  1147. - + " minutes until "
  1148. - + text[0] + " " + text[1] + " " + text[2])
  1149. - else:
  1150. - bot.say("Please use correct format: .countdown 2012 12 21")
  1151. - return NOLIMIT
  1152. diff --git a/sopel/modules/currency.py b/sopel/modules/currency.py
  1153. deleted file mode 100644
  1154. index 3bf0183..0000000
  1155. --- a/sopel/modules/currency.py
  1156. +++ /dev/null
  1157. @@ -1,105 +0,0 @@
  1158. -# coding=utf-8
  1159. -# Copyright 2013 Elsie Powell, embolalia.com
  1160. -# Licensed under the Eiffel Forum License 2
  1161. -from __future__ import unicode_literals, absolute_import, print_function, division
  1162. -
  1163. -import json
  1164. -import xmltodict
  1165. -import re
  1166. -
  1167. -from sopel import web
  1168. -from sopel.module import commands, example, NOLIMIT
  1169. -
  1170. -# The Canadian central bank has better exchange rate data than the Fed, the
  1171. -# Bank of England, or the European Central Bank. Who knew?
  1172. -base_url = 'http://www.bankofcanada.ca/stats/assets/rates_rss/noon/en_{}.xml'
  1173. -regex = re.compile(r'''
  1174. - (\d+(?:\.\d+)?) # Decimal number
  1175. - \s*([a-zA-Z]{3}) # 3-letter currency code
  1176. - \s+(?:in|as|of|to)\s+ # preposition
  1177. - ([a-zA-Z]{3}) # 3-letter currency code
  1178. - ''', re.VERBOSE)
  1179. -
  1180. -
  1181. -def get_rate(code):
  1182. - code = code.upper()
  1183. - if code == 'CAD':
  1184. - return 1, 'Canadian Dollar'
  1185. - elif code == 'BTC':
  1186. - rates = json.loads(web.get('https://api.bitcoinaverage.com/ticker/all'))
  1187. - return 1 / rates['CAD']['24h_avg'], 'Bitcoin—24hr average'
  1188. -
  1189. - data, headers = web.get(base_url.format(code), dont_decode=True, return_headers=True)
  1190. - if headers['_http_status'] == 404:
  1191. - return False, False
  1192. - namespaces = {
  1193. - 'http://www.cbwiki.net/wiki/index.php/Specification_1.1': 'cb',
  1194. - 'http://purl.org/rss/1.0/': None,
  1195. - 'http://www.w3.org/1999/02/22-rdf-syntax-ns#': 'rdf'
  1196. - }
  1197. - xml = xmltodict.parse(data, process_namespaces=True, namespaces=namespaces).get('rdf:RDF')
  1198. - namestring = xml.get('channel').get('title').get('#text')
  1199. - name = namestring[len('Bank of Canada noon rate: '):]
  1200. - name = re.sub(r'\s*\(noon\)\s*', '', name)
  1201. - rate = xml.get('item').get('cb:statistics').get('cb:exchangeRate').get('cb:value').get('#text')
  1202. - return float(rate), name
  1203. -
  1204. -
  1205. -@commands('cur', 'currency', 'exchange')
  1206. -@example('.cur 20 EUR in USD')
  1207. -def exchange(bot, trigger):
  1208. - """Show the exchange rate between two currencies"""
  1209. - if not trigger.group(2):
  1210. - return bot.reply("No search term. An example: .cur 20 EUR in USD")
  1211. - match = regex.match(trigger.group(2))
  1212. - if not match:
  1213. - # It's apologetic, because it's using Canadian data.
  1214. - bot.reply("Sorry, I didn't understand the input.")
  1215. - return NOLIMIT
  1216. -
  1217. - amount, of, to = match.groups()
  1218. - try:
  1219. - amount = float(amount)
  1220. - except:
  1221. - bot.reply("Sorry, I didn't understand the input.")
  1222. - display(bot, amount, of, to)
  1223. -
  1224. -
  1225. -def display(bot, amount, of, to):
  1226. - if not amount:
  1227. - bot.reply("Zero is zero, no matter what country you're in.")
  1228. - try:
  1229. - of_rate, of_name = get_rate(of)
  1230. - if not of_name:
  1231. - bot.reply("Unknown currency: %s" % of)
  1232. - return
  1233. - to_rate, to_name = get_rate(to)
  1234. - if not to_name:
  1235. - bot.reply("Unknown currency: %s" % to)
  1236. - return
  1237. - except Exception:
  1238. - bot.reply("Something went wrong while I was getting the exchange rate.")
  1239. - return NOLIMIT
  1240. -
  1241. - result = amount / of_rate * to_rate
  1242. - bot.say("{} {} ({}) = {} {} ({})".format(amount, of.upper(), of_name,
  1243. - result, to.upper(), to_name))
  1244. -
  1245. -
  1246. -@commands('btc', 'bitcoin')
  1247. -@example('.btc 20 EUR')
  1248. -def bitcoin(bot, trigger):
  1249. - #if 2 args, 1st is number and 2nd is currency. If 1 arg, it's either the number or the currency.
  1250. - to = trigger.group(4)
  1251. - amount = trigger.group(3)
  1252. - if not to:
  1253. - to = trigger.group(3) or 'USD'
  1254. - amount = 1
  1255. -
  1256. - try:
  1257. - amount = float(amount)
  1258. - except:
  1259. - bot.reply("Sorry, I didn't understand the input.")
  1260. - return NOLIMIT
  1261. -
  1262. - display(bot, amount, 'BTC', to)
  1263. diff --git a/sopel/modules/dice.py b/sopel/modules/dice.py
  1264. deleted file mode 100644
  1265. index 2ac0d06..0000000
  1266. --- a/sopel/modules/dice.py
  1267. +++ /dev/null
  1268. @@ -1,259 +0,0 @@
  1269. -# coding=utf-8
  1270. -"""
  1271. -dice.py - Dice Module
  1272. -Copyright 2010-2013, Dimitri "Tyrope" Molenaars, TyRope.nl
  1273. -Copyright 2013, Ari Koivula, <ari@koivu.la>
  1274. -Licensed under the Eiffel Forum License 2.
  1275. -
  1276. -http://sopel.chat/
  1277. -"""
  1278. -from __future__ import unicode_literals, absolute_import, print_function, division
  1279. -import random
  1280. -import re
  1281. -import operator
  1282. -
  1283. -import sopel.module
  1284. -from sopel.tools.calculation import eval_equation
  1285. -
  1286. -
  1287. -class DicePouch:
  1288. - def __init__(self, num_of_die, type_of_die, addition):
  1289. - """Initialize dice pouch and roll the dice.
  1290. -
  1291. - Args:
  1292. - num_of_die: number of dice in the pouch.
  1293. - type_of_die: how many faces the dice have.
  1294. - addition: how much is added to the result of the dice.
  1295. - """
  1296. - self.num = num_of_die
  1297. - self.type = type_of_die
  1298. - self.addition = addition
  1299. -
  1300. - self.dice = {}
  1301. - self.dropped = {}
  1302. -
  1303. - self.roll_dice()
  1304. -
  1305. - def roll_dice(self):
  1306. - """Roll all the dice in the pouch."""
  1307. - self.dice = {}
  1308. - self.dropped = {}
  1309. - for __ in range(self.num):
  1310. - number = random.randint(1, self.type)
  1311. - count = self.dice.setdefault(number, 0)
  1312. - self.dice[number] = count + 1
  1313. -
  1314. - def drop_lowest(self, n):
  1315. - """Drop n lowest dice from the result.
  1316. -
  1317. - Args:
  1318. - n: the number of dice to drop.
  1319. - """
  1320. -
  1321. - sorted_x = sorted(self.dice.items(), key=operator.itemgetter(0))
  1322. -
  1323. - for i, count in sorted_x:
  1324. - count = self.dice[i]
  1325. - if n == 0:
  1326. - break
  1327. - elif n < count:
  1328. - self.dice[i] = count - n
  1329. - self.dropped[i] = n
  1330. - break
  1331. - else:
  1332. - self.dice[i] = 0
  1333. - self.dropped[i] = count
  1334. - n = n - count
  1335. -
  1336. - for i, count in self.dropped.items():
  1337. - if self.dice[i] == 0:
  1338. - del self.dice[i]
  1339. -
  1340. - def get_simple_string(self):
  1341. - """Return the values of the dice like (2+2+2[+1+1])+1."""
  1342. - dice = self.dice.items()
  1343. - faces = ("+".join([str(face)] * times) for face, times in dice)
  1344. - dice_str = "+".join(faces)
  1345. -
  1346. - dropped_str = ""
  1347. - if self.dropped:
  1348. - dropped = self.dropped.items()
  1349. - dfaces = ("+".join([str(face)] * times) for face, times in dropped)
  1350. - dropped_str = "[+%s]" % ("+".join(dfaces),)
  1351. -
  1352. - plus_str = ""
  1353. - if self.addition:
  1354. - plus_str = "{:+d}".format(self.addition)
  1355. -
  1356. - return "(%s%s)%s" % (dice_str, dropped_str, plus_str)
  1357. -
  1358. - def get_compressed_string(self):
  1359. - """Return the values of the dice like (3x2[+2x1])+1."""
  1360. - dice = self.dice.items()
  1361. - faces = ("%dx%d" % (times, face) for face, times in dice)
  1362. - dice_str = "+".join(faces)
  1363. -
  1364. - dropped_str = ""
  1365. - if self.dropped:
  1366. - dropped = self.dropped.items()
  1367. - dfaces = ("%dx%d" % (times, face) for face, times in dropped)
  1368. - dropped_str = "[+%s]" % ("+".join(dfaces),)
  1369. -
  1370. - plus_str = ""
  1371. - if self.addition:
  1372. - plus_str = "{:+d}".format(self.addition)
  1373. -
  1374. - return "(%s%s)%s" % (dice_str, dropped_str, plus_str)
  1375. -
  1376. - def get_sum(self):
  1377. - """Get the sum of non-dropped dice and the addition."""
  1378. - result = self.addition
  1379. - for face, times in self.dice.items():
  1380. - result += face * times
  1381. - return result
  1382. -
  1383. - def get_number_of_faces(self):
  1384. - """Returns sum of different faces for dropped and not dropped dice
  1385. -
  1386. - This can be used to estimate, whether the result can be shown in
  1387. - compressed form in a reasonable amount of space.
  1388. - """
  1389. - return len(self.dice) + len(self.dropped)
  1390. -
  1391. -
  1392. -def _roll_dice(bot, dice_expression):
  1393. - result = re.search(
  1394. - r"""
  1395. - (?P<dice_num>-?\d*)
  1396. - d
  1397. - (?P<dice_type>-?\d+)
  1398. - (v(?P<drop_lowest>-?\d+))?
  1399. - $""",
  1400. - dice_expression,
  1401. - re.IGNORECASE | re.VERBOSE)
  1402. -
  1403. - dice_num = int(result.group('dice_num') or 1)
  1404. - dice_type = int(result.group('dice_type'))
  1405. -
  1406. - # Dice can't have zero or a negative number of sides.
  1407. - if dice_type <= 0:
  1408. - bot.reply("I don't have any dice with %d sides. =(" % dice_type)
  1409. - return None # Signal there was a problem
  1410. -
  1411. - # Can't roll a negative number of dice.
  1412. - if dice_num < 0:
  1413. - bot.reply("I'd rather not roll a negative amount of dice. =(")
  1414. - return None # Signal there was a problem
  1415. -
  1416. - # Upper limit for dice should be at most a million. Creating a dict with
  1417. - # more than a million elements already takes a noticeable amount of time
  1418. - # on a fast computer and ~55kB of memory.
  1419. - if dice_num > 1000:
  1420. - bot.reply('I only have 1000 dice. =(')
  1421. - return None # Signal there was a problem
  1422. -
  1423. - dice = DicePouch(dice_num, dice_type, 0)
  1424. -
  1425. - if result.group('drop_lowest'):
  1426. - drop = int(result.group('drop_lowest'))
  1427. - if drop >= 0:
  1428. - dice.drop_lowest(drop)
  1429. - else:
  1430. - bot.reply("I can't drop the lowest %d dice. =(" % drop)
  1431. -
  1432. - return dice
  1433. -
  1434. -
  1435. -@sopel.module.commands("roll")
  1436. -@sopel.module.commands("dice")
  1437. -@sopel.module.commands("d")
  1438. -@sopel.module.priority("medium")
  1439. -@sopel.module.example(".roll 3d1+1", 'You roll 3d1+1: (1+1+1)+1 = 4')
  1440. -@sopel.module.example(".roll 3d1v2+1", 'You roll 3d1v2+1: (1[+1+1])+1 = 2')
  1441. -@sopel.module.example(".roll 2d4", 'You roll 2d4: \(\d\+\d\) = \d', re=True)
  1442. -@sopel.module.example(".roll 100d1", '[^:]*: \(100x1\) = 100', re=True)
  1443. -@sopel.module.example(".roll 1001d1", 'I only have 1000 dice. =(')
  1444. -@sopel.module.example(".roll 1d1 + 1d1", 'You roll 1d1 + 1d1: (1) + (1) = 2')
  1445. -@sopel.module.example(".roll 1d1+1d1", 'You roll 1d1+1d1: (1)+(1) = 2')
  1446. -def roll(bot, trigger):
  1447. - """.dice XdY[vZ][+N], rolls dice and reports the result.
  1448. -
  1449. - X is the number of dice. Y is the number of faces in the dice. Z is the
  1450. - number of lowest dice to be dropped from the result. N is the constant to
  1451. - be applied to the end result.
  1452. - """
  1453. - # This regexp is only allowed to have one captured group, because having
  1454. - # more would alter the output of re.findall.
  1455. - dice_regexp = r"-?\d*[dD]-?\d+(?:[vV]-?\d+)?"
  1456. -
  1457. - # Get a list of all dice expressions, evaluate them and then replace the
  1458. - # expressions in the original string with the results. Replacing is done
  1459. - # using string formatting, so %-characters must be escaped.
  1460. - if not trigger.group(2):
  1461. - return bot.reply("No dice to roll.")
  1462. - arg_str = trigger.group(2)
  1463. - dice_expressions = re.findall(dice_regexp, arg_str)
  1464. - arg_str = arg_str.replace("%", "%%")
  1465. - arg_str = re.sub(dice_regexp, "%s", arg_str)
  1466. -
  1467. - f = lambda dice_expr: _roll_dice(bot, dice_expr)
  1468. - dice = list(map(f, dice_expressions))
  1469. -
  1470. - if None in dice:
  1471. - # Stop computing roll if there was a problem rolling dice.
  1472. - return
  1473. -
  1474. - def _get_eval_str(dice):
  1475. - return "(%d)" % (dice.get_sum(),)
  1476. -
  1477. - def _get_pretty_str(dice):
  1478. - if dice.num <= 10:
  1479. - return dice.get_simple_string()
  1480. - elif dice.get_number_of_faces() <= 10:
  1481. - return dice.get_compressed_string()
  1482. - else:
  1483. - return "(...)"
  1484. -
  1485. - eval_str = arg_str % (tuple(map(_get_eval_str, dice)))
  1486. - pretty_str = arg_str % (tuple(map(_get_pretty_str, dice)))
  1487. -
  1488. - # Showing the actual error will hopefully give a better hint of what is
  1489. - # wrong with the syntax than a generic error message.
  1490. - try:
  1491. - result = eval_equation(eval_str)
  1492. - except Exception as e:
  1493. - bot.reply("SyntaxError, eval(%s), %s" % (eval_str, e))
  1494. - return
  1495. -
  1496. - bot.reply("You roll %s: %s = %d" % (
  1497. - trigger.group(2), pretty_str, result))
  1498. -
  1499. -
  1500. -@sopel.module.commands("choice")
  1501. -@sopel.module.commands("ch")
  1502. -@sopel.module.commands("choose")
  1503. -@sopel.module.priority("medium")
  1504. -def choose(bot, trigger):
  1505. - """
  1506. - .choice option1|option2|option3 - Makes a difficult choice easy.
  1507. - """
  1508. - if not trigger.group(2):
  1509. - return bot.reply('I\'d choose an option, but you didn\'t give me any.')
  1510. - choices = [trigger.group(2)]
  1511. - for delim in '|\\/,':
  1512. - choices = trigger.group(2).split(delim)
  1513. - if len(choices) > 1:
  1514. - break
  1515. - # Use a different delimiter in the output, to prevent ambiguity.
  1516. - for show_delim in ',|/\\':
  1517. - if show_delim not in trigger.group(2):
  1518. - show_delim += ' '
  1519. - break
  1520. -
  1521. - pick = random.choice(choices)
  1522. - return bot.reply('Your options: %s. My choice: %s' % (show_delim.join(choices), pick))
  1523. -
  1524. -
  1525. -if __name__ == "__main__":
  1526. - from sopel.test_tools import run_example_tests
  1527. - run_example_tests(__file__)
  1528. diff --git a/sopel/modules/etymology.py b/sopel/modules/etymology.py
  1529. deleted file mode 100644
  1530. index e0663c8..0000000
  1531. --- a/sopel/modules/etymology.py
  1532. +++ /dev/null
  1533. @@ -1,97 +0,0 @@
  1534. -# coding=utf-8
  1535. -"""
  1536. -etymology.py - Sopel Etymology Module
  1537. -Copyright 2007-9, Sean B. Palmer, inamidst.com
  1538. -Licensed under the Eiffel Forum License 2.
  1539. -
  1540. -http://sopel.chat
  1541. -"""
  1542. -from __future__ import unicode_literals, absolute_import, print_function, division
  1543. -
  1544. -import re
  1545. -from sopel import web
  1546. -from sopel.module import commands, example, NOLIMIT
  1547. -
  1548. -etyuri = 'http://etymonline.com/?term=%s'
  1549. -etysearch = 'http://etymonline.com/?search=%s'
  1550. -
  1551. -r_definition = re.compile(r'(?ims)<dd[^>]*>.*?</dd>')
  1552. -r_tag = re.compile(r'<(?!!)[^>]+>')
  1553. -r_whitespace = re.compile(r'[\t\r\n ]+')
  1554. -
  1555. -abbrs = [
  1556. - 'cf', 'lit', 'etc', 'Ger', 'Du', 'Skt', 'Rus', 'Eng', 'Amer.Eng', 'Sp',
  1557. - 'Fr', 'N', 'E', 'S', 'W', 'L', 'Gen', 'J.C', 'dial', 'Gk',
  1558. - '19c', '18c', '17c', '16c', 'St', 'Capt', 'obs', 'Jan', 'Feb', 'Mar',
  1559. - 'Apr', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'c', 'tr', 'e', 'g'
  1560. -]
  1561. -t_sentence = r'^.*?(?<!%s)(?:\.(?= [A-Z0-9]|\Z)|\Z)'
  1562. -r_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs))
  1563. -
  1564. -
  1565. -def unescape(s):
  1566. - s = s.replace('&gt;', '>')
  1567. - s = s.replace('&lt;', '<')
  1568. - s = s.replace('&amp;', '&')
  1569. - return s
  1570. -
  1571. -
  1572. -def text(html):
  1573. - html = r_tag.sub('', html)
  1574. - html = r_whitespace.sub(' ', html)
  1575. - return unescape(html).strip()
  1576. -
  1577. -
  1578. -def etymology(word):
  1579. - # @@ <nsh> sbp, would it be possible to have a flag for .ety to get 2nd/etc
  1580. - # entries? - http://swhack.com/logs/2006-07-19#T15-05-29
  1581. -
  1582. - if len(word) > 25:
  1583. - raise ValueError("Word too long: %s[...]" % word[:10])
  1584. - word = {'axe': 'ax/axe'}.get(word, word)
  1585. -
  1586. - bytes = web.get(etyuri % word)
  1587. - definitions = r_definition.findall(bytes)
  1588. -
  1589. - if not definitions:
  1590. - return None
  1591. -
  1592. - defn = text(definitions[0])
  1593. - m = r_sentence.match(defn)
  1594. - if not m:
  1595. - return None
  1596. - sentence = m.group(0)
  1597. -
  1598. - maxlength = 275
  1599. - if len(sentence) > maxlength:
  1600. - sentence = sentence[:maxlength]
  1601. - words = sentence[:-5].split(' ')
  1602. - words.pop()
  1603. - sentence = ' '.join(words) + ' [...]'
  1604. -
  1605. - sentence = '"' + sentence.replace('"', "'") + '"'
  1606. - return sentence + ' - ' + (etyuri % word)
  1607. -
  1608. -
  1609. -@commands('ety')
  1610. -@example('.ety word')
  1611. -def f_etymology(bot, trigger):
  1612. - """Look up the etymology of a word"""
  1613. - word = trigger.group(2)
  1614. -
  1615. - try:
  1616. - result = etymology(word)
  1617. - except IOError:
  1618. - msg = "Can't connect to etymonline.com (%s)" % (etyuri % word)
  1619. - bot.msg(trigger.sender, msg)
  1620. - return NOLIMIT
  1621. - except (AttributeError, TypeError):
  1622. - result = None
  1623. -
  1624. - if result is not None:
  1625. - bot.msg(trigger.sender, result)
  1626. - else:
  1627. - uri = etysearch % word
  1628. - msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri)
  1629. - bot.msg(trigger.sender, msg)
  1630. - return NOLIMIT
  1631. diff --git a/sopel/modules/find.py b/sopel/modules/find.py
  1632. deleted file mode 100644
  1633. index 8a1f58a..0000000
  1634. --- a/sopel/modules/find.py
  1635. +++ /dev/null
  1636. @@ -1,139 +0,0 @@
  1637. -# coding=utf-8
  1638. -"""Sopel Spelling correction module
  1639. -
  1640. -This module will fix spelling errors if someone corrects them
  1641. -using the sed notation (s///) commonly found in vi/vim.
  1642. -"""
  1643. -# Copyright 2011, Michael Yanovich, yanovich.net
  1644. -# Copyright 2013, Elsie Powell, embolalia.com
  1645. -# Licensed under the Eiffel Forum License 2.
  1646. -# Contributions from: Matt Meinwald and Morgan Goose
  1647. -from __future__ import unicode_literals, absolute_import, print_function, division
  1648. -
  1649. -import re
  1650. -from sopel.tools import Identifier, SopelMemory
  1651. -from sopel.module import rule, priority
  1652. -from sopel.formatting import bold
  1653. -
  1654. -
  1655. -def setup(bot):
  1656. - bot.memory['find_lines'] = SopelMemory()
  1657. -
  1658. -
  1659. -@rule('.*')
  1660. -@priority('low')
  1661. -def collectlines(bot, trigger):
  1662. - """Create a temporary log of what people say"""
  1663. -
  1664. - # Don't log things in PM
  1665. - if trigger.is_privmsg:
  1666. - return
  1667. -
  1668. - # Add a log for the channel and nick, if there isn't already one
  1669. - if trigger.sender not in bot.memory['find_lines']:
  1670. - bot.memory['find_lines'][trigger.sender] = SopelMemory()
  1671. - if Identifier(trigger.nick) not in bot.memory['find_lines'][trigger.sender]:
  1672. - bot.memory['find_lines'][trigger.sender][Identifier(trigger.nick)] = list()
  1673. -
  1674. - # Create a temporary list of the user's lines in a channel
  1675. - templist = bot.memory['find_lines'][trigger.sender][Identifier(trigger.nick)]
  1676. - line = trigger.group()
  1677. - if line.startswith("s/"): # Don't remember substitutions
  1678. - return
  1679. - elif line.startswith("\x01ACTION"): # For /me messages
  1680. - line = line[:-1]
  1681. - templist.append(line)
  1682. - else:
  1683. - templist.append(line)
  1684. -
  1685. - del templist[:-10] # Keep the log to 10 lines per person
  1686. -
  1687. - bot.memory['find_lines'][trigger.sender][Identifier(trigger.nick)] = templist
  1688. -
  1689. -
  1690. -#Match nick, s/find/replace/flags. Flags and nick are optional, nick can be
  1691. -#followed by comma or colon, anything after the first space after the third
  1692. -#slash is ignored, you can escape slashes with backslashes, and if you want to
  1693. -#search for an actual backslash followed by an actual slash, you're shit out of
  1694. -#luck because this is the fucking regex of death as it is.
  1695. -@rule(r"""(?:
  1696. - (\S+) # Catch a nick in group 1
  1697. - [:,]\s+)? # Followed by colon/comma and whitespace, if given
  1698. - s/ # The literal s/
  1699. - ( # Group 2 is the thing to find
  1700. - (?:\\/ | [^/])+ # One or more non-slashes or escaped slashes
  1701. - )/( # Group 3 is what to replace with
  1702. - (?:\\/ | [^/])* # One or more non-slashes or escaped slashes
  1703. - )
  1704. - (?:/(\S+))? # Optional slash, followed by group 4 (flags)
  1705. - """)
  1706. -@priority('high')
  1707. -def findandreplace(bot, trigger):
  1708. - # Don't bother in PM
  1709. - if trigger.is_privmsg:
  1710. - return
  1711. -
  1712. - # Correcting other person vs self.
  1713. - rnick = Identifier(trigger.group(1) or trigger.nick)
  1714. -
  1715. - search_dict = bot.memory['find_lines']
  1716. - # only do something if there is conversation to work with
  1717. - if trigger.sender not in search_dict:
  1718. - return
  1719. - if Identifier(rnick) not in search_dict[trigger.sender]:
  1720. - return
  1721. -
  1722. - #TODO rest[0] is find, rest[1] is replace. These should be made variables of
  1723. - #their own at some point.
  1724. - rest = [trigger.group(2), trigger.group(3)]
  1725. - rest[0] = rest[0].replace(r'\/', '/')
  1726. - rest[1] = rest[1].replace(r'\/', '/')
  1727. - me = False # /me command
  1728. - flags = (trigger.group(4) or '')
  1729. -
  1730. - # If g flag is given, replace all. Otherwise, replace once.
  1731. - if 'g' in flags:
  1732. - count = -1
  1733. - else:
  1734. - count = 1
  1735. -
  1736. - # repl is a lambda function which performs the substitution. i flag turns
  1737. - # off case sensitivity. re.U turns on unicode replacement.
  1738. - if 'i' in flags:
  1739. - regex = re.compile(re.escape(rest[0]), re.U | re.I)
  1740. - repl = lambda s: re.sub(regex, rest[1], s, count == 1)
  1741. - else:
  1742. - repl = lambda s: s.replace(rest[0], rest[1], count)
  1743. -
  1744. - # Look back through the user's lines in the channel until you find a line
  1745. - # where the replacement works
  1746. - new_phrase = None
  1747. - for line in reversed(search_dict[trigger.sender][rnick]):
  1748. - if line.startswith("\x01ACTION"):
  1749. - me = True # /me command
  1750. - line = line[8:]
  1751. - else:
  1752. - me = False
  1753. - new_phrase = repl(line)
  1754. - if new_phrase != line: # we are done
  1755. - break
  1756. -
  1757. - if not new_phrase or new_phrase == line:
  1758. - return # Didn't find anything
  1759. -
  1760. - # Save the new "edited" message.
  1761. - action = (me and '\x01ACTION ') or '' # If /me message, prepend \x01ACTION
  1762. - templist = search_dict[trigger.sender][rnick]
  1763. - templist.append(action + new_phrase)
  1764. - search_dict[trigger.sender][rnick] = templist
  1765. - bot.memory['find_lines'] = search_dict
  1766. -
  1767. - # output
  1768. - if not me:
  1769. - new_phrase = '%s to say: %s' % (bold('meant'), new_phrase)
  1770. - if trigger.group(1):
  1771. - phrase = '%s thinks %s %s' % (trigger.nick, rnick, new_phrase)
  1772. - else:
  1773. - phrase = '%s %s' % (trigger.nick, new_phrase)
  1774. -
  1775. - bot.say(phrase)
  1776. diff --git a/sopel/modules/find_updates.py b/sopel/modules/find_updates.py
  1777. deleted file mode 100644
  1778. index b411447..0000000
  1779. --- a/sopel/modules/find_updates.py
  1780. +++ /dev/null
  1781. @@ -1,58 +0,0 @@
  1782. -# coding=utf-8
  1783. -"""Update checking module for Sopel.
  1784. -
  1785. -This is separated from version.py, so that it can be easily overridden by
  1786. -distribution packagers, and they can check their repositories rather than the
  1787. -Sopel website.
  1788. -"""
  1789. -# Copyright 2014, Elsie Powell, embolalia.com
  1790. -# Licensed under the Eiffel Forum License 2.
  1791. -from __future__ import unicode_literals, absolute_import, print_function, division
  1792. -
  1793. -import json
  1794. -
  1795. -import sopel
  1796. -import sopel.module
  1797. -import requests
  1798. -import sopel.tools
  1799. -
  1800. -wait_time = 24 * 60 * 60 # check once per day
  1801. -startup_check_run = False
  1802. -version_url = 'http://sopel.chat/latest.json'
  1803. -message = (
  1804. - 'A new Sopel version, {}, is available. I am running {}. Please update '
  1805. - 'me. Full release notes at {}'
  1806. -)
  1807. -unstable_message = (
  1808. - 'A new pre-release version, {}, is available. I am running {}. Please '
  1809. - 'update me. {}'
  1810. -)
  1811. -
  1812. -
  1813. -@sopel.module.event(sopel.tools.events.RPL_LUSERCLIENT)
  1814. -@sopel.module.rule('.*')
  1815. -def startup_version_check(bot, trigger):
  1816. - global startup_check_run
  1817. - if not startup_check_run:
  1818. - startup_check_run = True
  1819. - check_version(bot)
  1820. -
  1821. -
  1822. -@sopel.module.interval(wait_time)
  1823. -def check_version(bot):
  1824. - version = sopel.version_info
  1825. -
  1826. - info = requests.get(version_url).json()
  1827. - if version.releaselevel == 'final':
  1828. - latest = info['version']
  1829. - notes = info['release_notes']
  1830. - else:
  1831. - latest = info['unstable']
  1832. - notes = info.get('unstable_notes', '')
  1833. - if notes:
  1834. - notes = 'Full release notes at ' + notes
  1835. - latest_version = sopel._version_info(latest)
  1836. - msg = message.format(latest, sopel.__version__, notes)
  1837. -
  1838. - if version < latest_version:
  1839. - bot.msg(bot.config.core.owner, msg)
  1840. diff --git a/sopel/modules/help.py b/sopel/modules/help.py
  1841. deleted file mode 100644
  1842. index 1dadde4..0000000
  1843. --- a/sopel/modules/help.py
  1844. +++ /dev/null
  1845. @@ -1,72 +0,0 @@
  1846. -# coding=utf-8
  1847. -"""
  1848. -help.py - Sopel Help Module
  1849. -Copyright 2008, Sean B. Palmer, inamidst.com
  1850. -Copyright © 2013, Elad Alfassa, <elad@fedoraproject.org>
  1851. -Licensed under the Eiffel Forum License 2.
  1852. -
  1853. -http://sopel.chat
  1854. -"""
  1855. -from __future__ import unicode_literals, absolute_import, print_function, division
  1856. -
  1857. -import textwrap
  1858. -import collections
  1859. -
  1860. -from sopel.formatting import bold
  1861. -from sopel.module import commands, rule, example, priority
  1862. -
  1863. -
  1864. -@rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
  1865. -@example('.help tell')
  1866. -@commands('help', 'commands')
  1867. -@priority('low')
  1868. -def help(bot, trigger):
  1869. - """Shows a command's documentation, and possibly an example."""
  1870. - if trigger.group(2):
  1871. - name = trigger.group(2)
  1872. - name = name.lower()
  1873. -
  1874. - # number of lines of help to show
  1875. - threshold = 3
  1876. -
  1877. - if name in bot.doc:
  1878. - if len(bot.doc[name][0]) + (1 if bot.doc[name][1] else 0) > threshold:
  1879. - if trigger.nick != trigger.sender: # don't say that if asked in private
  1880. - bot.reply('The documentation for this command is too long; I\'m sending it to you in a private message.')
  1881. - msgfun = lambda l: bot.msg(trigger.nick, l)
  1882. - else:
  1883. - msgfun = bot.reply
  1884. -
  1885. - for line in bot.doc[name][0]:
  1886. - msgfun(line)
  1887. - if bot.doc[name][1]:
  1888. - msgfun('e.g. ' + bot.doc[name][1])
  1889. - else:
  1890. - if not trigger.is_privmsg:
  1891. - bot.reply("I'm sending you a list of my commands in a private message!")
  1892. - bot.say(
  1893. - 'You can see more info about any of these commands by doing .help '
  1894. - '<command> (e.g. .help time)',
  1895. - trigger.nick
  1896. - )
  1897. -
  1898. - name_length = max(6, max(len(k) for k in bot.command_groups.keys()))
  1899. - for category, cmds in collections.OrderedDict(sorted(bot.command_groups.items())).items():
  1900. - category = category.upper().ljust(name_length)
  1901. - cmds = ' '.join(cmds)
  1902. - msg = bold(category) + ' ' + cmds
  1903. - indent = ' ' * (name_length + 2)
  1904. - msg = textwrap.wrap(msg, subsequent_indent=indent)
  1905. - for line in msg:
  1906. - bot.say(line, trigger.nick)
  1907. -
  1908. -
  1909. -@rule('$nick' r'(?i)help(?:[?!]+)?$')
  1910. -@priority('low')
  1911. -def help2(bot, trigger):
  1912. - response = (
  1913. - 'Hi, I\'m a bot. Say ".commands" to me in private for a list ' +
  1914. - 'of my commands, or see http://sopel.chat for more ' +
  1915. - 'general details. My owner is %s.'
  1916. - ) % bot.config.core.owner
  1917. - bot.reply(response)
  1918. diff --git a/sopel/modules/ip.py b/sopel/modules/ip.py
  1919. deleted file mode 100644
  1920. index b2e2804..0000000
  1921. --- a/sopel/modules/ip.py
  1922. +++ /dev/null
  1923. @@ -1,139 +0,0 @@
  1924. -# coding=utf-8
  1925. -"""GeoIP lookup module"""
  1926. -# Copyright 2011, Dimitri Molenaars, TyRope.nl,
  1927. -# Copyright © 2013, Elad Alfassa <elad@fedoraproject.org>
  1928. -# Licensed under the Eiffel Forum License 2.
  1929. -
  1930. -from __future__ import unicode_literals, absolute_import, print_function, division
  1931. -
  1932. -import pygeoip
  1933. -import socket
  1934. -import os
  1935. -import gzip
  1936. -
  1937. -urlretrieve = None
  1938. -try:
  1939. - from urllib import urlretrieve
  1940. -except ImportError:
  1941. - try:
  1942. - # urlretrieve has been put under urllib.request in Python 3.
  1943. - # It's also deprecated so this should probably be replaced with
  1944. - # urllib2.
  1945. - from urllib.request import urlretrieve
  1946. - except ImportError:
  1947. - pass
  1948. -
  1949. -from sopel.config.types import StaticSection, FilenameAttribute
  1950. -from sopel.module import commands, example
  1951. -from sopel.logger import get_logger
  1952. -
  1953. -LOGGER = get_logger(__name__)
  1954. -
  1955. -
  1956. -class GeoipSection(StaticSection):
  1957. - GeoIP_db_path = FilenameAttribute('GeoIP_db_path', directory=True)
  1958. - """Path of the directory containing the GeoIP db files."""
  1959. -
  1960. -
  1961. -def configure(config):
  1962. - config.define_section('ip', GeoipSection)
  1963. - config.ip.configure_setting('GeoIP_db_path',
  1964. - 'Path of the GeoIP db files')
  1965. -
  1966. -
  1967. -def setup(bot=None):
  1968. - if not bot:
  1969. - return # Because of some weird pytest thing?
  1970. -
  1971. - bot.config.define_section('ip', GeoipSection)
  1972. -
  1973. -
  1974. -def _decompress(source, target, delete_after_decompression=True):
  1975. - """ Decompress a GZip file """
  1976. - f_in = gzip.open(source, 'rb')
  1977. - f_out = open(target, 'wb')
  1978. - f_out.writelines(f_in)
  1979. - f_out.close()
  1980. - f_in.close()
  1981. - if delete_after_decompression:
  1982. - os.remove(source)
  1983. -
  1984. -
  1985. -def _find_geoip_db(bot):
  1986. - """ Find the GeoIP database """
  1987. - config = bot.config
  1988. - if config.ip.GeoIP_db_path:
  1989. - cities_db = os.path.join(config.ip.GeoIP_db_path, 'GeoLiteCity.dat')
  1990. - ipasnum_db = os.path.join(config.ip.GeoIP_db_path, 'GeoIPASNum.dat')
  1991. - if os.path.isfile(cities_db) and os.path.isfile(ipasnum_db):
  1992. - return config.ip.GeoIP_db_path
  1993. - else:
  1994. - LOGGER.warning(
  1995. - 'GeoIP path configured but DB not found in configured path'
  1996. - )
  1997. - if (os.path.isfile(os.path.join(bot.config.core.homedir, 'GeoLiteCity.dat')) and
  1998. - os.path.isfile(os.path.join(bot.config.core.homedir, 'GeoIPASNum.dat'))):
  1999. - return bot.config.core.homedir
  2000. - elif (os.path.isfile(os.path.join('/usr/share/GeoIP', 'GeoLiteCity.dat')) and
  2001. - os.path.isfile(os.path.join('/usr/share/GeoIP', 'GeoIPASNum.dat'))):
  2002. - return '/usr/share/GeoIP'
  2003. - elif urlretrieve:
  2004. - LOGGER.warning('Downloading GeoIP database')
  2005. - bot.say('Downloading GeoIP database, please wait...')
  2006. - geolite_city_url = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz'
  2007. - geolite_ASN_url = 'http://download.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz'
  2008. - geolite_city_filepath = os.path.join(bot.config.core.homedir, 'GeoLiteCity.dat.gz')
  2009. - geolite_ASN_filepath = os.path.join(bot.config.core.homedir, 'GeoIPASNum.dat.gz')
  2010. - urlretrieve(geolite_city_url, geolite_city_filepath)
  2011. - urlretrieve(geolite_ASN_url, geolite_ASN_filepath)
  2012. - _decompress(geolite_city_filepath, geolite_city_filepath[:-3])
  2013. - _decompress(geolite_ASN_filepath, geolite_ASN_filepath[:-3])
  2014. - return bot.config.core.homedir
  2015. - else:
  2016. - return False
  2017. -
  2018. -
  2019. -@commands('iplookup', 'ip')
  2020. -@example('.ip 8.8.8.8',
  2021. - r'[IP/Host Lookup] Hostname: google-public-dns-a.google.com | Location: United States | Region: CA | ISP: AS15169 Google Inc.',
  2022. - re=True,
  2023. - ignore='Downloading GeoIP database, please wait...')
  2024. -def ip(bot, trigger):
  2025. - """IP Lookup tool"""
  2026. - if not trigger.group(2):
  2027. - return bot.reply("No search term.")
  2028. - query = trigger.group(2)
  2029. - db_path = _find_geoip_db(bot)
  2030. - if db_path is False:
  2031. - LOGGER.error('Can\'t find (or download) usable GeoIP database')
  2032. - bot.say('Sorry, I don\'t have a GeoIP database to use for this lookup')
  2033. - return False
  2034. - geolite_city_filepath = os.path.join(_find_geoip_db(bot), 'GeoLiteCity.dat')
  2035. - geolite_ASN_filepath = os.path.join(_find_geoip_db(bot), 'GeoIPASNum.dat')
  2036. - gi_city = pygeoip.GeoIP(geolite_city_filepath)
  2037. - gi_org = pygeoip.GeoIP(geolite_ASN_filepath)
  2038. - host = socket.getfqdn(query)
  2039. - response = "[IP/Host Lookup] Hostname: %s" % host
  2040. - try:
  2041. - response += " | Location: %s" % gi_city.country_name_by_name(query)
  2042. - except AttributeError:
  2043. - response += ' | Location: Unknown'
  2044. - except socket.gaierror:
  2045. - return bot.say('[IP/Host Lookup] Unable to resolve IP/Hostname')
  2046. -
  2047. - region_data = gi_city.region_by_name(query)
  2048. - try:
  2049. - region = region_data['region_code'] # pygeoip >= 0.3.0
  2050. - except KeyError:
  2051. - region = region_data['region_name'] # pygeoip < 0.3.0
  2052. - if region:
  2053. - response += " | Region: %s" % region
  2054. -
  2055. - isp = gi_org.org_by_name(query)
  2056. - response += " | ISP: %s" % isp
  2057. - bot.say(response)
  2058. -
  2059. -
  2060. -if __name__ == "__main__":
  2061. - from sopel.test_tools import run_example_tests
  2062. - run_example_tests(__file__)
  2063. diff --git a/sopel/modules/ipython.py b/sopel/modules/ipython.py
  2064. deleted file mode 100644
  2065. index 6df6112..0000000
  2066. --- a/sopel/modules/ipython.py
  2067. +++ /dev/null
  2068. @@ -1,78 +0,0 @@
  2069. -# coding=utf-8
  2070. -"""
  2071. -ipython.py - sopel ipython console!
  2072. -Copyright © 2014, Elad Alfassa <elad@fedoraproject.org>
  2073. -Licensed under the Eiffel Forum License 2.
  2074. -
  2075. -Sopel: http://sopel.chat/
  2076. -"""
  2077. -from __future__ import unicode_literals, absolute_import, print_function, division
  2078. -import sopel
  2079. -import sys
  2080. -if sys.version_info.major >= 3:
  2081. - # Backup stderr/stdout wrappers
  2082. - old_stdout = sys.stdout
  2083. - old_stderr = sys.stderr
  2084. -
  2085. - # IPython wants actual stderr and stdout. In Python 2, it only needed that
  2086. - # when actually starting the console, but in Python 3 it seems to need that
  2087. - # on import as well
  2088. - sys.stdout = sys.__stdout__
  2089. - sys.stderr = sys.__stderr__
  2090. -try:
  2091. - import IPython
  2092. - if hasattr(IPython, 'terminal'):
  2093. - from IPython.terminal.embed import InteractiveShellEmbed
  2094. - else:
  2095. - from IPython.frontend.terminal.embed import InteractiveShellEmbed
  2096. -finally:
  2097. - if sys.version_info.major >= 3:
  2098. - # Restore stderr/stdout wrappers
  2099. - sys.stdout = old_stdout
  2100. - sys.stderr = old_stderr
  2101. -
  2102. -console = None
  2103. -
  2104. -
  2105. -@sopel.module.commands('console')
  2106. -def interactive_shell(bot, trigger):
  2107. - """
  2108. - Starts an interactive IPython console
  2109. - """
  2110. - global console
  2111. - if not trigger.admin:
  2112. - bot.say('Only admins can start the interactive console')
  2113. - return
  2114. - if 'iconsole_running' in bot.memory and bot.memory['iconsole_running']:
  2115. - bot.say('Console already running')
  2116. - return
  2117. - if not sys.__stdout__.isatty():
  2118. - bot.say('A tty is required to start the console')
  2119. - return
  2120. - if bot._daemon:
  2121. - bot.say('Can\'t start console when running as a daemon')
  2122. - return
  2123. -
  2124. - # Backup stderr/stdout wrappers
  2125. - old_stdout = sys.stdout
  2126. - old_stderr = sys.stderr
  2127. -
  2128. - # IPython wants actual stderr and stdout
  2129. - sys.stdout = sys.__stdout__
  2130. - sys.stderr = sys.__stderr__
  2131. -
  2132. - banner1 = 'Sopel interactive shell (embedded IPython)'
  2133. - banner2 = '`bot` and `trigger` are available. To exit, type exit'
  2134. - exitmsg = 'Interactive shell closed'
  2135. -
  2136. - console = InteractiveShellEmbed(banner1=banner1, banner2=banner2,
  2137. - exit_msg=exitmsg)
  2138. -
  2139. - bot.memory['iconsole_running'] = True
  2140. - bot.say('console started')
  2141. - console()
  2142. - bot.memory['iconsole_running'] = False
  2143. -
  2144. - # Restore stderr/stdout wrappers
  2145. - sys.stdout = old_stdout
  2146. - sys.stderr = old_stderr
  2147. diff --git a/sopel/modules/isup.py b/sopel/modules/isup.py
  2148. deleted file mode 100644
  2149. index d0c939d..0000000
  2150. --- a/sopel/modules/isup.py
  2151. +++ /dev/null
  2152. @@ -1,36 +0,0 @@
  2153. -# coding=utf-8
  2154. -"""Simple website status check with isup.me"""
  2155. -# Author: Elsie Powell http://embolalia.com
  2156. -from __future__ import unicode_literals, absolute_import, print_function, division
  2157. -
  2158. -from sopel import web
  2159. -from sopel.module import commands
  2160. -
  2161. -
  2162. -@commands('isup')
  2163. -def isup(bot, trigger):
  2164. - """isup.me website status checker"""
  2165. - site = trigger.group(2)
  2166. - if not site:
  2167. - return bot.reply("What site do you want to check?")
  2168. -
  2169. - if site[:7] != 'http://' and site[:8] != 'https://':
  2170. - if '://' in site:
  2171. - protocol = site.split('://')[0] + '://'
  2172. - return bot.reply("Try it again without the %s" % protocol)
  2173. - else:
  2174. - site = 'http://' + site
  2175. -
  2176. - if not '.' in site:
  2177. - site += ".com"
  2178. -
  2179. - try:
  2180. - response = web.get(site)
  2181. - except Exception:
  2182. - bot.say(site + ' looks down from here.')
  2183. - return
  2184. -
  2185. - if response:
  2186. - bot.say(site + ' looks fine to me.')
  2187. - else:
  2188. - bot.say(site + ' is down from here.')
  2189. diff --git a/sopel/modules/lmgtfy.py b/sopel/modules/lmgtfy.py
  2190. deleted file mode 100644
  2191. index 7d0caeb..0000000
  2192. --- a/sopel/modules/lmgtfy.py
  2193. +++ /dev/null
  2194. @@ -1,19 +0,0 @@
  2195. -# coding=utf-8
  2196. -"""
  2197. -lmgtfy.py - Sopel Let me Google that for you module
  2198. -Copyright 2013, Dimitri Molenaars http://tyrope.nl/
  2199. -Licensed under the Eiffel Forum License 2.
  2200. -
  2201. -http://sopel.chat/
  2202. -"""
  2203. -from __future__ import unicode_literals, absolute_import, print_function, division
  2204. -from sopel.module import commands
  2205. -
  2206. -
  2207. -@commands('lmgtfy', 'lmgify', 'gify', 'gtfy')
  2208. -def googleit(bot, trigger):
  2209. - """Let me just... google that for you."""
  2210. - #No input
  2211. - if not trigger.group(2):
  2212. - return bot.say('http://google.com/')
  2213. - bot.say('http://lmgtfy.com/?q=' + trigger.group(2).replace(' ', '+'))
  2214. diff --git a/sopel/modules/meetbot.py b/sopel/modules/meetbot.py
  2215. deleted file mode 100644
  2216. index f0c007f..0000000
  2217. --- a/sopel/modules/meetbot.py
  2218. +++ /dev/null
  2219. @@ -1,436 +0,0 @@
  2220. -# coding=utf-8
  2221. -"""
  2222. -meetbot.py - Sopel meeting logger module
  2223. -Copyright © 2012, Elad Alfassa, <elad@fedoraproject.org>
  2224. -Licensed under the Eiffel Forum License 2.
  2225. -
  2226. -This module is an attempt to implement at least some of the functionallity of Debian's meetbot
  2227. -"""
  2228. -from __future__ import unicode_literals, absolute_import, print_function, division
  2229. -import time
  2230. -import os
  2231. -from sopel.config.types import (
  2232. - StaticSection, FilenameAttribute, ValidatedAttribute
  2233. -)
  2234. -from sopel.web import quote
  2235. -from sopel.modules.url import find_title
  2236. -from sopel.module import example, commands, rule, priority
  2237. -from sopel.tools import Ddict, Identifier
  2238. -import codecs
  2239. -
  2240. -
  2241. -class MeetbotSection(StaticSection):
  2242. - meeting_log_path = FilenameAttribute('meeting_log_path', directory=True,
  2243. - default='~/www/meetings')
  2244. - """Path to meeting logs storage directory
  2245. -
  2246. - This should be an absolute path, accessible on a webserver."""
  2247. - meeting_log_baseurl = ValidatedAttribute(
  2248. - 'meeting_log_baseurl',
  2249. - default='http://localhost/~sopel/meetings'
  2250. - )
  2251. - """Base URL for the meeting logs directory"""
  2252. -
  2253. -
  2254. -def configure(config):
  2255. - config.define_section('meetbot', MeetbotSection)
  2256. - config.meetbot.configure_setting(
  2257. - 'meeting_log_path',
  2258. - 'Enter the directory to store logs in.'
  2259. - )
  2260. - config.meetbot.configure_setting(
  2261. - 'meeting_log_baseurl',
  2262. - 'Enter the base URL for the meeting logs.',
  2263. - )
  2264. -
  2265. -
  2266. -def setup(bot):
  2267. - bot.config.define_section('meetbot', MeetbotSection)
  2268. -
  2269. -
  2270. -meetings_dict = Ddict(dict) # Saves metadata about currently running meetings
  2271. -"""
  2272. -meetings_dict is a 2D dict.
  2273. -
  2274. -Each meeting should have:
  2275. -channel
  2276. -time of start
  2277. -head (can stop the meeting, plus all abilities of chairs)
  2278. -chairs (can add infolines to the logs)
  2279. -title
  2280. -current subject
  2281. -comments (what people who aren't voiced want to add)
  2282. -
  2283. -Using channel as the meeting ID as there can't be more than one meeting in a channel at the same time.
  2284. -"""
  2285. -meeting_log_path = '' # To be defined on meeting start as part of sanity checks, used by logging functions so we don't have to pass them bot
  2286. -meeting_log_baseurl = '' # To be defined on meeting start as part of sanity checks, used by logging functions so we don't have to pass them bot
  2287. -meeting_actions = {} # A dict of channels to the actions that have been created in them. This way we can have .listactions spit them back out later on.
  2288. -
  2289. -
  2290. -#Get the logfile name for the meeting in the requested channel
  2291. -#Used by all logging functions
  2292. -def figure_logfile_name(channel):
  2293. - if meetings_dict[channel]['title'] is 'Untitled meeting':
  2294. - name = 'untitled'
  2295. - else:
  2296. - name = meetings_dict[channel]['title']
  2297. - # Real simple sluggifying. This bunch of characters isn't exhaustive, but
  2298. - # whatever. It's close enough for most situations, I think.
  2299. - for c in ' ./\\:*?"<>|&*`':
  2300. - name = name.replace(c, '-')
  2301. - timestring = time.strftime('%Y-%m-%d-%H:%M', time.gmtime(meetings_dict[channel]['start']))
  2302. - filename = timestring + '_' + name
  2303. - return filename
  2304. -
  2305. -
  2306. -#Start HTML log
  2307. -def logHTML_start(channel):
  2308. - logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
  2309. - timestring = time.strftime('%Y-%m-%d %H:%M', time.gmtime(meetings_dict[channel]['start']))
  2310. - title = '%s at %s, %s' % (meetings_dict[channel]['title'], channel, timestring)
  2311. - logfile.write('<!doctype html>\n<html>\n<head>\n<meta charset="utf-8">\n<title>%TITLE%</title>\n</head>\n<body>\n<h1>%TITLE%</h1>\n'.replace('%TITLE%', title))
  2312. - logfile.write('<h4>Meeting started by %s</h4><ul>\n' % meetings_dict[channel]['head'])
  2313. - logfile.close()
  2314. -
  2315. -
  2316. -#Write a list item in the HTML log
  2317. -def logHTML_listitem(item, channel):
  2318. - logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
  2319. - logfile.write('<li>' + item + '</li>\n')
  2320. - logfile.close()
  2321. -
  2322. -
  2323. -#End the HTML log
  2324. -def logHTML_end(channel):
  2325. - logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
  2326. - current_time = time.strftime('%H:%M:%S', time.gmtime())
  2327. - logfile.write('</ul>\n<h4>Meeting ended at %s UTC</h4>\n' % current_time)
  2328. - plainlog_url = meeting_log_baseurl + quote(channel + '/' + figure_logfile_name(channel) + '.log')
  2329. - logfile.write('<a href="%s">Full log</a>' % plainlog_url)
  2330. - logfile.write('\n</body>\n</html>')
  2331. - logfile.close()
  2332. -
  2333. -
  2334. -#Write a string to the plain text log
  2335. -def logplain(item, channel):
  2336. - current_time = time.strftime('%H:%M:%S', time.gmtime())
  2337. - logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.log', 'a', encoding='utf-8')
  2338. - logfile.write('[' + current_time + '] ' + item + '\r\n')
  2339. - logfile.close()
  2340. -
  2341. -
  2342. -#Check if a meeting is currently running
  2343. -def ismeetingrunning(channel):
  2344. - try:
  2345. - if meetings_dict[channel]['running']:
  2346. - return True
  2347. - else:
  2348. - return False
  2349. - except:
  2350. - return False
  2351. -
  2352. -
  2353. -#Check if nick is a chair or head of the meeting
  2354. -def ischair(nick, channel):
  2355. - try:
  2356. - if nick.lower() == meetings_dict[channel]['head'] or nick.lower() in meetings_dict[channel]['chairs']:
  2357. - return True
  2358. - else:
  2359. - return False
  2360. - except:
  2361. - return False
  2362. -
  2363. -
  2364. -#Start meeting (also preforms all required sanity checks)
  2365. -@commands('startmeeting')
  2366. -@example('.startmeeting title or .startmeeting')
  2367. -def startmeeting(bot, trigger):
  2368. - """
  2369. - Start a meeting.
  2370. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2371. - """
  2372. - if ismeetingrunning(trigger.sender):
  2373. - bot.say('Can\'t do that, there is already a meeting in progress here!')
  2374. - return
  2375. - if trigger.is_privmsg:
  2376. - bot.say('Can only start meetings in channels')
  2377. - return
  2378. - #Start the meeting
  2379. - meetings_dict[trigger.sender]['start'] = time.time()
  2380. - if not trigger.group(2):
  2381. - meetings_dict[trigger.sender]['title'] = 'Untitled meeting'
  2382. - else:
  2383. - meetings_dict[trigger.sender]['title'] = trigger.group(2)
  2384. - meetings_dict[trigger.sender]['head'] = trigger.nick.lower()
  2385. - meetings_dict[trigger.sender]['running'] = True
  2386. - meetings_dict[trigger.sender]['comments'] = []
  2387. -
  2388. - global meeting_log_path
  2389. - meeting_log_path = bot.config.meetbot.meeting_log_path
  2390. - if not meeting_log_path.endswith('/'):
  2391. - meeting_log_path = meeting_log_path + '/'
  2392. - global meeting_log_baseurl
  2393. - meeting_log_baseurl = bot.config.meetbot.meeting_log_baseurl
  2394. - if not meeting_log_baseurl.endswith('/'):
  2395. - meeting_log_baseurl = meeting_log_baseurl + '/'
  2396. - if not os.path.isdir(meeting_log_path + trigger.sender):
  2397. - try:
  2398. - os.makedirs(meeting_log_path + trigger.sender)
  2399. - except Exception:
  2400. - bot.say("Can't create log directory for this channel, meeting not started!")
  2401. - meetings_dict[trigger.sender] = Ddict(dict)
  2402. - raise
  2403. - return
  2404. - #Okay, meeting started!
  2405. - logplain('Meeting started by ' + trigger.nick.lower(), trigger.sender)
  2406. - logHTML_start(trigger.sender)
  2407. - meeting_actions[trigger.sender] = []
  2408. - bot.say('Meeting started! use .action, .agreed, .info, .chairs, .subject and .comments to control the meeting. to end the meeting, type .endmeeting')
  2409. - bot.say('Users without speaking permission can use .comment ' +
  2410. - trigger.sender + ' followed by their comment in a PM with me to '
  2411. - 'vocalize themselves.')
  2412. -
  2413. -
  2414. -#Change the current subject (will appear as <h3> in the HTML log)
  2415. -@commands('subject')
  2416. -@example('.subject roll call')
  2417. -def meetingsubject(bot, trigger):
  2418. - """
  2419. - Change the meeting subject.
  2420. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2421. - """
  2422. - if not ismeetingrunning(trigger.sender):
  2423. - bot.say('Can\'t do that, start meeting first')
  2424. - return
  2425. - if not trigger.group(2):
  2426. - bot.say('what is the subject?')
  2427. - return
  2428. - if not ischair(trigger.nick, trigger.sender):
  2429. - bot.say('Only meeting head or chairs can do that')
  2430. - return
  2431. - meetings_dict[trigger.sender]['current_subject'] = trigger.group(2)
  2432. - logfile = codecs.open(meeting_log_path + trigger.sender + '/' + figure_logfile_name(trigger.sender) + '.html', 'a', encoding='utf-8')
  2433. - logfile.write('</ul><h3>' + trigger.group(2) + '</h3><ul>')
  2434. - logfile.close()
  2435. - logplain('Current subject: ' + trigger.group(2) + ', (set by ' + trigger.nick + ')', trigger.sender)
  2436. - bot.say('Current subject: ' + trigger.group(2))
  2437. -
  2438. -
  2439. -#End the meeting
  2440. -@commands('endmeeting')
  2441. -@example('.endmeeting')
  2442. -def endmeeting(bot, trigger):
  2443. - """
  2444. - End a meeting.
  2445. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2446. - """
  2447. - if not ismeetingrunning(trigger.sender):
  2448. - bot.say('Can\'t do that, start meeting first')
  2449. - return
  2450. - if not ischair(trigger.nick, trigger.sender):
  2451. - bot.say('Only meeting head or chairs can do that')
  2452. - return
  2453. - meeting_length = time.time() - meetings_dict[trigger.sender]['start']
  2454. - #TODO: Humanize time output
  2455. - bot.say("Meeting ended! total meeting length %d seconds" % meeting_length)
  2456. - logHTML_end(trigger.sender)
  2457. - htmllog_url = meeting_log_baseurl + quote(trigger.sender + '/' + figure_logfile_name(trigger.sender) + '.html')
  2458. - logplain('Meeting ended by %s, total meeting length %d seconds' % (trigger.nick, meeting_length), trigger.sender)
  2459. - bot.say('Meeting minutes: ' + htmllog_url)
  2460. - meetings_dict[trigger.sender] = Ddict(dict)
  2461. - del meeting_actions[trigger.sender]
  2462. -
  2463. -
  2464. -#Set meeting chairs (people who can control the meeting)
  2465. -@commands('chairs')
  2466. -@example('.chairs Tyrope Jason elad')
  2467. -def chairs(bot, trigger):
  2468. - """
  2469. - Set the meeting chairs.
  2470. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2471. - """
  2472. - if not ismeetingrunning(trigger.sender):
  2473. - bot.say('Can\'t do that, start meeting first')
  2474. - return
  2475. - if not trigger.group(2):
  2476. - bot.say('Who are the chairs?')
  2477. - return
  2478. - if trigger.nick.lower() == meetings_dict[trigger.sender]['head']:
  2479. - meetings_dict[trigger.sender]['chairs'] = trigger.group(2).lower().split(' ')
  2480. - chairs_readable = trigger.group(2).lower().replace(' ', ', ')
  2481. - logplain('Meeting chairs are: ' + chairs_readable, trigger.sender)
  2482. - logHTML_listitem('<span style="font-weight: bold">Meeting chairs are: </span>' + chairs_readable, trigger.sender)
  2483. - bot.say('Meeting chairs are: ' + chairs_readable)
  2484. - else:
  2485. - bot.say("Only meeting head can set chairs")
  2486. -
  2487. -
  2488. -#Log action item in the HTML log
  2489. -@commands('action')
  2490. -@example('.action elad will develop a meetbot')
  2491. -def meetingaction(bot, trigger):
  2492. - """
  2493. - Log an action in the meeting log
  2494. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2495. - """
  2496. - if not ismeetingrunning(trigger.sender):
  2497. - bot.say('Can\'t do that, start meeting first')
  2498. - return
  2499. - if not trigger.group(2):
  2500. - bot.say('try .action someone will do something')
  2501. - return
  2502. - if not ischair(trigger.nick, trigger.sender):
  2503. - bot.say('Only meeting head or chairs can do that')
  2504. - return
  2505. - logplain('ACTION: ' + trigger.group(2), trigger.sender)
  2506. - logHTML_listitem('<span style="font-weight: bold">Action: </span>' + trigger.group(2), trigger.sender)
  2507. - meeting_actions[trigger.sender].append(trigger.group(2))
  2508. - bot.say('ACTION: ' + trigger.group(2))
  2509. -
  2510. -
  2511. -@commands('listactions')
  2512. -@example('.listactions')
  2513. -def listactions(bot, trigger):
  2514. - if not ismeetingrunning(trigger.sender):
  2515. - bot.say('Can\'t do that, start meeting first')
  2516. - return
  2517. - for action in meeting_actions[trigger.sender]:
  2518. - bot.say('ACTION: ' + action)
  2519. -
  2520. -
  2521. -#Log agreed item in the HTML log
  2522. -@commands('agreed')
  2523. -@example('.agreed Bowties are cool')
  2524. -def meetingagreed(bot, trigger):
  2525. - """
  2526. - Log an agreement in the meeting log.
  2527. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2528. - """
  2529. - if not ismeetingrunning(trigger.sender):
  2530. - bot.say('Can\'t do that, start meeting first')
  2531. - return
  2532. - if not trigger.group(2):
  2533. - bot.say('try .action someone will do something')
  2534. - return
  2535. - if not ischair(trigger.nick, trigger.sender):
  2536. - bot.say('Only meeting head or chairs can do that')
  2537. - return
  2538. - logplain('AGREED: ' + trigger.group(2), trigger.sender)
  2539. - logHTML_listitem('<span style="font-weight: bold">Agreed: </span>' + trigger.group(2), trigger.sender)
  2540. - bot.say('AGREED: ' + trigger.group(2))
  2541. -
  2542. -
  2543. -#Log link item in the HTML log
  2544. -@commands('link')
  2545. -@example('.link http://example.com')
  2546. -def meetinglink(bot, trigger):
  2547. - """
  2548. - Log a link in the meeing log.
  2549. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2550. - """
  2551. - if not ismeetingrunning(trigger.sender):
  2552. - bot.say('Can\'t do that, start meeting first')
  2553. - return
  2554. - if not trigger.group(2):
  2555. - bot.say('try .action someone will do something')
  2556. - return
  2557. - if not ischair(trigger.nick, trigger.sender):
  2558. - bot.say('Only meeting head or chairs can do that')
  2559. - return
  2560. - link = trigger.group(2)
  2561. - if not link.startswith("http"):
  2562. - link = "http://" + link
  2563. - try:
  2564. - title = find_title(link)
  2565. - except:
  2566. - title = ''
  2567. - logplain('LINK: %s [%s]' % (link, title), trigger.sender)
  2568. - logHTML_listitem('<a href="%s">%s</a>' % (link, title), trigger.sender)
  2569. - bot.say('LINK: ' + link)
  2570. -
  2571. -
  2572. -#Log informational item in the HTML log
  2573. -@commands('info')
  2574. -@example('.info all board members present')
  2575. -def meetinginfo(bot, trigger):
  2576. - """
  2577. - Log an informational item in the meeting log
  2578. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2579. - """
  2580. - if not ismeetingrunning(trigger.sender):
  2581. - bot.say('Can\'t do that, start meeting first')
  2582. - return
  2583. - if not trigger.group(2):
  2584. - bot.say('try .info some informative thing')
  2585. - return
  2586. - if not ischair(trigger.nick, trigger.sender):
  2587. - bot.say('Only meeting head or chairs can do that')
  2588. - return
  2589. - logplain('INFO: ' + trigger.group(2), trigger.sender)
  2590. - logHTML_listitem(trigger.group(2), trigger.sender)
  2591. - bot.say('INFO: ' + trigger.group(2))
  2592. -
  2593. -
  2594. -#called for every single message
  2595. -#Will log to plain text only
  2596. -@rule('(.*)')
  2597. -@priority('low')
  2598. -def log_meeting(bot, trigger):
  2599. - if not ismeetingrunning(trigger.sender):
  2600. - return
  2601. - if trigger.startswith('.endmeeting') or trigger.startswith('.chairs') or trigger.startswith('.action') or trigger.startswith('.info') or trigger.startswith('.startmeeting') or trigger.startswith('.agreed') or trigger.startswith('.link') or trigger.startswith('.subject'):
  2602. - return
  2603. - logplain('<' + trigger.nick + '> ' + trigger, trigger.sender)
  2604. -
  2605. -
  2606. -@commands('comment')
  2607. -def take_comment(bot, trigger):
  2608. - """
  2609. - Log a comment, to be shown with other comments when a chair uses .comments.
  2610. - Intended to allow commentary from those outside the primary group of people
  2611. - in the meeting.
  2612. -
  2613. - Used in private message only, as `.comment <#channel> <comment to add>`
  2614. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2615. - """
  2616. - if not trigger.sender.is_nick():
  2617. - return
  2618. - if not trigger.group(4): # <2 arguements were given
  2619. - bot.say('Usage: .comment <#channel> <comment to add>')
  2620. - return
  2621. -
  2622. - target, message = trigger.group(2).split(None, 1)
  2623. - target = Identifier(target)
  2624. - if not ismeetingrunning(target):
  2625. - bot.say("There's not currently a meeting in that channel.")
  2626. - else:
  2627. - meetings_dict[trigger.group(3)]['comments'].append((trigger.nick, message))
  2628. - bot.say("Your comment has been recorded. It will be shown when the"
  2629. - " chairs tell me to show the comments.")
  2630. - bot.msg(meetings_dict[trigger.group(3)]['head'], "A new comment has been recorded.")
  2631. -
  2632. -
  2633. -@commands('comments')
  2634. -def show_comments(bot, trigger):
  2635. - """
  2636. - Show the comments that have been logged for this meeting with .comment.
  2637. - https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
  2638. - """
  2639. - if not ismeetingrunning(trigger.sender):
  2640. - return
  2641. - if not ischair(trigger.nick, trigger.sender):
  2642. - bot.say('Only meeting head or chairs can do that')
  2643. - return
  2644. - comments = meetings_dict[trigger.sender]['comments']
  2645. - if comments:
  2646. - msg = 'The following comments were made:'
  2647. - bot.say(msg)
  2648. - logplain('<%s> %s' % (bot.nick, msg), trigger.sender)
  2649. - for comment in comments:
  2650. - msg = '<%s> %s' % comment
  2651. - bot.say(msg)
  2652. - logplain('<%s> %s' % (bot.nick, msg), trigger.sender)
  2653. - meetings_dict[trigger.sender]['comments'] = []
  2654. - else:
  2655. - bot.say('No comments have been logged.')
  2656. diff --git a/sopel/modules/movie.py b/sopel/modules/movie.py
  2657. deleted file mode 100644
  2658. index 8469f53..0000000
  2659. --- a/sopel/modules/movie.py
  2660. +++ /dev/null
  2661. @@ -1,48 +0,0 @@
  2662. -# coding=utf-8
  2663. -"""
  2664. -imdb.py - Sopel Movie Information Module
  2665. -Copyright © 2012-2013, Elad Alfassa, <elad@fedoraproject.org>
  2666. -Licensed under the Eiffel Forum License 2.
  2667. -
  2668. -This module relies on omdbapi.com
  2669. -"""
  2670. -from __future__ import unicode_literals, absolute_import, print_function, division
  2671. -import requests
  2672. -import sopel.module
  2673. -from sopel.logger import get_logger
  2674. -
  2675. -LOGGER = get_logger(__name__)
  2676. -
  2677. -
  2678. -@sopel.module.commands('movie', 'imdb')
  2679. -@sopel.module.example('.movie ThisTitleDoesNotExist', '[MOVIE] Movie not found!')
  2680. -@sopel.module.example('.movie Citizen Kane', '[MOVIE] Title: Citizen Kane | Year: 1941 | Rating: 8.4 | Genre: Drama, Mystery | IMDB Link: http://imdb.com/title/tt0033467')
  2681. -def movie(bot, trigger):
  2682. - """
  2683. - Returns some information about a movie, like Title, Year, Rating, Genre and IMDB Link.
  2684. - """
  2685. - if not trigger.group(2):
  2686. - return
  2687. - word = trigger.group(2).rstrip()
  2688. - uri = "http://www.omdbapi.com/"
  2689. - data = requests.get(uri, params={'t': word}, timeout=30).json()
  2690. - if data['Response'] == 'False':
  2691. - if 'Error' in data:
  2692. - message = '[MOVIE] %s' % data['Error']
  2693. - else:
  2694. - LOGGER.warning(
  2695. - 'Got an error from the OMDb api, search phrase was %s; data was %s',
  2696. - word, str(data))
  2697. - message = '[MOVIE] Got an error from OMDbapi'
  2698. - else:
  2699. - message = '[MOVIE] Title: ' + data['Title'] + \
  2700. - ' | Year: ' + data['Year'] + \
  2701. - ' | Rating: ' + data['imdbRating'] + \
  2702. - ' | Genre: ' + data['Genre'] + \
  2703. - ' | IMDB Link: http://imdb.com/title/' + data['imdbID']
  2704. - bot.say(message)
  2705. -
  2706. -
  2707. -if __name__ == "__main__":
  2708. - from sopel.test_tools import run_example_tests
  2709. - run_example_tests(__file__)
  2710. diff --git a/sopel/modules/ping.py b/sopel/modules/ping.py
  2711. deleted file mode 100644
  2712. index ee9c3d2..0000000
  2713. --- a/sopel/modules/ping.py
  2714. +++ /dev/null
  2715. @@ -1,29 +0,0 @@
  2716. -# coding=utf-8
  2717. -"""
  2718. -ping.py - Sopel Ping Module
  2719. -Author: Sean B. Palmer, inamidst.com
  2720. -About: http://sopel.chat
  2721. -"""
  2722. -from __future__ import unicode_literals, absolute_import, print_function, division
  2723. -
  2724. -import random
  2725. -from sopel.module import rule, priority, thread
  2726. -
  2727. -
  2728. -@rule(r'(?i)(hi|hello|hey),? $nickname[ \t]*$')
  2729. -def hello(bot, trigger):
  2730. - greeting = random.choice(('Hi', 'Hey', 'Hello'))
  2731. - punctuation = random.choice(('', '!'))
  2732. - bot.say(greeting + ' ' + trigger.nick + punctuation)
  2733. -
  2734. -
  2735. -@rule(r'(?i)(Fuck|Screw) you,? $nickname[ \t]*$')
  2736. -def rude(bot, trigger):
  2737. - bot.say('Watch your mouth, ' + trigger.nick + ', or I\'ll tell your mother!')
  2738. -
  2739. -
  2740. -@rule('$nickname!')
  2741. -@priority('high')
  2742. -@thread(False)
  2743. -def interjection(bot, trigger):
  2744. - bot.say(trigger.nick + '!')
  2745. diff --git a/sopel/modules/rand.py b/sopel/modules/rand.py
  2746. deleted file mode 100644
  2747. index b9b0a6e..0000000
  2748. --- a/sopel/modules/rand.py
  2749. +++ /dev/null
  2750. @@ -1,49 +0,0 @@
  2751. -# coding=utf-8
  2752. -"""
  2753. -rand.py - Rand Module
  2754. -Copyright 2013, Ari Koivula, <ari@koivu.la>
  2755. -Licensed under the Eiffel Forum License 2.
  2756. -
  2757. -http://sopel.chat
  2758. -"""
  2759. -from __future__ import unicode_literals, absolute_import, print_function, division
  2760. -
  2761. -from sopel.module import commands, example
  2762. -import random
  2763. -import sys
  2764. -
  2765. -
  2766. -@commands('rand')
  2767. -@example('.rand 2', r'random\(0, 2\) = (0|1|2)', re=True, repeat=10)
  2768. -@example('.rand -1 -1', 'random(-1, -1) = -1')
  2769. -@example('.rand', r'random\(0, \d+\) = \d+', re=True)
  2770. -@example('.rand 99 10', r'random\(10, 99\) = \d\d', re=True, repeat=10)
  2771. -@example('.rand 10 99', r'random\(10, 99\) = \d\d', re=True, repeat=10)
  2772. -def rand(bot, trigger):
  2773. - """Replies with a random number between first and second argument."""
  2774. - arg1 = trigger.group(3)
  2775. - arg2 = trigger.group(4)
  2776. -
  2777. - try:
  2778. - if arg2 is not None:
  2779. - low = int(arg1)
  2780. - high = int(arg2)
  2781. - elif arg1 is not None:
  2782. - low = 0
  2783. - high = int(arg1)
  2784. - else:
  2785. - low = 0
  2786. - high = sys.maxsize
  2787. - except (ValueError, TypeError):
  2788. - return bot.reply("Arguments must be of integer type")
  2789. -
  2790. - if low > high:
  2791. - low, high = high, low
  2792. -
  2793. - number = random.randint(low, high)
  2794. - bot.reply("random(%d, %d) = %d" % (low, high, number))
  2795. -
  2796. -
  2797. -if __name__ == "__main__":
  2798. - from sopel.test_tools import run_example_tests
  2799. - run_example_tests(__file__)
  2800. diff --git a/sopel/modules/reddit.py b/sopel/modules/reddit.py
  2801. deleted file mode 100644
  2802. index f5f79e0..0000000
  2803. --- a/sopel/modules/reddit.py
  2804. +++ /dev/null
  2805. @@ -1,191 +0,0 @@
  2806. -# coding=utf-8
  2807. -# Author: Elsie Powell, embolalia.com
  2808. -from __future__ import unicode_literals, absolute_import, print_function, division
  2809. -
  2810. -from sopel.module import commands, rule, example, require_chanmsg, NOLIMIT, OP
  2811. -from sopel.formatting import bold, color, colors
  2812. -from sopel.web import USER_AGENT
  2813. -from sopel.tools import SopelMemory, time
  2814. -import datetime as dt
  2815. -import praw
  2816. -import re
  2817. -import sys
  2818. -if sys.version_info.major >= 3:
  2819. - unicode = str
  2820. - if sys.version_info.minor >= 4:
  2821. - from html import unescape
  2822. - else:
  2823. - from html.parser import HTMLParser
  2824. - unescape = HTMLParser().unescape
  2825. -else:
  2826. - from HTMLParser import HTMLParser
  2827. - unescape = HTMLParser().unescape
  2828. -
  2829. -
  2830. -domain = r'https?://(?:www\.|np\.)?reddit\.com'
  2831. -post_url = '%s/r/.*?/comments/([\w-]+)' % domain
  2832. -user_url = '%s/u(ser)?/([\w-]+)' % domain
  2833. -post_regex = re.compile(post_url)
  2834. -user_regex = re.compile(user_url)
  2835. -
  2836. -
  2837. -def setup(bot):
  2838. - if not bot.memory.contains('url_callbacks'):
  2839. - bot.memory['url_callbacks'] = SopelMemory()
  2840. - bot.memory['url_callbacks'][post_regex] = rpost_info
  2841. - bot.memory['url_callbacks'][user_regex] = redditor_info
  2842. -
  2843. -
  2844. -def shutdown(bot):
  2845. - del bot.memory['url_callbacks'][post_regex]
  2846. - del bot.memory['url_callbacks'][user_regex]
  2847. -
  2848. -
  2849. -@rule('.*%s.*' % post_url)
  2850. -def rpost_info(bot, trigger, match=None):
  2851. - r = praw.Reddit(user_agent=USER_AGENT)
  2852. - match = match or trigger
  2853. - s = r.get_submission(submission_id=match.group(1))
  2854. -
  2855. - message = ('[REDDIT] {title} {link}{nsfw} | {points} points ({percent}) | '
  2856. - '{comments} comments | Posted by {author} | '
  2857. - 'Created at {created}')
  2858. -
  2859. - if s.is_self:
  2860. - link = '(self.{})'.format(s.subreddit.display_name)
  2861. - else:
  2862. - link = '({}) to r/{}'.format(s.url, s.subreddit.display_name)
  2863. -
  2864. - if s.over_18:
  2865. - nsfw = bold(color(' [NSFW]', colors.RED))
  2866. - sfw = bot.db.get_channel_value(trigger.sender, 'sfw')
  2867. - if sfw:
  2868. - link = '(link hidden)'
  2869. - bot.write(['KICK', trigger.sender, trigger.nick,
  2870. - 'Linking to NSFW content in a SFW channel.'])
  2871. - else:
  2872. - nsfw = ''
  2873. -
  2874. - if s.author:
  2875. - author = s.author.name
  2876. - else:
  2877. - author = '[deleted]'
  2878. -
  2879. - tz = time.get_timezone(bot.db, bot.config, None, trigger.nick,
  2880. - trigger.sender)
  2881. - time_created = dt.datetime.utcfromtimestamp(s.created_utc)
  2882. - created = time.format_time(bot.db, bot.config, tz, trigger.nick,
  2883. - trigger.sender, time_created)
  2884. -
  2885. - if s.score > 0:
  2886. - point_color = colors.GREEN
  2887. - else:
  2888. - point_color = colors.RED
  2889. -
  2890. - percent = color(unicode(s.upvote_ratio * 100) + '%', point_color)
  2891. -
  2892. - title = unescape(s.title)
  2893. - message = message.format(
  2894. - title=title, link=link, nsfw=nsfw, points=s.score, percent=percent,
  2895. - comments=s.num_comments, author=author, created=created)
  2896. -
  2897. - bot.say(message)
  2898. -
  2899. -
  2900. -# If you change this, you'll have to change some other things...
  2901. -@commands('redditor')
  2902. -@example('.redditor poem_for_your_sprog')
  2903. -def redditor_info(bot, trigger, match=None):
  2904. - """Show information about the given Redditor"""
  2905. - commanded = re.match(bot.config.core.prefix + 'redditor', trigger)
  2906. - r = praw.Reddit(user_agent=USER_AGENT)
  2907. - match = match or trigger
  2908. - try:
  2909. - u = r.get_redditor(match.group(2))
  2910. - except:
  2911. - if commanded:
  2912. - bot.say('No such Redditor.')
  2913. - return NOLIMIT
  2914. - else:
  2915. - return
  2916. - # Fail silently if it wasn't an explicit command.
  2917. -
  2918. - message = '[REDDITOR] ' + u.name
  2919. - now = dt.datetime.utcnow()
  2920. - cakeday_start = dt.datetime.utcfromtimestamp(u.created_utc)
  2921. - cakeday_start = cakeday_start.replace(year=now.year)
  2922. - day = dt.timedelta(days=1)
  2923. - year_div_by_400 = now.year % 400 == 0
  2924. - year_div_by_100 = now.year % 100 == 0
  2925. - year_div_by_4 = now.year % 4 == 0
  2926. - is_leap = year_div_by_400 or ((not year_div_by_100) and year_div_by_4)
  2927. - if (not is_leap) and ((cakeday_start.month, cakeday_start.day) == (2, 29)):
  2928. - # If cake day is 2/29 and it's not a leap year, cake day is 1/3.
  2929. - # Cake day begins at exact account creation time.
  2930. - is_cakeday = cakeday_start + day <= now <= cakeday_start + (2 * day)
  2931. - else:
  2932. - is_cakeday = cakeday_start <= now <= cakeday_start + day
  2933. -
  2934. - if is_cakeday:
  2935. - message = message + ' | 13Cake day'
  2936. - if commanded:
  2937. - message = message + ' | http://reddit.com/u/' + u.name
  2938. - if u.is_gold:
  2939. - message = message + ' | 08Gold'
  2940. - if u.is_mod:
  2941. - message = message + ' | 05Mod'
  2942. - message = message + (' | Link: ' + str(u.link_karma) + ' | Comment: '
  2943. - + str(u.comment_karma))
  2944. -
  2945. - bot.say(message)
  2946. -
  2947. -
  2948. -# If you change the groups here, you'll have to change some things above.
  2949. -@rule('.*%s.*' % user_url)
  2950. -def auto_redditor_info(bot, trigger):
  2951. - redditor_info(bot, trigger)
  2952. -
  2953. -
  2954. -@require_chanmsg('.setsfw is only permitted in channels')
  2955. -@commands('setsafeforwork', 'setsfw')
  2956. -@example('.setsfw true')
  2957. -@example('.setsfw false')
  2958. -def update_channel(bot, trigger):
  2959. - """
  2960. - Sets the Safe for Work status (true or false) for the current
  2961. - channel. Defaults to false.
  2962. - """
  2963. - if bot.privileges[trigger.sender][trigger.nick] < OP:
  2964. - return
  2965. - else:
  2966. - param = 'true'
  2967. - if trigger.group(2) and trigger.group(3):
  2968. - param = trigger.group(3).strip().lower()
  2969. - sfw = param == 'true'
  2970. - bot.db.set_channel_value(trigger.sender, 'sfw', sfw)
  2971. - if sfw:
  2972. - bot.reply('Got it. %s is now flagged as SFW.' % trigger.sender)
  2973. - else:
  2974. - bot.reply('Got it. %s is now flagged as NSFW.' % trigger.sender)
  2975. -
  2976. -
  2977. -@commands('getsafeforwork', 'getsfw')
  2978. -@example('.getsfw [channel]')
  2979. -def get_channel_sfw(bot, trigger):
  2980. - """
  2981. - Gets the preferred channel's Safe for Work status, or the current
  2982. - channel's status if no channel given.
  2983. - """
  2984. - channel = trigger.group(2)
  2985. - if not channel:
  2986. - channel = trigger.sender
  2987. - if channel.is_nick():
  2988. - return bot.say('.getsfw with no channel param is only permitted in channels')
  2989. -
  2990. - channel = channel.strip()
  2991. -
  2992. - sfw = bot.db.get_channel_value(channel, 'sfw')
  2993. - if sfw:
  2994. - bot.say('%s is flagged as SFW' % channel)
  2995. - else:
  2996. - bot.say('%s is flagged as NSFW' % channel)
  2997. diff --git a/sopel/modules/reload.py b/sopel/modules/reload.py
  2998. deleted file mode 100644
  2999. index 53bd720..0000000
  3000. --- a/sopel/modules/reload.py
  3001. +++ /dev/null
  3002. @@ -1,139 +0,0 @@
  3003. -# coding=utf-8
  3004. -"""
  3005. -reload.py - Sopel Module Reloader Module
  3006. -Copyright 2008, Sean B. Palmer, inamidst.com
  3007. -Licensed under the Eiffel Forum License 2.
  3008. -
  3009. -http://sopel.chat
  3010. -"""
  3011. -from __future__ import unicode_literals, absolute_import, print_function, division
  3012. -
  3013. -import collections
  3014. -import sys
  3015. -import time
  3016. -from sopel.tools import iteritems
  3017. -import sopel.loader
  3018. -import sopel.module
  3019. -import subprocess
  3020. -
  3021. -
  3022. -@sopel.module.nickname_commands("reload")
  3023. -@sopel.module.priority("low")
  3024. -@sopel.module.thread(False)
  3025. -def f_reload(bot, trigger):
  3026. - """Reloads a module, for use by admins only."""
  3027. - if not trigger.admin:
  3028. - return
  3029. -
  3030. - name = trigger.group(2)
  3031. -
  3032. - if not name or name == '*' or name.upper() == 'ALL THE THINGS':
  3033. - bot._callables = {
  3034. - 'high': collections.defaultdict(list),
  3035. - 'medium': collections.defaultdict(list),
  3036. - 'low': collections.defaultdict(list)
  3037. - }
  3038. - bot._command_groups = collections.defaultdict(list)
  3039. - bot.setup()
  3040. - return bot.reply('done')
  3041. -
  3042. - if name not in sys.modules:
  3043. - return bot.reply('%s: not loaded, try the `load` command' % name)
  3044. -
  3045. - old_module = sys.modules[name]
  3046. -
  3047. - old_callables = {}
  3048. - for obj_name, obj in iteritems(vars(old_module)):
  3049. - bot.unregister(obj)
  3050. -
  3051. - # Also remove all references to sopel callables from top level of the
  3052. - # module, so that they will not get loaded again if reloading the
  3053. - # module does not override them.
  3054. - for obj_name in old_callables.keys():
  3055. - delattr(old_module, obj_name)
  3056. -
  3057. - # Also delete the setup function
  3058. - if hasattr(old_module, "setup"):
  3059. - delattr(old_module, "setup")
  3060. -
  3061. - modules = sopel.loader.enumerate_modules(bot.config)
  3062. - path, type_ = modules[name]
  3063. - load_module(bot, name, path, type_)
  3064. -
  3065. -
  3066. -def load_module(bot, name, path, type_):
  3067. - module, mtime = sopel.loader.load_module(name, path, type_)
  3068. - relevant_parts = sopel.loader.clean_module(module, bot.config)
  3069. -
  3070. - bot.register(*relevant_parts)
  3071. -
  3072. - # TODO sys.modules[name] = module
  3073. - if hasattr(module, 'setup'):
  3074. - module.setup(bot)
  3075. -
  3076. - modified = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(mtime))
  3077. -
  3078. - bot.reply('%r (version: %s)' % (module, modified))
  3079. -
  3080. -
  3081. -@sopel.module.nickname_commands('update')
  3082. -def f_update(bot, trigger):
  3083. - if not trigger.admin:
  3084. - return
  3085. -
  3086. - """Pulls the latest versions of all modules from Git"""
  3087. - proc = subprocess.Popen('/usr/bin/git pull',
  3088. - stdout=subprocess.PIPE,
  3089. - stderr=subprocess.PIPE, shell=True)
  3090. - bot.reply(proc.communicate()[0])
  3091. -
  3092. - f_reload(bot, trigger)
  3093. -
  3094. -
  3095. -@sopel.module.nickname_commands("load")
  3096. -@sopel.module.priority("low")
  3097. -@sopel.module.thread(False)
  3098. -def f_load(bot, trigger):
  3099. - """Loads a module, for use by admins only."""
  3100. - if not trigger.admin:
  3101. - return
  3102. -
  3103. - name = trigger.group(2)
  3104. - path = ''
  3105. - if not name:
  3106. - return bot.reply('Load what?')
  3107. -
  3108. - if name in sys.modules:
  3109. - return bot.reply('Module already loaded, use reload')
  3110. -
  3111. - mods = sopel.loader.enumerate_modules(bot.config)
  3112. - if name not in mods:
  3113. - return bot.reply('Module %s not found' % name)
  3114. - path, type_ = mods[name]
  3115. - load_module(bot, name, path, type_)
  3116. -
  3117. -
  3118. -# Catch PM based messages
  3119. -@sopel.module.commands("reload")
  3120. -@sopel.module.priority("low")
  3121. -@sopel.module.thread(False)
  3122. -def pm_f_reload(bot, trigger):
  3123. - """Wrapper for allowing delivery of .reload command via PM"""
  3124. - if trigger.is_privmsg:
  3125. - f_reload(bot, trigger)
  3126. -
  3127. -
  3128. -@sopel.module.commands('update')
  3129. -def pm_f_update(bot, trigger):
  3130. - """Wrapper for allowing delivery of .update command via PM"""
  3131. - if trigger.is_privmsg:
  3132. - f_update(bot, trigger)
  3133. -
  3134. -
  3135. -@sopel.module.commands("load")
  3136. -@sopel.module.priority("low")
  3137. -@sopel.module.thread(False)
  3138. -def pm_f_load(bot, trigger):
  3139. - """Wrapper for allowing delivery of .load command via PM"""
  3140. - if trigger.is_privmsg:
  3141. - f_load(bot, trigger)
  3142. diff --git a/sopel/modules/remind.py b/sopel/modules/remind.py
  3143. deleted file mode 100644
  3144. index 7f59a57..0000000
  3145. --- a/sopel/modules/remind.py
  3146. +++ /dev/null
  3147. @@ -1,228 +0,0 @@
  3148. -# coding=utf-8
  3149. -"""
  3150. -remind.py - Sopel Reminder Module
  3151. -Copyright 2011, Sean B. Palmer, inamidst.com
  3152. -Licensed under the Eiffel Forum License 2.
  3153. -
  3154. -http://sopel.chat
  3155. -"""
  3156. -from __future__ import unicode_literals, absolute_import, print_function, division
  3157. -
  3158. -import os
  3159. -import re
  3160. -import time
  3161. -import threading
  3162. -import collections
  3163. -import codecs
  3164. -from datetime import datetime
  3165. -from sopel.module import commands, example, NOLIMIT
  3166. -import sopel.tools
  3167. -from sopel.tools.time import get_timezone, format_time
  3168. -
  3169. -try:
  3170. - import pytz
  3171. -except:
  3172. - pytz = None
  3173. -
  3174. -
  3175. -def filename(self):
  3176. - name = self.nick + '-' + self.config.core.host + '.reminders.db'
  3177. - return os.path.join(self.config.core.homedir, name)
  3178. -
  3179. -
  3180. -def load_database(name):
  3181. - data = {}
  3182. - if os.path.isfile(name):
  3183. - f = codecs.open(name, 'r', encoding='utf-8')
  3184. - for line in f:
  3185. - unixtime, channel, nick, message = line.split('\t')
  3186. - message = message.rstrip('\n')
  3187. - t = int(float(unixtime)) # WTFs going on here?
  3188. - reminder = (channel, nick, message)
  3189. - try:
  3190. - data[t].append(reminder)
  3191. - except KeyError:
  3192. - data[t] = [reminder]
  3193. - f.close()
  3194. - return data
  3195. -
  3196. -
  3197. -def dump_database(name, data):
  3198. - f = codecs.open(name, 'w', encoding='utf-8')
  3199. - for unixtime, reminders in sopel.tools.iteritems(data):
  3200. - for channel, nick, message in reminders:
  3201. - f.write('%s\t%s\t%s\t%s\n' % (unixtime, channel, nick, message))
  3202. - f.close()
  3203. -
  3204. -
  3205. -def setup(bot):
  3206. - bot.rfn = filename(bot)
  3207. - bot.rdb = load_database(bot.rfn)
  3208. -
  3209. - def monitor(bot):
  3210. - time.sleep(5)
  3211. - while True:
  3212. - now = int(time.time())
  3213. - unixtimes = [int(key) for key in bot.rdb]
  3214. - oldtimes = [t for t in unixtimes if t <= now]
  3215. - if oldtimes:
  3216. - for oldtime in oldtimes:
  3217. - for (channel, nick, message) in bot.rdb[oldtime]:
  3218. - if message:
  3219. - bot.msg(channel, nick + ': ' + message)
  3220. - else:
  3221. - bot.msg(channel, nick + '!')
  3222. - del bot.rdb[oldtime]
  3223. - dump_database(bot.rfn, bot.rdb)
  3224. - time.sleep(2.5)
  3225. -
  3226. - targs = (bot,)
  3227. - t = threading.Thread(target=monitor, args=targs)
  3228. - t.start()
  3229. -
  3230. -scaling = collections.OrderedDict([
  3231. - ('years', 365.25 * 24 * 3600),
  3232. - ('year', 365.25 * 24 * 3600),
  3233. - ('yrs', 365.25 * 24 * 3600),
  3234. - ('y', 365.25 * 24 * 3600),
  3235. -
  3236. - ('months', 29.53059 * 24 * 3600),
  3237. - ('month', 29.53059 * 24 * 3600),
  3238. - ('mo', 29.53059 * 24 * 3600),
  3239. -
  3240. - ('weeks', 7 * 24 * 3600),
  3241. - ('week', 7 * 24 * 3600),
  3242. - ('wks', 7 * 24 * 3600),
  3243. - ('wk', 7 * 24 * 3600),
  3244. - ('w', 7 * 24 * 3600),
  3245. -
  3246. - ('days', 24 * 3600),
  3247. - ('day', 24 * 3600),
  3248. - ('d', 24 * 3600),
  3249. -
  3250. - ('hours', 3600),
  3251. - ('hour', 3600),
  3252. - ('hrs', 3600),
  3253. - ('hr', 3600),
  3254. - ('h', 3600),
  3255. -
  3256. - ('minutes', 60),
  3257. - ('minute', 60),
  3258. - ('mins', 60),
  3259. - ('min', 60),
  3260. - ('m', 60),
  3261. -
  3262. - ('seconds', 1),
  3263. - ('second', 1),
  3264. - ('secs', 1),
  3265. - ('sec', 1),
  3266. - ('s', 1),
  3267. -])
  3268. -
  3269. -periods = '|'.join(scaling.keys())
  3270. -
  3271. -
  3272. -@commands('in')
  3273. -@example('.in 3h45m Go to class')
  3274. -def remind(bot, trigger):
  3275. - """Gives you a reminder in the given amount of time."""
  3276. - if not trigger.group(2):
  3277. - bot.say("Missing arguments for reminder command.")
  3278. - return NOLIMIT
  3279. - if trigger.group(3) and not trigger.group(4):
  3280. - bot.say("No message given for reminder.")
  3281. - return NOLIMIT
  3282. - duration = 0
  3283. - message = filter(None, re.split('(\d+(?:\.\d+)? ?(?:(?i)' + periods + ')) ?',
  3284. - trigger.group(2))[1:])
  3285. - reminder = ''
  3286. - stop = False
  3287. - for piece in message:
  3288. - grp = re.match('(\d+(?:\.\d+)?) ?(.*) ?', piece)
  3289. - if grp and not stop:
  3290. - length = float(grp.group(1))
  3291. - factor = scaling.get(grp.group(2).lower(), 60)
  3292. - duration += length * factor
  3293. - else:
  3294. - reminder = reminder + piece
  3295. - stop = True
  3296. - if duration == 0:
  3297. - return bot.reply("Sorry, didn't understand the input.")
  3298. -
  3299. - if duration % 1:
  3300. - duration = int(duration) + 1
  3301. - else:
  3302. - duration = int(duration)
  3303. - timezone = get_timezone(
  3304. - bot.db, bot.config, None, trigger.nick, trigger.sender)
  3305. - create_reminder(bot, trigger, duration, reminder, timezone)
  3306. -
  3307. -
  3308. -@commands('at')
  3309. -@example('.at 13:47 Do your homework!')
  3310. -def at(bot, trigger):
  3311. - """
  3312. - Gives you a reminder at the given time. Takes hh:mm:ssTimezone
  3313. - message. Timezone is any timezone Sopel takes elsewhere; the best choices
  3314. - are those from the tzdb; a list of valid options is available at
  3315. - http://sopel.chat/tz . The seconds and timezone are optional.
  3316. - """
  3317. - if not trigger.group(2):
  3318. - bot.say("No arguments given for reminder command.")
  3319. - return NOLIMIT
  3320. - if trigger.group(3) and not trigger.group(4):
  3321. - bot.say("No message given for reminder.")
  3322. - return NOLIMIT
  3323. - regex = re.compile(r'(\d+):(\d+)(?::(\d+))?([^\s\d]+)? (.*)')
  3324. - match = regex.match(trigger.group(2))
  3325. - if not match:
  3326. - bot.reply("Sorry, but I didn't understand your input.")
  3327. - return NOLIMIT
  3328. - hour, minute, second, tz, message = match.groups()
  3329. - if not second:
  3330. - second = '0'
  3331. -
  3332. - if pytz:
  3333. - timezone = get_timezone(bot.db, bot.config, tz,
  3334. - trigger.nick, trigger.sender)
  3335. - if not timezone:
  3336. - timezone = 'UTC'
  3337. - now = datetime.now(pytz.timezone(timezone))
  3338. - at_time = datetime(now.year, now.month, now.day,
  3339. - int(hour), int(minute), int(second),
  3340. - tzinfo=now.tzinfo)
  3341. - timediff = at_time - now
  3342. - else:
  3343. - if tz and tz.upper() != 'UTC':
  3344. - bot.reply("I don't have timzeone support installed.")
  3345. - return NOLIMIT
  3346. - now = datetime.now()
  3347. - at_time = datetime(now.year, now.month, now.day,
  3348. - int(hour), int(minute), int(second))
  3349. - timediff = at_time - now
  3350. -
  3351. - duration = timediff.seconds
  3352. -
  3353. - if duration < 0:
  3354. - duration += 86400
  3355. - create_reminder(bot, trigger, duration, message, 'UTC')
  3356. -
  3357. -
  3358. -def create_reminder(bot, trigger, duration, message, tz):
  3359. - t = int(time.time()) + duration
  3360. - reminder = (trigger.sender, trigger.nick, message)
  3361. - try:
  3362. - bot.rdb[t].append(reminder)
  3363. - except KeyError:
  3364. - bot.rdb[t] = [reminder]
  3365. -
  3366. - dump_database(bot.rfn, bot.rdb)
  3367. -
  3368. - if duration >= 60:
  3369. - remind_at = datetime.utcfromtimestamp(t)
  3370. - timef = format_time(bot.db, bot.config, tz, trigger.nick,
  3371. - trigger.sender, remind_at)
  3372. -
  3373. - bot.reply('Okay, will remind at %s' % timef)
  3374. - else:
  3375. - bot.reply('Okay, will remind in %s secs' % duration)
  3376. diff --git a/sopel/modules/safety.py b/sopel/modules/safety.py
  3377. deleted file mode 100644
  3378. index 17c850d..0000000
  3379. --- a/sopel/modules/safety.py
  3380. +++ /dev/null
  3381. @@ -1,197 +0,0 @@
  3382. -# coding=utf-8
  3383. -"""
  3384. -safety.py - Alerts about malicious URLs
  3385. -Copyright © 2014, Elad Alfassa, <elad@fedoraproject.org>
  3386. -Licensed under the Eiffel Forum License 2.
  3387. -
  3388. -This module uses virustotal.com
  3389. -"""
  3390. -from __future__ import unicode_literals, absolute_import, print_function, division
  3391. -
  3392. -import sopel.web as web
  3393. -from sopel.config.types import StaticSection, ValidatedAttribute, ListAttribute
  3394. -from sopel.formatting import color, bold
  3395. -from sopel.logger import get_logger
  3396. -from sopel.module import OP
  3397. -import sopel.tools
  3398. -import sys
  3399. -import json
  3400. -import time
  3401. -import os.path
  3402. -import re
  3403. -
  3404. -if sys.version_info.major > 2:
  3405. - unicode = str
  3406. - from urllib.request import urlretrieve
  3407. - from urllib.parse import urlparse
  3408. -else:
  3409. - from urllib import urlretrieve
  3410. - from urlparse import urlparse
  3411. -
  3412. -LOGGER = get_logger(__name__)
  3413. -
  3414. -vt_base_api_url = 'https://www.virustotal.com/vtapi/v2/url/'
  3415. -malware_domains = set()
  3416. -known_good = []
  3417. -
  3418. -
  3419. -class SafetySection(StaticSection):
  3420. - enabled_by_default = ValidatedAttribute('enabled_by_default', bool, default=True)
  3421. - """Enable URL safety in all channels where it isn't explicitly disabled."""
  3422. - known_good = ListAttribute('known_good')
  3423. - """List of "known good" domains to ignore."""
  3424. - vt_api_key = ValidatedAttribute('vt_api_key')
  3425. - """Optional VirusTotal API key."""
  3426. -
  3427. -
  3428. -def configure(config):
  3429. - config.define_section('safety', SafetySection)
  3430. - config.safety.configure_setting(
  3431. - 'enabled_by_default',
  3432. - "Enable URL safety in channels that don't specifically disable it?",
  3433. - )
  3434. - config.safety.configure_setting(
  3435. - 'known_good',
  3436. - 'Enter any domains to whitelist',
  3437. - )
  3438. - config.safety.configure_setting(
  3439. - 'vt_api_key',
  3440. - "Optionally, enter a VirusTotal API key to improve malicious URL "
  3441. - "protection.\nOtherwise, only the Malwarebytes DB will be used."
  3442. - )
  3443. -
  3444. -
  3445. -def setup(bot):
  3446. - bot.config.define_section('safety', SafetySection)
  3447. -
  3448. - bot.memory['safety_cache'] = sopel.tools.SopelMemory()
  3449. - for item in bot.config.safety.known_good:
  3450. - known_good.append(re.compile(item, re.I))
  3451. -
  3452. - loc = os.path.join(bot.config.homedir, 'malwaredomains.txt')
  3453. - if os.path.isfile(loc):
  3454. - if os.path.getmtime(loc) < time.time() - 24 * 60 * 60 * 7:
  3455. - # File exists but older than one week, update
  3456. - _download_malwaredomains_db(loc)
  3457. - else:
  3458. - _download_malwaredomains_db(loc)
  3459. - with open(loc, 'r') as f:
  3460. - for line in f:
  3461. - clean_line = unicode(line).strip().lower()
  3462. - if clean_line != '':
  3463. - malware_domains.add(clean_line)
  3464. -
  3465. -
  3466. -def _download_malwaredomains_db(path):
  3467. - print('Downloading malwaredomains db...')
  3468. - urlretrieve('http://mirror1.malwaredomains.com/files/justdomains', path)
  3469. -
  3470. -
  3471. -@sopel.module.rule('(?u).*(https?://\S+).*')
  3472. -@sopel.module.priority('high')
  3473. -def url_handler(bot, trigger):
  3474. - """ Check for malicious URLs """
  3475. - check = True # Enable URL checking
  3476. - strict = False # Strict mode: kick on malicious URL
  3477. - positives = 0 # Number of engines saying it's malicious
  3478. - total = 0 # Number of total engines
  3479. - use_vt = True # Use VirusTotal
  3480. - check = bot.config.safety.enabled_by_default
  3481. - if check is None:
  3482. - # If not set, assume default
  3483. - check = True
  3484. - # DB overrides config:
  3485. - setting = bot.db.get_channel_value(trigger.sender, 'safety')
  3486. - if setting is not None:
  3487. - if setting == 'off':
  3488. - return # Not checking
  3489. - elif setting in ['on', 'strict', 'local', 'local strict']:
  3490. - check = True
  3491. - if setting == 'strict' or setting == 'local strict':
  3492. - strict = True
  3493. - if setting == 'local' or setting == 'local strict':
  3494. - use_vt = False
  3495. -
  3496. - if not check:
  3497. - return # Not overriden by DB, configured default off
  3498. -
  3499. - netloc = urlparse(trigger.group(1)).netloc
  3500. - if any(regex.search(netloc) for regex in known_good):
  3501. - return # Whitelisted
  3502. -
  3503. - apikey = bot.config.safety.vt_api_key
  3504. - try:
  3505. - if apikey is not None and use_vt:
  3506. - payload = {'resource': unicode(trigger),
  3507. - 'apikey': apikey,
  3508. - 'scan': '1'}
  3509. -
  3510. - if trigger not in bot.memory['safety_cache']:
  3511. - result = web.post(vt_base_api_url + 'report', payload)
  3512. - if sys.version_info.major > 2:
  3513. - result = result.decode('utf-8')
  3514. - result = json.loads(result)
  3515. - age = time.time()
  3516. - data = {'positives': result['positives'],
  3517. - 'total': result['total'],
  3518. - 'age': age}
  3519. - bot.memory['safety_cache'][trigger] = data
  3520. - if len(bot.memory['safety_cache']) > 1024:
  3521. - _clean_cache(bot)
  3522. - else:
  3523. - print('using cache')
  3524. - result = bot.memory['safety_cache'][trigger]
  3525. - positives = result['positives']
  3526. - total = result['total']
  3527. - except Exception:
  3528. - LOGGER.debug('Error from checking URL with VT.', exc_info=True)
  3529. - pass # Ignoring exceptions with VT so MalwareDomains will always work
  3530. -
  3531. - if unicode(netloc).lower() in malware_domains:
  3532. - # malwaredomains is more trustworthy than some VT engines
  3533. - # therefor it gets a weight of 10 engines when calculating confidence
  3534. - positives += 10
  3535. - total += 10
  3536. -
  3537. - if positives > 1:
  3538. - # Possibly malicious URL detected!
  3539. - confidence = '{}%'.format(round((positives / total) * 100))
  3540. - msg = 'link posted by %s is possibly malicious ' % bold(trigger.nick)
  3541. - msg += '(confidence %s - %s/%s)' % (confidence, positives, total)
  3542. - bot.say('[' + bold(color('WARNING', 'red')) + '] ' + msg)
  3543. - if strict:
  3544. - bot.write(['KICK', trigger.sender, trigger.nick,
  3545. - 'Posted a malicious link'])
  3546. -
  3547. -
  3548. -@sopel.module.commands('safety')
  3549. -def toggle_safety(bot, trigger):
  3550. - """ Set safety setting for channel """
  3551. - if not trigger.admin and bot.privileges[trigger.sender][trigger.nick] < OP:
  3552. - bot.reply('Only channel operators can change safety settings')
  3553. - return
  3554. - allowed_states = ['strict', 'on', 'off', 'local', 'local strict']
  3555. - if not trigger.group(2) or trigger.group(2).lower() not in allowed_states:
  3556. - options = ' / '.join(allowed_states)
  3557. - bot.reply('Available options: %s' % options)
  3558. - return
  3559. -
  3560. - channel = trigger.sender.lower()
  3561. - bot.db.set_channel_value(channel, 'safety', trigger.group(2).lower())
  3562. - bot.reply('Safety is now set to "%s" on this channel' % trigger.group(2))
  3563. -
  3564. -
  3565. -# Clean the cache every day, also when > 1024 entries
  3566. -@sopel.module.interval(24 * 60 * 60)
  3567. -def _clean_cache(bot):
  3568. - """ Cleanup old entries in URL cache """
  3569. - # TODO probably should be using locks here, to make sure stuff doesn't
  3570. - # explode
  3571. - oldest_key_age = 0
  3572. - oldest_key = ''
  3573. - for key, data in sopel.tools.iteritems(bot.memory['safety_cache']):
  3574. - if data['age'] > oldest_key_age:
  3575. - oldest_key_age = data['age']
  3576. - oldest_key = key
  3577. - if oldest_key in bot.memory['safety_cache']:
  3578. - del bot.memory['safety_cache'][oldest_key]
  3579. diff --git a/sopel/modules/search.py b/sopel/modules/search.py
  3580. deleted file mode 100644
  3581. index e77d5db..0000000
  3582. --- a/sopel/modules/search.py
  3583. +++ /dev/null
  3584. @@ -1,128 +0,0 @@
  3585. -# coding=utf-8
  3586. -# Copyright 2008-9, Sean B. Palmer, inamidst.com
  3587. -# Copyright 2012, Elsie Powell, embolalia.com
  3588. -# Licensed under the Eiffel Forum License 2.
  3589. -from __future__ import unicode_literals, absolute_import, print_function, division
  3590. -
  3591. -import re
  3592. -from sopel import web
  3593. -from sopel.module import commands, example
  3594. -import json
  3595. -import sys
  3596. -
  3597. -if sys.version_info.major < 3:
  3598. - from urllib import quote_plus
  3599. -else:
  3600. - from urllib.parse import quote_plus
  3601. -
  3602. -
  3603. -def formatnumber(n):
  3604. - """Format a number with beautiful commas."""
  3605. - parts = list(str(n))
  3606. - for i in range((len(parts) - 3), 0, -3):
  3607. - parts.insert(i, ',')
  3608. - return ''.join(parts)
  3609. -
  3610. -r_bing = re.compile(r'<h3><a href="([^"]+)"')
  3611. -
  3612. -
  3613. -def bing_search(query, lang='en-GB'):
  3614. - base = 'http://www.bing.com/search?mkt=%s&q=' % lang
  3615. - bytes = web.get(base + query)
  3616. - m = r_bing.search(bytes)
  3617. - if m:
  3618. - return m.group(1)
  3619. -
  3620. -r_duck = re.compile(r'nofollow" class="[^"]+" href="(.*?)">')
  3621. -
  3622. -
  3623. -def duck_search(query):
  3624. - query = query.replace('!', '')
  3625. - uri = 'http://duckduckgo.com/html/?q=%s&kl=uk-en' % query
  3626. - bytes = web.get(uri)
  3627. - if 'web-result' in bytes: # filter out the adds on top of the page
  3628. - bytes = bytes.split('web-result')[1]
  3629. - m = r_duck.search(bytes)
  3630. - if m:
  3631. - return web.decode(m.group(1))
  3632. -
  3633. -# Alias google_search to duck_search
  3634. -google_search = duck_search
  3635. -
  3636. -
  3637. -def duck_api(query):
  3638. - if '!bang' in query.lower():
  3639. - return 'https://duckduckgo.com/bang.html'
  3640. -
  3641. - # This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)
  3642. - # It seems that duckduckgo api redirects to its Instant answer API html page
  3643. - # if the query constains special charactares that aren't urlencoded.
  3644. - # So in order to always get a JSON response back the query is urlencoded
  3645. - query = quote_plus(query)
  3646. - uri = 'http://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query
  3647. - results = json.loads(web.get(uri))
  3648. - if results['Redirect']:
  3649. - return results['Redirect']
  3650. - else:
  3651. - return None
  3652. -
  3653. -
  3654. -@commands('duck', 'ddg', 'g')
  3655. -@example('.duck privacy or .duck !mcwiki obsidian')
  3656. -def duck(bot, trigger):
  3657. - """Queries Duck Duck Go for the specified input."""
  3658. - query = trigger.group(2)
  3659. - if not query:
  3660. - return bot.reply('.ddg what?')
  3661. -
  3662. - # If the API gives us something, say it and stop
  3663. - result = duck_api(query)
  3664. - if result:
  3665. - bot.reply(result)
  3666. - return
  3667. -
  3668. - # Otherwise, look it up on the HTMl version
  3669. - uri = duck_search(query)
  3670. -
  3671. - if uri:
  3672. - bot.reply(uri)
  3673. - if 'last_seen_url' in bot.memory:
  3674. - bot.memory['last_seen_url'][trigger.sender] = uri
  3675. - else:
  3676. - bot.reply("No results found for '%s'." % query)
  3677. -
  3678. -
  3679. -@commands('search')
  3680. -@example('.search nerdfighter')
  3681. -def search(bot, trigger):
  3682. - """Searches Bing and Duck Duck Go."""
  3683. - if not trigger.group(2):
  3684. - return bot.reply('.search for what?')
  3685. - query = trigger.group(2)
  3686. - bu = bing_search(query) or '-'
  3687. - du = duck_search(query) or '-'
  3688. -
  3689. - if bu == du:
  3690. - result = '%s (b, d)' % bu
  3691. - else:
  3692. - if len(bu) > 150:
  3693. - bu = '(extremely long link)'
  3694. - if len(du) > 150:
  3695. - du = '(extremely long link)'
  3696. - result = '%s (b), %s (d)' % (bu, du)
  3697. -
  3698. - bot.reply(result)
  3699. -
  3700. -
  3701. -@commands('suggest')
  3702. -def suggest(bot, trigger):
  3703. - """Suggest terms starting with given input"""
  3704. - if not trigger.group(2):
  3705. - return bot.reply("No query term.")
  3706. - query = trigger.group(2)
  3707. - uri = 'http://websitedev.de/temp-bin/suggest.pl?q='
  3708. - answer = web.get(uri + query.replace('+', '%2B'))
  3709. - if answer:
  3710. - bot.say(answer)
  3711. - else:
  3712. - bot.reply('Sorry, no result.')
  3713. diff --git a/sopel/modules/seen.py b/sopel/modules/seen.py
  3714. deleted file mode 100644
  3715. index 262059c..0000000
  3716. --- a/sopel/modules/seen.py
  3717. +++ /dev/null
  3718. @@ -1,59 +0,0 @@
  3719. -# coding=utf-8
  3720. -"""
  3721. -seen.py - Sopel Seen Module
  3722. -Copyright 2008, Sean B. Palmer, inamidst.com
  3723. -Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
  3724. -Licensed under the Eiffel Forum License 2.
  3725. -
  3726. -http://sopel.chat
  3727. -"""
  3728. -from __future__ import unicode_literals, absolute_import, print_function, division
  3729. -
  3730. -import time
  3731. -import datetime
  3732. -from sopel.tools import Identifier
  3733. -from sopel.tools.time import get_timezone, format_time
  3734. -from sopel.module import commands, rule, priority, thread
  3735. -
  3736. -
  3737. -@commands('seen')
  3738. -def seen(bot, trigger):
  3739. - """Reports when and where the user was last seen."""
  3740. - if not trigger.group(2):
  3741. - bot.say(".seen <nick> - Reports when <nick> was last seen.")
  3742. - return
  3743. - nick = trigger.group(2).strip()
  3744. - timestamp = bot.db.get_nick_value(nick, 'seen_timestamp')
  3745. - if timestamp:
  3746. - channel = bot.db.get_nick_value(nick, 'seen_channel')
  3747. - message = bot.db.get_nick_value(nick, 'seen_message')
  3748. - action = bot.db.get_nick_value(nick, 'seen_action')
  3749. -
  3750. - tz = get_timezone(bot.db, bot.config, None, trigger.nick,
  3751. - trigger.sender)
  3752. - saw = datetime.datetime.utcfromtimestamp(timestamp)
  3753. - timestamp = format_time(bot.db, bot.config, tz, trigger.nick,
  3754. - trigger.sender, saw)
  3755. -
  3756. - msg = "I last saw {} at {}".format(nick, timestamp)
  3757. - if Identifier(channel) == trigger.sender:
  3758. - if action:
  3759. - msg = msg + " in here, doing " + nick + " " + message
  3760. - else:
  3761. - msg = msg + " in here, saying " + message
  3762. - else:
  3763. - msg += " in another channel."
  3764. - bot.say(str(trigger.nick) + ': ' + msg)
  3765. - else:
  3766. - bot.say("Sorry, I haven't seen {} around.".format(nick))
  3767. -
  3768. -
  3769. -@thread(False)
  3770. -@rule('(.*)')
  3771. -@priority('low')
  3772. -def note(bot, trigger):
  3773. - if not trigger.is_privmsg:
  3774. - bot.db.set_nick_value(trigger.nick, 'seen_timestamp', time.time())
  3775. - bot.db.set_nick_value(trigger.nick, 'seen_channel', trigger.sender)
  3776. - bot.db.set_nick_value(trigger.nick, 'seen_message', trigger)
  3777. - bot.db.set_nick_value(trigger.nick, 'seen_action', 'intent' in trigger.tags)
  3778. diff --git a/sopel/modules/spellcheck.py b/sopel/modules/spellcheck.py
  3779. deleted file mode 100644
  3780. index f722595..0000000
  3781. --- a/sopel/modules/spellcheck.py
  3782. +++ /dev/null
  3783. @@ -1,54 +0,0 @@
  3784. -# coding=utf-8
  3785. -"""
  3786. -spellcheck.py - Sopel spell check Module
  3787. -Copyright © 2012, Elad Alfassa, <elad@fedoraproject.org>
  3788. -Copyright © 2012, Lior Ramati
  3789. -Licensed under the Eiffel Forum License 2.
  3790. -
  3791. -http://sopel.chat
  3792. -
  3793. -This module relies on pyenchant, on Fedora and Red Hat based system, it can be found in the package python-enchant
  3794. -"""
  3795. -from __future__ import unicode_literals, absolute_import, print_function, division
  3796. -try:
  3797. - import enchant
  3798. -except ImportError:
  3799. - enchant = None
  3800. -from sopel.module import commands, example
  3801. -
  3802. -
  3803. -@commands('spellcheck', 'spell')
  3804. -@example('.spellcheck stuff')
  3805. -def spellcheck(bot, trigger):
  3806. - """
  3807. - Says whether the given word is spelled correctly, and gives suggestions if
  3808. - it's not.
  3809. - """
  3810. - if not enchant:
  3811. - bot.say("Missing pyenchant module.")
  3812. - if not trigger.group(2):
  3813. - return
  3814. - word = trigger.group(2).rstrip()
  3815. - if " " in word:
  3816. - bot.say("One word at a time, please")
  3817. - return
  3818. - dictionary = enchant.Dict("en_US")
  3819. - dictionary_uk = enchant.Dict("en_GB")
  3820. - # I don't want to make anyone angry, so I check both American and British English.
  3821. - if dictionary_uk.check(word):
  3822. - if dictionary.check(word):
  3823. - bot.say(word + " is spelled correctly")
  3824. - else:
  3825. - bot.say(word + " is spelled correctly (British)")
  3826. - elif dictionary.check(word):
  3827. - bot.say(word + " is spelled correctly (American)")
  3828. - else:
  3829. - msg = word + " is not spelled correctly. Maybe you want one of these spellings:"
  3830. - sugWords = []
  3831. - for suggested_word in dictionary.suggest(word):
  3832. - sugWords.append(suggested_word)
  3833. - for suggested_word in dictionary_uk.suggest(word):
  3834. - sugWords.append(suggested_word)
  3835. - for suggested_word in sorted(set(sugWords)): # removes duplicates
  3836. - msg = msg + " '" + suggested_word + "',"
  3837. - bot.say(msg)
  3838. diff --git a/sopel/modules/tell.py b/sopel/modules/tell.py
  3839. deleted file mode 100644
  3840. index ab7c397..0000000
  3841. --- a/sopel/modules/tell.py
  3842. +++ /dev/null
  3843. @@ -1,183 +0,0 @@
  3844. -# coding=utf-8
  3845. -"""
  3846. -tell.py - Sopel Tell and Ask Module
  3847. -Copyright 2008, Sean B. Palmer, inamidst.com
  3848. -Licensed under the Eiffel Forum License 2.
  3849. -
  3850. -http://sopel.chat
  3851. -"""
  3852. -from __future__ import unicode_literals, absolute_import, print_function, division
  3853. -
  3854. -import os
  3855. -import time
  3856. -import threading
  3857. -import sys
  3858. -from sopel.tools import Identifier, iterkeys
  3859. -from sopel.tools.time import get_timezone, format_time
  3860. -from sopel.module import commands, nickname_commands, rule, priority, example
  3861. -
  3862. -maximum = 4
  3863. -
  3864. -
  3865. -def loadReminders(fn, lock):
  3866. - lock.acquire()
  3867. - try:
  3868. - result = {}
  3869. - f = open(fn)
  3870. - for line in f:
  3871. - line = line.strip()
  3872. - if sys.version_info.major < 3:
  3873. - line = line.decode('utf-8')
  3874. - if line:
  3875. - try:
  3876. - tellee, teller, verb, timenow, msg = line.split('\t', 4)
  3877. - except ValueError:
  3878. - continue # @@ hmm
  3879. - result.setdefault(tellee, []).append((teller, verb, timenow, msg))
  3880. - f.close()
  3881. - finally:
  3882. - lock.release()
  3883. - return result
  3884. -
  3885. -
  3886. -def dumpReminders(fn, data, lock):
  3887. - lock.acquire()
  3888. - try:
  3889. - f = open(fn, 'w')
  3890. - for tellee in iterkeys(data):
  3891. - for remindon in data[tellee]:
  3892. - line = '\t'.join((tellee,) + remindon)
  3893. - try:
  3894. - to_write = line + '\n'
  3895. - if sys.version_info.major < 3:
  3896. - to_write = to_write.encode('utf-8')
  3897. - f.write(to_write)
  3898. - except IOError:
  3899. - break
  3900. - try:
  3901. - f.close()
  3902. - except IOError:
  3903. - pass
  3904. - finally:
  3905. - lock.release()
  3906. - return True
  3907. -
  3908. -
  3909. -def setup(self):
  3910. - fn = self.nick + '-' + self.config.core.host + '.tell.db'
  3911. - self.tell_filename = os.path.join(self.config.core.homedir, fn)
  3912. - if not os.path.exists(self.tell_filename):
  3913. - try:
  3914. - f = open(self.tell_filename, 'w')
  3915. - except OSError:
  3916. - pass
  3917. - else:
  3918. - f.write('')
  3919. - f.close()
  3920. - self.memory['tell_lock'] = threading.Lock()
  3921. - self.memory['reminders'] = loadReminders(self.tell_filename, self.memory['tell_lock'])
  3922. -
  3923. -
  3924. -@commands('tell', 'ask')
  3925. -@nickname_commands('tell', 'ask')
  3926. -@example('$nickname, tell Embolalia he broke something again.')
  3927. -def f_remind(bot, trigger):
  3928. - """Give someone a message the next time they're seen"""
  3929. - teller = trigger.nick
  3930. - verb = trigger.group(1)
  3931. -
  3932. - if not trigger.group(3):
  3933. - bot.reply("%s whom?" % verb)
  3934. - return
  3935. -
  3936. - tellee = trigger.group(3).rstrip('.,:;')
  3937. - msg = trigger.group(2).lstrip(tellee).lstrip()
  3938. -
  3939. - if not msg:
  3940. - bot.reply("%s %s what?" % (verb, tellee))
  3941. - return
  3942. -
  3943. - tellee = Identifier(tellee)
  3944. -
  3945. - if not os.path.exists(bot.tell_filename):
  3946. - return
  3947. -
  3948. - if len(tellee) > 20:
  3949. - return bot.reply('That nickname is too long.')
  3950. - if tellee == bot.nick:
  3951. - return bot.reply("I'm here now, you can tell me whatever you want!")
  3952. -
  3953. - if not tellee in (Identifier(teller), bot.nick, 'me'):
  3954. - tz = get_timezone(bot.db, bot.config, None, tellee)
  3955. - timenow = format_time(bot.db, bot.config, tz, tellee)
  3956. - bot.memory['tell_lock'].acquire()
  3957. - try:
  3958. - if not tellee in bot.memory['reminders']:
  3959. - bot.memory['reminders'][tellee] = [(teller, verb, timenow, msg)]
  3960. - else:
  3961. - bot.memory['reminders'][tellee].append((teller, verb, timenow, msg))
  3962. - finally:
  3963. - bot.memory['tell_lock'].release()
  3964. -
  3965. - response = "I'll pass that on when %s is around." % tellee
  3966. -
  3967. - bot.reply(response)
  3968. - elif Identifier(teller) == tellee:
  3969. - bot.say('You can %s yourself that.' % verb)
  3970. - else:
  3971. - bot.say("Hey, I'm not as stupid as Monty you know!")
  3972. -
  3973. - dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
  3974. -
  3975. -
  3976. -def getReminders(bot, channel, key, tellee):
  3977. - lines = []
  3978. - template = "%s: %s <%s> %s %s %s"
  3979. - today = time.strftime('%d %b', time.gmtime())
  3980. -
  3981. - bot.memory['tell_lock'].acquire()
  3982. - try:
  3983. - for (teller, verb, datetime, msg) in bot.memory['reminders'][key]:
  3984. - if datetime.startswith(today):
  3985. - datetime = datetime[len(today) + 1:]
  3986. - lines.append(template % (tellee, datetime, teller, verb, tellee, msg))
  3987. -
  3988. - try:
  3989. - del bot.memory['reminders'][key]
  3990. - except KeyError:
  3991. - bot.msg(channel, 'Er...')
  3992. - finally:
  3993. - bot.memory['tell_lock'].release()
  3994. - return lines
  3995. -
  3996. -
  3997. -@rule('(.*)')
  3998. -@priority('low')
  3999. -def message(bot, trigger):
  4000. -
  4001. - tellee = trigger.nick
  4002. - channel = trigger.sender
  4003. -
  4004. - if not os.path.exists(bot.tell_filename):
  4005. - return
  4006. -
  4007. - reminders = []
  4008. - remkeys = list(reversed(sorted(bot.memory['reminders'].keys())))
  4009. -
  4010. - for remkey in remkeys:
  4011. - if not remkey.endswith('*') or remkey.endswith(':'):
  4012. - if tellee == remkey:
  4013. - reminders.extend(getReminders(bot, channel, remkey, tellee))
  4014. - elif tellee.startswith(remkey.rstrip('*:')):
  4015. - reminders.extend(getReminders(bot, channel, remkey, tellee))
  4016. -
  4017. - for line in reminders[:maximum]:
  4018. - bot.say(line)
  4019. -
  4020. - if reminders[maximum:]:
  4021. - bot.say('Further messages sent privately')
  4022. - for line in reminders[maximum:]:
  4023. - bot.msg(tellee, line)
  4024. -
  4025. - if len(bot.memory['reminders'].keys()) != remkeys:
  4026. - dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
  4027. diff --git a/sopel/modules/tld.py b/sopel/modules/tld.py
  4028. deleted file mode 100644
  4029. index 62284b5..0000000
  4030. --- a/sopel/modules/tld.py
  4031. +++ /dev/null
  4032. @@ -1,69 +0,0 @@
  4033. -# coding=utf-8
  4034. -"""
  4035. -tld.py - Sopel TLD Module
  4036. -Copyright 2009-10, Michael Yanovich, yanovich.net
  4037. -Licensed under the Eiffel Forum License 2.
  4038. -
  4039. -http://sopel.chat
  4040. -"""
  4041. -from __future__ import unicode_literals, absolute_import, print_function, division
  4042. -
  4043. -from sopel import web
  4044. -from sopel.module import commands, example
  4045. -import re
  4046. -import sys
  4047. -if sys.version_info.major >= 3:
  4048. - unicode = str
  4049. -
  4050. -uri = 'https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains'
  4051. -r_tag = re.compile(r'<(?!!)[^>]+>')
  4052. -
  4053. -
  4054. -@commands('tld')
  4055. -@example('.tld ru')
  4056. -def gettld(bot, trigger):
  4057. - """Show information about the given Top Level Domain."""
  4058. - page = web.get(uri)
  4059. - tld = trigger.group(2)
  4060. - if tld[0] == '.':
  4061. - tld = tld[1:]
  4062. - search = r'(?i)<td><a href="\S+" title="\S+">\.{0}</a></td>\n(<td><a href=".*</a></td>\n)?<td>([A-Za-z0-9].*?)</td>\n<td>(.*)</td>\n<td[^>]*>(.*?)</td>\n<td[^>]*>(.*?)</td>\n'
  4063. - search = search.format(tld)
  4064. - re_country = re.compile(search)
  4065. - matches = re_country.findall(page)
  4066. - if not matches:
  4067. - search = r'(?i)<td><a href="\S+" title="(\S+)">\.{0}</a></td>\n<td><a href=".*">(.*)</a></td>\n<td>([A-Za-z0-9].*?)</td>\n<td[^>]*>(.*?)</td>\n<td[^>]*>(.*?)</td>\n'
  4068. - search = search.format(tld)
  4069. - re_country = re.compile(search)
  4070. - matches = re_country.findall(page)
  4071. - if matches:
  4072. - matches = list(matches[0])
  4073. - i = 0
  4074. - while i < len(matches):
  4075. - matches[i] = r_tag.sub("", matches[i])
  4076. - i += 1
  4077. - desc = matches[2]
  4078. - if len(desc) > 400:
  4079. - desc = desc[:400] + "..."
  4080. - reply = "%s -- %s. IDN: %s, DNSSEC: %s" % (matches[1], desc,
  4081. - matches[3], matches[4])
  4082. - bot.reply(reply)
  4083. - else:
  4084. - search = r'<td><a href="\S+" title="\S+">.{0}</a></td>\n<td><span class="flagicon"><img.*?\">(.*?)</a></td>\n<td[^>]*>(.*?)</td>\n<td[^>]*>(.*?)</td>\n<td[^>]*>(.*?)</td>\n<td[^>]*>(.*?)</td>\n<td[^>]*>(.*?)</td>\n'
  4085. - search = search.format(unicode(tld))
  4086. - re_country = re.compile(search)
  4087. - matches = re_country.findall(page)
  4088. - if matches:
  4089. - matches = matches[0]
  4090. - dict_val = dict()
  4091. - dict_val["country"], dict_val["expl"], dict_val["notes"], dict_val["idn"], dict_val["dnssec"], dict_val["sld"] = matches
  4092. - for key in dict_val:
  4093. - if dict_val[key] == "&#160;":
  4094. - dict_val[key] = "N/A"
  4095. - dict_val[key] = r_tag.sub('', dict_val[key])
  4096. - if len(dict_val["notes"]) > 400:
  4097. - dict_val["notes"] = dict_val["notes"][:400] + "..."
  4098. - reply = "%s (%s, %s). IDN: %s, DNSSEC: %s, SLD: %s" % (dict_val["country"], dict_val["expl"], dict_val["notes"], dict_val["idn"], dict_val["dnssec"], dict_val["sld"])
  4099. - else:
  4100. - reply = "No matches found for TLD: {0}".format(unicode(tld))
  4101. - bot.reply(reply)
  4102. diff --git a/sopel/modules/translate.py b/sopel/modules/translate.py
  4103. deleted file mode 100644
  4104. index ef9281f..0000000
  4105. --- a/sopel/modules/translate.py
  4106. +++ /dev/null
  4107. @@ -1,203 +0,0 @@
  4108. -# coding=utf-8
  4109. -"""
  4110. -translate.py - Sopel Translation Module
  4111. -Copyright 2008, Sean B. Palmer, inamidst.com
  4112. -Copyright © 2013-2014, Elad Alfassa <elad@fedoraproject.org>
  4113. -Licensed under the Eiffel Forum License 2.
  4114. -
  4115. -http://sopel.chat
  4116. -"""
  4117. -from __future__ import unicode_literals, absolute_import, print_function, division
  4118. -from sopel import web
  4119. -from sopel.module import rule, commands, priority, example
  4120. -import json
  4121. -import sys
  4122. -import random
  4123. -import requests
  4124. -mangle_lines = {}
  4125. -if sys.version_info.major >= 3:
  4126. - unicode = str
  4127. -
  4128. -
  4129. -def translate(text, in_lang='auto', out_lang='en'):
  4130. - raw = False
  4131. - if unicode(out_lang).endswith('-raw'):
  4132. - out_lang = out_lang[:-4]
  4133. - raw = True
  4134. -
  4135. - headers = {
  4136. - 'User-Agent': 'Mozilla/5.0' +
  4137. - '(X11; U; Linux i686)' +
  4138. - 'Gecko/20071127 Firefox/2.0.0.11'
  4139. - }
  4140. -
  4141. - query = {
  4142. - "client": "gtx",
  4143. - "sl": in_lang,
  4144. - "tl": out_lang,
  4145. - "dt": "t",
  4146. - "q": text,
  4147. - }
  4148. - url = "http://translate.googleapis.com/translate_a/single"
  4149. - result = requests.get(url, params=query, timeout=40, headers=headers).text
  4150. -
  4151. - if result == '[,,""]':
  4152. - return None, in_lang
  4153. -
  4154. - while ',,' in result:
  4155. - result = result.replace(',,', ',null,')
  4156. - result = result.replace('[,', '[null,')
  4157. -
  4158. - data = json.loads(result)
  4159. -
  4160. - if raw:
  4161. - return str(data), 'en-raw'
  4162. -
  4163. - try:
  4164. - language = data[2] # -2][0][0]
  4165. - except:
  4166. - language = '?'
  4167. -
  4168. - return ''.join(x[0] for x in data[0]), language
  4169. -
  4170. -
  4171. -@rule(u'$nickname[,:]\s+(?:([a-z]{2}) +)?(?:([a-z]{2}|en-raw) +)?["“](.+?)["”]\? *$')
  4172. -@example('$nickname: "mon chien"? or $nickname: fr "mon chien"?')
  4173. -@priority('low')
  4174. -def tr(bot, trigger):
  4175. - """Translates a phrase, with an optional language hint."""
  4176. - in_lang, out_lang, phrase = trigger.groups()
  4177. -
  4178. - if (len(phrase) > 350) and (not trigger.admin):
  4179. - return bot.reply('Phrase must be under 350 characters.')
  4180. -
  4181. - if phrase.strip() == '':
  4182. - return bot.reply('You need to specify a string for me to translate!')
  4183. -
  4184. - in_lang = in_lang or 'auto'
  4185. - out_lang = out_lang or 'en'
  4186. -
  4187. - if in_lang != out_lang:
  4188. - msg, in_lang = translate(phrase, in_lang, out_lang)
  4189. - if sys.version_info.major < 3 and isinstance(msg, str):
  4190. - msg = msg.decode('utf-8')
  4191. - if msg:
  4192. - msg = web.decode(msg) # msg.replace('&#39;', "'")
  4193. - msg = '"%s" (%s to %s, translate.google.com)' % (msg, in_lang, out_lang)
  4194. - else:
  4195. - msg = 'The %s to %s translation failed, are you sure you specified valid language abbreviations?' % (in_lang, out_lang)
  4196. -
  4197. - bot.reply(msg)
  4198. - else:
  4199. - bot.reply('Language guessing failed, so try suggesting one!')
  4200. -
  4201. -
  4202. -@commands('translate', 'tr')
  4203. -@example('.tr :en :fr my dog', '"mon chien" (en to fr, translate.google.com)')
  4204. -@example('.tr היי', '"Hey" (iw to en, translate.google.com)')
  4205. -@example('.tr mon chien', '"my dog" (fr to en, translate.google.com)')
  4206. -def tr2(bot, trigger):
  4207. - """Translates a phrase, with an optional language hint."""
  4208. - command = trigger.group(2)
  4209. -
  4210. - if not command:
  4211. - return bot.reply('You did not give me anything to translate')
  4212. -
  4213. - def langcode(p):
  4214. - return p.startswith(':') and (2 < len(p) < 10) and p[1:].isalpha()
  4215. -
  4216. - args = ['auto', 'en']
  4217. -
  4218. - for i in range(2):
  4219. - if ' ' not in command:
  4220. - break
  4221. - prefix, cmd = command.split(' ', 1)
  4222. - if langcode(prefix):
  4223. - args[i] = prefix[1:]
  4224. - command = cmd
  4225. - phrase = command
  4226. -
  4227. - if (len(phrase) > 350) and (not trigger.admin):
  4228. - return bot.reply('Phrase must be under 350 characters.')
  4229. -
  4230. - if phrase.strip() == '':
  4231. - return bot.reply('You need to specify a string for me to translate!')
  4232. -
  4233. - src, dest = args
  4234. - if src != dest:
  4235. - msg, src = translate(phrase, src, dest)
  4236. - if sys.version_info.major < 3 and isinstance(msg, str):
  4237. - msg = msg.decode('utf-8')
  4238. - if msg:
  4239. - msg = web.decode(msg) # msg.replace('&#39;', "'")
  4240. - msg = '"%s" (%s to %s, translate.google.com)' % (msg, src, dest)
  4241. - else:
  4242. - msg = 'The %s to %s translation failed, are you sure you specified valid language abbreviations?' % (src, dest)
  4243. -
  4244. - bot.reply(msg)
  4245. - else:
  4246. - bot.reply('Language guessing failed, so try suggesting one!')
  4247. -
  4248. -
  4249. -def get_random_lang(long_list, short_list):
  4250. - random_index = random.randint(0, len(long_list) - 1)
  4251. - random_lang = long_list[random_index]
  4252. - if random_lang not in short_list:
  4253. - short_list.append(random_lang)
  4254. - else:
  4255. - return get_random_lang(long_list, short_list)
  4256. - return short_list
  4257. -
  4258. -
  4259. -@commands('mangle', 'mangle2')
  4260. -def mangle(bot, trigger):
  4261. - """Repeatedly translate the input until it makes absolutely no sense."""
  4262. - global mangle_lines
  4263. - long_lang_list = ['fr', 'de', 'es', 'it', 'no', 'he', 'la', 'ja', 'cy', 'ar', 'yi', 'zh', 'nl', 'ru', 'fi', 'hi', 'af', 'jw', 'mr', 'ceb', 'cs', 'ga', 'sv', 'eo', 'el', 'ms', 'lv']
  4264. - lang_list = []
  4265. - for __ in range(0, 8):
  4266. - lang_list = get_random_lang(long_lang_list, lang_list)
  4267. - random.shuffle(lang_list)
  4268. - if trigger.group(2) is None:
  4269. - try:
  4270. - phrase = (mangle_lines[trigger.sender.lower()], '')
  4271. - except:
  4272. - bot.reply("What do you want me to mangle?")
  4273. - return
  4274. - else:
  4275. - phrase = (trigger.group(2).strip(), '')
  4276. - if phrase[0] == '':
  4277. - bot.reply("What do you want me to mangle?")
  4278. - return
  4279. - for lang in lang_list:
  4280. - backup = phrase
  4281. - try:
  4282. - phrase = translate(phrase[0], 'en', lang)
  4283. - except:
  4284. - phrase = False
  4285. - if not phrase:
  4286. - phrase = backup
  4287. - break
  4288. -
  4289. - try:
  4290. - phrase = translate(phrase[0], lang, 'en')
  4291. - except:
  4292. - phrase = backup
  4293. - continue
  4294. -
  4295. - if not phrase:
  4296. - phrase = backup
  4297. - break
  4298. - bot.reply(phrase[0])
  4299. -
  4300. -
  4301. -@rule('(.*)')
  4302. -@priority('low')
  4303. -def collect_mangle_lines(bot, trigger):
  4304. - global mangle_lines
  4305. - mangle_lines[trigger.sender.lower()] = "%s said '%s'" % (trigger.nick, (trigger.group(0).strip()))
  4306. -
  4307. -
  4308. -if __name__ == "__main__":
  4309. - from sopel.test_tools import run_example_tests
  4310. - run_example_tests(__file__)
  4311. diff --git a/sopel/modules/unicode_info.py b/sopel/modules/unicode_info.py
  4312. deleted file mode 100644
  4313. index be5f44d..0000000
  4314. --- a/sopel/modules/unicode_info.py
  4315. +++ /dev/null
  4316. @@ -1,49 +0,0 @@
  4317. -# coding=utf-8
  4318. -"""Codepoints Module"""
  4319. -# Copyright 2013, Elsie Powell, embolalia.com
  4320. -# Copyright 2008, Sean B. Palmer, inamidst.com
  4321. -# Licensed under the Eiffel Forum License 2.
  4322. -from __future__ import unicode_literals, absolute_import, print_function, division
  4323. -import unicodedata
  4324. -import sys
  4325. -from sopel.module import commands, example, NOLIMIT
  4326. -
  4327. -if sys.version_info.major >= 3:
  4328. - unichr = chr
  4329. -
  4330. -
  4331. -@commands('u')
  4332. -@example('.u ‽', 'U+203D INTERROBANG (‽)')
  4333. -@example('.u 203D', 'U+203D INTERROBANG (‽)')
  4334. -def codepoint(bot, trigger):
  4335. - arg = trigger.group(2).strip()
  4336. - if len(arg) == 0:
  4337. - bot.reply('What code point do you want me to look up?')
  4338. - return NOLIMIT
  4339. - elif len(arg) > 1:
  4340. - if arg.startswith('U+'):
  4341. - arg = arg[2:]
  4342. - try:
  4343. - arg = unichr(int(arg, 16))
  4344. - except:
  4345. - bot.reply("That's not a valid code point.")
  4346. - return NOLIMIT
  4347. -
  4348. - # Get the hex value for the code point, and drop the 0x from the front
  4349. - point = str(hex(ord(u'' + arg)))[2:]
  4350. - # Make the hex 4 characters long with preceding 0s, and all upper case
  4351. - point = point.rjust(4, str('0')).upper()
  4352. - try:
  4353. - name = unicodedata.name(arg)
  4354. - except ValueError:
  4355. - return 'U+%s (No name found)' % point
  4356. -
  4357. - if not unicodedata.combining(arg):
  4358. - template = 'U+%s %s (%s)'
  4359. - else:
  4360. - template = 'U+%s %s (\xe2\x97\x8c%s)'
  4361. - bot.say(template % (point, name, arg))
  4362. -
  4363. -if __name__ == "__main__":
  4364. - from sopel.test_tools import run_example_tests
  4365. - run_example_tests(__file__)
  4366. diff --git a/sopel/modules/units.py b/sopel/modules/units.py
  4367. deleted file mode 100644
  4368. index 5355948..0000000
  4369. --- a/sopel/modules/units.py
  4370. +++ /dev/null
  4371. @@ -1,186 +0,0 @@
  4372. -# coding=utf-8
  4373. -"""
  4374. -units.py - Unit conversion module for Sopel
  4375. -Copyright © 2013, Elad Alfassa, <elad@fedoraproject.org>
  4376. -Copyright © 2013, Dimitri Molenaars, <tyrope@tyrope.nl>
  4377. -Licensed under the Eiffel Forum License 2.
  4378. -
  4379. -"""
  4380. -from __future__ import unicode_literals, absolute_import, print_function, division
  4381. -from sopel.module import commands, example, NOLIMIT
  4382. -import re
  4383. -
  4384. -find_temp = re.compile('(-?[0-9]*\.?[0-9]*)[ °]*(K|C|F)', re.IGNORECASE)
  4385. -find_length = re.compile('([0-9]*\.?[0-9]*)[ ]*(mile[s]?|mi|inch|in|foot|feet|ft|yard[s]?|yd|(?:milli|centi|kilo|)meter[s]?|[mkc]?m|ly|light-year[s]?|au|astronomical unit[s]?|parsec[s]?|pc)', re.IGNORECASE)
  4386. -find_mass = re.compile('([0-9]*\.?[0-9]*)[ ]*(lb|lbm|pound[s]?|ounce|oz|(?:kilo|)gram(?:me|)[s]?|[k]?g)', re.IGNORECASE)
  4387. -
  4388. -
  4389. -def f_to_c(temp):
  4390. - return (float(temp) - 32) * 5 / 9
  4391. -
  4392. -
  4393. -def c_to_k(temp):
  4394. - return temp + 273.15
  4395. -
  4396. -
  4397. -def c_to_f(temp):
  4398. - return (9.0 / 5.0 * temp + 32)
  4399. -
  4400. -
  4401. -def k_to_c(temp):
  4402. - return temp - 273.15
  4403. -
  4404. -
  4405. -@commands('temp')
  4406. -@example('.temp 100F', '37.78°C = 100.00°F = 310.93K')
  4407. -@example('.temp 100C', '100.00°C = 212.00°F = 373.15K')
  4408. -@example('.temp 100K', '-173.15°C = -279.67°F = 100.00K')
  4409. -def temperature(bot, trigger):
  4410. - """
  4411. - Convert temperatures
  4412. - """
  4413. - try:
  4414. - source = find_temp.match(trigger.group(2)).groups()
  4415. - except (AttributeError, TypeError):
  4416. - bot.reply("That's not a valid temperature.")
  4417. - return NOLIMIT
  4418. - unit = source[1].upper()
  4419. - numeric = float(source[0])
  4420. - celsius = 0
  4421. - if unit == 'C':
  4422. - celsius = numeric
  4423. - elif unit == 'F':
  4424. - celsius = f_to_c(numeric)
  4425. - elif unit == 'K':
  4426. - celsius = k_to_c(numeric)
  4427. -
  4428. - kelvin = c_to_k(celsius)
  4429. - fahrenheit = c_to_f(celsius)
  4430. - bot.reply("{:.2f}°C = {:.2f}°F = {:.2f}K".format(celsius, fahrenheit, kelvin))
  4431. -
  4432. -
  4433. -@commands('length', 'distance')
  4434. -@example('.distance 3m', '3.00m = 9 feet, 10.11 inches')
  4435. -@example('.distance 3km', '3.00km = 1.86 miles')
  4436. -@example('.distance 3 miles', '4.83km = 3.00 miles')
  4437. -@example('.distance 3 inch', '7.62cm = 3.00 inches')
  4438. -@example('.distance 3 feet', '91.44cm = 3 feet, 0.00 inches')
  4439. -@example('.distance 3 yards', '2.74m = 9 feet, 0.00 inches')
  4440. -@example('.distance 155cm', '1.55m = 5 feet, 1.02 inches')
  4441. -@example('.length 3 ly', '28382191417742.40km = 17635876112814.77 miles')
  4442. -@example('.length 3 au', '448793612.10km = 278867421.71 miles')
  4443. -@example('.length 3 parsec', '92570329129020.20km = 57520535754731.61 miles')
  4444. -def distance(bot, trigger):
  4445. - """
  4446. - Convert distances
  4447. - """
  4448. - try:
  4449. - source = find_length.match(trigger.group(2)).groups()
  4450. - except (AttributeError, TypeError):
  4451. - bot.reply("That's not a valid length unit.")
  4452. - return NOLIMIT
  4453. - unit = source[1].lower()
  4454. - numeric = float(source[0])
  4455. - meter = 0
  4456. - if unit in ("meters", "meter", "m"):
  4457. - meter = numeric
  4458. - elif unit in ("millimeters", "millimeter", "mm"):
  4459. - meter = numeric / 1000
  4460. - elif unit in ("kilometers", "kilometer", "km"):
  4461. - meter = numeric * 1000
  4462. - elif unit in ("miles", "mile", "mi"):
  4463. - meter = numeric / 0.00062137
  4464. - elif unit in ("inch", "in"):
  4465. - meter = numeric / 39.370
  4466. - elif unit in ("centimeters", "centimeter", "cm"):
  4467. - meter = numeric / 100
  4468. - elif unit in ("feet", "foot", "ft"):
  4469. - meter = numeric / 3.2808
  4470. - elif unit in ("yards", "yard", "yd"):
  4471. - meter = numeric / (3.2808 / 3)
  4472. - elif unit in ("light-year", "light-years", "ly"):
  4473. - meter = numeric * 9460730472580800
  4474. - elif unit in ("astronomical unit", "astronomical units", "au"):
  4475. - meter = numeric * 149597870700
  4476. - elif unit in ("parsec", "parsecs", "pc"):
  4477. - meter = numeric * 30856776376340068
  4478. -
  4479. - if meter >= 1000:
  4480. - metric_part = '{:.2f}km'.format(meter / 1000)
  4481. - elif meter < 0.01:
  4482. - metric_part = '{:.2f}mm'.format(meter * 1000)
  4483. - elif meter < 1:
  4484. - metric_part = '{:.2f}cm'.format(meter * 100)
  4485. - else:
  4486. - metric_part = '{:.2f}m'.format(meter)
  4487. -
  4488. - # Shit like this makes me hate being an American.
  4489. - inch = meter * 39.37
  4490. - foot = int(inch) // 12
  4491. - inch = inch - (foot * 12)
  4492. - yard = foot // 3
  4493. - mile = meter * 0.000621371192
  4494. -
  4495. - if yard > 500:
  4496. - stupid_part = '{:.2f} miles'.format(mile)
  4497. - else:
  4498. - parts = []
  4499. - if yard >= 100:
  4500. - parts.append('{} yards'.format(yard))
  4501. - foot -= (yard * 3)
  4502. -
  4503. - if foot == 1:
  4504. - parts.append('1 foot')
  4505. - elif foot != 0:
  4506. - parts.append('{:.0f} feet'.format(foot))
  4507. -
  4508. - parts.append('{:.2f} inches'.format(inch))
  4509. -
  4510. - stupid_part = ', '.join(parts)
  4511. -
  4512. - bot.reply('{} = {}'.format(metric_part, stupid_part))
  4513. -
  4514. -
  4515. -@commands('weight', 'mass')
  4516. -def mass(bot, trigger):
  4517. - """
  4518. - Convert mass
  4519. - """
  4520. - try:
  4521. - source = find_mass.match(trigger.group(2)).groups()
  4522. - except (AttributeError, TypeError):
  4523. - bot.reply("That's not a valid mass unit.")
  4524. - return NOLIMIT
  4525. - unit = source[1].lower()
  4526. - numeric = float(source[0])
  4527. - metric = 0
  4528. - if unit in ("gram", "grams", "gramme", "grammes", "g"):
  4529. - metric = numeric
  4530. - elif unit in ("kilogram", "kilograms", "kilogramme", "kilogrammes", "kg"):
  4531. - metric = numeric * 1000
  4532. - elif unit in ("lb", "lbm", "pound", "pounds"):
  4533. - metric = numeric * 453.59237
  4534. - elif unit in ("oz", "ounce"):
  4535. - metric = numeric * 28.35
  4536. -
  4537. - if metric >= 1000:
  4538. - metric_part = '{:.2f}kg'.format(metric / 1000)
  4539. - else:
  4540. - metric_part = '{:.2f}g'.format(metric)
  4541. -
  4542. - ounce = metric * .035274
  4543. - pound = int(ounce) // 16
  4544. - ounce = ounce - (pound * 16)
  4545. -
  4546. - if pound > 1:
  4547. - stupid_part = '{} pounds'.format(pound)
  4548. - if ounce > 0.01:
  4549. - stupid_part += ' {:.2f} ounces'.format(ounce)
  4550. - else:
  4551. - stupid_part = '{:.2f} oz'.format(ounce)
  4552. -
  4553. - bot.reply('{} = {}'.format(metric_part, stupid_part))
  4554. -
  4555. -if __name__ == "__main__":
  4556. - from sopel.test_tools import run_example_tests
  4557. - run_example_tests(__file__)
  4558. diff --git a/sopel/modules/uptime.py b/sopel/modules/uptime.py
  4559. deleted file mode 100644
  4560. index 7e3fe1d..0000000
  4561. --- a/sopel/modules/uptime.py
  4562. +++ /dev/null
  4563. @@ -1,27 +0,0 @@
  4564. -# coding=utf-8
  4565. -"""
  4566. -uptime.py - Uptime module
  4567. -Copyright 2014, Fabian Neundorf
  4568. -Licensed under the Eiffel Forum License 2.
  4569. -
  4570. -http://sopel.chat
  4571. -"""
  4572. -from __future__ import unicode_literals, absolute_import, print_function, division
  4573. -
  4574. -from sopel.module import commands
  4575. -import datetime
  4576. -
  4577. -
  4578. -def setup(bot):
  4579. - if "uptime" not in bot.memory:
  4580. - bot.memory["uptime"] = datetime.datetime.utcnow()
  4581. -
  4582. -
  4583. -@commands('uptime')
  4584. -def uptime(bot, trigger):
  4585. - """.uptime - Returns the uptime of Sopel."""
  4586. - delta = datetime.timedelta(seconds=round((datetime.datetime.utcnow() -
  4587. - bot.memory["uptime"])
  4588. - .total_seconds()))
  4589. - bot.say("I've been sitting here for {} and I keep "
  4590. - "going!".format(delta))
  4591. diff --git a/sopel/modules/url.py b/sopel/modules/url.py
  4592. deleted file mode 100644
  4593. index d2a83df..0000000
  4594. --- a/sopel/modules/url.py
  4595. +++ /dev/null
  4596. @@ -1,237 +0,0 @@
  4597. -# coding=utf-8
  4598. -"""URL title module"""
  4599. -# Copyright 2010-2011, Michael Yanovich, yanovich.net, Kenneth Sham
  4600. -# Copyright 2012-2013 Elsie Powell
  4601. -# Copyright 2013 Lior Ramati (firerogue517@gmail.com)
  4602. -# Copyright © 2014 Elad Alfassa <elad@fedoraproject.org>
  4603. -# Licensed under the Eiffel Forum License 2.
  4604. -from __future__ import unicode_literals, absolute_import, print_function, division
  4605. -
  4606. -import re
  4607. -from contextlib import closing
  4608. -from sopel import web, tools
  4609. -from sopel.module import commands, rule, example
  4610. -from sopel.config.types import ValidatedAttribute, ListAttribute, StaticSection
  4611. -
  4612. -import requests
  4613. -
  4614. -url_finder = None
  4615. -# These are used to clean up the title tag before actually parsing it. Not the
  4616. -# world's best way to do this, but it'll do for now.
  4617. -title_tag_data = re.compile('<(/?)title( [^>]+)?>', re.IGNORECASE)
  4618. -quoted_title = re.compile('[\'"]<title>[\'"]', re.IGNORECASE)
  4619. -# This is another regex that presumably does something important.
  4620. -re_dcc = re.compile(r'(?i)dcc\ssend')
  4621. -# This sets the maximum number of bytes that should be read in order to find
  4622. -# the title. We don't want it too high, or a link to a big file/stream will
  4623. -# just keep downloading until there's no more memory. 640k ought to be enough
  4624. -# for anybody.
  4625. -max_bytes = 655360
  4626. -
  4627. -
  4628. -class UrlSection(StaticSection):
  4629. - # TODO some validation rules maybe?
  4630. - exclude = ListAttribute('exclude')
  4631. - exclusion_char = ValidatedAttribute('exclusion_char', default='!')
  4632. -
  4633. -
  4634. -def configure(config):
  4635. - config.define_section('url', UrlSection)
  4636. - config.url.configure_setting(
  4637. - 'exclude',
  4638. - 'Enter regular expressions for each URL you would like to exclude.'
  4639. - )
  4640. - config.url.configure_setting(
  4641. - 'exclusion_char',
  4642. - 'Enter a character which can be prefixed to suppress URL titling'
  4643. - )
  4644. -
  4645. -
  4646. -def setup(bot=None):
  4647. - global url_finder
  4648. -
  4649. - # TODO figure out why this is needed, and get rid of it, because really?
  4650. - if not bot:
  4651. - return
  4652. - bot.config.define_section('url', UrlSection)
  4653. -
  4654. - if bot.config.url.exclude:
  4655. - regexes = [re.compile(s) for s in bot.config.url.exclude]
  4656. - else:
  4657. - regexes = []
  4658. -
  4659. - # We're keeping these in their own list, rather than putting then in the
  4660. - # callbacks list because 1, it's easier to deal with modules that are still
  4661. - # using this list, and not the newer callbacks list and 2, having a lambda
  4662. - # just to pass is kinda ugly.
  4663. - if not bot.memory.contains('url_exclude'):
  4664. - bot.memory['url_exclude'] = regexes
  4665. - else:
  4666. - exclude = bot.memory['url_exclude']
  4667. - if regexes:
  4668. - exclude.extend(regexes)
  4669. - bot.memory['url_exclude'] = exclude
  4670. -
  4671. - # Ensure that url_callbacks and last_seen_url are in memory
  4672. - if not bot.memory.contains('url_callbacks'):
  4673. - bot.memory['url_callbacks'] = tools.SopelMemory()
  4674. - if not bot.memory.contains('last_seen_url'):
  4675. - bot.memory['last_seen_url'] = tools.SopelMemory()
  4676. -
  4677. - url_finder = re.compile(r'(?u)(%s?(?:http|https|ftp)(?:://\S+))' %
  4678. - (bot.config.url.exclusion_char), re.IGNORECASE)
  4679. -
  4680. -
  4681. -@commands('title')
  4682. -@example('.title http://google.com', '[ Google ] - google.com')
  4683. -def title_command(bot, trigger):
  4684. - """
  4685. - Show the title or URL information for the given URL, or the last URL seen
  4686. - in this channel.
  4687. - """
  4688. - if not trigger.group(2):
  4689. - if trigger.sender not in bot.memory['last_seen_url']:
  4690. - return
  4691. - matched = check_callbacks(bot, trigger,
  4692. - bot.memory['last_seen_url'][trigger.sender],
  4693. - True)
  4694. - if matched:
  4695. - return
  4696. - else:
  4697. - urls = [bot.memory['last_seen_url'][trigger.sender]]
  4698. - else:
  4699. - urls = re.findall(url_finder, trigger)
  4700. -
  4701. - results = process_urls(bot, trigger, urls)
  4702. - for title, domain in results[:4]:
  4703. - bot.reply('[ %s ] - %s' % (title, domain))
  4704. -
  4705. -
  4706. -@rule('(?u).*(https?://\S+).*')
  4707. -def title_auto(bot, trigger):
  4708. - """
  4709. - Automatically show titles for URLs. For shortened URLs/redirects, find
  4710. - where the URL redirects to and show the title for that (or call a function
  4711. - from another module to give more information).
  4712. - """
  4713. - if re.match(bot.config.core.prefix + 'title', trigger):
  4714. - return
  4715. -
  4716. - # Avoid fetching known malicious links
  4717. - if 'safety_cache' in bot.memory and trigger in bot.memory['safety_cache']:
  4718. - if bot.memory['safety_cache'][trigger]['positives'] > 1:
  4719. - return
  4720. -
  4721. - urls = re.findall(url_finder, trigger)
  4722. - if len(urls) == 0:
  4723. - return
  4724. -
  4725. - results = process_urls(bot, trigger, urls)
  4726. - bot.memory['last_seen_url'][trigger.sender] = urls[-1]
  4727. -
  4728. - for title, domain in results[:4]:
  4729. - message = '[ %s ] - %s' % (title, domain)
  4730. - # Guard against responding to other instances of this bot.
  4731. - if message != trigger:
  4732. - bot.say(message)
  4733. -
  4734. -
  4735. -def process_urls(bot, trigger, urls):
  4736. - """
  4737. - For each URL in the list, ensure that it isn't handled by another module.
  4738. - If not, find where it redirects to, if anywhere. If that redirected URL
  4739. - should be handled by another module, dispatch the callback for it.
  4740. - Return a list of (title, hostname) tuples for each URL which is not handled by
  4741. - another module.
  4742. - """
  4743. -
  4744. - results = []
  4745. - for url in urls:
  4746. - if not url.startswith(bot.config.url.exclusion_char):
  4747. - # Magic stuff to account for international domain names
  4748. - try:
  4749. - url = web.iri_to_uri(url)
  4750. - except:
  4751. - pass
  4752. - # First, check that the URL we got doesn't match
  4753. - matched = check_callbacks(bot, trigger, url, False)
  4754. - if matched:
  4755. - continue
  4756. - # Finally, actually show the URL
  4757. - title = find_title(url)
  4758. - if title:
  4759. - results.append((title, get_hostname(url)))
  4760. - return results
  4761. -
  4762. -
  4763. -def check_callbacks(bot, trigger, url, run=True):
  4764. - """
  4765. - Check the given URL against the callbacks list. If it matches, and ``run``
  4766. - is given as ``True``, run the callback function, otherwise pass. Returns
  4767. - ``True`` if the url matched anything in the callbacks list.
  4768. - """
  4769. - # Check if it matches the exclusion list first
  4770. - matched = any(regex.search(url) for regex in bot.memory['url_exclude'])
  4771. - # Then, check if there's anything in the callback list
  4772. - for regex, function in tools.iteritems(bot.memory['url_callbacks']):
  4773. - match = regex.search(url)
  4774. - if match:
  4775. - if run:
  4776. - function(bot, trigger, match)
  4777. - matched = True
  4778. - return matched
  4779. -
  4780. -
  4781. -def find_title(url):
  4782. - """Return the title for the given URL."""
  4783. - response = requests.get(url, stream=True)
  4784. - try:
  4785. - content = ''
  4786. - for byte in response.iter_content(chunk_size=512, decode_unicode=True):
  4787. - if not isinstance(byte, bytes):
  4788. - content += byte
  4789. - else:
  4790. - break
  4791. - if '</title>' in content or len(content) > max_bytes:
  4792. - break
  4793. - except UnicodeDecodeError:
  4794. - return # Fail silently when data can't be decoded
  4795. - finally:
  4796. - # need to close the connexion because we have not read all the data
  4797. - response.close()
  4798. -
  4799. - # Some cleanup that I don't really grok, but was in the original, so
  4800. - # we'll keep it (with the compiled regexes made global) for now.
  4801. - content = title_tag_data.sub(r'<\1title>', content)
  4802. - content = quoted_title.sub('', content)
  4803. -
  4804. - start = content.find('<title>')
  4805. - end = content.find('</title>')
  4806. - if start == -1 or end == -1:
  4807. - return
  4808. - title = web.decode(content[start + 7:end])
  4809. - title = title.strip()[:200]
  4810. -
  4811. - title = ' '.join(title.split()) # cleanly remove multiple spaces
  4812. -
  4813. - # More cryptic regex substitutions. This one looks to be myano's invention.
  4814. - title = re_dcc.sub('', title)
  4815. -
  4816. - return title or None
  4817. -
  4818. -
  4819. -def get_hostname(url):
  4820. - idx = 7
  4821. - if url.startswith('https://'):
  4822. - idx = 8
  4823. - elif url.startswith('ftp://'):
  4824. - idx = 6
  4825. - hostname = url[idx:]
  4826. - slash = hostname.find('/')
  4827. - if slash != -1:
  4828. - hostname = hostname[:slash]
  4829. - return hostname
  4830. -
  4831. -if __name__ == "__main__":
  4832. - from sopel.test_tools import run_example_tests
  4833. - run_example_tests(__file__)
  4834. diff --git a/sopel/modules/version.py b/sopel/modules/version.py
  4835. deleted file mode 100644
  4836. index a1b4219..0000000
  4837. --- a/sopel/modules/version.py
  4838. +++ /dev/null
  4839. @@ -1,81 +0,0 @@
  4840. -# coding=utf-8
  4841. -"""
  4842. -version.py - Sopel Version Module
  4843. -Copyright 2009, Silas Baronda
  4844. -Copyright 2014, Dimitri Molenaars <tyrope@tyrope.nl>
  4845. -Licensed under the Eiffel Forum License 2.
  4846. -
  4847. -http://sopel.chat
  4848. -"""
  4849. -from __future__ import unicode_literals, absolute_import, print_function, division
  4850. -
  4851. -from datetime import datetime
  4852. -import sopel
  4853. -import re
  4854. -from os import path
  4855. -
  4856. -log_line = re.compile('\S+ (\S+) (.*? <.*?>) (\d+) (\S+)\tcommit[^:]*: (.+)')
  4857. -
  4858. -
  4859. -def git_info():
  4860. - repo = path.join(path.dirname(path.dirname(path.dirname(__file__))), '.git')
  4861. - head = path.join(repo, 'HEAD')
  4862. - if path.isfile(head):
  4863. - with open(head) as h:
  4864. - head_loc = h.readline()[5:-1] # strip ref: and \n
  4865. - head_file = path.join(repo, head_loc)
  4866. - if path.isfile(head_file):
  4867. - with open(head_file) as h:
  4868. - sha = h.readline()
  4869. - if sha:
  4870. - return sha
  4871. -
  4872. -
  4873. -@sopel.module.commands('version')
  4874. -def version(bot, trigger):
  4875. - """Display the latest commit version, if Sopel is running in a git repo."""
  4876. - release = sopel.__version__
  4877. - sha = git_info()
  4878. - if not sha:
  4879. - msg = 'Sopel v. ' + release
  4880. - if release[-4:] == '-git':
  4881. - msg += ' at unknown commit.'
  4882. - bot.reply(msg)
  4883. - return
  4884. -
  4885. - bot.reply("Sopel v. {} at commit: {}".format(sopel.__version__, sha))
  4886. -
  4887. -
  4888. -@sopel.module.intent('VERSION')
  4889. -@sopel.module.rate(20)
  4890. -@sopel.module.rule('.*')
  4891. -def ctcp_version(bot, trigger):
  4892. - print('wat')
  4893. - bot.write(('NOTICE', trigger.nick),
  4894. - '\x01VERSION Sopel IRC Bot version %s\x01' % sopel.__version__)
  4895. -
  4896. -
  4897. -@sopel.module.rule('\x01SOURCE\x01')
  4898. -@sopel.module.rate(20)
  4899. -def ctcp_source(bot, trigger):
  4900. - bot.write(('NOTICE', trigger.nick),
  4901. - '\x01SOURCE https://github.com/sopel-irc/sopel/\x01')
  4902. -
  4903. -
  4904. -@sopel.module.rule('\x01PING\s(.*)\x01')
  4905. -@sopel.module.rate(10)
  4906. -def ctcp_ping(bot, trigger):
  4907. - text = trigger.group()
  4908. - text = text.replace("PING ", "")
  4909. - text = text.replace("\x01", "")
  4910. - bot.write(('NOTICE', trigger.nick),
  4911. - '\x01PING {0}\x01'.format(text))
  4912. -
  4913. -
  4914. -@sopel.module.rule('\x01TIME\x01')
  4915. -@sopel.module.rate(20)
  4916. -def ctcp_time(bot, trigger):
  4917. - dt = datetime.now()
  4918. - current_time = dt.strftime("%A, %d. %B %Y %I:%M%p")
  4919. - bot.write(('NOTICE', trigger.nick),
  4920. - '\x01TIME {0}\x01'.format(current_time))
  4921. diff --git a/sopel/modules/weather.py b/sopel/modules/weather.py
  4922. deleted file mode 100644
  4923. index ea03b46..0000000
  4924. --- a/sopel/modules/weather.py
  4925. +++ /dev/null
  4926. @@ -1,183 +0,0 @@
  4927. -# coding=utf-8
  4928. -# Copyright 2008, Sean B. Palmer, inamidst.com
  4929. -# Copyright 2012, Elsie Powell, embolalia.com
  4930. -# Licensed under the Eiffel Forum License 2.
  4931. -from __future__ import unicode_literals, absolute_import, print_function, division
  4932. -
  4933. -from sopel import web
  4934. -from sopel.module import commands, example, NOLIMIT
  4935. -
  4936. -import xmltodict
  4937. -
  4938. -
  4939. -def woeid_search(query):
  4940. - """
  4941. - Find the first Where On Earth ID for the given query. Result is the etree
  4942. - node for the result, so that location data can still be retrieved. Returns
  4943. - None if there is no result, or the woeid field is empty.
  4944. - """
  4945. - query = 'q=select * from geo.places where text="%s"' % query
  4946. - body = web.get('http://query.yahooapis.com/v1/public/yql?' + query,
  4947. - dont_decode=True)
  4948. - parsed = xmltodict.parse(body).get('query')
  4949. - results = parsed.get('results')
  4950. - if results is None or results.get('place') is None:
  4951. - return None
  4952. - if type(results.get('place')) is list:
  4953. - return results.get('place')[0]
  4954. - return results.get('place')
  4955. -
  4956. -
  4957. -def get_cover(parsed):
  4958. - try:
  4959. - condition = parsed['channel']['item']['yweather:condition']
  4960. - except KeyError:
  4961. - return 'unknown'
  4962. - text = condition['@text']
  4963. - # code = int(condition['code'])
  4964. - # TODO parse code to get those little icon thingies.
  4965. - return text
  4966. -
  4967. -
  4968. -def get_temp(parsed):
  4969. - try:
  4970. - condition = parsed['channel']['item']['yweather:condition']
  4971. - temp = int(condition['@temp'])
  4972. - except (KeyError, ValueError):
  4973. - return 'unknown'
  4974. - f = round((temp * 1.8) + 32, 2)
  4975. - return (u'%d\u00B0C (%d\u00B0F)' % (temp, f))
  4976. -
  4977. -
  4978. -def get_humidity(parsed):
  4979. - try:
  4980. - humidity = parsed['channel']['yweather:atmosphere']['@humidity']
  4981. - except (KeyError, ValueError):
  4982. - return 'unknown'
  4983. - return "Humidity: %s%%" % humidity
  4984. -
  4985. -
  4986. -def get_wind(parsed):
  4987. - try:
  4988. - wind_data = parsed['channel']['yweather:wind']
  4989. - kph = float(wind_data['@speed'])
  4990. - m_s = float(round(kph / 3.6, 1))
  4991. - speed = int(round(kph / 1.852, 0))
  4992. - degrees = int(wind_data['@direction'])
  4993. - except (KeyError, ValueError):
  4994. - return 'unknown'
  4995. -
  4996. - if speed < 1:
  4997. - description = 'Calm'
  4998. - elif speed < 4:
  4999. - description = 'Light air'
  5000. - elif speed < 7:
  5001. - description = 'Light breeze'
  5002. - elif speed < 11:
  5003. - description = 'Gentle breeze'
  5004. - elif speed < 16:
  5005. - description = 'Moderate breeze'
  5006. - elif speed < 22:
  5007. - description = 'Fresh breeze'
  5008. - elif speed < 28:
  5009. - description = 'Strong breeze'
  5010. - elif speed < 34:
  5011. - description = 'Near gale'
  5012. - elif speed < 41:
  5013. - description = 'Gale'
  5014. - elif speed < 48:
  5015. - description = 'Strong gale'
  5016. - elif speed < 56:
  5017. - description = 'Storm'
  5018. - elif speed < 64:
  5019. - description = 'Violent storm'
  5020. - else:
  5021. - description = 'Hurricane'
  5022. -
  5023. - if (degrees <= 22.5) or (degrees > 337.5):
  5024. - degrees = u'\u2193'
  5025. - elif (degrees > 22.5) and (degrees <= 67.5):
  5026. - degrees = u'\u2199'
  5027. - elif (degrees > 67.5) and (degrees <= 112.5):
  5028. - degrees = u'\u2190'
  5029. - elif (degrees > 112.5) and (degrees <= 157.5):
  5030. - degrees = u'\u2196'
  5031. - elif (degrees > 157.5) and (degrees <= 202.5):
  5032. - degrees = u'\u2191'
  5033. - elif (degrees > 202.5) and (degrees <= 247.5):
  5034. - degrees = u'\u2197'
  5035. - elif (degrees > 247.5) and (degrees <= 292.5):
  5036. - degrees = u'\u2192'
  5037. - elif (degrees > 292.5) and (degrees <= 337.5):
  5038. - degrees = u'\u2198'
  5039. -
  5040. - return description + ' ' + str(m_s) + 'm/s (' + degrees + ')'
  5041. -
  5042. -
  5043. -@commands('weather', 'wea')
  5044. -@example('.weather London')
  5045. -def weather(bot, trigger):
  5046. - """.weather location - Show the weather at the given location."""
  5047. -
  5048. - location = trigger.group(2)
  5049. - woeid = ''
  5050. - if not location:
  5051. - woeid = bot.db.get_nick_value(trigger.nick, 'woeid')
  5052. - if not woeid:
  5053. - return bot.msg(trigger.sender, "I don't know where you live. " +
  5054. - 'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.')
  5055. - else:
  5056. - location = location.strip()
  5057. - woeid = bot.db.get_nick_value(location, 'woeid')
  5058. - if woeid is None:
  5059. - first_result = woeid_search(location)
  5060. - if first_result is not None:
  5061. - woeid = first_result.get('woeid')
  5062. -
  5063. - if not woeid:
  5064. - return bot.reply("I don't know where that is.")
  5065. -
  5066. - query = web.urlencode({'w': woeid, 'u': 'c'})
  5067. - raw = web.get('http://weather.yahooapis.com/forecastrss?' + query,
  5068. - dont_decode=True)
  5069. - parsed = xmltodict.parse(raw).get('rss')
  5070. - location = parsed.get('channel').get('title')
  5071. -
  5072. - cover = get_cover(parsed)
  5073. - temp = get_temp(parsed)
  5074. - humidity = get_humidity(parsed)
  5075. - wind = get_wind(parsed)
  5076. - bot.say(u'%s: %s, %s, %s, %s' % (location, cover, temp, humidity, wind))
  5077. -
  5078. -
  5079. -@commands('setlocation', 'setwoeid')
  5080. -@example('.setlocation Columbus, OH')
  5081. -def update_woeid(bot, trigger):
  5082. - """Set your default weather location."""
  5083. - if not trigger.group(2):
  5084. - bot.reply('Give me a location, like "Washington, DC" or "London".')
  5085. - return NOLIMIT
  5086. -
  5087. - first_result = woeid_search(trigger.group(2))
  5088. - if first_result is None:
  5089. - return bot.reply("I don't know where that is.")
  5090. -
  5091. - woeid = first_result.get('woeid')
  5092. -
  5093. - bot.db.set_nick_value(trigger.nick, 'woeid', woeid)
  5094. -
  5095. - neighborhood = first_result.get('locality2') or ''
  5096. - if neighborhood:
  5097. - neighborhood = neighborhood.get('#text') + ', '
  5098. - city = first_result.get('locality1') or ''
  5099. - # This is to catch cases like 'Bawlf, Alberta' where the location is
  5100. - # thought to be a "LocalAdmin" rather than a "Town"
  5101. - if city:
  5102. - city = city.get('#text')
  5103. - else:
  5104. - city = first_result.get('name')
  5105. - state = first_result.get('admin1').get('#text') or ''
  5106. - country = first_result.get('country').get('#text') or ''
  5107. - uzip = first_result.get('postal').get('#text') or ''
  5108. - bot.reply('I now have you at WOEID %s (%s%s, %s, %s %s)' %
  5109. - (woeid, neighborhood, city, state, country, uzip))
  5110. diff --git a/sopel/modules/wikipedia.py b/sopel/modules/wikipedia.py
  5111. deleted file mode 100644
  5112. index 69dffb9..0000000
  5113. --- a/sopel/modules/wikipedia.py
  5114. +++ /dev/null
  5115. @@ -1,133 +0,0 @@
  5116. -# coding=utf-8
  5117. -# Copyright 2013 Elsie Powell - embolalia.com
  5118. -# Licensed under the Eiffel Forum License 2.
  5119. -from __future__ import unicode_literals, absolute_import, print_function, division
  5120. -from sopel import web, tools
  5121. -from sopel.config.types import StaticSection, ValidatedAttribute
  5122. -from sopel.module import NOLIMIT, commands, example, rule
  5123. -import json
  5124. -import re
  5125. -
  5126. -import sys
  5127. -if sys.version_info.major < 3:
  5128. - from urlparse import unquote as _unquote
  5129. - unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')
  5130. -else:
  5131. - from urllib.parse import unquote
  5132. -
  5133. -REDIRECT = re.compile(r'^REDIRECT (.*)')
  5134. -
  5135. -
  5136. -class WikipediaSection(StaticSection):
  5137. - default_lang = ValidatedAttribute('default_lang', default='en')
  5138. - """The default language to find articles from."""
  5139. - lang_per_channel = ValidatedAttribute('lang_per_channel')
  5140. -
  5141. -
  5142. -def setup(bot):
  5143. - bot.config.define_section('wikipedia', WikipediaSection)
  5144. -
  5145. - regex = re.compile('([a-z]+).(wikipedia.org/wiki/)([^ ]+)')
  5146. - if not bot.memory.contains('url_callbacks'):
  5147. - bot.memory['url_callbacks'] = tools.SopelMemory()
  5148. - bot.memory['url_callbacks'][regex] = mw_info
  5149. -
  5150. -
  5151. -def configure(config):
  5152. - config.define_section('wikipedia', WikipediaSection)
  5153. - config.wikipedia.configure_setting(
  5154. - 'default_lang',
  5155. - "Enter the default language to find articles from."
  5156. - )
  5157. -
  5158. -
  5159. -def mw_search(server, query, num):
  5160. - """
  5161. - Searches the specified MediaWiki server for the given query, and returns
  5162. - the specified number of results.
  5163. - """
  5164. - search_url = ('http://%s/w/api.php?format=json&action=query'
  5165. - '&list=search&srlimit=%d&srprop=timestamp&srwhat=text'
  5166. - '&srsearch=') % (server, num)
  5167. - search_url += query
  5168. - query = json.loads(web.get(search_url))
  5169. - if 'query' in query:
  5170. - query = query['query']['search']
  5171. - return [r['title'] for r in query]
  5172. - else:
  5173. - return None
  5174. -
  5175. -
  5176. -def say_snippet(bot, server, query, show_url=True):
  5177. - page_name = query.replace('_', ' ')
  5178. - query = query.replace(' ', '_')
  5179. - snippet = mw_snippet(server, query)
  5180. - msg = '[WIKIPEDIA] {} | "{}"'.format(page_name, snippet)
  5181. - if show_url:
  5182. - msg = msg + ' | https://{}/wiki/{}'.format(server, query)
  5183. - bot.say(msg)
  5184. -
  5185. -
  5186. -def mw_snippet(server, query):
  5187. - """
  5188. - Retrives a snippet of the specified length from the given page on the given
  5189. - server.
  5190. - """
  5191. - snippet_url = ('https://' + server + '/w/api.php?format=json'
  5192. - '&action=query&prop=extracts&exintro&explaintext'
  5193. - '&exchars=300&redirects&titles=')
  5194. - snippet_url += query
  5195. - snippet = json.loads(web.get(snippet_url))
  5196. - snippet = snippet['query']['pages']
  5197. -
  5198. - # For some reason, the API gives the page *number* as the key, so we just
  5199. - # grab the first page number in the results.
  5200. - snippet = snippet[list(snippet.keys())[0]]
  5201. -
  5202. - return snippet['extract']
  5203. -
  5204. -
  5205. -@rule('.*/([a-z]+\.wikipedia.org)/wiki/([^ ]+).*')
  5206. -def mw_info(bot, trigger, found_match=None):
  5207. - """
  5208. - Retrives a snippet of the specified length from the given page on the given
  5209. - server.
  5210. - """
  5211. - match = found_match or trigger
  5212. - say_snippet(bot, match.group(1), unquote(match.group(2)), show_url=False)
  5213. -
  5214. -
  5215. -@commands('w', 'wiki', 'wik')
  5216. -@example('.w San Francisco')
  5217. -def wikipedia(bot, trigger):
  5218. - lang = bot.config.wikipedia.default_lang
  5219. -
  5220. - #change lang if channel has custom language set
  5221. - if (trigger.sender and not trigger.sender.is_nick() and
  5222. - bot.config.wikipedia.lang_per_channel):
  5223. - customlang = re.search('(' + trigger.sender + '):(\w+)',
  5224. - bot.config.wikipedia.lang_per_channel)
  5225. - if customlang is not None:
  5226. - lang = customlang.group(2)
  5227. -
  5228. - if trigger.group(2) is None:
  5229. - bot.reply("What do you want me to look up?")
  5230. - return NOLIMIT
  5231. -
  5232. - query = trigger.group(2)
  5233. - args = re.search(r'^-([a-z]{2,12})\s(.*)', query)
  5234. - if args is not None:
  5235. - lang = args.group(1)
  5236. - query = args.group(2)
  5237. -
  5238. - if not query:
  5239. - bot.reply('What do you want me to look up?')
  5240. - return NOLIMIT
  5241. - server = lang + '.wikipedia.org'
  5242. - query = mw_search(server, query, 1)
  5243. - if not query:
  5244. - bot.reply("I can't find any results for that.")
  5245. - return NOLIMIT
  5246. - else:
  5247. - query = query[0]
  5248. - say_snippet(bot, server, query)
  5249. diff --git a/sopel/modules/wiktionary.py b/sopel/modules/wiktionary.py
  5250. deleted file mode 100644
  5251. index 844181c..0000000
  5252. --- a/sopel/modules/wiktionary.py
  5253. +++ /dev/null
  5254. @@ -1,101 +0,0 @@
  5255. -# coding=utf-8
  5256. -"""
  5257. -wiktionary.py - Sopel Wiktionary Module
  5258. -Copyright 2009, Sean B. Palmer, inamidst.com
  5259. -Licensed under the Eiffel Forum License 2.
  5260. -
  5261. -http://sopel.chat
  5262. -"""
  5263. -from __future__ import unicode_literals, absolute_import, print_function, division
  5264. -
  5265. -import re
  5266. -from sopel import web
  5267. -from sopel.module import commands, example
  5268. -
  5269. -uri = 'http://en.wiktionary.org/w/index.php?title=%s&printable=yes'
  5270. -r_tag = re.compile(r'<[^>]+>')
  5271. -r_ul = re.compile(r'(?ims)<ul>.*?</ul>')
  5272. -
  5273. -
  5274. -def text(html):
  5275. - text = r_tag.sub('', html).strip()
  5276. - text = text.replace('\n', ' ')
  5277. - text = text.replace('\r', '')
  5278. - text = text.replace('(intransitive', '(intr.')
  5279. - text = text.replace('(transitive', '(trans.')
  5280. - return text
  5281. -
  5282. -
  5283. -def wikt(word):
  5284. - bytes = web.get(uri % web.quote(word))
  5285. - bytes = r_ul.sub('', bytes)
  5286. -
  5287. - mode = None
  5288. - etymology = None
  5289. - definitions = {}
  5290. - for line in bytes.splitlines():
  5291. - if 'id="Etymology"' in line:
  5292. - mode = 'etymology'
  5293. - elif 'id="Noun"' in line:
  5294. - mode = 'noun'
  5295. - elif 'id="Verb"' in line:
  5296. - mode = 'verb'
  5297. - elif 'id="Adjective"' in line:
  5298. - mode = 'adjective'
  5299. - elif 'id="Adverb"' in line:
  5300. - mode = 'adverb'
  5301. - elif 'id="Interjection"' in line:
  5302. - mode = 'interjection'
  5303. - elif 'id="Particle"' in line:
  5304. - mode = 'particle'
  5305. - elif 'id="Preposition"' in line:
  5306. - mode = 'preposition'
  5307. - elif 'id="' in line:
  5308. - mode = None
  5309. -
  5310. - elif (mode == 'etmyology') and ('<p>' in line):
  5311. - etymology = text(line)
  5312. - elif (mode is not None) and ('<li>' in line):
  5313. - definitions.setdefault(mode, []).append(text(line))
  5314. -
  5315. - if '<hr' in line:
  5316. - break
  5317. - return etymology, definitions
  5318. -
  5319. -parts = ('preposition', 'particle', 'noun', 'verb',
  5320. - 'adjective', 'adverb', 'interjection')
  5321. -
  5322. -
  5323. -def format(result, definitions, number=2):
  5324. - for part in parts:
  5325. - if part in definitions:
  5326. - defs = definitions[part][:number]
  5327. - result += u' — {}: '.format(part)
  5328. - n = ['%s. %s' % (i + 1, e.strip(' .')) for i, e in enumerate(defs)]
  5329. - result += ', '.join(n)
  5330. - return result.strip(' .,')
  5331. -
  5332. -
  5333. -@commands('wt', 'define', 'dict')
  5334. -@example('.wt bailiwick')
  5335. -def wiktionary(bot, trigger):
  5336. - """Look up a word on Wiktionary."""
  5337. - word = trigger.group(2)
  5338. - if word is None:
  5339. - bot.reply('You must tell me what to look up!')
  5340. - return
  5341. -
  5342. - _etymology, definitions = wikt(word)
  5343. - if not definitions:
  5344. - bot.say("Couldn't get any definitions for %s." % word)
  5345. - return
  5346. -
  5347. - result = format(word, definitions)
  5348. - if len(result) < 150:
  5349. - result = format(word, definitions, 3)
  5350. - if len(result) < 150:
  5351. - result = format(word, definitions, 5)
  5352. -
  5353. - if len(result) > 300:
  5354. - result = result[:295] + '[...]'
  5355. - bot.say(result)
  5356. diff --git a/sopel/modules/xkcd.py b/sopel/modules/xkcd.py
  5357. deleted file mode 100644
  5358. index d2baef9..0000000
  5359. --- a/sopel/modules/xkcd.py
  5360. +++ /dev/null
  5361. @@ -1,105 +0,0 @@
  5362. -# coding=utf-8
  5363. -# Copyright 2010, Michael Yanovich (yanovich.net), and Morgan Goose
  5364. -# Copyright 2012, Lior Ramati
  5365. -# Copyright 2013, Elsie Powell (embolalia.com)
  5366. -# Licensed under the Eiffel Forum License 2.
  5367. -from __future__ import unicode_literals, absolute_import, print_function, division
  5368. -
  5369. -import json
  5370. -import random
  5371. -import re
  5372. -import requests
  5373. -from sopel import web
  5374. -from sopel.modules.search import google_search
  5375. -from sopel.module import commands
  5376. -
  5377. -ignored_sites = [
  5378. - # For google searching
  5379. - 'almamater.xkcd.com',
  5380. - 'blog.xkcd.com',
  5381. - 'blag.xkcd.com',
  5382. - 'forums.xkcd.com',
  5383. - 'fora.xkcd.com',
  5384. - 'forums3.xkcd.com',
  5385. - 'store.xkcd.com',
  5386. - 'wiki.xkcd.com',
  5387. - 'what-if.xkcd.com',
  5388. -]
  5389. -sites_query = ' site:xkcd.com -site:' + ' -site:'.join(ignored_sites)
  5390. -
  5391. -
  5392. -def get_info(number=None):
  5393. - if number:
  5394. - url = 'http://xkcd.com/{}/info.0.json'.format(number)
  5395. - else:
  5396. - url = 'http://xkcd.com/info.0.json'
  5397. - data = requests.get(url).json()
  5398. - data['url'] = 'http://xkcd.com/' + str(data['num'])
  5399. - return data
  5400. -
  5401. -
  5402. -def google(query):
  5403. - url = google_search(query + sites_query)
  5404. - if not url:
  5405. - return None
  5406. - match = re.match('(?:https?://)?xkcd.com/(\d+)/?', url)
  5407. - if match:
  5408. - return match.group(1)
  5409. -
  5410. -
  5411. -@commands('xkcd')
  5412. -def xkcd(bot, trigger):
  5413. - """
  5414. - .xkcd - Finds an xkcd comic strip. Takes one of 3 inputs:
  5415. - If no input is provided it will return a random comic
  5416. - If numeric input is provided it will return that comic, or the nth-latest
  5417. - comic if the number is non-positive
  5418. - If non-numeric input is provided it will return the first google result for those keywords on the xkcd.com site
  5419. - """
  5420. - # get latest comic for rand function and numeric input
  5421. - latest = get_info()
  5422. - max_int = latest['num']
  5423. -
  5424. - # if no input is given (pre - lior's edits code)
  5425. - if not trigger.group(2): # get rand comic
  5426. - random.seed()
  5427. - requested = get_info(random.randint(1, max_int + 1))
  5428. - else:
  5429. - query = trigger.group(2).strip()
  5430. -
  5431. - numbered = re.match(r"^(#|\+|-)?(\d+)$", query)
  5432. - if numbered:
  5433. - query = int(numbered.group(2))
  5434. - if numbered.group(1) == "-":
  5435. - query = -query
  5436. - if query > max_int:
  5437. - bot.say(("Sorry, comic #{} hasn't been posted yet. "
  5438. - "The last comic was #{}").format(query, max_int))
  5439. - return
  5440. - elif query <= -max_int:
  5441. - bot.say(("Sorry, but there were only {} comics "
  5442. - "released yet so far").format(max_int))
  5443. - return
  5444. - elif abs(query) == 0:
  5445. - requested = latest
  5446. - elif query == 404 or max_int + query == 404:
  5447. - bot.say("404 - Not Found") # don't error on that one
  5448. - return
  5449. - elif query > 0:
  5450. - requested = get_info(query)
  5451. - else:
  5452. - # Negative: go back that many from current
  5453. - requested = get_info(max_int + query)
  5454. - else:
  5455. - # Non-number: google.
  5456. - if (query.lower() == "latest" or query.lower() == "newest"):
  5457. - requested = latest
  5458. - else:
  5459. - number = google(query)
  5460. - if not number:
  5461. - bot.say('Could not find any comics for that query.')
  5462. - return
  5463. - requested = get_info(number)
  5464. -
  5465. - message = '{} [{}]'.format(requested['url'], requested['title'])
  5466. - bot.say(message)
  5467. diff --git a/sopel/trigger.py b/sopel/trigger.py
  5468. index 1a03744..a91d5c4 100644
  5469. --- a/sopel/trigger.py
  5470. +++ b/sopel/trigger.py
  5471. @@ -50,9 +50,9 @@ class PreTrigger(object):
  5472.  
  5473. # TODO note what this is doing and why
  5474. if ' :' in line:
  5475. - argstr, text = line.split(' :', 1)
  5476. + argstr, self.text = line.split(' :', 1)
  5477. self.args = argstr.split(' ')
  5478. - self.args.append(text)
  5479. + self.args.append(self.text)
  5480. else:
  5481. self.args = line.split(' ')
  5482. self.text = self.args[-1]
  5483. @@ -141,6 +141,7 @@ class Trigger(unicode):
  5484. ``('#example', '-m')``
  5485. """
  5486. tags = property(lambda self: self._pretrigger.tags)
  5487. + text = property(lambda self: self._pretrigger.text)
  5488. """A map of the IRCv3 message tags on the message."""
  5489. admin = property(lambda self: self._admin)
  5490. """True if the nick which triggered the command is one of the bot's admins.
  5491. --
  5492. 2.4.6
  5493.  
  5494.  
  5495. From 4444bd1d630f85f6937a85b2c37ef1c7c7badf23 Mon Sep 17 00:00:00 2001
  5496. From: S00ng <???>
  5497. Date: Sun, 1 May 2016 15:22:08 +0200
  5498. Subject: [PATCH 2/4] init machine learning
  5499.  
  5500. ---
  5501. sopel/modules/helloworld.py | 18 ++++++++++++++++++
  5502. sopeldatawriter.py | 10 ++++++++++
  5503. 2 files changed, 28 insertions(+)
  5504. create mode 100644 sopel/modules/helloworld.py
  5505. create mode 100644 sopeldatawriter.py
  5506.  
  5507. diff --git a/sopel/modules/helloworld.py b/sopel/modules/helloworld.py
  5508. new file mode 100644
  5509. index 0000000..a7553cf
  5510. --- /dev/null
  5511. +++ b/sopel/modules/helloworld.py
  5512. @@ -0,0 +1,18 @@
  5513. +from sopel import module
  5514. +import sopeldatawriter
  5515. +
  5516. +@module.rule('\w*')
  5517. +def check(bot, trigger):
  5518. + sopeldatawriter.user_data_write_csv(trigger.nick,trigger.text,"testfile.csv")
  5519. + '''
  5520. +@module.rule('hello|(hello\s*(everyone|d4t4))')
  5521. +def hi(bot, trigger):
  5522. + bot.say('Hello, ' + trigger.nick)
  5523. +@module.rule('[a-zA-Z0-9_\s]*d4t4[a-zA-Z0-9_\s]*new[a-zA-Z0-9_\s]*here[a-zA-Z0-9_\s]*')
  5524. +def new(bot, trigger):
  5525. + bot.say('Yes I am new here')
  5526. +@module.rule('\s*\w*\s*d4t4\s*\w*\s*introduce\s*\w*\s*yourself\s*\w*\s*')
  5527. +def introduce(bot, trigger):
  5528. + bot.say('My Name is D4t4. I am 1 day old, I have no gender , I am programmed from s00ng')
  5529. + bot.say('I am a sopel module')
  5530. +'''
  5531. diff --git a/sopeldatawriter.py b/sopeldatawriter.py
  5532. new file mode 100644
  5533. index 0000000..77910cb
  5534. --- /dev/null
  5535. +++ b/sopeldatawriter.py
  5536. @@ -0,0 +1,10 @@
  5537. +import csv
  5538. +
  5539. +def user_data_write_csv(nick,text,file):
  5540. + with open(file, 'a') as csvfile:
  5541. + csvwriter = csv.writer(csvfile, delimiter=' ',quotechar='|', quoting=csv.QUOTE_MINIMAL)
  5542. + csvwriter.writerow([nick,text])
  5543. +def user_data_write_raw(nick,text,file):
  5544. + with open(file, "a") as f:
  5545. + data= "".join([trigger.nick ,"\n", trigger.text ,"\n"])
  5546. + f.write(data)
  5547. --
  5548. 2.4.6
  5549.  
  5550.  
  5551. From 89c614e45522e8c6066b7d006d874b8f68e4342d Mon Sep 17 00:00:00 2001
  5552. From: s00ng <???>
  5553. Date: Fri, 3 Jun 2016 22:18:16 +0200
  5554. Subject: [PATCH 3/4] fix KeyError with multiple channels
  5555.  
  5556. ---
  5557. sopel/tools/target.py | 9 +++++----
  5558. sopeldatawriter.py | 1 +
  5559. 2 files changed, 6 insertions(+), 4 deletions(-)
  5560.  
  5561. diff --git a/sopel/tools/target.py b/sopel/tools/target.py
  5562. index 2ff0a91..963a439 100644
  5563. --- a/sopel/tools/target.py
  5564. +++ b/sopel/tools/target.py
  5565. @@ -62,10 +62,11 @@ class Channel(object):
  5566. """The topic of the channel."""
  5567.  
  5568. def clear_user(self, nick):
  5569. - user = self.users[nick]
  5570. - user.channels.pop(self.name, None)
  5571. - del self.users[nick]
  5572. - del self.privileges[nick]
  5573. + if nick in self.users:
  5574. + user = self.users[nick]
  5575. + user.channels.pop(self.name, None)
  5576. + del self.users[nick]
  5577. + del self.privileges[nick]
  5578.  
  5579. def add_user(self, user):
  5580. assert isinstance(user, User)
  5581. diff --git a/sopeldatawriter.py b/sopeldatawriter.py
  5582. index 77910cb..47b77f1 100644
  5583. --- a/sopeldatawriter.py
  5584. +++ b/sopeldatawriter.py
  5585. @@ -4,6 +4,7 @@ def user_data_write_csv(nick,text,file):
  5586. with open(file, 'a') as csvfile:
  5587. csvwriter = csv.writer(csvfile, delimiter=' ',quotechar='|', quoting=csv.QUOTE_MINIMAL)
  5588. csvwriter.writerow([nick,text])
  5589. + csvfile.close()
  5590. def user_data_write_raw(nick,text,file):
  5591. with open(file, "a") as f:
  5592. data= "".join([trigger.nick ,"\n", trigger.text ,"\n"])
  5593. --
  5594. 2.4.6
  5595.  
  5596.  
  5597. From ca1568d577018943fd22aab83129cc58ab45a0e8 Mon Sep 17 00:00:00 2001
  5598. From: s00ng <???>
  5599. Date: Fri, 3 Jun 2016 22:20:19 +0200
  5600. Subject: [PATCH 4/4] close the file after write in it
  5601.  
  5602. ---
  5603. sopeldatawriter.py | 1 +
  5604. 1 file changed, 1 insertion(+)
  5605.  
  5606. diff --git a/sopeldatawriter.py b/sopeldatawriter.py
  5607. index 47b77f1..ea4d50f 100644
  5608. --- a/sopeldatawriter.py
  5609. +++ b/sopeldatawriter.py
  5610. @@ -9,3 +9,4 @@ def user_data_write_raw(nick,text,file):
  5611. with open(file, "a") as f:
  5612. data= "".join([trigger.nick ,"\n", trigger.text ,"\n"])
  5613. f.write(data)
  5614. + f.close()
  5615. --
  5616. 2.4.6
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement