Licensed under the Eiffel Forum License 2.
http://sopel.chat
"""
from __future__ import unicode_literals, absolute_import, print_function, division
from datetime import datetime
import sopel
import re
from os import path
log_line = re.compile('\S+ (\S+) (.*? <.*?>) (\d+) (\S+)\tcommit[^:]*: (.+)')
def git_info():
repo = path.join(path.dirname(path.dirname(path.dirname(__file__))), '.git')
head = path.join(repo, 'HEAD')
if path.isfile(head):
with open(head) as h:
head_loc = h.readline()[5:-1] # strip ref: and \n
head_file = path.join(repo, head_loc)
if path.isfile(head_file):
with open(head_file) as h:
sha = h.readline()
if sha:
return sha
@sopel.module.commands('version')
def version(bot, trigger):
"""Display the latest commit version, if Sopel is running in a git repo."""
release = sopel.__version__
sha = git_info()
if not sha:
msg = 'Sopel v. ' + release
if release[-4:] == '-git':
msg += ' at unknown commit.'
bot.reply(msg)
return
bot.reply("Sopel v. {} at commit: {}".format(sopel.__version__, sha))
@sopel.module.intent('VERSION')
@sopel.module.rate(20)
@sopel.module.rule('.*')
def ctcp_version(bot, trigger):
print('wat')
bot.write(('NOTICE', trigger.nick),
'\x01VERSION Sopel IRC Bot version %s\x01' % sopel.__version__)
@sopel.module.rule('\x01SOURCE\x01')
@sopel.module.rate(20)
def ctcp_source(bot, trigger):
bot.write(('NOTICE', trigger.nick),
'\x01SOURCE https://github.com/sopel-irc/sopel/\x01')
@sopel.module.rule('\x01PING\s(.*)\x01')
@sopel.module.rate(10)
def ctcp_ping(bot, trigger):
text = trigger.group()
text = text.replace("PING ", "")
text = text.replace("\x01", "")
bot.write(('NOTICE', trigger.nick),
'\x01PING {0}\x01'.format(text))
@sopel.module.rule('\x01TIME\x01')
@sopel.module.rate(20)
def ctcp_time(bot, trigger):
dt = datetime.now()
current_time = dt.strftime("%A, %d. %B %Y %I:%M%p")
bot.write(('NOTICE', trigger.nick),
'\x01TIME {0}\x01'.format(current_time))
sopel-6.3.0/sopel/modules/weather.py 0000664 0000000 0000000 00000013714 12653772322 0017464 0 ustar 00root root 0000000 0000000 # coding=utf-8
# Copyright 2008, Sean B. Palmer, inamidst.com
# Copyright 2012, Elsie Powell, embolalia.com
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division
from sopel import web
from sopel.module import commands, example, NOLIMIT
import xmltodict
def woeid_search(query):
"""
Find the first Where On Earth ID for the given query. Result is the etree
node for the result, so that location data can still be retrieved. Returns
None if there is no result, or the woeid field is empty.
"""
query = 'q=select * from geo.places where text="%s"' % query
body = web.get('http://query.yahooapis.com/v1/public/yql?' + query,
dont_decode=True)
parsed = xmltodict.parse(body).get('query')
results = parsed.get('results')
if results is None or results.get('place') is None:
return None
if type(results.get('place')) is list:
return results.get('place')[0]
return results.get('place')
def get_cover(parsed):
try:
condition = parsed['channel']['item']['yweather:condition']
except KeyError:
return 'unknown'
text = condition['@text']
# code = int(condition['code'])
# TODO parse code to get those little icon thingies.
return text
def get_temp(parsed):
try:
condition = parsed['channel']['item']['yweather:condition']
temp = int(condition['@temp'])
except (KeyError, ValueError):
return 'unknown'
f = round((temp * 1.8) + 32, 2)
return (u'%d\u00B0C (%d\u00B0F)' % (temp, f))
def get_humidity(parsed):
try:
humidity = parsed['channel']['yweather:atmosphere']['@humidity']
except (KeyError, ValueError):
return 'unknown'
return "Humidity: %s%%" % humidity
def get_wind(parsed):
try:
wind_data = parsed['channel']['yweather:wind']
kph = float(wind_data['@speed'])
m_s = float(round(kph / 3.6, 1))
speed = int(round(kph / 1.852, 0))
degrees = int(wind_data['@direction'])
except (KeyError, ValueError):
return 'unknown'
if speed < 1:
description = 'Calm'
elif speed < 4:
description = 'Light air'
elif speed < 7:
description = 'Light breeze'
elif speed < 11:
description = 'Gentle breeze'
elif speed < 16:
description = 'Moderate breeze'
elif speed < 22:
description = 'Fresh breeze'
elif speed < 28:
description = 'Strong breeze'
elif speed < 34:
description = 'Near gale'
elif speed < 41:
description = 'Gale'
elif speed < 48:
description = 'Strong gale'
elif speed < 56:
description = 'Storm'
elif speed < 64:
description = 'Violent storm'
else:
description = 'Hurricane'
if (degrees <= 22.5) or (degrees > 337.5):
degrees = u'\u2193'
elif (degrees > 22.5) and (degrees <= 67.5):
degrees = u'\u2199'
elif (degrees > 67.5) and (degrees <= 112.5):
degrees = u'\u2190'
elif (degrees > 112.5) and (degrees <= 157.5):
degrees = u'\u2196'
elif (degrees > 157.5) and (degrees <= 202.5):
degrees = u'\u2191'
elif (degrees > 202.5) and (degrees <= 247.5):
degrees = u'\u2197'
elif (degrees > 247.5) and (degrees <= 292.5):
degrees = u'\u2192'
elif (degrees > 292.5) and (degrees <= 337.5):
degrees = u'\u2198'
return description + ' ' + str(m_s) + 'm/s (' + degrees + ')'
@commands('weather', 'wea')
@example('.weather London')
def weather(bot, trigger):
""".weather location - Show the weather at the given location."""
location = trigger.group(2)
woeid = ''
if not location:
woeid = bot.db.get_nick_value(trigger.nick, 'woeid')
if not woeid:
return bot.msg(trigger.sender, "I don't know where you live. " +
'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.')
else:
location = location.strip()
woeid = bot.db.get_nick_value(location, 'woeid')
if woeid is None:
first_result = woeid_search(location)
if first_result is not None:
woeid = first_result.get('woeid')
if not woeid:
return bot.reply("I don't know where that is.")
query = web.urlencode({'w': woeid, 'u': 'c'})
raw = web.get('http://weather.yahooapis.com/forecastrss?' + query,
dont_decode=True)
parsed = xmltodict.parse(raw).get('rss')
location = parsed.get('channel').get('title')
cover = get_cover(parsed)
temp = get_temp(parsed)
humidity = get_humidity(parsed)
wind = get_wind(parsed)
bot.say(u'%s: %s, %s, %s, %s' % (location, cover, temp, humidity, wind))
@commands('setlocation', 'setwoeid')
@example('.setlocation Columbus, OH')
def update_woeid(bot, trigger):
"""Set your default weather location."""
if not trigger.group(2):
bot.reply('Give me a location, like "Washington, DC" or "London".')
return NOLIMIT
first_result = woeid_search(trigger.group(2))
if first_result is None:
return bot.reply("I don't know where that is.")
woeid = first_result.get('woeid')
bot.db.set_nick_value(trigger.nick, 'woeid', woeid)
neighborhood = first_result.get('locality2') or ''
if neighborhood:
neighborhood = neighborhood.get('#text') + ', '
city = first_result.get('locality1') or ''
# This is to catch cases like 'Bawlf, Alberta' where the location is
# thought to be a "LocalAdmin" rather than a "Town"
if city:
city = city.get('#text')
else:
city = first_result.get('name')
state = first_result.get('admin1').get('#text') or ''
country = first_result.get('country').get('#text') or ''
uzip = first_result.get('postal').get('#text') or ''
bot.reply('I now have you at WOEID %s (%s%s, %s, %s %s)' %
(woeid, neighborhood, city, state, country, uzip))
sopel-6.3.0/sopel/modules/wikipedia.py 0000664 0000000 0000000 00000010345 12653772322 0017770 0 ustar 00root root 0000000 0000000 # coding=utf-8
# Copyright 2013 Elsie Powell - embolalia.com
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division
from sopel import web, tools
from sopel.config.types import StaticSection, ValidatedAttribute
from sopel.module import NOLIMIT, commands, example, rule
import json
import re
import sys
if sys.version_info.major < 3:
from urlparse import unquote as _unquote
unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')
else:
from urllib.parse import unquote
REDIRECT = re.compile(r'^REDIRECT (.*)')
class WikipediaSection(StaticSection):
default_lang = ValidatedAttribute('default_lang', default='en')
"""The default language to find articles from."""
lang_per_channel = ValidatedAttribute('lang_per_channel')
def setup(bot):
bot.config.define_section('wikipedia', WikipediaSection)
regex = re.compile('([a-z]+).(wikipedia.org/wiki/)([^ ]+)')
if not bot.memory.contains('url_callbacks'):
bot.memory['url_callbacks'] = tools.SopelMemory()
bot.memory['url_callbacks'][regex] = mw_info
def configure(config):
config.define_section('wikipedia', WikipediaSection)
config.wikipedia.configure_setting(
'default_lang',
"Enter the default language to find articles from."
)
def mw_search(server, query, num):
"""
Searches the specified MediaWiki server for the given query, and returns
the specified number of results.
"""
search_url = ('http://%s/w/api.php?format=json&action=query'
'&list=search&srlimit=%d&srprop=timestamp&srwhat=text'
'&srsearch=') % (server, num)
search_url += query
query = json.loads(web.get(search_url))
if 'query' in query:
query = query['query']['search']
return [r['title'] for r in query]
else:
return None
def say_snippet(bot, server, query, show_url=True):
page_name = query.replace('_', ' ')
query = query.replace(' ', '_')
snippet = mw_snippet(server, query)
msg = '[WIKIPEDIA] {} | "{}"'.format(page_name, snippet)
if show_url:
msg = msg + ' | https://{}/wiki/{}'.format(server, query)
bot.say(msg)
def mw_snippet(server, query):
"""
Retrives a snippet of the specified length from the given page on the given
server.
"""
snippet_url = ('https://' + server + '/w/api.php?format=json'
'&action=query&prop=extracts&exintro&explaintext'
'&exchars=300&redirects&titles=')
snippet_url += query
snippet = json.loads(web.get(snippet_url))
snippet = snippet['query']['pages']
# For some reason, the API gives the page *number* as the key, so we just
# grab the first page number in the results.
snippet = snippet[list(snippet.keys())[0]]
return snippet['extract']
@rule('.*/([a-z]+\.wikipedia.org)/wiki/([^ ]+).*')
def mw_info(bot, trigger, found_match=None):
"""
Retrives a snippet of the specified length from the given page on the given
server.
"""
match = found_match or trigger
say_snippet(bot, match.group(1), unquote(match.group(2)), show_url=False)
@commands('w', 'wiki', 'wik')
@example('.w San Francisco')
def wikipedia(bot, trigger):
lang = bot.config.wikipedia.default_lang
#change lang if channel has custom language set
if (trigger.sender and not trigger.sender.is_nick() and
bot.config.wikipedia.lang_per_channel):
customlang = re.search('(' + trigger.sender + '):(\w+)',
bot.config.wikipedia.lang_per_channel)
if customlang is not None:
lang = customlang.group(2)
if trigger.group(2) is None:
bot.reply("What do you want me to look up?")
return NOLIMIT
query = trigger.group(2)
args = re.search(r'^-([a-z]{2,12})\s(.*)', query)
if args is not None:
lang = args.group(1)
query = args.group(2)
if not query:
bot.reply('What do you want me to look up?')
return NOLIMIT
server = lang + '.wikipedia.org'
query = mw_search(server, query, 1)
if not query:
bot.reply("I can't find any results for that.")
return NOLIMIT
else:
query = query[0]
say_snippet(bot, server, query)
sopel-6.3.0/sopel/modules/wiktionary.py 0000664 0000000 0000000 00000005505 12653772322 0020224 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""
wiktionary.py - Sopel Wiktionary Module
Copyright 2009, Sean B. Palmer, inamidst.com
Licensed under the Eiffel Forum License 2.
http://sopel.chat
"""
from __future__ import unicode_literals, absolute_import, print_function, division
import re
from sopel import web
from sopel.module import commands, example
uri = 'http://en.wiktionary.org/w/index.php?title=%s&printable=yes'
r_tag = re.compile(r'<[^>]+>')
r_ul = re.compile(r'(?ims)')
def text(html):
text = r_tag.sub('', html).strip()
text = text.replace('\n', ' ')
text = text.replace('\r', '')
text = text.replace('(intransitive', '(intr.')
text = text.replace('(transitive', '(trans.')
return text
def wikt(word):
bytes = web.get(uri % web.quote(word))
bytes = r_ul.sub('', bytes)
mode = None
etymology = None
definitions = {}
for line in bytes.splitlines():
if 'id="Etymology"' in line:
mode = 'etymology'
elif 'id="Noun"' in line:
mode = 'noun'
elif 'id="Verb"' in line:
mode = 'verb'
elif 'id="Adjective"' in line:
mode = 'adjective'
elif 'id="Adverb"' in line:
mode = 'adverb'
elif 'id="Interjection"' in line:
mode = 'interjection'
elif 'id="Particle"' in line:
mode = 'particle'
elif 'id="Preposition"' in line:
mode = 'preposition'
elif 'id="' in line:
mode = None
elif (mode == 'etmyology') and ('' in line):
etymology = text(line)
elif (mode is not None) and ('
' in line):
definitions.setdefault(mode, []).append(text(line))
if '
300:
result = result[:295] + '[...]'
bot.say(result)
sopel-6.3.0/sopel/modules/xkcd.py 0000664 0000000 0000000 00000006612 12653772322 0016755 0 ustar 00root root 0000000 0000000 # coding=utf-8
# Copyright 2010, Michael Yanovich (yanovich.net), and Morgan Goose
# Copyright 2012, Lior Ramati
# Copyright 2013, Elsie Powell (embolalia.com)
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division
import json
import random
import re
from sopel import web
from sopel.modules.search import google_search
from sopel.module import commands
ignored_sites = [
# For google searching
'almamater.xkcd.com',
'blog.xkcd.com',
'blag.xkcd.com',
'forums.xkcd.com',
'fora.xkcd.com',
'forums3.xkcd.com',
'store.xkcd.com',
'wiki.xkcd.com',
'what-if.xkcd.com',
]
sites_query = ' site:xkcd.com -site:' + ' -site:'.join(ignored_sites)
def get_info(number=None):
if number:
url = 'http://xkcd.com/{}/info.0.json'.format(number)
else:
url = 'http://xkcd.com/info.0.json'
data = requests.get(url).json()
data['url'] = 'http://xkcd.com/' + str(data['num'])
return data
def google(query):
url = google_search(query + sites_query)
if not url:
return None
match = re.match('(?:https?://)?xkcd.com/(\d+)/?', url)
if match:
return match.group(1)
@commands('xkcd')
def xkcd(bot, trigger):
"""
.xkcd - Finds an xkcd comic strip. Takes one of 3 inputs:
If no input is provided it will return a random comic
If numeric input is provided it will return that comic, or the nth-latest
comic if the number is non-positive
If non-numeric input is provided it will return the first google result for those keywords on the xkcd.com site
"""
# get latest comic for rand function and numeric input
latest = get_info()
max_int = latest['num']
# if no input is given (pre - lior's edits code)
if not trigger.group(2): # get rand comic
random.seed()
requested = get_info(random.randint(1, max_int + 1))
else:
query = trigger.group(2).strip()
numbered = re.match(r"^(#|\+|-)?(\d+)$", query)
if numbered:
query = int(numbered.group(2))
if numbered.group(1) == "-":
query = -query
if query > max_int:
bot.say(("Sorry, comic #{} hasn't been posted yet. "
"The last comic was #{}").format(query, max_int))
return
elif query <= -max_int:
bot.say(("Sorry, but there were only {} comics "
"released yet so far").format(max_int))
return
elif abs(query) == 0:
requested = latest
elif query == 404 or max_int + query == 404:
bot.say("404 - Not Found") # don't error on that one
return
elif query > 0:
requested = get_info(query)
else:
# Negative: go back that many from current
requested = get_info(max_int + query)
else:
# Non-number: google.
if (query.lower() == "latest" or query.lower() == "newest"):
requested = latest
else:
number = google(query)
if not number:
bot.say('Could not find any comics for that query.')
return
requested = get_info(number)
message = '{} [{}]'.format(requested['url'], requested['title'])
bot.say(message)
sopel-6.3.0/sopel/run_script.py 0000775 0000000 0000000 00000017422 12653772322 0016550 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python2.7
# coding=utf-8
"""
Sopel - An IRC Bot
Copyright 2008, Sean B. Palmer, inamidst.com
Copyright © 2012-2014, Elad Alfassa
Licensed under the Eiffel Forum License 2.
http://sopel.chat
"""
from __future__ import unicode_literals, absolute_import, print_function, division
import sys
from sopel.tools import stderr
if sys.version_info < (2, 7):
stderr('Error: Requires Python 2.7 or later. Try python2.7 sopel')
sys.exit(1)
if sys.version_info.major == 3 and sys.version_info.minor < 3:
stderr('Error: When running on Python 3, Python 3.3 is required.')
sys.exit(1)
import os
import argparse
import signal
from sopel.__init__ import run, __version__
from sopel.config import Config, _create_config, ConfigurationError, _wizard
import sopel.tools as tools
homedir = os.path.join(os.path.expanduser('~'), '.sopel')
def enumerate_configs(extension='.cfg'):
configfiles = []
if os.path.isdir(homedir):
sopel_dotdirfiles = os.listdir(homedir) # Preferred
for item in sopel_dotdirfiles:
if item.endswith(extension):
configfiles.append(item)
return configfiles
def find_config(name, extension='.cfg'):
if os.path.isfile(name):
return name
configs = enumerate_configs(extension)
if name in configs or name + extension in configs:
if name + extension in configs:
name = name + extension
return os.path.join(homedir, name)
def main(argv=None):
global homedir
# Step One: Parse The Command Line
try:
parser = argparse.ArgumentParser(description='Sopel IRC Bot',
usage='%(prog)s [options]')
parser.add_argument('-c', '--config', metavar='filename',
help='use a specific configuration file')
parser.add_argument("-d", '--fork', action="store_true",
dest="deamonize", help="Deamonize sopel")
parser.add_argument("-q", '--quit', action="store_true", dest="quit",
help="Gracefully quit Sopel")
parser.add_argument("-k", '--kill', action="store_true", dest="kill",
help="Kill Sopel")
parser.add_argument("-l", '--list', action="store_true",
dest="list_configs",
help="List all config files found")
parser.add_argument("-m", '--migrate', action="store_true",
dest="migrate_configs",
help="Migrate config files to the new format")
parser.add_argument('--quiet', action="store_true", dest="quiet",
help="Supress all output")
parser.add_argument('-w', '--configure-all', action='store_true',
dest='wizard', help='Run the configuration wizard.')
parser.add_argument('--configure-modules', action='store_true',
dest='mod_wizard', help=(
'Run the configuration wizard, but only for the '
'module configuration options.'))
parser.add_argument('-v', '--version', action="store_true",
dest="version", help="Show version number and exit")
opts = parser.parse_args()
# Step Two: "Do not run as root" checks.
try:
# Linux/Mac
if os.getuid() == 0 or os.geteuid() == 0:
stderr('Error: Do not run Sopel with root privileges.')
sys.exit(1)
except AttributeError:
# Windows
if os.environ.get("USERNAME") == "Administrator":
stderr('Error: Do not run Sopel as Administrator.')
sys.exit(1)
if opts.version:
py_ver = '%s.%s.%s' % (sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro)
print('Sopel %s (running on python %s)' % (__version__, py_ver))
print('http://sopel.chat/')
return
elif opts.wizard:
_wizard('all', opts.config)
return
elif opts.mod_wizard:
_wizard('mod', opts.config)
return
if opts.list_configs:
configs = enumerate_configs()
print('Config files in ~/.sopel:')
if len(configs) is 0:
print('\tNone found')
else:
for config in configs:
print('\t%s' % config)
print('-------------------------')
return
config_name = opts.config or 'default'
configpath = find_config(config_name)
if not os.path.isfile(configpath):
print("Welcome to Sopel!\nI can't seem to find the configuration file, so let's generate it!\n")
if not configpath.endswith('.cfg'):
configpath = configpath + '.cfg'
_create_config(configpath)
configpath = find_config(config_name)
try:
config_module = Config(configpath)
except ConfigurationError as e:
stderr(e)
sys.exit(2)
if config_module.core.not_configured:
stderr('Bot is not configured, can\'t start')
# exit with code 2 to prevent auto restart on fail by systemd
sys.exit(2)
logfile = os.path.os.path.join(config_module.core.logdir, 'stdio.log')
config_module._is_deamonized = opts.deamonize
sys.stderr = tools.OutputRedirect(logfile, True, opts.quiet)
sys.stdout = tools.OutputRedirect(logfile, False, opts.quiet)
# Handle --quit, --kill and saving the PID to file
pid_dir = config_module.core.pid_dir
if opts.config is None:
pid_file_path = os.path.join(pid_dir, 'sopel.pid')
else:
basename = os.path.basename(opts.config)
if basename.endswith('.cfg'):
basename = basename[:-4]
pid_file_path = os.path.join(pid_dir, 'sopel-%s.pid' % basename)
if os.path.isfile(pid_file_path):
with open(pid_file_path, 'r') as pid_file:
try:
old_pid = int(pid_file.read())
except ValueError:
old_pid = None
if old_pid is not None and tools.check_pid(old_pid):
if not opts.quit and not opts.kill:
stderr('There\'s already a Sopel instance running with this config file')
stderr('Try using the --quit or the --kill options')
sys.exit(1)
elif opts.kill:
stderr('Killing the sopel')
os.kill(old_pid, signal.SIGKILL)
sys.exit(0)
elif opts.quit:
stderr('Signaling Sopel to stop gracefully')
if hasattr(signal, 'SIGUSR1'):
os.kill(old_pid, signal.SIGUSR1)
else:
os.kill(old_pid, signal.SIGTERM)
sys.exit(0)
elif old_pid is None or (not tools.check_pid(old_pid)
and (opts.kill or opts.quit)):
stderr('Sopel is not running!')
sys.exit(1)
elif opts.quit or opts.kill:
stderr('Sopel is not running!')
sys.exit(1)
if opts.deamonize:
child_pid = os.fork()
if child_pid is not 0:
sys.exit()
with open(pid_file_path, 'w') as pid_file:
pid_file.write(str(os.getpid()))
# Step Five: Initialise And Run sopel
run(config_module, pid_file_path)
except KeyboardInterrupt:
print("\n\nInterrupted")
os._exit(1)
if __name__ == '__main__':
main()
sopel-6.3.0/sopel/test_tools.py 0000664 0000000 0000000 00000013664 12653772322 0016560 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""This module has classes and functions that can help in writing tests.
test_tools.py - Sopel misc tools
Copyright 2013, Ari Koivula,
Licensed under the Eiffel Forum License 2.
https://sopel.chat
"""
from __future__ import unicode_literals, absolute_import, print_function, division
import os
import re
import sys
import tempfile
try:
import ConfigParser
except ImportError:
import configparser as ConfigParser
import sopel.config
import sopel.config.core_section
import sopel.tools
import sopel.trigger
class MockConfig(sopel.config.Config):
def __init__(self):
self.filename = tempfile.mkstemp()[1]
#self._homedir = tempfile.mkdtemp()
#self.filename = os.path.join(self._homedir, 'test.cfg')
self.parser = ConfigParser.RawConfigParser(allow_no_value=True)
self.parser.add_section('core')
self.parser.set('core', 'owner', 'Embolalia')
self.define_section('core', sopel.config.core_section.CoreSection)
self.get = self.parser.get
def define_section(self, name, cls_):
if not self.parser.has_section(name):
self.parser.add_section(name)
setattr(self, name, cls_(self, name))
class MockSopel(object):
def __init__(self, nick, admin=False, owner=False):
self.nick = nick
self.user = "sopel"
self.channels = ["#channel"]
self.memory = sopel.tools.SopelMemory()
self.ops = {}
self.halfplus = {}
self.voices = {}
self.config = MockConfig()
self._init_config()
if admin:
self.config.core.admins = [self.nick]
if owner:
self.config.core.owner = self.nick
def _init_config(self):
cfg = self.config
cfg.parser.set('core', 'admins', '')
cfg.parser.set('core', 'owner', '')
home_dir = os.path.join(os.path.expanduser('~'), '.sopel')
if not os.path.exists(home_dir):
os.mkdir(home_dir)
cfg.parser.set('core', 'homedir', home_dir)
class MockSopelWrapper(object):
def __init__(self, bot, pretrigger):
self.bot = bot
self.pretrigger = pretrigger
self.output = []
def _store(self, string, recipent=None):
self.output.append(string.strip())
say = reply = action = _store
def __getattr__(self, attr):
return getattr(self.bot, attr)
def get_example_test(tested_func, msg, results, privmsg, admin,
owner, repeat, use_regexp, ignore=[]):
"""Get a function that calls tested_func with fake wrapper and trigger.
Args:
tested_func - A sopel callable that accepts SopelWrapper and Trigger.
msg - Message that is supposed to trigger the command.
results - Expected output from the callable.
privmsg - If true, make the message appear to have sent in a private
message to the bot. If false, make it appear to have come from a
channel.
admin - If true, make the message appear to have come from an admin.
owner - If true, make the message appear to have come from an owner.
repeat - How many times to repeat the test. Usefull for tests that
return random stuff.
use_regexp = Bool. If true, results is in regexp format.
ignore - List of strings to ignore.
"""
def test():
bot = MockSopel("NickName", admin=admin, owner=owner)
match = None
if hasattr(tested_func, "commands"):
for command in tested_func.commands:
regexp = sopel.tools.get_command_regexp(".", command)
match = regexp.match(msg)
if match:
break
assert match, "Example did not match any command."
sender = bot.nick if privmsg else "#channel"
hostmask = "%s!%s@%s " % (bot.nick, "UserName", "example.com")
# TODO enable message tags
full_message = ':{} PRIVMSG {} :{}'.format(hostmask, sender, msg)
pretrigger = sopel.trigger.PreTrigger(bot.nick, full_message)
trigger = sopel.trigger.Trigger(bot.config, pretrigger, match)
module = sys.modules[tested_func.__module__]
if hasattr(module, 'setup'):
module.setup(bot)
def isnt_ignored(value):
"""Return True if value doesn't match any re in ignore list."""
for ignored_line in ignore:
if re.match(ignored_line, value):
return False
return True
for _i in range(repeat):
wrapper = MockSopelWrapper(bot, trigger)
tested_func(wrapper, trigger)
wrapper.output = list(filter(isnt_ignored, wrapper.output))
assert len(wrapper.output) == len(results)
for result, output in zip(results, wrapper.output):
if type(output) is bytes:
output = output.decode('utf-8')
if use_regexp:
if not re.match(result, output):
assert result == output
else:
assert result == output
return test
def insert_into_module(func, module_name, base_name, prefix):
"""Add a function into a module."""
func.__module__ = module_name
module = sys.modules[module_name]
# Make sure the func method does not overwrite anything.
for i in range(1000):
func.__name__ = str("%s_%s_%s" % (prefix, base_name, i))
if not hasattr(module, func.__name__):
break
setattr(module, func.__name__, func)
def run_example_tests(filename, tb='native', multithread=False, verbose=False):
# These are only required when running tests, so import them here rather
# than at the module level.
import pytest
from multiprocessing import cpu_count
args = [filename, "-s"]
args.extend(['--tb', tb])
if verbose:
args.extend(['-v'])
if multithread and cpu_count() > 1:
args.extend(["-n", str(cpu_count())])
pytest.main(args)
sopel-6.3.0/sopel/tools/ 0000775 0000000 0000000 00000000000 12653772322 0015135 5 ustar 00root root 0000000 0000000 sopel-6.3.0/sopel/tools/__init__.py 0000664 0000000 0000000 00000025221 12653772322 0017250 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Useful miscellaneous tools and shortcuts for Sopel modules
*Availability: 3+*
"""
# tools.py - Sopel misc tools
# Copyright 2008, Sean B. Palmer, inamidst.com
# Copyright © 2012, Elad Alfassa
# Copyright 2012, Elsie Powell, embolalia.com
# Licensed under the Eiffel Forum License 2.
# https://sopel.chat
from __future__ import unicode_literals, absolute_import, print_function, division
import sys
import os
import re
import threading
import codecs
import traceback
from collections import defaultdict
from sopel.tools._events import events # NOQA
if sys.version_info.major >= 3:
raw_input = input
unicode = str
iteritems = dict.items
itervalues = dict.values
iterkeys = dict.keys
else:
iteritems = dict.iteritems
itervalues = dict.itervalues
iterkeys = dict.iterkeys
_channel_prefixes = ('#', '&', '+', '!')
def get_input(prompt):
"""Get decoded input from the terminal (equivalent to python 3's ``input``).
"""
if sys.version_info.major >= 3:
return input(prompt)
else:
return raw_input(prompt).decode('utf8')
def get_raising_file_and_line(tb=None):
"""Return the file and line number of the statement that raised the tb.
Returns: (filename, lineno) tuple
"""
if not tb:
tb = sys.exc_info()[2]
filename, lineno, _context, _line = traceback.extract_tb(tb)[-1]
return filename, lineno
def get_command_regexp(prefix, command):
"""Return a compiled regexp object that implements the command."""
# Escape all whitespace with a single backslash. This ensures that regexp
# in the prefix is treated as it was before the actual regexp was changed
# to use the verbose syntax.
prefix = re.sub(r"(\s)", r"\\\1", prefix)
# This regexp match equivalently and produce the same
# groups 1 and 2 as the old regexp: r'^%s(%s)(?: +(.*))?$'
# The only differences should be handling all whitespace
# like spaces and the addition of groups 3-6.
pattern = r"""
(?:{prefix})({command}) # Command as group 1.
(?:\s+ # Whitespace to end command.
( # Rest of the line as group 2.
(?:(\S+))? # Parameters 1-4 as groups 3-6.
(?:\s+(\S+))?
(?:\s+(\S+))?
(?:\s+(\S+))?
.* # Accept anything after the parameters.
# Leave it up to the module to parse
# the line.
))? # Group 2 must be None, if there are no
# parameters.
$ # EoL, so there are no partial matches.
""".format(prefix=prefix, command=command)
return re.compile(pattern, re.IGNORECASE | re.VERBOSE)
def deprecated(old):
def new(*args, **kwargs):
print('Function %s is deprecated.' % old.__name__, file=sys.stderr)
trace = traceback.extract_stack()
for line in traceback.format_list(trace[:-1]):
stderr(line[:-1])
return old(*args, **kwargs)
new.__doc__ = old.__doc__
new.__name__ = old.__name__
return new
# from
# http://parand.com/say/index.php/2007/07/13/simple-multi-dimensional-dictionaries-in-python/
# A simple class to make mutli dimensional dict easy to use
class Ddict(dict):
"""Class for multi-dimensional ``dict``.
A simple helper class to ease the creation of multi-dimensional ``dict``\s.
"""
def __init__(self, default=None):
self.default = default
def __getitem__(self, key):
if key not in self:
self[key] = self.default()
return dict.__getitem__(self, key)
class Identifier(unicode):
"""A `unicode` subclass which acts appropriately for IRC identifiers.
When used as normal `unicode` objects, case will be preserved.
However, when comparing two Identifier objects, or comparing a Identifier
object with a `unicode` object, the comparison will be case insensitive.
This case insensitivity includes the case convention conventions regarding
``[]``, ``{}``, ``|``, ``\\``, ``^`` and ``~`` described in RFC 2812.
"""
def __new__(cls, identifier):
# According to RFC2812, identifiers have to be in the ASCII range.
# However, I think it's best to let the IRCd determine that, and we'll
# just assume unicode. It won't hurt anything, and is more internally
# consistent. And who knows, maybe there's another use case for this
# weird case convention.
s = unicode.__new__(cls, identifier)
s._lowered = Identifier._lower(identifier)
return s
def lower(self):
"""Return the identifier converted to lower-case per RFC 2812."""
return self._lowered
@staticmethod
def _lower(identifier):
"""Returns `identifier` in lower case per RFC 2812."""
# The tilde replacement isn't needed for identifiers, but is for
# channels, which may be useful at some point in the future.
low = identifier.lower().replace('{', '[').replace('}', ']')
low = low.replace('|', '\\').replace('^', '~')
return low
def __repr__(self):
return "%s(%r)" % (
self.__class__.__name__,
self.__str__()
)
def __hash__(self):
return self._lowered.__hash__()
def __lt__(self, other):
if isinstance(other, Identifier):
return self._lowered < other._lowered
return self._lowered < Identifier._lower(other)
def __le__(self, other):
if isinstance(other, Identifier):
return self._lowered <= other._lowered
return self._lowered <= Identifier._lower(other)
def __gt__(self, other):
if isinstance(other, Identifier):
return self._lowered > other._lowered
return self._lowered > Identifier._lower(other)
def __ge__(self, other):
if isinstance(other, Identifier):
return self._lowered >= other._lowered
return self._lowered >= Identifier._lower(other)
def __eq__(self, other):
if isinstance(other, Identifier):
return self._lowered == other._lowered
return self._lowered == Identifier._lower(other)
def __ne__(self, other):
return not (self == other)
def is_nick(self):
"""Returns True if the Identifier is a nickname (as opposed to channel)
"""
return self and not self.startswith(_channel_prefixes)
class OutputRedirect(object):
"""Redirect te output to the terminal and a log file.
A simplified object used to write to both the terminal and a log file.
"""
def __init__(self, logpath, stderr=False, quiet=False):
"""Create an object which will to to a file and the terminal.
Create an object which will log to the file at ``logpath`` as well as
the terminal.
If ``stderr`` is given and true, it will write to stderr rather than
stdout.
If ``quiet`` is given and True, data will be written to the log file
only, but not the terminal.
"""
self.logpath = logpath
self.stderr = stderr
self.quiet = quiet
def write(self, string):
"""Write the given ``string`` to the logfile and terminal."""
if not self.quiet:
try:
if self.stderr:
sys.__stderr__.write(string)
else:
sys.__stdout__.write(string)
except:
pass
with codecs.open(self.logpath, 'ab', encoding="utf8",
errors='xmlcharrefreplace') as logfile:
try:
logfile.write(string)
except UnicodeDecodeError:
# we got an invalid string, safely encode it to utf-8
logfile.write(unicode(string, 'utf8', errors="replace"))
def flush(self):
if self.stderr:
sys.__stderr__.flush()
else:
sys.__stdout__.flush()
# These seems to trace back to when we thought we needed a try/except on prints,
# because it looked like that was why we were having problems. We'll drop it in
# 4.0^H^H^H5.0^H^H^H6.0^H^H^Hsome version when someone can be bothered.
@deprecated
def stdout(string):
print(string)
def stderr(string):
"""Print the given ``string`` to stderr.
This is equivalent to ``print >> sys.stderr, string``
"""
print(string, file=sys.stderr)
def check_pid(pid):
"""Check if a process is running with the given ``PID``.
*Availability: Only on POSIX systems*
Return ``True`` if there is a process running with the given ``PID``.
"""
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
def get_hostmask_regex(mask):
"""Return a compiled `re.RegexObject` for an IRC hostmask"""
mask = re.escape(mask)
mask = mask.replace(r'\*', '.*')
return re.compile(mask + '$', re.I)
class SopelMemory(dict):
"""A simple thread-safe dict implementation.
*Availability: 4.0; available as ``Sopel.SopelMemory`` in 3.1.0 - 3.2.0*
In order to prevent exceptions when iterating over the values and changing
them at the same time from different threads, we use a blocking lock on
``__setitem__`` and ``contains``.
"""
def __init__(self, *args):
dict.__init__(self, *args)
self.lock = threading.Lock()
def __setitem__(self, key, value):
self.lock.acquire()
result = dict.__setitem__(self, key, value)
self.lock.release()
return result
def __contains__(self, key):
"""Check if a key is in the dict.
It locks it for writes when doing so.
"""
self.lock.acquire()
result = dict.__contains__(self, key)
self.lock.release()
return result
def contains(self, key):
"""Backwards compatability with 3.x, use `in` operator instead."""
return self.__contains__(key)
class SopelMemoryWithDefault(defaultdict):
"""Same as SopelMemory, but subclasses from collections.defaultdict."""
def __init__(self, *args):
defaultdict.__init__(self, *args)
self.lock = threading.Lock()
def __setitem__(self, key, value):
self.lock.acquire()
result = defaultdict.__setitem__(self, key, value)
self.lock.release()
return result
def __contains__(self, key):
"""Check if a key is in the dict.
It locks it for writes when doing so.
"""
self.lock.acquire()
result = defaultdict.__contains__(self, key)
self.lock.release()
return result
def contains(self, key):
"""Backwards compatability with 3.x, use `in` operator instead."""
return self.__contains__(key)
sopel-6.3.0/sopel/tools/_events.py 0000664 0000000 0000000 00000013056 12653772322 0017157 0 ustar 00root root 0000000 0000000 # coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division
class events(object):
"""An enumeration of all the standardized and notable IRC numeric events
This allows you to do, for example, @module.event(events.RPL_WELCOME)
rather than @module.event('001')
"""
# ###################################################### Non-RFC / Non-IRCv3
# Only add things here if they're actually in common use across multiple
# ircds.
RPL_ISUPPORT = '005'
RPL_WHOSPCRPL = '354'
# ################################################################### IRC v3
# ## 3.1
# CAP
ERR_INVALIDCAPCMD = '410'
# SASL
RPL_LOGGEDIN = '900'
RPL_LOGGEDOUT = '901'
ERR_NICKLOCKED = '902'
RPL_SASLSUCCESS = '903'
ERR_SASLFAIL = '904'
ERR_SASLTOOLONG = '905'
ERR_SASLABORTED = '906'
ERR_SASLALREADY = '907'
RPL_SASLMECHS = '908'
# TLS
RPL_STARTTLS = '670'
ERR_STARTTLS = '691'
# ## 3.2
# Metadata
RPL_WHOISKEYVALUE = '760'
RPL_KEYVALUE = '761'
RPL_METADATAEND = '762'
ERR_METADATALIMIT = '764'
ERR_TARGETINVALID = '765'
ERR_NOMATCHINGKEY = '766'
ERR_KEYINVALID = '767'
ERR_KEYNOTSET = '768'
ERR_KEYNOPERMISSION = '769'
# Monitor
RPL_MONONLINE = '730'
RPL_MONOFFLINE = '731'
RPL_MONLIST = '732'
RPL_ENDOFMONLIST = '733'
ERR_MONLISTFULL = '734'
# ################################################################# RFC 1459
# ## 6.1 Error Replies.
ERR_NOSUCHNICK = '401'
ERR_NOSUCHSERVER = '402'
ERR_NOSUCHCHANNEL = '403'
ERR_CANNOTSENDTOCHAN = '404'
ERR_TOOMANYCHANNELS = '405'
ERR_WASNOSUCHNICK = '406'
ERR_TOOMANYTARGETS = '407'
ERR_NOORIGIN = '409'
ERR_NORECIPIENT = '411'
ERR_NOTEXTTOSEND = '412'
ERR_NOTOPLEVEL = '413'
ERR_WILDTOPLEVEL = '414'
ERR_UNKNOWNCOMMAND = '421'
ERR_NOMOTD = '422'
ERR_NOADMININFO = '423'
ERR_FILEERROR = '424'
ERR_NONICKNAMEGIVEN = '431'
ERR_ERRONEUSNICKNAME = '432'
ERR_NICKNAMEINUSE = '433'
ERR_NICKCOLLISION = '436'
ERR_USERNOTINCHANNEL = '441'
ERR_NOTONCHANNEL = '442'
ERR_USERONCHANNEL = '443'
ERR_NOLOGIN = '444'
ERR_SUMMONDISABLED = '445'
ERR_USERSDISABLED = '446'
ERR_NOTREGISTERED = '451'
ERR_NEEDMOREPARAMS = '461'
ERR_ALREADYREGISTRED = '462'
ERR_NOPERMFORHOST = '463'
ERR_PASSWDMISMATCH = '464'
ERR_YOUREBANNEDCREEP = '465'
ERR_KEYSET = '467'
ERR_CHANNELISFULL = '471'
ERR_UNKNOWNMODE = '472'
ERR_INVITEONLYCHAN = '473'
ERR_BANNEDFROMCHAN = '474'
ERR_BADCHANNELKEY = '475'
ERR_NOPRIVILEGES = '481'
ERR_CHANOPRIVSNEEDED = '482'
ERR_CANTKILLSERVER = '483'
ERR_NOOPERHOST = '491'
ERR_UMODEUNKNOWNFLAG = '501'
ERR_USERSDONTMATCH = '502'
# ## 6.2 Command responses.
RPL_NONE = '300'
RPL_USERHOST = '302'
RPL_ISON = '303'
RPL_AWAY = '301'
RPL_UNAWAY = '305'
RPL_NOWAWAY = '306'
RPL_WHOISUSER = '311'
RPL_WHOISSERVER = '312'
RPL_WHOISOPERATOR = '313'
RPL_WHOISIDLE = '317'
RPL_ENDOFWHOIS = '318'
RPL_WHOISCHANNELS = '319'
RPL_WHOWASUSER = '314'
RPL_ENDOFWHOWAS = '369'
RPL_LISTSTART = '321'
RPL_LIST = '322'
RPL_LISTEND = '323'
RPL_CHANNELMODEIS = '324'
RPL_NOTOPIC = '331'
RPL_TOPIC = '332'
RPL_INVITING = '341'
RPL_SUMMONING = '342'
RPL_VERSION = '351'
RPL_WHOREPLY = '352'
RPL_ENDOFWHO = '315'
RPL_NAMREPLY = '353'
RPL_ENDOFNAMES = '366'
RPL_LINKS = '364'
RPL_ENDOFLINKS = '365'
RPL_BANLIST = '367'
RPL_ENDOFBANLIST = '368'
RPL_INFO = '371'
RPL_ENDOFINFO = '374'
RPL_MOTDSTART = '375'
RPL_MOTD = '372'
RPL_ENDOFMOTD = '376'
RPL_YOUREOPER = '381'
RPL_REHASHING = '382'
RPL_TIME = '391'
RPL_USERSSTART = '392'
RPL_USERS = '393'
RPL_ENDOFUSERS = '394'
RPL_NOUSERS = '395'
RPL_TRACELINK = '200'
RPL_TRACECONNECTING = '201'
RPL_TRACEHANDSHAKE = '202'
RPL_TRACEUNKNOWN = '203'
RPL_TRACEOPERATOR = '204'
RPL_TRACEUSER = '205'
RPL_TRACESERVER = '206'
RPL_TRACENEWTYPE = '208'
RPL_TRACELOG = '261'
RPL_STATSLINKINFO = '211'
RPL_STATSCOMMANDS = '212'
RPL_STATSCLINE = '213'
RPL_STATSNLINE = '214'
RPL_STATSILINE = '215'
RPL_STATSKLINE = '216'
RPL_STATSYLINE = '218'
RPL_ENDOFSTATS = '219'
RPL_STATSLLINE = '241'
RPL_STATSUPTIME = '242'
RPL_STATSOLINE = '243'
RPL_STATSHLINE = '244'
RPL_UMODEIS = '221'
RPL_LUSERCLIENT = '251'
RPL_LUSEROP = '252'
RPL_LUSERUNKNOWN = '253'
RPL_LUSERCHANNELS = '254'
RPL_LUSERME = '255'
RPL_ADMINME = '256'
RPL_ADMINLOC1 = '257'
RPL_ADMINLOC2 = '258'
RPL_ADMINEMAIL = '259'
# ################################################################# RFC 2812
# ## 5.1 Command responses
RPL_WELCOME = '001'
RPL_YOURHOST = '002'
RPL_CREATED = '003'
RPL_MYINFO = '004'
RPL_BOUNCE = '005'
RPL_UNIQOPIS = '325'
RPL_INVITELIST = '346'
RPL_ENDOFINVITELIST = '347'
RPL_EXCEPTLIST = '348'
RPL_ENDOFEXCEPTLIST = '349'
RPL_YOURESERVICE = '383'
RPL_TRACESERVICE = '207'
RPL_TRACECLASS = '209'
RPL_TRACERECONNECT = '210'
RPL_TRACEEND = '262'
RPL_SERVLIST = '234'
RPL_SERVLISTEND = '235'
RPL_TRYAGAIN = '263'
# ## 5.2 Error Replies
ERR_NOSUCHSERVICE = '408'
ERR_BADMASK = '415'
ERR_UNAVAILRESOURCE = '437'
ERR_YOUWILLBEBANNED = '466'
ERR_BADCHANMASK = '476'
ERR_NOCHANMODES = '477'
ERR_BANLISTFULL = '478'
ERR_RESTRICTED = '484'
ERR_UNIQOPPRIVSNEEDED = '485'
sopel-6.3.0/sopel/tools/calculation.py 0000664 0000000 0000000 00000016045 12653772322 0020013 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Tools to help safely do calculations from user input"""
from __future__ import unicode_literals, absolute_import, print_function, division
import time
import numbers
import operator
import ast
__all__ = ['eval_equation']
class ExpressionEvaluator:
"""A generic class for evaluating limited forms of Python expressions.
Instances can overwrite binary_ops and unary_ops attributes with dicts of
the form {ast.Node, function}. When the ast.Node being used as key is
found, it will be evaluated using the given function.
"""
class Error(Exception):
pass
def __init__(self, bin_ops=None, unary_ops=None):
self.binary_ops = bin_ops or {}
self.unary_ops = unary_ops or {}
def __call__(self, expression_str, timeout=5.0):
"""Evaluate a python expression and return the result.
Raises:
SyntaxError: If the given expression_str is not a valid python
statement.
ExpressionEvaluator.Error: If the instance of ExpressionEvaluator
does not have a handler for the ast.Node.
"""
ast_expression = ast.parse(expression_str, mode='eval')
return self._eval_node(ast_expression.body, time.time() + timeout)
def _eval_node(self, node, timeout):
"""Recursively evaluate the given ast.Node.
Uses self.binary_ops and self.unary_ops for the implementation.
A subclass could overwrite this to handle more nodes, calling it only
for nodes it does not implement it self.
Raises:
ExpressionEvaluator.Error: If it can't handle the ast.Node.
"""
if isinstance(node, ast.Num):
return node.n
elif (isinstance(node, ast.BinOp) and
type(node.op) in self.binary_ops):
left = self._eval_node(node.left, timeout)
right = self._eval_node(node.right, timeout)
if time.time() > timeout:
raise ExpressionEvaluator.Error(
"Time for evaluating expression ran out.")
return self.binary_ops[type(node.op)](left, right)
elif (isinstance(node, ast.UnaryOp) and
type(node.op) in self.unary_ops):
operand = self._eval_node(node.operand, timeout)
if time.time() > timeout:
raise ExpressionEvaluator.Error(
"Time for evaluating expression ran out.")
return self.unary_ops[type(node.op)](operand)
raise ExpressionEvaluator.Error(
"Ast.Node '%s' not implemented." % (type(node).__name__,))
def guarded_mul(left, right):
"""Decorate a function to raise an error for values > limit."""
# Only handle ints because floats will overflow anyway.
if not isinstance(left, numbers.Integral):
pass
elif not isinstance(right, numbers.Integral):
pass
elif left in (0, 1) or right in (0, 1):
# Ignore trivial cases.
pass
elif left.bit_length() + right.bit_length() > 664386:
# 664386 is the number of bits (10**100000)**2 has, which is instant on
# my laptop, while (10**1000000)**2 has a noticeable delay. It could
# certainly be improved.
raise ValueError(
"Value is too large to be handled in limited time and memory.")
return operator.mul(left, right)
def pow_complexity(num, exp):
"""Estimate the worst case time pow(num, exp) takes to calculate.
This function is based on experimetal data from the time it takes to
calculate "num**exp" on laptop with i7-2670QM processor on a 32 bit
CPython 2.7.6 interpreter on Windows.
It tries to implement this surface: x=exp, y=num
1e5 2e5 3e5 4e5 5e5 6e5 7e5 8e5 9e5
e1 0.03 0.09 0.16 0.25 0.35 0.46 0.60 0.73 0.88
e2 0.08 0.24 0.46 0.73 1.03 1.40 1.80 2.21 2.63
e3 0.15 0.46 0.87 1.39 1.99 2.63 3.35 4.18 5.15
e4 0.24 0.73 1.39 2.20 3.11 4.18 5.39 6.59 7.88
e5 0.34 1.03 2.00 3.12 4.48 5.97 7.56 9.37 11.34
e6 0.46 1.39 2.62 4.16 5.97 7.86 10.09 12.56 15.39
e7 0.60 1.79 3.34 5.39 7.60 10.16 13.00 16.23 19.44
e8 0.73 2.20 4.18 6.60 9.37 12.60 16.26 19.83 23.70
e9 0.87 2.62 5.15 7.93 11.34 15.44 19.40 23.66 28.58
For powers of 2 it tries to implement this surface:
1e7 2e7 3e7 4e7 5e7 6e7 7e7 8e7 9e7
1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
2 0.21 0.44 0.71 0.92 1.20 1.49 1.66 1.95 2.23
4 0.43 0.91 1.49 1.96 2.50 3.13 3.54 4.10 4.77
8 0.70 1.50 2.24 3.16 3.83 4.66 5.58 6.56 7.67
The function number were selected by starting with the theoretical
complexity of exp * log2(num)**2 and fiddling with the exponents
untill it more or less matched with the table.
Because this function is based on a limited set of data it might
not give accurate results outside these boundaries. The results
derived from large num and exp were quite accurate for small num
and very large exp though, except when num was a power of 2.
"""
if num in (0, 1) or exp in (0, 1):
return 0
elif (num & (num - 1)) == 0:
# For powers of 2 the scaling is a bit different.
return exp ** 1.092 * num.bit_length() ** 1.65 / 623212911.121
else:
return exp ** 1.590 * num.bit_length() ** 1.73 / 36864057619.3
def guarded_pow(left, right):
# Only handle ints because floats will overflow anyway.
if not isinstance(left, numbers.Integral):
pass
elif not isinstance(right, numbers.Integral):
pass
elif pow_complexity(left, right) < 0.5:
# Value 0.5 is arbitary and based on a estimated runtime of 0.5s
# on a fairly decent laptop processor.
pass
else:
raise ValueError("Pow expression too complex to calculate.")
return operator.pow(left, right)
class EquationEvaluator(ExpressionEvaluator):
__bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: guarded_mul,
ast.Div: operator.truediv,
ast.Pow: guarded_pow,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
ast.BitXor: guarded_pow
}
__unary_ops = {
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def __init__(self):
ExpressionEvaluator.__init__(
self,
bin_ops=self.__bin_ops,
unary_ops=self.__unary_ops
)
def __call__(self, expression_str):
result = ExpressionEvaluator.__call__(self, expression_str)
# This wrapper is here so additional sanity checks could be done
# on the result of the eval, but currently none are done.
return result
eval_equation = EquationEvaluator()
"""Evaluates a Python equation expression and returns the result.
Supports addition (+), subtraction (-), multiplication (*), division (/),
power (**) and modulo (%).
"""
sopel-6.3.0/sopel/tools/jobs.py 0000664 0000000 0000000 00000016670 12653772322 0016456 0 ustar 00root root 0000000 0000000 # coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division
import copy
import datetime
import sys
import threading
import time
if sys.version_info.major >= 3:
unicode = str
basestring = str
py3 = True
else:
py3 = False
try:
import Queue
except ImportError:
import queue as Queue
class released(object):
"""A context manager that releases a lock temporarily."""
def __init__(self, lock):
self.lock = lock
def __enter__(self):
self.lock.release()
def __exit__(self, _type, _value, _traceback):
self.lock.acquire()
class PriorityQueue(Queue.PriorityQueue):
"""A priority queue with a peek method."""
def peek(self):
"""Return a copy of the first element without removing it."""
self.not_empty.acquire()
try:
while not self._qsize():
self.not_empty.wait()
# Return a copy to avoid corrupting the heap. This is important
# for thread safety if the object is mutable.
return copy.deepcopy(self.queue[0])
finally:
self.not_empty.release()
class JobScheduler(threading.Thread):
"""Calls jobs assigned to it in steady intervals.
JobScheduler is a thread that keeps track of Jobs and calls them every
X seconds, where X is a property of the Job. It maintains jobs in a
priority queue, where the next job to be called is always the first
item.
Thread safety is maintained with a mutex that is released during long
operations, so methods add_job and clear_jobs can be safely called from
the main thread.
"""
min_reaction_time = 30.0 # seconds
"""How often should scheduler checks for changes in the job list."""
def __init__(self, bot):
"""Requires bot as argument for logging."""
threading.Thread.__init__(self)
self.bot = bot
self._jobs = PriorityQueue()
# While PriorityQueue it self is thread safe, this mutex is needed
# to stop old jobs being put into new queue after clearing the
# queue.
self._mutex = threading.Lock()
# self.cleared is used for more fine grained locking.
self._cleared = False
def add_job(self, job):
"""Add a Job to the current job queue."""
self._jobs.put(job)
def clear_jobs(self):
"""Clear current Job queue and start fresh."""
if self._jobs.empty():
# Guards against getting stuck waiting for self._mutex when
# thread is waiting for self._jobs to not be empty.
return
with self._mutex:
self._cleared = True
self._jobs = PriorityQueue()
def run(self):
"""Run forever."""
while True:
try:
self._do_next_job()
except Exception:
# Modules exceptions are caught earlier, so this is a bit
# more serious. Options are to either stop the main thread
# or continue this thread and hope that it won't happen
# again.
self.bot.error()
# Sleep a bit to guard against busy-looping and filling
# the log with useless error messages.
time.sleep(10.0) # seconds
def _do_next_job(self):
"""Wait until there is a job and do it."""
with self._mutex:
# Wait until the next job should be executed.
# This has to be a loop, because signals stop time.sleep().
while True:
job = self._jobs.peek()
difference = job.next_time - time.time()
duration = min(difference, self.min_reaction_time)
if duration <= 0:
break
with released(self._mutex):
time.sleep(duration)
self._cleared = False
job = self._jobs.get()
with released(self._mutex):
if job.func.thread:
t = threading.Thread(
target=self._call, args=(job.func,)
)
t.start()
else:
self._call(job.func)
job.next()
# If jobs were cleared during the call, don't put an old job
# into the new job queue.
if not self._cleared:
self._jobs.put(job)
def _call(self, func):
"""Wrapper for collecting errors from modules."""
# Sopel.bot.call is way too specialized to be used instead.
try:
func(self.bot)
except Exception:
self.bot.error()
class Job(object):
"""Hold information about when a function should be called next.
Job is a simple structure that hold information about when a function
should be called next.
They can be put in a priority queue, in which case the Job that should
be executed next is returned.
Calling the method next modifies the Job object for the next time it
should be executed. Current time is used to decide when the job should
be executed next so it should only be called right after the function
was called.
"""
max_catchup = 5
"""
This governs how much the scheduling of jobs is allowed
to get behind before they are simply thrown out to avoid
calling the same function too many times at once.
"""
def __init__(self, interval, func):
"""Initialize Job.
Args:
interval: number of seconds between calls to func
func: function to be called
"""
self.next_time = time.time() + interval
self.interval = interval
self.func = func
def next(self):
"""Update self.next_time with the assumption func was just called.
Returns: A modified job object.
"""
last_time = self.next_time
current_time = time.time()
delta = last_time + self.interval - current_time
if last_time > current_time + self.interval:
# Clock appears to have moved backwards. Reset
# the timer to avoid waiting for the clock to
# catch up to whatever time it was previously.
self.next_time = current_time + self.interval
elif delta < 0 and abs(delta) > self.interval * self.max_catchup:
# Execution of jobs is too far behind. Give up on
# trying to catch up and reset the time, so that
# will only be repeated a maximum of
# self.max_catchup times.
self.next_time = current_time - \
self.interval * self.max_catchup
else:
self.next_time = last_time + self.interval
return self
def __cmp__(self, other):
"""Compare Job objects according to attribute next_time."""
return self.next_time - other.next_time
if py3:
def __lt__(self, other):
return self.next_time < other.next_time
def __gt__(self, other):
return self.next_time > other.next_time
def __str__(self):
"""Return a string representation of the Job object.
Example result:
)>
"""
iso_time = str(datetime.fromtimestamp(self.next_time))
return "" % \
(iso_time, self.interval, self.func)
def __iter__(self):
"""This is an iterator. Never stops though."""
return self
sopel-6.3.0/sopel/tools/target.py 0000664 0000000 0000000 00000005504 12653772322 0017001 0 ustar 00root root 0000000 0000000 # coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division
import functools
from sopel.tools import Identifier
@functools.total_ordering
class User(object):
"""A representation of a user Sopel is aware of."""
def __init__(self, nick, user, host):
assert isinstance(nick, Identifier)
self.nick = nick
"""The user's nickname."""
self.user = user
"""The user's local username."""
self.host = host
"""The user's hostname."""
self.channels = {}
"""The channels the user is in.
This maps channel name ``Identifier``\s to ``Channel`` objects."""
self.account = None
"""The IRC services account of the user.
This relies on IRCv3 account tracking being enabled."""
self.away = None
"""Whether the user is marked as away."""
hostmask = property(lambda self: '{}!{}@{}'.format(self.nick, self.user,
self.host))
"""The user's full hostmask."""
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.nick == other.nick
def __lt__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.nick < other.nick
@functools.total_ordering
class Channel(object):
"""A representation of a channel Sopel is in."""
def __init__(self, name):
assert isinstance(name, Identifier)
self.name = name
"""The name of the channel."""
self.users = {}
"""The users in the channel.
This maps username ``Identifier``\s to channel objects."""
self.privileges = {}
"""The permissions of the users in the channel.
This maps username ``Identifier``s to bitwise integer values. This can
be compared to appropriate constants from ``sopel.module``."""
self.topic = ''
"""The topic of the channel."""
def clear_user(self, nick):
user = self.users[nick]
user.channels.pop(self.name, None)
del self.users[nick]
del self.privileges[nick]
def add_user(self, user):
assert isinstance(user, User)
self.users[user.nick] = user
self.privileges[user.nick] = 0
user.channels[self.name] = self
def rename_user(self, old, new):
if old in self.users:
self.users[new] = self.users.pop(old)
if old in self.privileges:
self.privileges[new] = self.privileges.pop(old)
def __eq__(self, other):
if not isinstance(other, Channel):
return NotImplemented
return self.name == other.name
def __lt__(self, other):
if not isinstance(other, Channel):
return NotImplemented
return self.name < other.name
sopel-6.3.0/sopel/tools/time.py 0000664 0000000 0000000 00000012173 12653772322 0016451 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Tools for getting and displaying the time."""
from __future__ import unicode_literals, absolute_import, print_function, division
import datetime
try:
import pytz
except:
pytz = False
def validate_timezone(zone):
"""Return an IETF timezone from the given IETF zone or common abbreviation.
If the length of the zone is 4 or less, it will be upper-cased before being
looked up; otherwise it will be title-cased. This is the expected
case-insensitivity behavior in the majority of cases. For example, ``'edt'``
and ``'america/new_york'`` will both return ``'America/New_York'``.
If the zone is not valid, ``ValueError`` will be raised. If ``pytz`` is not
available, and the given zone is anything other than ``'UTC'``,
``ValueError`` will be raised.
"""
if zone is None:
return None
if not pytz:
if zone.upper() != 'UTC':
raise ValueError('Only UTC available, since pytz is not installed.')
else:
return zone
zone = '/'.join(reversed(zone.split(', '))).replace(' ', '_')
if len(zone) <= 4:
zone = zone.upper()
else:
zone = zone.title()
if zone in pytz.all_timezones:
return zone
else:
raise ValueError("Invalid time zone.")
def validate_format(tformat):
"""Returns the format, if valid, else None"""
try:
time = datetime.datetime.utcnow()
time.strftime(tformat)
except:
raise ValueError('Invalid time format')
return tformat
def get_timezone(db=None, config=None, zone=None, nick=None, channel=None):
"""Find, and return, the approriate timezone
Time zone is pulled in the following priority:
1. `zone`, if it is valid
2. The timezone for the channel or nick `zone` in `db` if one is set and
valid.
3. The timezone for the nick `nick` in `db`, if one is set and valid.
4. The timezone for the channel `channel` in `db`, if one is set and valid.
5. The default timezone in `config`, if one is set and valid.
If `db` is not given, or given but not set up, steps 2 and 3 will be
skipped. If `config` is not given, step 4 will be skipped. If no step
yeilds a valid timezone, `None` is returned.
Valid timezones are those present in the IANA Time Zone Database. Prior to
checking timezones, two translations are made to make the zone names more
human-friendly. First, the string is split on `', '`, the pieces reversed,
and then joined with `'/'`. Next, remaining spaces are replaced with `'_'`.
Finally, strings longer than 4 characters are made title-case, and those 4
characters and shorter are made upper-case. This means "new york, america"
becomes "America/New_York", and "utc" becomes "UTC".
This function relies on `pytz` being available. If it is not available,
`None` will always be returned.
"""
def _check(zone):
try:
return validate_timezone(zone)
except ValueError:
return None
if not pytz:
return None
tz = None
if zone:
tz = _check(zone)
if not tz:
tz = _check(
db.get_nick_or_channel_value(zone, 'timezone'))
if not tz and nick:
tz = _check(db.get_nick_value(nick, 'timezone'))
if not tz and channel:
tz = _check(db.get_channel_value(channel, 'timezone'))
if not tz and config and config.core.default_timezone:
tz = _check(config.core.default_timezone)
return tz
def format_time(db=None, config=None, zone=None, nick=None, channel=None,
time=None):
"""Return a formatted string of the given time in the given zone.
`time`, if given, should be a naive `datetime.datetime` object and will be
treated as being in the UTC timezone. If it is not given, the current time
will be used. If `zone` is given and `pytz` is available, `zone` must be
present in the IANA Time Zone Database; `get_timezone` can be helpful for
this. If `zone` is not given or `pytz` is not available, UTC will be
assumed.
The format for the string is chosen in the following order:
1. The format for the nick `nick` in `db`, if one is set and valid.
2. The format for the channel `channel` in `db`, if one is set and valid.
3. The default format in `config`, if one is set and valid.
4. ISO-8601
If `db` is not given or is not set up, steps 1 and 2 are skipped. If config
is not given, step 3 will be skipped."""
tformat = None
if db:
if nick:
tformat = db.get_nick_value(nick, 'time_format')
if not tformat and channel:
tformat = db.get_channel_value(channel, 'time_format')
if not tformat and config and config.core.default_time_format:
tformat = config.core.default_time_format
if not tformat:
tformat = '%Y-%m-%d - %T%Z'
if not time:
time = datetime.datetime.utcnow()
if not pytz or not zone:
return time.strftime(tformat)
else:
if not time.tzinfo:
utc = pytz.timezone('UTC')
time = utc.localize(time)
zone = pytz.timezone(zone)
return time.astimezone(zone).strftime(tformat)
sopel-6.3.0/sopel/trigger.py 0000664 0000000 0000000 00000015726 12653772322 0016025 0 ustar 00root root 0000000 0000000 # coding=utf-8
from __future__ import unicode_literals, absolute_import, print_function, division
import re
import sys
import datetime
import sopel.tools
if sys.version_info.major >= 3:
unicode = str
basestring = str
class PreTrigger(object):
"""A parsed message from the server, which has not been matched against
any rules."""
component_regex = re.compile(r'([^!]*)!?([^@]*)@?(.*)')
intent_regex = re.compile('\x01(\\S+) (.*)\x01')
def __init__(self, own_nick, line):
"""own_nick is the bot's nick, needed to correctly parse sender.
line is the full line from the server."""
line = line.strip('\r')
self.line = line
# Break off IRCv3 message tags, if present
self.tags = {}
if line.startswith('@'):
tagstring, line = line.split(' ', 1)
for tag in tagstring[1:].split(';'):
tag = tag.split('=', 1)
if len(tag) > 1:
self.tags[tag[0]] = tag[1]
else:
self.tags[tag[0]] = None
self.time = datetime.datetime.utcnow()
if 'time' in self.tags:
try:
self.time = datetime.datetime.strptime(self.tags['time'], '%Y-%m-%dT%H:%M:%S.%fZ')
except ValueError:
pass # Server isn't conforming to spec, ignore the server-time
# TODO note what this is doing and why
if line.startswith(':'):
self.hostmask, line = line[1:].split(' ', 1)
else:
self.hostmask = None
# TODO note what this is doing and why
if ' :' in line:
argstr, text = line.split(' :', 1)
self.args = argstr.split(' ')
self.args.append(text)
else:
self.args = line.split(' ')
self.text = self.args[-1]
self.event = self.args[0]
self.args = self.args[1:]
components = PreTrigger.component_regex.match(self.hostmask or '').groups()
self.nick, self.user, self.host = components
self.nick = sopel.tools.Identifier(self.nick)
# If we have arguments, the first one is the sender
if self.args:
target = sopel.tools.Identifier(self.args[0])
else:
target = None
# Unless we're messaging the bot directly, in which case that second
# arg will be our bot's name.
if target and target.lower() == own_nick.lower():
target = self.nick
self.sender = target
# Parse CTCP into a form consistent with IRCv3 intents
if self.event == 'PRIVMSG' or self.event == 'NOTICE':
intent_match = PreTrigger.intent_regex.match(self.args[-1])
if intent_match:
intent, message = intent_match.groups()
self.tags['intent'] = intent
self.args[-1] = message or ''
class Trigger(unicode):
"""A line from the server, which has matched a callable's rules.
Note that CTCP messages (`PRIVMSG`es and `NOTICE`es which start and end
with `'\\x01'`) will have the `'\\x01'` bytes stripped, and the command
(e.g. `ACTION`) placed mapped to the `'intent'` key in `Trigger.tags`.
"""
sender = property(lambda self: self._pretrigger.sender)
"""The channel from which the message was sent.
In a private message, this is the nick that sent the message."""
time = property(lambda self: self._pretrigger.time)
"""A datetime object at which the message was received by the IRC server.
If the server does not support server-time, then `time` will be the time
that the message was received by Sopel"""
raw = property(lambda self: self._pretrigger.line)
"""The entire message, as sent from the server. This includes the CTCP
\\x01 bytes and command, if they were included."""
is_privmsg = property(lambda self: self._is_privmsg)
"""True if the trigger is from a user, False if it's from a channel."""
hostmask = property(lambda self: self._pretrigger.hostmask)
"""Hostmask of the person who sent the message as !@"""
user = property(lambda self: self._pretrigger.user)
"""Local username of the person who sent the message"""
nick = property(lambda self: self._pretrigger.nick)
"""The :class:`sopel.tools.Identifier` of the person who sent the message.
"""
host = property(lambda self: self._pretrigger.host)
"""The hostname of the person who sent the message"""
event = property(lambda self: self._pretrigger.event)
"""The IRC event (e.g. ``PRIVMSG`` or ``MODE``) which triggered the
message."""
match = property(lambda self: self._match)
"""The regular expression :class:`re.MatchObject` for the triggering line.
"""
group = property(lambda self: self._match.group)
"""The ``group`` function of the ``match`` attribute.
See Python :mod:`re` documentation for details."""
groups = property(lambda self: self._match.groups)
"""The ``groups`` function of the ``match`` attribute.
See Python :mod:`re` documentation for details."""
args = property(lambda self: self._pretrigger.args)
"""
A tuple containing each of the arguments to an event. These are the
strings passed between the event name and the colon. For example,
setting ``mode -m`` on the channel ``#example``, args would be
``('#example', '-m')``
"""
tags = property(lambda self: self._pretrigger.tags)
"""A map of the IRCv3 message tags on the message."""
admin = property(lambda self: self._admin)
"""True if the nick which triggered the command is one of the bot's admins.
"""
owner = property(lambda self: self._owner)
"""True if the nick which triggered the command is the bot's owner."""
account = property(lambda self: self.tags.get('account') or self._account)
"""The account name of the user sending the message.
This is only available if either the account-tag or the account-notify and
extended-join capabilites are available. If this isn't the case, or the user
sending the message isn't logged in, this will be None.
"""
def __new__(cls, config, message, match, account=None):
self = unicode.__new__(cls, message.args[-1] if message.args else '')
self._account = account
self._pretrigger = message
self._match = match
self._is_privmsg = message.sender and message.sender.is_nick()
def match_host_or_nick(pattern):
pattern = sopel.tools.get_hostmask_regex(pattern)
return bool(
pattern.match(self.nick) or
pattern.match('@'.join((self.nick, self.host)))
)
if config.core.owner_account:
self._owner = config.core.owner_account == self.account
else:
self._owner = match_host_or_nick(config.core.owner)
self._admin = (
self._owner or
self.account in config.core.admin_accounts or
any(match_host_or_nick(item) for item in config.core.admins)
)
return self
sopel-6.3.0/sopel/web.py 0000664 0000000 0000000 00000015756 12653772322 0015142 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""
*Availability: 3+, depreacted in 6.2.0*
The web class contains essential web-related functions for interaction with web
applications or websites in your modules. It supports HTTP GET, HTTP POST and
HTTP HEAD.
"""
# Copyright © 2008, Sean B. Palmer, inamidst.com
# Copyright © 2009, Michael Yanovich
# Copyright © 2012, Dimitri Molenaars, Tyrope.nl.
# Copyright © 2012-2013, Elad Alfassa,
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division
import re
import sys
import urllib
import os.path
import requests
from sopel import __version__
from sopel.tools import deprecated
if sys.version_info.major < 3:
import httplib
from htmlentitydefs import name2codepoint
from urlparse import urlparse
from urlparse import urlunparse
else:
import http.client as httplib
from html.entities import name2codepoint
from urllib.parse import urlparse
from urllib.parse import urlunparse
unichr = chr
unicode = str
try:
import ssl
if not hasattr(ssl, 'match_hostname'):
# Attempt to import ssl_match_hostname from python-backports
import backports.ssl_match_hostname
ssl.match_hostname = backports.ssl_match_hostname.match_hostname
ssl.CertificateError = backports.ssl_match_hostname.CertificateError
has_ssl = True
except ImportError:
has_ssl = False
USER_AGENT = 'Sopel/{} (http://sopel.chat)'.format(__version__)
default_headers = {'User-Agent': USER_AGENT}
ca_certs = None # Will be overriden when config loads. This is for an edge case.
class MockHttpResponse(httplib.HTTPResponse):
"Mock HTTPResponse with data that comes from requests."
def __init__(self, response):
self.headers = response.headers
self.status = response.status_code
self.reason = response.reason
self.close = response.close
self.read = response.raw.read
self.url = response.url
def geturl(self):
return self.url
# HTTP GET
@deprecated
def get(uri, timeout=20, headers=None, return_headers=False,
limit_bytes=None, verify_ssl=True, dont_decode=False):
"""Execute an HTTP GET query on `uri`, and return the result. Deprecated.
`timeout` is an optional argument, which represents how much time we should
wait before throwing a timeout exception. It defaults to 20, but can be set
to higher values if you are communicating with a slow web application.
`headers` is a dict of HTTP headers to send with the request. If
`return_headers` is True, return a tuple of (bytes, headers)
`limit_bytes` is ignored.
"""
if not uri.startswith('http'):
uri = "http://" + uri
if headers is None:
headers = default_headers
else:
headers = default_headers.update(headers)
u = requests.get(uri, timeout=timeout, headers=headers, verify=verify_ssl)
bytes = u.content
u.close()
headers = u.headers
if not dont_decode:
bytes = u.text
if not return_headers:
return bytes
else:
headers['_http_status'] = u.status_code
return (bytes, headers)
# Get HTTP headers
@deprecated
def head(uri, timeout=20, headers=None, verify_ssl=True):
"""Execute an HTTP GET query on `uri`, and return the headers. Deprecated.
`timeout` is an optional argument, which represents how much time we should
wait before throwing a timeout exception. It defaults to 20, but can be set
to higher values if you are communicating with a slow web application.
"""
if not uri.startswith('http'):
uri = "http://" + uri
if headers is None:
headers = default_headers
else:
headers = default_headers.update(headers)
u = requests.get(uri, timeout=timeout, headers=headers, verify=verify_ssl)
info = u.headers
u.close()
return info
# HTTP POST
@deprecated
def post(uri, query, limit_bytes=None, timeout=20, verify_ssl=True, return_headers=False):
"""Execute an HTTP POST query. Deprecated.
`uri` is the target URI, and `query` is the POST data. `headers` is a dict
of HTTP headers to send with the request.
`limit_bytes` is ignored.
"""
if not uri.startswith('http'):
uri = "http://" + uri
u = requests.post(uri, timeout=timeout, verify=verify_ssl, data=query)
bytes = u.raw.read(limit_bytes)
headers = u.headers
u.close()
if not return_headers:
return bytes
else:
headers['_http_status'] = u.status_code
return (bytes, headers)
r_entity = re.compile(r'&([^;\s]+);')
def entity(match):
value = match.group(1).lower()
if value.startswith('#x'):
return unichr(int(value[2:], 16))
elif value.startswith('#'):
return unichr(int(value[1:]))
elif value in name2codepoint:
return unichr(name2codepoint[value])
return '[' + value + ']'
def decode(html):
return r_entity.sub(entity, html)
# For internal use in web.py, (modules can use this if they need a urllib
# object they can execute read() on) Both handles redirects and makes sure
# input URI is UTF-8
@deprecated
def get_urllib_object(uri, timeout, headers=None, verify_ssl=True, data=None):
"""Return an HTTPResponse object for `uri` and `timeout` and `headers`. Deprecated
"""
if headers is None:
headers = default_headers
else:
headers = default_headers.update(headers)
if data is not None:
response = requests.post(uri, timeout=timeout, verify=verify_ssl,
data=data, headers=headers)
else:
response = requests.get(uri, timeout=timeout, verify=verify_ssl,
headers=headers)
return MockHttpResponse(response)
# Identical to urllib2.quote
def quote(string, safe='/'):
"""Like urllib2.quote but handles unicode properly."""
if sys.version_info.major < 3:
if isinstance(string, unicode):
string = string.encode('utf8')
string = urllib.quote(string, safe.encode('utf8'))
else:
string = urllib.parse.quote(str(string), safe)
return string
def quote_query(string):
"""Quotes the query parameters."""
parsed = urlparse(string)
string = string.replace(parsed.query, quote(parsed.query, "/=&"), 1)
return string
# Functions for international domain name magic
def urlencode_non_ascii(b):
regex = '[\x80-\xFF]'
if sys.version_info.major > 2:
regex = b'[\x80-\xFF]'
return re.sub(regex, lambda c: '%%%02x' % ord(c.group(0)), b)
def iri_to_uri(iri):
parts = urlparse(iri)
parts_seq = (part.encode('idna') if parti == 1 else urlencode_non_ascii(part.encode('utf-8')) for parti, part in enumerate(parts))
if sys.version_info.major > 2:
parts_seq = list(parts_seq)
parsed = urlunparse(parts_seq)
if sys.version_info.major > 2:
return parsed.decode()
else:
return parsed
if sys.version_info.major < 3:
urlencode = urllib.urlencode
else:
urlencode = urllib.parse.urlencode
sopel-6.3.0/test/ 0000775 0000000 0000000 00000000000 12653772322 0013632 5 ustar 00root root 0000000 0000000 sopel-6.3.0/test/test_config.py 0000664 0000000 0000000 00000001665 12653772322 0016520 0 ustar 00root root 0000000 0000000 # coding=utf-8
from __future__ import unicode_literals, division, print_function, absolute_import
import os
import tempfile
import unittest
from sopel import config
from sopel.config import types
class FakeConfigSection(types.StaticSection):
attr = types.ValidatedAttribute('attr')
class ConfigFunctionalTest(unittest.TestCase):
def read_config(self):
configo = config.Config(self.filename)
configo.define_section('fake', FakeConfigSection)
return configo
def setUp(self):
self.filename = tempfile.mkstemp()[1]
with open(self.filename, 'w') as fileo:
fileo.write(
"[core]\n"
"owner=embolalia"
)
self.config = self.read_config()
def tearDown(self):
os.remove(self.filename)
def test_validated_string_when_none(self):
self.config.fake.attr = None
self.assertEquals(self.config.fake.attr, None)
sopel-6.3.0/test/test_db.py 0000664 0000000 0000000 00000017076 12653772322 0015643 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Tests for the new database functionality.
TODO: Most of these tests assume functionality tested in other tests. This is
enough to get everything working (and is better than nothing), but best
practice would probably be not to do that."""
from __future__ import unicode_literals
from __future__ import absolute_import
import json
import os
import sqlite3
import sys
import tempfile
import pytest
from sopel.db import SopelDB
from sopel.test_tools import MockConfig
from sopel.tools import Identifier
db_filename = tempfile.mkstemp()[1]
if sys.version_info.major >= 3:
unicode = str
basestring = str
iteritems = dict.items
itervalues = dict.values
iterkeys = dict.keys
else:
iteritems = dict.iteritems
itervalues = dict.itervalues
iterkeys = dict.iterkeys
@pytest.fixture
def db():
config = MockConfig()
config.core.db_filename = db_filename
db = SopelDB(config)
# TODO add tests to ensure db creation works properly, too.
return db
def teardown_function(function):
os.remove(db_filename)
def test_get_nick_id(db):
conn = sqlite3.connect(db_filename)
tests = [
[None, 'embolalia', Identifier('Embolalia')],
# Ensures case conversion is handled properly
[None, '[][]', Identifier('[]{}')],
# Unicode, just in case
[None, 'embölaliå', Identifier('EmbölaliÅ')],
]
for test in tests:
test[0] = db.get_nick_id(test[2])
nick_id, slug, nick = test
with conn:
cursor = conn.cursor()
registered = cursor.execute(
'SELECT nick_id, slug, canonical FROM nicknames WHERE canonical IS ?', [nick]
).fetchall()
assert len(registered) == 1
assert registered[0][1] == slug and registered[0][2] == nick
# Check that each nick ended up with a different id
assert len(set(test[0] for test in tests)) == len(tests)
# Check that the retrieval actually is idempotent
for test in tests:
nick_id = test[0]
new_id = db.get_nick_id(test[2])
assert nick_id == new_id
# Even if the case is different
for test in tests:
nick_id = test[0]
new_id = db.get_nick_id(Identifier(test[2].upper()))
assert nick_id == new_id
def test_alias_nick(db):
nick = 'Embolalia'
aliases = ['EmbölaliÅ', 'Embo`work', 'Embo']
nick_id = db.get_nick_id(nick)
for alias in aliases:
db.alias_nick(nick, alias)
for alias in aliases:
assert db.get_nick_id(alias) == nick_id
db.alias_nick('both', 'arenew') # Shouldn't fail.
with pytest.raises(ValueError):
db.alias_nick('Eve', nick)
with pytest.raises(ValueError):
db.alias_nick(nick, nick)
def test_set_nick_value(db):
conn = sqlite3.connect(db_filename)
cursor = conn.cursor()
nick = 'Embolalia'
nick_id = db.get_nick_id(nick)
data = {
'key': 'value',
'number_key': 1234,
'unicode': 'EmbölaliÅ',
}
def check():
for key, value in iteritems(data):
db.set_nick_value(nick, key, value)
for key, value in iteritems(data):
found_value = cursor.execute(
'SELECT value FROM nick_values WHERE nick_id = ? AND key = ?',
[nick_id, key]
).fetchone()[0]
assert json.loads(unicode(found_value)) == value
check()
# Test updates
data['number_key'] = 'not a number anymore!'
data['unicode'] = 'This is different toö!'
check()
def test_get_nick_value(db):
conn = sqlite3.connect(db_filename)
cursor = conn.cursor()
nick = 'Embolalia'
nick_id = db.get_nick_id(nick)
data = {
'key': 'value',
'number_key': 1234,
'unicode': 'EmbölaliÅ',
}
for key, value in iteritems(data):
cursor.execute('INSERT INTO nick_values VALUES (?, ?, ?)',
[nick_id, key, json.dumps(value, ensure_ascii=False)])
conn.commit()
for key, value in iteritems(data):
found_value = db.get_nick_value(nick, key)
assert found_value == value
def test_unalias_nick(db):
conn = sqlite3.connect(db_filename)
nick = 'Embolalia'
nick_id = 42
conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)',
[nick_id, Identifier(nick).lower(), nick])
aliases = ['EmbölaliÅ', 'Embo`work', 'Embo']
for alias in aliases:
conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)',
[nick_id, Identifier(alias).lower(), alias])
conn.commit()
for alias in aliases:
db.unalias_nick(alias)
for alias in aliases:
found = conn.execute(
'SELECT * FROM nicknames WHERE nick_id = ?',
[nick_id]).fetchall()
assert len(found) == 1
def test_delete_nick_group(db):
conn = sqlite3.connect(db_filename)
aliases = ['Embolalia', 'Embo']
nick_id = 42
for alias in aliases:
conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)',
[nick_id, Identifier(alias).lower(), alias])
conn.commit()
db.set_nick_value(aliases[0], 'foo', 'bar')
db.set_nick_value(aliases[1], 'spam', 'eggs')
db.delete_nick_group(aliases[0])
# Nothing else has created values, so we know the tables are empty
nicks = conn.execute('SELECT * FROM nicknames').fetchall()
assert len(nicks) == 0
data = conn.execute('SELECT * FROM nick_values').fetchone()
assert data is None
def test_merge_nick_groups(db):
conn = sqlite3.connect(db_filename)
aliases = ['Embolalia', 'Embo']
for nick_id, alias in enumerate(aliases):
conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)',
[nick_id, Identifier(alias).lower(), alias])
conn.commit()
finals = (('foo', 'bar'), ('bar', 'blue'), ('spam', 'eggs'))
db.set_nick_value(aliases[0], finals[0][0], finals[0][1])
db.set_nick_value(aliases[0], finals[1][0], finals[1][1])
db.set_nick_value(aliases[1], 'foo', 'baz')
db.set_nick_value(aliases[1], finals[2][0], finals[2][1])
db.merge_nick_groups(aliases[0], aliases[1])
nick_ids = conn.execute('SELECT nick_id FROM nicknames')
nick_id = nick_ids.fetchone()[0]
alias_id = nick_ids.fetchone()[0]
assert nick_id == alias_id
for key, value in finals:
found = conn.execute(
'SELECT value FROM nick_values WHERE nick_id = ? AND key = ?',
[nick_id, key]).fetchone()[0]
assert json.loads(unicode(found)) == value
def test_set_channel_value(db):
conn = sqlite3.connect(db_filename)
db.set_channel_value('#asdf', 'qwer', 'zxcv')
result = conn.execute(
'SELECT value FROM channel_values WHERE channel = ? and key = ?',
['#asdf', 'qwer']).fetchone()[0]
assert result == '"zxcv"'
def test_get_channel_value(db):
conn = sqlite3.connect(db_filename)
conn.execute("INSERT INTO channel_values VALUES ('#asdf', 'qwer', '\"zxcv\"')")
conn.commit()
result = db.get_channel_value('#asdf', 'qwer')
assert result == 'zxcv'
def test_get_nick_or_channel_value(db):
db.set_nick_value('asdf', 'qwer', 'poiu')
db.set_channel_value('#asdf', 'qwer', '/.,m')
assert db.get_nick_or_channel_value('asdf', 'qwer') == 'poiu'
assert db.get_nick_or_channel_value('#asdf', 'qwer') == '/.,m'
def test_get_preferred_value(db):
db.set_nick_value('asdf', 'qwer', 'poiu')
db.set_channel_value('#asdf', 'qwer', '/.,m')
db.set_channel_value('#asdf', 'lkjh', '1234')
names = ['asdf', '#asdf']
assert db.get_preferred_value(names, 'qwer') == 'poiu'
assert db.get_preferred_value(names, 'lkjh') == '1234'
sopel-6.3.0/test/test_formatting.py 0000664 0000000 0000000 00000001325 12653772322 0017416 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Tests for message formatting"""
from __future__ import unicode_literals, absolute_import, print_function, division
import pytest
from sopel.formatting import colors, color, bold, underline
def test_color():
text = 'Hello World'
assert color(text) == text
assert color(text, colors.PINK) == '\x0313' + text + '\x03'
assert color(text, colors.PINK, colors.TEAL) == '\x0313,10' + text + '\x03'
pytest.raises(ValueError, color, text, 100)
pytest.raises(ValueError, color, text, 'INVALID')
def test_bold():
text = 'Hello World'
assert bold(text) == '\x02' + text + '\x02'
def test_underline():
text = 'Hello World'
assert underline(text) == '\x1f' + text + '\x1f'
sopel-6.3.0/test/test_module.py 0000664 0000000 0000000 00000012450 12653772322 0016532 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Tests for message formatting"""
from __future__ import unicode_literals, absolute_import, print_function, division
import pytest
from sopel.trigger import PreTrigger, Trigger
from sopel.test_tools import MockSopel, MockSopelWrapper
from sopel.tools import Identifier
from sopel import module
@pytest.fixture
def sopel():
bot = MockSopel('Sopel')
bot.config.core.owner = 'Bar'
return bot
@pytest.fixture
def bot(sopel, pretrigger):
bot = MockSopelWrapper(sopel, pretrigger)
bot.privileges = dict()
bot.privileges[Identifier('#Sopel')] = dict()
bot.privileges[Identifier('#Sopel')][Identifier('Foo')] = module.VOICE
return bot
@pytest.fixture
def pretrigger():
line = ':Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
return PreTrigger(Identifier('Foo'), line)
@pytest.fixture
def pretrigger_pm():
line = ':Foo!foo@example.com PRIVMSG Sopel :Hello, world'
return PreTrigger(Identifier('Foo'), line)
@pytest.fixture
def trigger_owner(bot):
line = ':Bar!bar@example.com PRIVMSG #Sopel :Hello, world'
return Trigger(bot.config, PreTrigger(Identifier('Bar'), line), None)
@pytest.fixture
def trigger(bot, pretrigger):
return Trigger(bot.config, pretrigger, None)
@pytest.fixture
def trigger_pm(bot, pretrigger_pm):
return Trigger(bot.config, pretrigger_pm, None)
def test_unblockable():
@module.unblockable
def mock(bot, trigger, match):
return True
assert mock.unblockable is True
def test_interval():
@module.interval(5)
def mock(bot, trigger, match):
return True
assert mock.interval == [5]
def test_rule():
@module.rule('.*')
def mock(bot, trigger, match):
return True
assert mock.rule == ['.*']
def test_thread():
@module.thread(True)
def mock(bot, trigger, match):
return True
assert mock.thread is True
def test_commands():
@module.commands('sopel')
def mock(bot, trigger, match):
return True
assert mock.commands == ['sopel']
def test_nick_commands():
@module.nickname_commands('sopel')
def mock(bot, trigger, match):
return True
assert mock.rule == ["""
^
$nickname[:,]? # Nickname.
\s+(sopel) # Command as group 1.
(?:\s+ # Whitespace to end command.
( # Rest of the line as group 2.
(?:(\S+))? # Parameters 1-4 as groups 3-6.
(?:\s+(\S+))?
(?:\s+(\S+))?
(?:\s+(\S+))?
.* # Accept anything after the parameters. Leave it up to
# the module to parse the line.
))? # Group 1 must be None, if there are no parameters.
$ # EoL, so there are no partial matches.
"""]
def test_priority():
@module.priority('high')
def mock(bot, trigger, match):
return True
assert mock.priority == 'high'
def test_event():
@module.event('301')
def mock(bot, trigger, match):
return True
assert mock.event == ['301']
def test_intent():
@module.intent('ACTION')
def mock(bot, trigger, match):
return True
assert mock.intents == ['ACTION']
def test_rate():
@module.rate(5)
def mock(bot, trigger, match):
return True
assert mock.rate == 5
def test_require_privmsg(bot, trigger, trigger_pm):
@module.require_privmsg('Try again in a PM')
def mock(bot, trigger, match=None):
return True
assert mock(bot, trigger) is not True
assert mock(bot, trigger_pm) is True
@module.require_privmsg
def mock_(bot, trigger, match=None):
return True
assert mock_(bot, trigger) is not True
assert mock_(bot, trigger_pm) is True
def test_require_chanmsg(bot, trigger, trigger_pm):
@module.require_chanmsg('Try again in a channel')
def mock(bot, trigger, match=None):
return True
assert mock(bot, trigger) is True
assert mock(bot, trigger_pm) is not True
@module.require_chanmsg
def mock_(bot, trigger, match=None):
return True
assert mock(bot, trigger) is True
assert mock(bot, trigger_pm) is not True
def test_require_privilege(bot, trigger):
@module.require_privilege(module.VOICE)
def mock_v(bot, trigger, match=None):
return True
assert mock_v(bot, trigger) is True
@module.require_privilege(module.OP, 'You must be at least opped!')
def mock_o(bot, trigger, match=None):
return True
assert mock_o(bot, trigger) is not True
def test_require_admin(bot, trigger, trigger_owner):
@module.require_admin('You must be an admin')
def mock(bot, trigger, match=None):
return True
assert mock(bot, trigger) is not True
@module.require_admin
def mock_(bot, trigger, match=None):
return True
assert mock_(bot, trigger_owner) is True
def test_require_owner(bot, trigger, trigger_owner):
@module.require_owner('You must be an owner')
def mock(bot, trigger, match=None):
return True
assert mock(bot, trigger) is not True
@module.require_owner
def mock_(bot, trigger, match=None):
return True
assert mock_(bot, trigger_owner) is True
def test_example(bot, trigger):
@module.commands('mock')
@module.example('.mock', 'True')
def mock(bot, trigger, match=None):
return True
assert mock(bot, trigger) is True
sopel-6.3.0/test/test_trigger.py 0000664 0000000 0000000 00000013636 12653772322 0016717 0 ustar 00root root 0000000 0000000 # coding=utf-8
"""Tests for message parsing"""
from __future__ import unicode_literals, absolute_import, print_function, division
import re
import pytest
import datetime
from sopel.test_tools import MockConfig
from sopel.trigger import PreTrigger, Trigger
from sopel.tools import Identifier
@pytest.fixture
def nick():
return Identifier('Sopel')
def test_basic_pretrigger(nick):
line = ':Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {}
assert pretrigger.hostmask == 'Foo!foo@example.com'
assert pretrigger.line == line
assert pretrigger.args == ['#Sopel', 'Hello, world']
assert pretrigger.event == 'PRIVMSG'
assert pretrigger.nick == Identifier('Foo')
assert pretrigger.user == 'foo'
assert pretrigger.host == 'example.com'
assert pretrigger.sender == '#Sopel'
def test_pm_pretrigger(nick):
line = ':Foo!foo@example.com PRIVMSG Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {}
assert pretrigger.hostmask == 'Foo!foo@example.com'
assert pretrigger.line == line
assert pretrigger.args == ['Sopel', 'Hello, world']
assert pretrigger.event == 'PRIVMSG'
assert pretrigger.nick == Identifier('Foo')
assert pretrigger.user == 'foo'
assert pretrigger.host == 'example.com'
assert pretrigger.sender == Identifier('Foo')
def test_tags_pretrigger(nick):
line = '@foo=bar;baz;sopel.chat/special=value :Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {'baz': None,
'foo': 'bar',
'sopel.chat/special': 'value'}
assert pretrigger.hostmask == 'Foo!foo@example.com'
assert pretrigger.line == line
assert pretrigger.args == ['#Sopel', 'Hello, world']
assert pretrigger.event == 'PRIVMSG'
assert pretrigger.nick == Identifier('Foo')
assert pretrigger.user == 'foo'
assert pretrigger.host == 'example.com'
assert pretrigger.sender == '#Sopel'
def test_intents_pretrigger(nick):
line = '@intent=ACTION :Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {'intent': 'ACTION'}
assert pretrigger.hostmask == 'Foo!foo@example.com'
assert pretrigger.line == line
assert pretrigger.args == ['#Sopel', 'Hello, world']
assert pretrigger.event == 'PRIVMSG'
assert pretrigger.nick == Identifier('Foo')
assert pretrigger.user == 'foo'
assert pretrigger.host == 'example.com'
assert pretrigger.sender == '#Sopel'
def test_unusual_pretrigger(nick):
line = 'PING'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {}
assert pretrigger.hostmask is None
assert pretrigger.line == line
assert pretrigger.args == []
assert pretrigger.event == 'PING'
def test_ctcp_intent_pretrigger(nick):
line = ':Foo!foo@example.com PRIVMSG Sopel :\x01VERSION\x01'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {'intent': 'VERSION'}
assert pretrigger.hostmask == 'Foo!foo@example.com'
assert pretrigger.line == line
assert pretrigger.args == ['Sopel', '']
assert pretrigger.event == 'PRIVMSG'
assert pretrigger.nick == Identifier('Foo')
assert pretrigger.user == 'foo'
assert pretrigger.host == 'example.com'
assert pretrigger.sender == Identifier('Foo')
def test_ctcp_data_pretrigger(nick):
line = ':Foo!foo@example.com PRIVMSG Sopel :\x01PING 1123321\x01'
pretrigger = PreTrigger(nick, line)
assert pretrigger.tags == {'intent': 'PING'}
assert pretrigger.hostmask == 'Foo!foo@example.com'
assert pretrigger.line == line
assert pretrigger.args == ['Sopel', '1123321']
assert pretrigger.event == 'PRIVMSG'
assert pretrigger.nick == Identifier('Foo')
assert pretrigger.user == 'foo'
assert pretrigger.host == 'example.com'
assert pretrigger.sender == Identifier('Foo')
def test_intents_trigger(nick):
line = '@intent=ACTION :Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
config = MockConfig()
config.core.owner = 'Foo'
config.core.admins = ['Bar']
fakematch = re.match('.*', line)
trigger = Trigger(config, pretrigger, fakematch)
assert trigger.sender == '#Sopel'
assert trigger.raw == line
assert trigger.is_privmsg is False
assert trigger.hostmask == 'Foo!foo@example.com'
assert trigger.user == 'foo'
assert trigger.nick == Identifier('Foo')
assert trigger.host == 'example.com'
assert trigger.event == 'PRIVMSG'
assert trigger.match == fakematch
assert trigger.group == fakematch.group
assert trigger.groups == fakematch.groups
assert trigger.args == ['#Sopel', 'Hello, world']
assert trigger.tags == {'intent': 'ACTION'}
assert trigger.admin is True
assert trigger.owner is True
def test_ircv3_account_tag_trigger(nick):
line = '@account=Foo :Nick_Is_Not_Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
config = MockConfig()
config.core.owner_account = 'Foo'
config.core.admins = ['Bar']
fakematch = re.match('.*', line)
trigger = Trigger(config, pretrigger, fakematch)
assert trigger.admin is True
assert trigger.owner is True
def test_ircv3_server_time_trigger(nick):
line = '@time=2016-01-09T03:15:42.000Z :Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
config = MockConfig()
config.core.owner = 'Foo'
config.core.admins = ['Bar']
fakematch = re.match('.*', line)
trigger = Trigger(config, pretrigger, fakematch)
assert trigger.time == datetime.datetime(2016, 1, 9, 3, 15, 42, 0)
# Spec-breaking string
line = '@time=2016-01-09T04:20 :Foo!foo@example.com PRIVMSG #Sopel :Hello, world'
pretrigger = PreTrigger(nick, line)
assert pretrigger.time is not None