diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index e044f79..0000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# CloudBot editor configuration normalization -# Copied from Drupal (GPL) -# @see http://editorconfig.org/ - -# This is the top-most .editorconfig file; do not search in parent directories. -root = true - -# All files. -[*] -end_of_line = LF -indent_style = space -indent_size = 4 - -# Not in the spec yet: -# @see https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 5fa7446..2f47b6d --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ persist config -config.ssl gitflow *.db *.log @@ -8,8 +7,7 @@ gitflow *.pyc *.orig .project +.pydevproject .geany *.sublime-project -*.sublime-workspace -.idea/ -plugins/data/GeoLiteCity.dat +*.sublime-workspace \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 45407c1..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,55 +0,0 @@ -# How to contribute - -I like to encourage you to contribute to the repository. -This should be as easy as possible for you but there are a few things to consider when contributing. -The following guidelines for contribution should be followed if you want to submit a pull request. - -## TL;DR - -* Read [Github documentation](http://help.github.com/) and [Pull Request documentation](http://help.github.com/send-pull-requests/) -* Fork the repository -* Edit the files, add new files -* Check the files with [`pep8`](https://pypi.python.org/pypi/pep8), fix any reported errors -* Check that the files work as expected in CloudBot -* Create a new branch with a descriptive name for your feature (optional) -* Commit changes, push to your fork on GitHub -* Create a new pull request, provide a short summary of changes in the title line, with more information in the description field. -* After submitting the pull request, join the IRC channel (irc.esper.net #cloudbot) and paste a link to the pull request so people are aware of it -* After discussion, your pull request will be accepted or rejected. - -## How to prepare - -* You need a [GitHub account](https://github.com/signup/free) -* Submit an [issue ticket](https://github.com/ClouDev/CloudBot/issues) for your issue if the is no one yet. - * Describe the issue and include steps to reproduce if it's a bug. - * Ensure to mention the earliest version that you know is affected. -* If you are able and want to fix this, fork the repository on GitHub - -## Make Changes - -* In your forked repository, create a topic branch for your upcoming patch. (e.g. `feature--autoplay` or `bugfix--ios-crash`) - * Usually this is based on the develop branch. - * Create a branch based on master; `git branch - fix/develop/my_contribution develop` then checkout the new branch with `git - checkout fix/develop/my_contribution`. Please avoid working directly on the `develop` branch. -* Make sure you stick to the coding style that is used already. -* Make use of the [`.editorconfig`](http://editorconfig.org/) file. -* Make commits of logical units and describe them properly. -* Check for unnecessary whitespace with `git diff --check` before committing. -* Check your changes with [`pep8`](https://pypi.python.org/pypi/pep8). Ignore messages about line length. - -## Submit Changes - -* Push your changes to a topic branch in your fork of the repository. -* Open a pull request to the original repository and choose the right original branch you want to patch. - _Advanced users may use [`hub`](https://github.com/defunkt/hub#git-pull-request) gem for that._ -* If not done in commit messages (which you really should do) please reference and update your issue with the code changes. But _please do not close the issue yourself_. -_Notice: You can [turn your previously filed issues into a pull-request here](http://issue2pr.herokuapp.com/)._ -* Even if you have write access to the repository, do not directly push or merge pull-requests. Let another team member review your pull request and approve. - -# Additional Resources - -* [General GitHub documentation](http://help.github.com/) -* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) -* [Read the Issue Guidelines by @necolas](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md) for more details -* [This CONTRIBUTING.md from here](https://github.com/anselmh/CONTRIBUTING.md) diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 5495bc9..0000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,34 +0,0 @@ -Thanks to everyone who has contributed to CloudBot! Come in IRC and ping me if I forgot anyone. - -Luke Rogers (lukeroge) -Neersighted -blha303 -cybojenix -KsaRedFx -nathanblaney -thenoodle68 -nasonfish -urbels -puffrfish -Sepero -TheFiZi -mikeleigh -Spudstabber -frozenMC -frdmn - - - -We are using code from the following projects: -./plugins/mlia.py - https://github.com/infinitylabs/UguuBot -./plugins/horoscope.py - https://github.com/infinitylabs/UguuBot -color section in ./plugins/utility.py - https://github.com/hitzler/homero - -Special Thanks: -Rmmh (created skybot!) -lahwran (for his advice and stuff I stole from his skybot fork!) -TheNoodle (for helping with some plugins when I was first starting out) - -If any of your code is in here and you don't have credit, I'm sorry. I didn't keep track of a lot of code I added in the early days of the project. - -You are all awesome :) diff --git a/DOCUMENTATION b/DOCUMENTATION new file mode 100755 index 0000000..a5daa54 --- /dev/null +++ b/DOCUMENTATION @@ -0,0 +1 @@ +Please see the wiki @ http://git.io/cloudbotircwiki \ No newline at end of file diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 06d64ac..3485fac --- a/README.md +++ b/README.md @@ -1,12 +1,25 @@ -# CloudBot +# CloudBot/DEV ## About -CloudBot is a Python IRC bot based on [Skybot](http://git.io/skybot) by [rmmh](http://git.io/rmmh). +CloudBot is a Python IRC bot very heavily based on [Skybot](http://git.io/skybot) by [rmmh](http://git.io/rmmh). + +### Goals + +* Easy to use wrapper +* Intuitive configuration +* Fully controlled from IRC +* Fully compatable with existing skybot plugins +* Easily extendable + * Thorough documentation + * Cross-platform +* Muti-threaded, efficient + * Automatic reloading + * Little boilerplate ## Getting and using CloudBot -### Download +### Download Get CloudBot at [https://github.com/ClouDev/CloudBot/zipball/develop](https://github.com/ClouDev/CloudBot/zipball/develop "Get CloudBot from Github!"). @@ -14,33 +27,51 @@ Unzip the resulting file, and continue to read this document. ### Install -Before you can run the bot, you need to install a few Python dependencies. LXML is required while Enchant and PyDNS are needed for several plugins. - - -These can be installed with `pip` (The Python package manager): +Before you can run the bot, you need to install a few Python dependencies. These can be installed with `pip` (The Python package manager): [sudo] pip install -r requirements.txt - -If you use `pip`, you will also need the following packages on linux or `pip` will fail to install the requirements. - ```python, python-dev, libenchant-dev, libenchant1c2a, libxslt-dev, libxml2-dev.``` #### How to install `pip` curl -O http://python-distribute.org/distribute_setup.py # or download with your browser on windows python distribute_setup.py easy_install pip - -If you are unable to use pip, there are Windows installers for LXML available for [64 bit](https://pypi.python.org/packages/2.7/l/lxml/lxml-2.3.win-amd64-py2.7.exe) and [32 bit](https://pypi.python.org/packages/2.7/l/lxml/lxml-2.3.win32-py2.7.exe) versions of Python. ### Run -Before you run the bot, rename `config.default` to `config` and edit it with your preferred settings. +Once you have installed the required dependencies, there are two ways you can run the bot: -Once you have installed the required dependencies and renamed the config file, you can run the bot! Make sure you are in the correct folder and run the following command: +#### Launcher + +**Note:** Due to some issues with the launcher we recommend you run the bot manually as detailed below. + +The launcher will start the bot as a background process, and allow the bot to close and restart itself. This is only supported on unix-like machines (not Windows). + +For the launcher to work properly, install `screen`, or `daemon` (daemon is recommended): + +`apt-get install screen` + +`apt-get install daemon` + +Once you have installed either `screen` or `daemon`, run the start command: + +`./cloudbot start` + +It will generate a default config for you. Once you have edited the config, run it again with the same command: + +`./cloudbot start` + +This will start up your bot as a background process. To stop it, use `./cloudbot stop`. (Config docs at the [wiki](http://git.io/cloudbotircconfig)) + +#### Manually + +To manually run the bot and get console output, run it with: `python bot.py` -On Windows you can usually just double-click `bot.py` to start the bot, as long as you have Python installed correctly. +On Windows you can usually just double-click the `bot.py` file to start the bot, as long as you have Python installed correctly. + +(note: running the bot without the launcher breaks the start and restart commands) ## Getting help with CloudBot @@ -52,8 +83,6 @@ To write your own plugins, visit the [Plugin Wiki Page](http://git.io/cloudbotir More at the [Wiki Main Page](http://git.io/cloudbotircwiki). -(some of the information on the wiki is outdated and needs to be rewritten) - ### Support The developers reside in [#CloudBot](irc://irc.esper.net/cloudbot) on [EsperNet](http://esper.net) and would be glad to help you. @@ -62,25 +91,31 @@ If you think you have found a bug/have a idea/suggestion, please **open a issue* ### Requirements -CloudBot runs on **Python** *2.7.x*. It is currently developed on **Windows** *8* with **Python** *2.7.5*. +CloudBot runs on **Python** *2.7.x*. It is developed on **Ubuntu** *12.04* with **Python** *2.7.3*. -It **requires the Python module** lXML. -The module `Enchant` is needed for the spellcheck plugin. -The module `PyDNS` is needed for SRV record lookup in the mcping plugin. +It **requires the Python module** `lXML`, and `Enchant` is needed for the spellcheck plugin. -**Windows** users: Windows compatibility some plugins is **broken** (such as ping), but we do intend to add it. Eventually. +The programs `daemon` or `screen` are recomended for the launcher to run optimaly. + +**Windows** users: Windows compatibility with the launcher and some plugins is **broken** (such as ping), but we do intend to add it.³ ## Example CloudBots -You can find a number of example bots in [#CloudBot](irc://irc.esper.net/cloudbot "Connect via IRC to #CloudBot on irc.esper.net"). +The developers of CloudBot run two CloudBots on [Espernet](http://esper.net). + +They can both be found in [#CloudBot](irc://irc.esper.net/cloudbot "Connect via IRC to #CloudBot on irc.esper.net"). + +**mau5bot** is the semi-stable bot, and runs on the latest stable development version of CloudBot. (mau5bot is running on **Ubuntu Server** *12.04* with **Python** *2.7.3*) + +**neerbot** is unstable bot, and runs on the `HEAD` of the `develop` branch. (neerbot is running on **Debian** *Wheezy/Testing* with **Python** *2.7.2*) ## License CloudBot is **licensed** under the **GPL v3** license. The terms are as follows. - CloudBot + CloudBot/DEV - Copyright © 2011-2013 Luke Rogers + Copyright © 2011-2012 Luke Rogers / ClouDev - <[cloudev.github.com](http://cloudev.github.com)> CloudBot is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -94,3 +129,7 @@ CloudBot is **licensed** under the **GPL v3** license. The terms are as follows. You should have received a copy of the GNU General Public License along with CloudBot. If not, see . + +## Notes + +³ eventually diff --git a/cloudbot.py b/bot.py similarity index 55% rename from cloudbot.py rename to bot.py index 91515db..6b721b2 100755 --- a/cloudbot.py +++ b/bot.py @@ -1,32 +1,49 @@ #!/usr/bin/env python +__author__ = "ClouDev" +__authors__ = ["Lukeroge", "neersighted"] +__copyright__ = "Copyright 2012, ClouDev" +__credits__ = ["thenoodle", "_frozen", "rmmh"] +__license__ = "GPL v3" +__version__ = "DEV" +__maintainer__ = "ClouDev" +__email__ = "cloudev@neersighted.com" +__status__ = "Development" + import os import Queue import sys import time -import re +import platform -sys.path += ['plugins', 'lib'] # add stuff to the sys.path for easy imports +sys.path += ['plugins'] # so 'import hook' works without duplication +sys.path += ['lib'] os.chdir(sys.path[0] or '.') # do stuff relative to the install directory class Bot(object): pass -print 'CloudBot DEV ' +print 'CloudBot %s (%s) ' % (__version__, __status__) + +# print debug info +opsys = platform.platform() +python_imp = platform.python_implementation() +python_ver = platform.python_version() +architecture = ' '.join(platform.architecture()) + +print "Operating System: %s, Python " \ + "Version: %s %s, Architecture: %s" \ + "" % (opsys, python_imp, python_ver, architecture) -# create new bot object bot = Bot() -bot.vars = {} - -# record start time for the uptime command bot.start_time = time.time() -print 'Begin Plugin Loading.' +print 'Loading plugins...' # bootstrap the reloader eval(compile(open(os.path.join('core', 'reload.py'), 'U').read(), - os.path.join('core', 'reload.py'), 'exec')) + os.path.join('core', 'reload.py'), 'exec')) reload(init=True) config() @@ -39,17 +56,14 @@ bot.conns = {} try: for name, conf in bot.config['connections'].iteritems(): - # strip all spaces and capitalization from the connection name - name = name.replace(" ", "_") - name = re.sub('[^A-Za-z0-9_]+', '', name) print 'Connecting to server: %s' % conf['server'] if conf.get('ssl'): bot.conns[name] = SSLIRC(name, conf['server'], conf['nick'], conf=conf, - port=conf.get('port', 6667), channels=conf['channels'], - ignore_certificate_errors=conf.get('ignore_cert', True)) + port=conf.get('port', 6667), channels=conf['channels'], + ignore_certificate_errors=conf.get('ignore_cert', True)) else: bot.conns[name] = IRC(name, conf['server'], conf['nick'], conf=conf, - port=conf.get('port', 6667), channels=conf['channels']) + port=conf.get('port', 6667), channels=conf['channels']) except Exception as e: print 'ERROR: malformed config file', e sys.exit() diff --git a/disabled_stuff/cloudbot.sh b/cloudbot old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/cloudbot.sh rename to cloudbot diff --git a/config.default b/config.default deleted file mode 100644 index 4bda1b0..0000000 --- a/config.default +++ /dev/null @@ -1,77 +0,0 @@ -{ - "connections": { - "hackint": { - "server": "irc.hackint.eu", - "nick": "antibot", - "user": "antibot", - "realname": "CloudBot - http://git.io/cloudbotirc", - "mode": "", - "_nickserv_password": "", - "-nickserv_user": "", - "channels": [ - "#ChaosChemnitz", - "#logbot" - ], - "invite_join": true, - "auto_rejoin": false, - "command_prefix": "." - } - }, - "disabled_plugins": [], - "disabled_commands": [], - "acls": {}, - "api_keys": { - "tvdb": "", - "wolframalpha": "", - "lastfm": "", - "rottentomatoes": "", - "soundcloud": "", - "twitter_consumer_key": "", - "twitter_consumer_secret": "", - "twitter_access_token": "", - "twitter_access_secret": "", - "wunderground": "", - "googletranslate": "", - "rdio_key": "", - "rdio_secret": "" - }, - "permissions": { - "admins": { - "perms": [ - "adminonly", - "addfactoid", - "delfactoid", - "ignore", - "botcontrol", - "permissions_users", - "op" - ], - "users": [ - "examplea!user@example.com", - "exampleb!user@example.com" - ] - }, - "moderators": { - "perms": [ - "addfactoid", - "delfactoid", - "ignore" - ], - "users": [ - "stummi!~Stummi@stummi.org" - ] - } - }, - "plugins": { - "factoids": { - "prefix": false - }, - "ignore": { - "ignored": [] - } - }, - "censored_strings": [ - "mypass", - "mysecret" - ] -} diff --git a/core/config.py b/core/config.py old mode 100644 new mode 100755 index c813ea5..4b81960 --- a/core/config.py +++ b/core/config.py @@ -7,8 +7,60 @@ def save(conf): json.dump(conf, open('config', 'w'), sort_keys=True, indent=2) if not os.path.exists('config'): - print "Please rename 'config.default' to 'config' to set up your bot!" - print "For help, see http://git.io/cloudbotirc" + open('config', 'w').write(inspect.cleandoc( + r''' + { + "connections": + { + "EsperNet": + { + "server": "irc.esper.net", + "nick": "MyNewCloudBot", + "user": "cloudbot", + "realname": "CloudBot - http://git.io/cloudbotirc", + "nickserv_password": "", + "channels": ["#cloudbot"], + "invite_join": true, + "auto_rejoin": false, + "command_prefix": "." + } + }, + "disabled_plugins": [], + "disabled_commands": [], + "acls": {}, + "api_keys": + { + "geoip": "INSERT API KEY FROM ipinfodb.com HERE", + "tvdb": "INSERT API KEY FROM thetvdb.com HERE", + "bitly_user": "INSERT USERNAME FROM bitly.com HERE", + "bitly_api": "INSERT API KEY FROM bitly.com HERE", + "wolframalpha": "INSERT API KEY FROM wolframalpha.com HERE", + "lastfm": "INSERT API KEY FROM lastfm HERE", + "rottentomatoes": "INSERT API KEY FROM rottentomatoes HERE", + "mc_user": "INSERT minecraft USERNAME HERE", + "mc_pass": "INSERT minecraft PASSWORD HERE" + }, + "plugins": + { + "factoids": + { + "prefix": false + }, + "ignore": + { + "ignored": [] + } + }, + "censored_strings": + [ + "mypass", + "mysecret" + ], + "admins": ["myname@myhost"] + }''') + '\n') + print "Config generated!" + print "Please edit the config now!" + print "For help, see http://git.io/cloudbotircwiki" print "Thank you for using CloudBot!" sys.exit() @@ -25,3 +77,4 @@ def config(): bot._config_mtime = 0 + diff --git a/core/db.py b/core/db.py old mode 100644 new mode 100755 index 6bdf8fa..fee8d9d --- a/core/db.py +++ b/core/db.py @@ -6,7 +6,7 @@ threaddbs = {} def get_db_connection(conn, name=''): - """returns an sqlite3 connection to a persistent database""" + "returns an sqlite3 connection to a persistent database" if not name: name = '{}.db'.format(conn.name) diff --git a/core/irc.py b/core/irc.py old mode 100644 new mode 100755 index 40831e3..c073f33 --- a/core/irc.py +++ b/core/irc.py @@ -17,18 +17,16 @@ def decode(txt): def censor(text): - text = text.replace('\n', '').replace('\r', '') replacement = '[censored]' if 'censored_strings' in bot.config: - if bot.config['censored_strings']: - words = map(re.escape, bot.config['censored_strings']) - regex = re.compile('({})'.format("|".join(words))) - text = regex.sub(replacement, text) + words = map(re.escape, bot.config['censored_strings']) + regex = re.compile('(%s)' % "|".join(words)) + text = regex.sub(replacement, text) return text class crlf_tcp(object): - """Handles tcp connections that consist of utf-8 lines ending with crlf""" + "Handles tcp connections that consist of utf-8 lines ending with crlf" def __init__(self, host, port, timeout=300): self.ibuffer = "" @@ -44,16 +42,7 @@ class crlf_tcp(object): return socket.socket(socket.AF_INET, socket.TCP_NODELAY) def run(self): - noerror = 0 - while 1: - try: - self.socket.connect((self.host, self.port)) - break - except socket.gaierror as e: - time.sleep(5) - except socket.timeout as e: - time.sleep(5) - + self.socket.connect((self.host, self.port)) thread.start_new_thread(self.recv_loop, ()) thread.start_new_thread(self.send_loop, ()) @@ -64,25 +53,17 @@ class crlf_tcp(object): return socket.timeout def handle_receive_exception(self, error, last_timestamp): - print("Receive exception: %s" % (error)) if time.time() - last_timestamp > self.timeout: - print("Receive timeout. Restart connection.") self.iqueue.put(StopIteration) self.socket.close() return True return False - def handle_send_exception(self, error): - print("Send exception: %s" % (error)) - self.iqueue.put(StopIteration) - self.socket.close() - return True - def recv_loop(self): last_timestamp = time.time() while True: try: - data = self.recv_from_socket(4096) + data = self.recv_from_socket(4096) self.ibuffer += data if data: last_timestamp = time.time() @@ -96,8 +77,6 @@ class crlf_tcp(object): if self.handle_receive_exception(e, last_timestamp): return continue - except AttributeError: - return while '\r\n' in self.ibuffer: line, self.ibuffer = self.ibuffer.split('\r\n', 1) @@ -105,31 +84,24 @@ class crlf_tcp(object): def send_loop(self): while True: - try: - line = self.oqueue.get().splitlines()[0][:500] - if line == StopIteration: - return - print ">>> %r" % line - self.obuffer += line.encode('utf-8', 'replace') + '\r\n' - while self.obuffer: - sent = self.socket.send(self.obuffer) - self.obuffer = self.obuffer[sent:] + line = self.oqueue.get().splitlines()[0][:500] + print ">>> %r" % line + self.obuffer += line.encode('utf-8', 'replace') + '\r\n' + while self.obuffer: + sent = self.socket.send(self.obuffer) + self.obuffer = self.obuffer[sent:] - except socket.error as e: - self.handle_send_exception(e) - return class crlf_ssl_tcp(crlf_tcp): - """Handles ssl tcp connetions that consist of utf-8 lines ending with crlf""" - + "Handles ssl tcp connetions that consist of utf-8 lines ending with crlf" def __init__(self, host, port, ignore_cert_errors, timeout=300): self.ignore_cert_errors = ignore_cert_errors crlf_tcp.__init__(self, host, port, timeout) def create_socket(self): return wrap_socket(crlf_tcp.create_socket(self), server_side=False, - cert_reqs=CERT_NONE if self.ignore_cert_errors else - CERT_REQUIRED) + cert_reqs=CERT_NONE if self.ignore_cert_errors else + CERT_REQUIRED) def recv_from_socket(self, nbytes): return self.socket.read(nbytes) @@ -139,14 +111,10 @@ class crlf_ssl_tcp(crlf_tcp): def handle_receive_exception(self, error, last_timestamp): # this is terrible - #if not "timed out" in error.args[0]: - # raise + if not "timed out" in error.args[0]: + raise return crlf_tcp.handle_receive_exception(self, error, last_timestamp) - def handle_send_exception(self, error): - return crlf_tcp.handle_send_exception(self, error) - - irc_prefix_rem = re.compile(r'(.*?) (.*?) (.*)').match irc_noprefix_rem = re.compile(r'()(.*?) (.*)').match irc_netmask_rem = re.compile(r':?([^!@]*)!?([^@]*)@?(.*)').match @@ -154,8 +122,7 @@ irc_param_ref = re.compile(r'(?:^|(?<= ))(:.*|[^ ]+)').findall class IRC(object): - """handles the IRC protocol""" - + "handles the IRC protocol" def __init__(self, name, server, nick, port=6667, channels=[], conf={}): self.name = name self.channels = channels @@ -163,8 +130,6 @@ class IRC(object): self.server = server self.port = port self.nick = nick - self.history = {} - self.vars = {} self.out = Queue.Queue() # responses from the server are placed here # format: [rawline, prefix, command, params, @@ -182,8 +147,8 @@ class IRC(object): self.set_pass(self.conf.get('server_password')) self.set_nick(self.nick) self.cmd("USER", - [conf.get('user', 'cloudbot'), "3", "*", conf.get('realname', - 'CloudBot - http://git.io/cloudbot')]) + [conf.get('user', 'cloudbot'), "3", "*", conf.get('realname', + 'CloudBot - http://git.io/cloudbot')]) def parse_loop(self): while True: @@ -200,7 +165,7 @@ class IRC(object): else: prefix, command, params = irc_noprefix_rem(msg).groups() nick, user, host = irc_netmask_rem(prefix).groups() - mask = nick + "!" + user + "@" + host + mask = user + "@" + host paramlist = irc_param_ref(params) lastparam = "" if paramlist: @@ -209,7 +174,7 @@ class IRC(object): lastparam = paramlist[-1] # put the parsed message in the response queue self.out.put([msg, prefix, command, params, nick, user, host, - mask, paramlist, lastparam]) + mask, paramlist, lastparam]) # if the server pings us, pong them back if command == "PING": self.cmd("PONG", paramlist) @@ -223,7 +188,7 @@ class IRC(object): def join(self, channel): """ makes the bot join a channel """ - self.send("JOIN {}".format(channel)) + self.send("JOIN %s" % channel) if channel not in self.channels: self.channels.append(channel) @@ -234,18 +199,13 @@ class IRC(object): self.channels.remove(channel) def msg(self, target, text): - """ makes the bot send a PRIVMSG to a target """ + """ makes the bot send a message to a user """ self.cmd("PRIVMSG", [target, text]) - def ctcp(self, target, ctcp_type, text): - """ makes the bot send a PRIVMSG CTCP to a target """ - out = u"\x01{} {}\x01".format(ctcp_type, text) - self.cmd("PRIVMSG", [target, out]) - def cmd(self, command, params=None): if params: - params[-1] = u':' + params[-1] - self.send(u"{} {}".format(command, ' '.join(params))) + params[-1] = ':' + params[-1] + self.send(command + ' ' + ' '.join(map(censor, params))) else: self.send(command) diff --git a/core/main.py b/core/main.py old mode 100644 new mode 100755 index 0054b0a..e95f2ae --- a/core/main.py +++ b/core/main.py @@ -7,40 +7,35 @@ thread.stack_size(1024 * 512) # reduce vm size class Input(dict): def __init__(self, conn, raw, prefix, command, params, - nick, user, host, mask, paraml, msg): + nick, user, host, mask, paraml, msg): chan = paraml[0].lower() if chan == conn.nick.lower(): # is a PM chan = nick - def message(message, target=chan): - """sends a message to a specific or current channel/user""" - conn.msg(target, message) + def say(msg): + conn.msg(chan, msg) - def reply(message, target=chan): - """sends a message to the current channel/user with a prefix""" - if target == nick: - conn.msg(target, message) + def pm(msg): + conn.msg(nick, msg) + + def reply(msg): + if chan == nick: # PMs don't need prefixes + conn.msg(chan, msg) else: - conn.msg(target, u"({}) {}".format(nick, message)) + conn.msg(chan, '(' + nick + ') ' + msg) - def action(message, target=chan): - """sends an action to the current channel/user or a specific channel/user""" - conn.ctcp(target, "ACTION", message) + def me(msg): + conn.msg(chan, "\x01%s %s\x01" % ("ACTION", msg)) - def ctcp(message, ctcp_type, target=chan): - """sends an ctcp to the current channel/user or a specific channel/user""" - conn.ctcp(target, ctcp_type, message) - - def notice(message, target=nick): - """sends a notice to the current channel/user or a specific channel/user""" - conn.cmd('NOTICE', [target, message]) + def notice(msg): + conn.cmd('NOTICE', [nick, msg]) dict.__init__(self, conn=conn, raw=raw, prefix=prefix, command=command, - params=params, nick=nick, user=user, host=host, mask=mask, - paraml=paraml, msg=msg, server=conn.server, chan=chan, - notice=notice, message=message, reply=reply, bot=bot, - action=action, ctcp=ctcp, lastparam=paraml[-1]) + params=params, nick=nick, user=user, host=host, mask=mask, + paraml=paraml, msg=msg, server=conn.server, chan=chan, + notice=notice, say=say, reply=reply, pm=pm, bot=bot, + me=me, lastparam=paraml[-1]) # make dict keys accessible as attributes def __getattr__(self, key): @@ -82,8 +77,7 @@ def do_sieve(sieve, bot, input, func, type, args): class Handler(object): - """Runs plugins in their own threads (ensures order)""" - + '''Runs plugins in their own threads (ensures order)''' def __init__(self, func): self.func = func self.input_queue = Queue.Queue() @@ -109,7 +103,6 @@ class Handler(object): run(self.func, input) except: import traceback - traceback.print_exc() def stop(self): @@ -122,10 +115,11 @@ class Handler(object): def dispatch(input, kind, func, args, autohelp=False): for sieve, in bot.plugs['sieve']: input = do_sieve(sieve, bot, input, func, kind, args) - if input is None: + if input == None: return - if not (not autohelp or not args.get('autohelp', True) or input.inp or not (func.__doc__ is not None)): + if autohelp and args.get('autohelp', True) and not input.inp \ + and func.__doc__ is not None: input.notice(input.conn.conf["command_prefix"] + func.__doc__) return @@ -159,9 +153,9 @@ def main(conn, out): if inp.command == 'PRIVMSG': # COMMANDS if inp.chan == inp.nick: # private message, no command prefix - prefix = '^(?:[{}]?|'.format(command_prefix) + prefix = '^(?:[%s]?|' % command_prefix else: - prefix = '^(?:[{}]|'.format(command_prefix) + prefix = '^(?:[%s]|' % command_prefix command_re = prefix + inp.conn.nick command_re += r'[,;:]+\s+)(\w+)(?:$|\s+)(.*)' @@ -174,8 +168,8 @@ def main(conn, out): if isinstance(command, list): # multiple potential matches input = Input(conn, *out) - input.notice("Did you mean {} or {}?".format - (', '.join(command[:-1]), command[-1])) + input.notice("Did you mean %s or %s?" % + (', '.join(command[:-1]), command[-1])) elif command in bot.commands: input = Input(conn, *out) input.trigger = trigger diff --git a/core/reload.py b/core/reload.py old mode 100644 new mode 100755 index f1bfeb6..f952fc4 --- a/core/reload.py +++ b/core/reload.py @@ -17,8 +17,8 @@ def make_signature(f): return f.func_code.co_filename, f.func_name, f.func_code.co_firstlineno -def format_plug(plug, kind='', lpad=0): - out = ' ' * lpad + '{}:{}:{}'.format(*make_signature(plug[0])) +def format_plug(plug, kind='', lpad=0, width=40): + out = ' ' * lpad + '%s:%s:%s' % make_signature(plug[0]) if kind == 'command': out += ' ' * (50 - len(out)) + plug[1]['name'] @@ -49,7 +49,7 @@ def reload(init=False): try: eval(compile(open(filename, 'U').read(), filename, 'exec'), - globals()) + globals()) except Exception: traceback.print_exc() if init: # stop if there's an error (syntax?) in a core @@ -111,19 +111,20 @@ def reload(init=False): if not init: print '### new plugin (type: %s) loaded:' % \ - type, format_plug(data) + type, format_plug(data) if changed: bot.commands = {} for plug in bot.plugs['command']: name = plug[1]['name'].lower() if not re.match(r'^\w+$', name): - print '### ERROR: invalid command name "{}" ({})'.format(name, format_plug(plug)) + print '### ERROR: invalid command name "%s" (%s)' % (name, + format_plug(plug)) continue if name in bot.commands: - print "### ERROR: command '{}' already registered ({}, {})".format(name, - format_plug(bot.commands[name]), - format_plug(plug)) + print "### ERROR: command '%s' already registered (%s, %s)" % \ + (name, format_plug(bot.commands[name]), + format_plug(plug)) continue bot.commands[name] = plug @@ -154,7 +155,7 @@ def reload(init=False): for kind, plugs in sorted(bot.plugs.iteritems()): if kind == 'command': continue - print ' {}:'.format(kind) + print ' %s:' % kind for plug in plugs: print format_plug(plug, kind=kind, lpad=6) print diff --git a/disabled_plugins/antiflood.py b/disabled_plugins/antiflood.py new file mode 100755 index 0000000..62626a3 --- /dev/null +++ b/disabled_plugins/antiflood.py @@ -0,0 +1,33 @@ +def yaml_load(filename): + import yaml + fileHandle = open(filename, 'r') + stuff = yaml.load(fileHandle.read()) + fileHandle.close() + return stuff + +def yaml_save(stuff, filename): + import yaml + fileHandle = open (filename, 'w' ) + fileHandle.write (yaml.dump(stuff)) + fileHandle.close() + +from util import hook + +@hook.event('*') +def tellinput(paraml, input=None, say=None): +# import time +# now = time.time() +# spam = yaml_load('spam') +# if spam[input.nick]: +# spam[input.nick].append(time.time()) +# else: +# spam[input.nick] = [time.time()] +# for x in spam[input.nick]: +# if now - x > 5: +# spam[input.nick].pop(x) +# if len(spam[input.nick]) > 8: +# say(":O") +# say("HOW COULD YOU "+input.nick) +# say("lol!") +# yaml_save(spam,'spam') + return diff --git a/disabled_stuff/mtg.py b/disabled_plugins/mtg.py old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/mtg.py rename to disabled_plugins/mtg.py diff --git a/disabled_stuff/mygengo_translate.py b/disabled_plugins/mygengo_translate.py old mode 100644 new mode 100755 similarity index 99% rename from disabled_stuff/mygengo_translate.py rename to disabled_plugins/mygengo_translate.py index 6e7b006..d61ab14 --- a/disabled_stuff/mygengo_translate.py +++ b/disabled_plugins/mygengo_translate.py @@ -1,7 +1,7 @@ # BING translation plugin by Lukeroge and neersighted from util import hook from util import http -import re +import re import htmlentitydefs import mygengo diff --git a/disabled_stuff/repaste.py b/disabled_plugins/repaste.py old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/repaste.py rename to disabled_plugins/repaste.py diff --git a/disabled_plugins/suggest.py b/disabled_plugins/suggest.py new file mode 100755 index 0000000..92f9405 --- /dev/null +++ b/disabled_plugins/suggest.py @@ -0,0 +1,33 @@ +import json +import random +import re + +from util import hook, http + + +@hook.command +def suggest(inp, inp_unstripped=''): + ".suggest [#n] -- gets a random/the nth suggested google search" + + inp = inp_unstripped + m = re.match('^#(\d+) (.+)$', inp) + if m: + num, inp = m.groups() + num = int(num) + if num > 10: + return 'I can only get the first ten suggestions.' + else: + num = 0 + + page = http.get('http://google.com/complete/search', output='json', client='hp', q=inp) + page_json = page.split('(', 1)[1][:-1] + suggestions = json.loads(page_json)[1] + if not suggestions: + return 'No suggestions found.' + if num: + if len(suggestions) + 1 <= num: + return 'I only got %d suggestions.' % len(suggestions) + out = suggestions[num - 1] + else: + out = random.choice(suggestions) + return '#%d: %s' % (int(out[2][0]) + 1, out[0].replace('', '').replace('', '')) \ No newline at end of file diff --git a/disabled_stuff/urlhistory.py b/disabled_plugins/urlhistory.py old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/urlhistory.py rename to disabled_plugins/urlhistory.py diff --git a/disabled_stuff/wordoftheday.py b/disabled_plugins/wordoftheday.py old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/wordoftheday.py rename to disabled_plugins/wordoftheday.py diff --git a/disabled_stuff/wrapper.old b/disabled_plugins/wrapper.old old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/wrapper.old rename to disabled_plugins/wrapper.old diff --git a/disabled_stuff/attacks.py b/disabled_stuff/attacks.py deleted file mode 100644 index feb00b8..0000000 --- a/disabled_stuff/attacks.py +++ /dev/null @@ -1,72 +0,0 @@ -import random - -from util import hook - - -with open("plugins/data/larts.txt") as f: - larts = [line.strip() for line in f.readlines() - if not line.startswith("//")] - -with open("plugins/data/insults.txt") as f: - insults = [line.strip() for line in f.readlines() - if not line.startswith("//")] - -with open("plugins/data/flirts.txt") as f: - flirts = [line.strip() for line in f.readlines() - if not line.startswith("//")] - - -@hook.command -def lart(inp, action=None, nick=None, conn=None, notice=None): - """lart -- LARTs .""" - target = inp.strip() - - if " " in target: - notice("Invalid username!") - return - - # if the user is trying to make the bot slap itself, slap them - if target.lower() == conn.nick.lower() or target.lower() == "itself": - target = nick - - values = {"user": target} - phrase = random.choice(larts) - - # act out the message - action(phrase.format(**values)) - - -@hook.command -def insult(inp, nick=None, action=None, conn=None, notice=None): - """insult -- Makes the bot insult .""" - target = inp.strip() - - if " " in target: - notice("Invalid username!") - return - - if target == conn.nick.lower() or target == "itself": - target = nick - else: - target = inp - - out = 'insults {}... "{}"'.format(target, random.choice(insults)) - action(out) - - -@hook.command -def flirt(inp, action=None, conn=None, notice=None): - """flirt -- Make the bot flirt with .""" - target = inp.strip() - - if " " in target: - notice("Invalid username!") - return - - if target == conn.nick.lower() or target == "itself": - target = 'itself' - else: - target = inp - - out = 'flirts with {}... "{}"'.format(target, random.choice(flirts)) - action(out) diff --git a/disabled_stuff/cleverbot.py b/disabled_stuff/cleverbot.py deleted file mode 100644 index 6604d8b..0000000 --- a/disabled_stuff/cleverbot.py +++ /dev/null @@ -1,121 +0,0 @@ -# from jessi bot -import urllib2 -import hashlib -import re -import unicodedata -from util import hook - -# these are just parts required -# TODO: Merge them. - -arglist = ['', 'y', '', '', '', '', '', '', '', '', 'wsf', '', - '', '', '', '', '', '', '', '0', 'Say', '1', 'false'] - -always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' - 'abcdefghijklmnopqrstuvwxyz' - '0123456789' '_.-') - -headers = {'X-Moz': 'prefetch', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1)Gecko/20100101 Firefox/7.0', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Referer': 'http://www.cleverbot.com', - 'Pragma': 'no-cache', 'Cache-Control': 'no-cache, no-cache', 'Accept-Language': 'en-us;q=0.8,en;q=0.5'} - -keylist = ['stimulus', 'start', 'sessionid', 'vText8', 'vText7', 'vText6', - 'vText5', 'vText4', 'vText3', 'vText2', 'icognoid', - 'icognocheck', 'prevref', 'emotionaloutput', 'emotionalhistory', - 'asbotname', 'ttsvoice', 'typing', 'lineref', 'fno', 'sub', - 'islearning', 'cleanslate'] - -MsgList = list() - - -def quote(s, safe='/'): # quote('abc def') -> 'abc%20def' - s = s.encode('utf-8') - s = s.decode('utf-8') - print "s= " + s - print "safe= " + safe - safe += always_safe - safe_map = dict() - for i in range(256): - c = chr(i) - safe_map[c] = (c in safe) and c or ('%%%02X' % i) - try: - res = map(safe_map.__getitem__, s) - except: - print "blank" - return '' - print "res= " + ''.join(res) - return ''.join(res) - - -def encode(keylist, arglist): - text = str() - for i in range(len(keylist)): - k = keylist[i] - v = quote(arglist[i]) - text += '&' + k + '=' + v - text = text[1:] - return text - - -def Send(): - data = encode(keylist, arglist) - digest_txt = data[9:29] - new_hash = hashlib.md5(digest_txt).hexdigest() - arglist[keylist.index('icognocheck')] = new_hash - data = encode(keylist, arglist) - req = urllib2.Request('http://www.cleverbot.com/webservicemin', - data, headers) - f = urllib2.urlopen(req) - reply = f.read() - return reply - - -def parseAnswers(text): - d = dict() - keys = ['text', 'sessionid', 'logurl', 'vText8', 'vText7', 'vText6', - 'vText5', 'vText4', 'vText3', 'vText2', 'prevref', 'foo', - 'emotionalhistory', 'ttsLocMP3', 'ttsLocTXT', 'ttsLocTXT3', - 'ttsText', 'lineRef', 'lineURL', 'linePOST', 'lineChoices', - 'lineChoicesAbbrev', 'typingData', 'divert'] - values = text.split('\r') - i = 0 - for key in keys: - d[key] = values[i] - i += 1 - return d - - -def ask(inp): - arglist[keylist.index('stimulus')] = inp - if MsgList: - arglist[keylist.index('lineref')] = '!0' + str(len( - MsgList) / 2) - asw = Send() - MsgList.append(inp) - answer = parseAnswers(asw) - for k, v in answer.iteritems(): - try: - arglist[keylist.index(k)] = v - except ValueError: - pass - arglist[keylist.index('emotionaloutput')] = str() - text = answer['ttsText'] - MsgList.append(text) - return text - - -@hook.command("cb") -def cleverbot(inp, reply=None): - reply(ask(inp)) - - -''' # TODO: add in command to control extra verbose per channel -@hook.event('PRIVMSG') -def cbevent(inp, reply=None): - reply(ask(inp)) - -@hook.command("cbver", permissions=['cleverbot']) -def cleverbotverbose(inp, notice=None): - if on in input -''' diff --git a/disabled_stuff/correction.py b/disabled_stuff/correction.py deleted file mode 100644 index 7617e11..0000000 --- a/disabled_stuff/correction.py +++ /dev/null @@ -1,37 +0,0 @@ -from util import hook - -import re - -CORRECTION_RE = r'^(s|S)/.*/.*/?\S*$' - - -@hook.regex(CORRECTION_RE) -def correction(match, input=None, conn=None, message=None): - split = input.msg.split("/") - - if len(split) == 4: - nick = split[3].lower() - else: - nick = None - - find = split[1] - replace = split[2] - - for item in conn.history[input.chan].__reversed__(): - name, timestamp, msg = item - if msg.startswith("s/"): - # don't correct corrections, it gets really confusing - continue - if nick: - if nick != name.lower(): - continue - if find in msg: - if "\x01ACTION" in msg: - msg = msg.replace("\x01ACTION ", "/me ").replace("\x01", "") - message(u"Correction, <{}> {}".format(name, msg.replace(find, "\x02" + replace + "\x02"))) - return - else: - continue - - return u"Did not find {} in any recent messages.".format(find) - diff --git a/disabled_stuff/cryptocoins.py b/disabled_stuff/cryptocoins.py deleted file mode 100644 index 42d5945..0000000 --- a/disabled_stuff/cryptocoins.py +++ /dev/null @@ -1,60 +0,0 @@ -from util import http, hook - -## CONSTANTS - -exchanges = { - "blockchain": { - "api_url": "https://blockchain.info/ticker", - "func": lambda data: u"Blockchain // Buy: \x0307${:,.2f}\x0f -" - u" Sell: \x0307${:,.2f}\x0f".format(data["USD"]["buy"], data["USD"]["sell"]) - }, - "coinbase": { - "api_url": "https://coinbase.com/api/v1/prices/spot_rate", - "func": lambda data: u"Coinbase // Current: \x0307${:,.2f}\x0f".format(float(data['amount'])) - }, - "bitpay": { - "api_url": "https://bitpay.com/api/rates", - "func": lambda data: u"Bitpay // Current: \x0307${:,.2f}\x0f".format(data[0]['rate']) - }, - "bitstamp": { - "api_url": "https://www.bitstamp.net/api/ticker/", - "func": lambda data: u"BitStamp // Current: \x0307${:,.2f}\x0f - High: \x0307${:,.2f}\x0f -" - u" Low: \x0307${:,.2f}\x0f - Volume: {:,.2f} BTC".format(float(data['last']), - float(data['high']), - float(data['low']), - float(data['volume'])) - } -} - - -## HOOK FUNCTIONS - -@hook.command("btc", autohelp=False) -@hook.command(autohelp=False) -def bitcoin(inp): - """bitcoin -- Gets current exchange rate for bitcoins from several exchanges, default is Blockchain. - Supports MtGox, Bitpay, Coinbase and BitStamp.""" - inp = inp.lower() - - if inp: - if inp in exchanges: - exchange = exchanges[inp] - else: - return "Invalid Exchange" - else: - exchange = exchanges["blockchain"] - - data = http.get_json(exchange["api_url"]) - func = exchange["func"] - return func(data) - - -@hook.command("ltc", autohelp=False) -@hook.command(autohelp=False) -def litecoin(inp, message=None): - """litecoin -- gets current exchange rate for litecoins from BTC-E""" - data = http.get_json("https://btc-e.com/api/2/ltc_usd/ticker") - ticker = data['ticker'] - message("Current: \x0307${:,.2f}\x0f - High: \x0307${:,.2f}\x0f" - " - Low: \x0307${:,.2f}\x0f - Volume: {:,.2f} LTC".format(ticker['buy'], ticker['high'], ticker['low'], - ticker['vol_cur'])) diff --git a/disabled_stuff/cypher.py b/disabled_stuff/cypher.py deleted file mode 100644 index b54248a..0000000 --- a/disabled_stuff/cypher.py +++ /dev/null @@ -1,39 +0,0 @@ -import base64 - -from util import hook - - -def encode(key, clear): - enc = [] - for i in range(len(clear)): - key_c = key[i % len(key)] - enc_c = chr((ord(clear[i]) + ord(key_c)) % 256) - enc.append(enc_c) - return base64.urlsafe_b64encode("".join(enc)) - - -def decode(key, enc): - dec = [] - enc = base64.urlsafe_b64decode(enc.encode('ascii', 'ignore')) - for i in range(len(enc)): - key_c = key[i % len(key)] - dec_c = chr((256 + ord(enc[i]) - ord(key_c)) % 256) - dec.append(dec_c) - return "".join(dec) - - -@hook.command -def cypher(inp): - """cypher -- Cyphers with .""" - - passwd = inp.split(" ")[0] - inp = " ".join(inp.split(" ")[1:]) - return encode(passwd, inp) - - -@hook.command -def decypher(inp): - """decypher -- Decyphers with .""" - passwd = inp.split(" ")[0] - inp = " ".join(inp.split(" ")[1:]) - return decode(passwd, inp) diff --git a/disabled_stuff/data/itemids.txt b/disabled_stuff/data/itemids.txt deleted file mode 100644 index 4f0ce1d..0000000 --- a/disabled_stuff/data/itemids.txt +++ /dev/null @@ -1,620 +0,0 @@ - 1 Stone - 1:1 Granite - 1:2 Polished Granite - 1:3 Diorite - 1:4 Polished Diorite - 1:5 Andesite - 1:6 Polished Andesite - 2 Grass - 3 Dirt - 3:1 Dirt (No Grass) - 3:2 Podzol - 4 Cobblestone - 5 Wooden Plank (Oak) - 5:1 Wooden Plank (Spruce) - 5:2 Wooden Plank (Birch) - 5:3 Wooden Plank (Jungle) - 5:4 Wooden Plank (Acacia) - 5:5 Wooden Plank (Dark Oak) - 6 Sapling (Oak) - 6:1 Sapling (Spruce) - 6:2 Sapling (Birch) - 6:3 Sapling (Jungle) - 6:4 Sapling (Acacia) - 6:5 Sapling (Dark Oak) - 7 Bedrock - 8 Water - 9 Water (No Spread) - 10 Lava - 11 Lava (No Spread) - 12 Sand - 12:1 Red Sand - 13 Gravel - 14 Gold Ore - 15 Iron Ore - 16 Coal Ore - 17 Wood (Oak) - 17:1 Wood (Spruce) - 17:2 Wood (Birch) - 17:3 Wood (Jungle) - 17:4 Wood (Oak 4) - 17:5 Wood (Oak 5) - 18 Leaves (Oak) - 18:1 Leaves (Spruce) - 18:2 Leaves (Birch) - 18:3 Leaves (Jungle) - 19 Sponge - 20 Glass - 21 Lapis Lazuli Ore - 22 Lapis Lazuli Block - 23 Dispenser - 24 Sandstone - 24:1 Sandstone (Chiseled) - 24:2 Sandstone (Smooth) - 25 Note Block - 26 Bed (Block) - 27 Rail (Powered) - 28 Rail (Detector) - 29 Sticky Piston - 30 Cobweb - 31 Tall Grass (Dead Shrub) - 31:1 Tall Grass - 31:2 Tall Grass (Fern) - 32 Dead Shrub - 33 Piston - 34 Piston (Head) - 35 Wool - 35:1 Orange Wool - 35:2 Magenta Wool - 35:3 Light Blue Wool - 35:4 Yellow Wool - 35:5 Lime Wool - 35:6 Pink Wool - 35:7 Gray Wool - 35:8 Light Gray Wool - 35:9 Cyan Wool - 35:10 Purple Wool - 35:11 Blue Wool - 35:12 Brown Wool - 35:13 Green Wool - 35:14 Red Wool - 35:15 Black Wool - 36 Piston (Moving) - 37 Dandelion - 38 Poppy - 38:1 Blue Orchid - 38:2 Allium - 38:4 Red Tulip - 38:5 Orange Tulip - 38:6 White Tulip - 38:7 Pink Tulip - 38:8 Oxeye Daisy - 39 Brown Mushroom - 40 Red Mushroom - 41 Block of Gold - 42 Block of Iron - 43 Stone Slab (Double) - 43:1 Sandstone Slab (Double) - 43:2 Wooden Slab (Double) - 43:3 Cobblestone Slab (Double) - 43:4 Brick Slab (Double) - 43:5 Stone Brick Slab (Double) - 43:6 Nether Brick Slab (Double) - 43:7 Quartz Slab (Double) - 43:8 Smooth Stone Slab (Double) - 43:9 Smooth Sandstone Slab (Double) - 44 Stone Slab - 44:1 Sandstone Slab - 44:2 Wooden Slab - 44:3 Cobblestone Slab - 44:4 Brick Slab - 44:5 Stone Brick Slab - 44:6 Nether Brick Slab - 44:7 Quartz Slab - 45 Brick - 46 TNT - 47 Bookshelf - 48 Moss Stone - 49 Obsidian - 50 Torch - 51 Fire - 52 Mob Spawner - 53 Wooden Stairs (Oak) - 54 Chest - 55 Redstone Wire - 56 Diamond Ore - 57 Block of Diamond - 58 Workbench - 59 Wheat (Crop) - 60 Farmland - 61 Furnace - 62 Furnace (Smelting) - 63 Sign (Block) - 64 Wood Door (Block) - 65 Ladder - 66 Rail - 67 Cobblestone Stairs - 68 Sign (Wall Block) - 69 Lever - 70 Stone Pressure Plate - 71 Iron Door (Block) - 72 Wooden Pressure Plate - 73 Redstone Ore - 74 Redstone Ore (Glowing) - 75 Redstone Torch (Off) - 76 Redstone Torch - 77 Button (Stone) - 78 Snow - 79 Ice - 80 Snow Block - 81 Cactus - 82 Clay Block - 83 Sugar Cane (Block) - 84 Jukebox - 85 Fence - 86 Pumpkin - 87 Netherrack - 88 Soul Sand - 89 Glowstone - 90 Portal - 91 Jack-O-Lantern - 92 Cake (Block) - 93 Redstone Repeater (Block Off) - 94 Redstone Repeater (Block On) - 95 Stained Glass (White) - 95:1 Stained Glass (Orange) - 95:2 Stained Glass (Magenta) - 95:3 Stained Glass (Light Blue) - 95:4 Stained Glass (Yellow) - 95:5 Stained Glass (Lime) - 95:6 Stained Glass (Pink) - 95:7 Stained Glass (Gray) - 95:8 Stained Glass (Light Grey) - 95:9 Stained Glass (Cyan) - 95:10 Stained Glass (Purple) - 95:11 Stained Glass (Blue) - 95:12 Stained Glass (Brown) - 95:13 Stained Glass (Green) - 95:14 Stained Glass (Red) - 95:15 Stained Glass (Black) - 96 Trapdoor - 97 Monster Egg (Stone) - 97:1 Monster Egg (Cobblestone) - 97:2 Monster Egg (Stone Brick) - 97:3 Monster Egg (Mossy Stone Brick) - 97:4 Monster Egg (Cracked Stone) - 97:5 Monster Egg (Chiseled Stone) - 98 Stone Bricks - 98:1 Mossy Stone Bricks - 98:2 Cracked Stone Bricks - 98:3 Chiseled Stone Brick - 99 Brown Mushroom (Block) - 100 Red Mushroom (Block) - 101 Iron Bars - 102 Glass Pane - 103 Melon (Block) - 104 Pumpkin Vine - 105 Melon Vine - 106 Vines - 107 Fence Gate - 108 Brick Stairs - 109 Stone Brick Stairs - 110 Mycelium - 111 Lily Pad - 112 Nether Brick - 113 Nether Brick Fence - 114 Nether Brick Stairs - 115 Nether Wart - 116 Enchantment Table - 117 Brewing Stand (Block) - 118 Cauldron (Block) - 119 End Portal - 120 End Portal Frame - 121 End Stone - 122 Dragon Egg - 123 Redstone Lamp - 124 Redstone Lamp (On) - 125 Oak-Wood Slab (Double) - 125:1 Spruce-Wood Slab (Double) - 125:2 Birch-Wood Slab (Double) - 125:3 Jungle-Wood Slab (Double) - 125:4 Acacia Wood Slab (Double) - 125:5 Dark Oak Wood Slab (Double) - 126 Oak-Wood Slab - 126:1 Spruce-Wood Slab - 126:2 Birch-Wood Slab - 126:3 Jungle-Wood Slab - 126:4 Acacia Wood Slab - 126:5 Dark Oak Wood Slab - 127 Cocoa Plant - 128 Sandstone Stairs - 129 Emerald Ore - 130 Ender Chest - 131 Tripwire Hook - 132 Tripwire - 133 Block of Emerald - 134 Wooden Stairs (Spruce) - 135 Wooden Stairs (Birch) - 136 Wooden Stairs (Jungle) - 137 Command Block - 138 Beacon - 139 Cobblestone Wall - 139:1 Mossy Cobblestone Wall - 140 Flower Pot (Block) - 141 Carrot (Crop) - 142 Potatoes (Crop) - 143 Button (Wood) - 144 Head Block (Skeleton) - 144:1 Head Block (Wither) - 144:2 Head Block (Zombie) - 144:3 Head Block (Steve) - 144:4 Head Block (Creeper) - 145 Anvil - 145:1 Anvil (Slightly Damaged) - 145:2 Anvil (Very Damaged) - 146 Trapped Chest - 147 Weighted Pressure Plate (Light) - 148 Weighted Pressure Plate (Heavy) - 149 Redstone Comparator (Off) - 150 Redstone Comparator (On) - 151 Daylight Sensor - 152 Block of Redstone - 153 Nether Quartz Ore - 154 Hopper - 155 Quartz Block - 155:1 Chiseled Quartz Block - 155:2 Pillar Quartz Block - 156 Quartz Stairs - 157 Rail (Activator) - 158 Dropper - 159 Stained Clay (White) - 159:1 Stained Clay (Orange) - 159:2 Stained Clay (Magenta) - 159:3 Stained Clay (Light Blue) - 159:4 Stained Clay (Yellow) - 159:5 Stained Clay (Lime) - 159:6 Stained Clay (Pink) - 159:7 Stained Clay (Gray) - 159:8 Stained Clay (Light Gray) - 159:9 Stained Clay (Cyan) - 159:10 Stained Clay (Purple) - 159:11 Stained Clay (Blue) - 159:12 Stained Clay (Brown) - 159:13 Stained Clay (Green) - 159:14 Stained Clay (Red) - 159:15 Stained Clay (Black) - 160 Stained Glass Pane (White) - 160:1 Stained Glass Pane (Orange) - 160:2 Stained Glass Pane (Magenta) - 160:3 Stained Glass Pane (Light Blue) - 160:4 Stained Glass Pane (Yellow) - 160:5 Stained Glass Pane (Lime) - 160:6 Stained Glass Pane (Pink) - 160:7 Stained Glass Pane (Gray) - 160:8 Stained Glass Pane (Light Gray) - 160:9 Stained Glass Pane (Cyan) - 160:10 Stained Glass Pane (Purple) - 160:11 Stained Glass Pane (Blue) - 160:12 Stained Glass Pane (Brown) - 160:13 Stained Glass Pane (Green) - 160:14 Stained Glass Pane (Red) - 160:15 Stained Glass Pane (Black) - 162 Wood (Acacia Oak) - 162:1 Wood (Dark Oak) - 163 Wooden Stairs (Acacia) - 164 Wooden Stairs (Dark Oak) - 165 Slime Block - 170 Hay Bale - 171 Carpet (White) - 171:1 Carpet (Orange) - 171:2 Carpet (Magenta) - 171:3 Carpet (Light Blue) - 171:4 Carpet (Yellow) - 171:5 Carpet (Lime) - 171:6 Carpet (Pink) - 171:7 Carpet (Grey) - 171:8 Carpet (Light Gray) - 171:9 Carpet (Cyan) - 171:10 Carpet (Purple) - 171:11 Carpet (Blue) - 171:12 Carpet (Brown) - 171:13 Carpet (Green) - 171:14 Carpet (Red) - 171:15 Carpet (Black) - 172 Hardened Clay - 173 Block of Coal - 174 Packed Ice - 175 Sunflower - 175:1 Lilac - 175:2 Double Tallgrass - 175:3 Large Fern - 175:4 Rose Bush - 175:5 Peony - 256 Iron Shovel - 257 Iron Pickaxe - 258 Iron Axe - 259 Flint and Steel - 260 Apple - 261 Bow - 262 Arrow - 263 Coal - 263:1 Charcoal - 264 Diamond Gem - 265 Iron Ingot - 266 Gold Ingot - 267 Iron Sword - 268 Wooden Sword - 269 Wooden Shovel - 270 Wooden Pickaxe - 271 Wooden Axe - 272 Stone Sword - 273 Stone Shovel - 274 Stone Pickaxe - 275 Stone Axe - 276 Diamond Sword - 277 Diamond Shovel - 278 Diamond Pickaxe - 279 Diamond Axe - 280 Stick - 281 Bowl - 282 Mushroom Stew - 283 Gold Sword - 284 Gold Shovel - 285 Gold Pickaxe - 286 Gold Axe - 287 String - 288 Feather - 289 Gunpowder - 290 Wooden Hoe - 291 Stone Hoe - 292 Iron Hoe - 293 Diamond Hoe - 294 Gold Hoe - 295 Wheat Seeds - 296 Wheat - 297 Bread - 298 Leather Helmet - 299 Leather Chestplate - 300 Leather Leggings - 301 Leather Boots - 302 Chainmail Helmet - 303 Chainmail Chestplate - 304 Chainmail Leggings - 305 Chainmail Boots - 306 Iron Helmet - 307 Iron Chestplate - 308 Iron Leggings - 309 Iron Boots - 310 Diamond Helmet - 311 Diamond Chestplate - 312 Diamond Leggings - 313 Diamond Boots - 314 Gold Helmet - 315 Gold Chestplate - 316 Gold Leggings - 317 Gold Boots - 318 Flint - 319 Raw Porkchop - 320 Cooked Porkchop - 321 Painting - 322 Golden Apple - 322:1 Enchanted Golden Apple - 323 Sign - 324 Wooden Door - 325 Bucket - 326 Bucket (Water) - 327 Bucket (Lava) - 328 Minecart - 329 Saddle - 330 Iron Door - 331 Redstone Dust - 332 Snowball - 333 Boat - 334 Leather - 335 Bucket (Milk) - 336 Clay Brick - 337 Clay - 338 Sugar Cane - 339 Paper - 340 Book - 341 Slime Ball - 342 Minecart (Storage) - 343 Minecart (Powered) - 344 Egg - 345 Compass - 346 Fishing Rod - 347 Watch - 348 Glowstone Dust - 349 Raw Fish - 349:1 Raw Salmon - 349:2 Clownfish - 349:3 Pufferfish - 350 Cooked Fish - 350:1 Cooked Salmon - 350:2 Clownfish - 350:3 Pufferfish - 351 Ink Sack - 351:1 Rose Red Dye - 351:2 Cactus Green Dye - 351:3 Cocoa Bean - 351:4 Lapis Lazuli - 351:5 Purple Dye - 351:6 Cyan Dye - 351:7 Light Gray Dye - 351:8 Gray Dye - 351:9 Pink Dye - 351:10 Lime Dye - 351:11 Dandelion Yellow Dye - 351:12 Light Blue Dye - 351:13 Magenta Dye - 351:14 Orange Dye - 351:15 Bone Meal - 352 Bone - 353 Sugar - 354 Cake - 355 Bed - 356 Redstone Repeater - 357 Cookie - 358 Map - 359 Shears - 360 Melon (Slice) - 361 Pumpkin Seeds - 362 Melon Seeds - 363 Raw Beef - 364 Steak - 365 Raw Chicken - 366 Cooked Chicken - 367 Rotten Flesh - 368 Ender Pearl - 369 Blaze Rod - 370 Ghast Tear - 371 Gold Nugget - 372 Nether Wart Seeds - 373 Water Bottle - 373:16 Awkward Potion - 373:32 Thick Potion - 373:64 Mundane Potion - 373:8193 Regeneration Potion (0:45) - 373:8194 Swiftness Potion (3:00) - 373:8195 Fire Resistance Potion (3:00) - 373:8196 Poison Potion (0:45) - 373:8197 Healing Potion - 373:8198 Night Vision Potion (3:00) - 373:8200 Weakness Potion (1:30) - 373:8201 Strength Potion (3:00) - 373:8202 Slowness Potion (1:30) - 373:8204 Harming Potion - 373:8205 Water Breathing Potion (3:00) - 373:8206 Invisibility Potion (3:00) - 373:8225 Regeneration Potion II (0:22) - 373:8226 Swiftness Potion II (1:30) - 373:8228 Poison Potion II (0:22) - 373:8229 Healing Potion II - 373:8233 Strength Potion II (1:30) - 373:8236 Harming Potion II - 373:8257 Regeneration Potion (2:00) - 373:8258 Swiftness Potion (8:00) - 373:8259 Fire Resistance Potion (8:00) - 373:8260 Poison Potion (2:00) - 373:8262 Night Vision Potion (8:00) - 373:8264 Weakness Potion (4:00) - 373:8265 Strength Potion (8:00) - 373:8266 Slowness Potion (4:00) - 373:8269 Water Breathing Potion (8:00) - 373:8270 Invisibility Potion (8:00) - 373:8289 Regeneration Potion II (1:00) - 373:8290 Swiftness Potion II (4:00) - 373:8292 Poison Potion II (1:00) - 373:8297 Strength Potion II (4:00) - 373:16385 Regeneration Splash (0:33) - 373:16386 Swiftness Splash (2:15) - 373:16387 Fire Resistance Splash (2:15) - 373:16388 Poison Splash (0:33) - 373:16389 Healing Splash - 373:16390 Night Vision Splash (2:15) - 373:16392 Weakness Splash (1:07) - 373:16393 Strength Splash (2:15) - 373:16394 Slowness Splash (1:07) - 373:16396 Harming Splash - 373:16397 Breathing Splash (2:15) - 373:16398 Invisibility Splash (2:15) - 373:16417 Regeneration Splash II (0:16) - 373:16418 Swiftness Splash II (1:07) - 373:16420 Poison Splash II (0:16) - 373:16421 Healing Splash II - 373:16425 Strength Splash II (1:07) - 373:16428 Harming Splash II - 373:16449 Regeneration Splash (1:30) - 373:16450 Swiftness Splash (6:00) - 373:16451 Fire Resistance Splash (6:00) - 373:16452 Poison Splash (1:30) - 373:16454 Night Vision Splash (6:00) - 373:16456 Weakness Splash (3:00) - 373:16457 Strength Splash (6:00) - 373:16458 Slowness Splash (3:00) - 373:16461 Breathing Splash (6:00) - 373:16462 Invisibility Splash (6:00) - 373:16481 Regeneration Splash II (0:45) - 373:16482 Swiftness Splash II (3:00) - 373:16484 Poison Splash II (0:45) - 373:16489 Strength Splash II (3:00) - 374 Glass Bottle - 375 Spider Eye - 376 Fermented Spider Eye - 377 Blaze Powder - 378 Magma Cream - 379 Brewing Stand - 380 Cauldron - 381 Eye of Ender - 382 Glistering Melon (Slice) - 383:50 Spawn Egg (Creeper) - 383:51 Spawn Egg (Skeleton) - 383:52 Spawn Egg (Spider) - 383:54 Spawn Egg (Zombie) - 383:55 Spawn Egg (Slime) - 383:56 Spawn Egg (Ghast) - 383:57 Spawn Egg (Zombie Pigmen) - 383:58 Spawn Egg (Endermen) - 383:59 Spawn Egg (Cave Spider) - 383:60 Spawn Egg (Silverfish) - 383:61 Spawn Egg (Blaze) - 383:62 Spawn Egg (Magma Cube) - 383:65 Spawn Egg (Bat) - 383:66 Spawn Egg (Witch) - 383:90 Spawn Egg (Pig) - 383:91 Spawn Egg (Sheep) - 383:92 Spawn Egg (Cow) - 383:93 Spawn Egg (Chicken) - 383:94 Spawn Egg (Squid) - 383:95 Spawn Egg (Wolf) - 383:96 Spawn Egg (Mooshroom) - 383:98 Spawn Egg (Ocelot) - 383:100 Spawn Egg (Horse) - 383:120 Spawn Egg (Villager) - 384 Bottle of Enchanting - 385 Fire Charge - 386 Book and Quill - 387 Written Book - 388 Emerald - 389 Item Frame - 390 Flower Pot - 391 Carrot - 392 Potato - 393 Baked Potato - 394 Poisonous Potato - 395 Empty Map - 396 Golden Carrot - 397 Head (Skeleton) - 397:1 Head (Wither) - 397:2 Head (Zombie) - 397:3 Head (Steve) - 397:4 Head (Creeper) - 398 Carrot on a Stick - 399 Nether Star - 400 Pumpkin Pie - 401 Firework Rocket - 402 Firework Star - 403 Enchanted Book - 404 Redstone Comparator - 405 Nether Brick (Item) - 406 Nether Quartz - 407 Minecart (TNT) - 408 Minecart (Hopper) - 417 Iron Horse Armor - 418 Gold Horse Armor - 419 Diamond Horse Armor - 420 Lead - 421 Name Tag - 422 Minecart (Command Block) - 2256 Music Disk (13) - 2257 Music Disk (Cat) - 2258 Music Disk (Blocks) - 2259 Music Disk (Chirp) - 2260 Music Disk (Far) - 2261 Music Disk (Mall) - 2262 Music Disk (Mellohi) - 2263 Music Disk (Stal) - 2264 Music Disk (Strad) - 2265 Music Disk (Ward) - 2266 Music Disk (11) - 2267 Music Disk (Wait) diff --git a/disabled_stuff/data/kills.json b/disabled_stuff/data/kills.json deleted file mode 100644 index 5f6d046..0000000 --- a/disabled_stuff/data/kills.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "templates": [ - "rips off {user}'s {limbs} and leaves them to die.", - "grabs {user}'s head and rips it clean off their body.", - "grabs a {gun} and riddles {user}'s body with bullets.", - "gags and ties {user} then throws them off a {tall_thing}.", - "crushes {user} with a huge spiked {spiked_thing}.", - "glares at {user} until they die of boredom.", - "stabs {user} in the heart a few times with a {weapon_stab}.", - "rams a {weapon_explosive} up {user}'s ass and lets off a few rounds.", - "crushes {user}'s skull in with a {weapon_crush}.", - "unleashes the armies of Isengard on {user}.", - "gags and ties {user} then throws them off a {tall_thing} to their death.", - "reaches out and punches right through {user}'s chest.", - "slices {user}'s limbs off with a {weapon_slice}.", - "throws {user} to Cthulu and watches them get ripped to shreds.", - "feeds {user} to an owlbear who then proceeds to maul them violently.", - "turns {user} into a snail and covers then in salt.", - "snacks on {user}'s dismembered body.", - "stuffs {bomb} up {user}'s ass and waits for it to go off.", - "puts {user} into a sack, throws the sack in the river, and hurls the river into space.", - "goes bowling with {user}'s bloody disembodied head.", - "sends {user} to /dev/null!", - "feeds {user} coke and mentos till they violently explode." - ], - "parts": { - "gun": [ - "AK47", - "machine gun", - "automatic pistol", - "Uzi" - ], - "limbs": [ - "legs", - "arms", - "limbs" - ], - "weapon_stab": [ - "knife", - "shard of glass", - "sword blade", - "butchers knife", - "corkscrew" - ], - "weapon_slice": [ - "sharpened katana", - "chainsaw", - "polished axe" - ], - "weapon_crush": [ - "spiked mace", - "baseball bat", - "wooden club", - "massive steel ball", - "heavy iron rod" - ], - "weapon_explosive": [ - "rocket launcher", - "grenade launcher", - "napalm launcher" - ], - "tall_thing": [ - "bridge", - "tall building", - "cliff", - "mountain" - ], - "spiked_thing": [ - "boulder", - "rock", - "barrel of rocks" - ], - "bomb": [ - "a bomb", - "some TNT", - "a bunch of C4" - ] - } -} diff --git a/disabled_stuff/data/slaps.json b/disabled_stuff/data/slaps.json deleted file mode 100644 index 6ec0166..0000000 --- a/disabled_stuff/data/slaps.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "templates":[ - "{hits} {user} with a {item}.", - "{hits} {user} around a bit with a {item}.", - "{throws} a {item} at {user}.", - "{throws} a few {item}s at {user}.", - "grabs a {item} and {throws} it in {user}'s face.", - "launches a {item} in {user}'s general direction.", - "sits on {user}'s face while slamming a {item} into their crotch.", - "starts slapping {user} silly with a {item}.", - "holds {user} down and repeatedly {hits} them with a {item}.", - "prods {user} with a {item}.", - "picks up a {item} and {hits} {user} with it.", - "ties {user} to a chair and {throws} a {item} at them.", - "{hits} {user} {where} with a {item}.", - "ties {user} to a pole and whips them with a {item}." - ], - "parts": { - "item":[ - "cast iron skillet", - "large trout", - "baseball bat", - "wooden cane", - "nail", - "printer", - "shovel", - "pair of trousers", - "CRT monitor", - "diamond sword", - "baguette", - "physics textbook", - "toaster", - "portrait of Richard Stallman", - "television", - "mau5head", - "five ton truck", - "roll of duct tape", - "book", - "laptop", - "old television", - "sack of rocks", - "rainbow trout", - "cobblestone block", - "lava bucket", - "rubber chicken", - "spiked bat", - "gold block", - "fire extinguisher", - "heavy rock", - "chunk of dirt" - ], - "throws": [ - "throws", - "flings", - "chucks" - ], - "hits": [ - "hits", - "whacks", - "slaps", - "smacks" - ], - "where": [ - "in the chest", - "on the head", - "on the bum" - ] - } -} diff --git a/disabled_stuff/domainr.py b/disabled_stuff/domainr.py deleted file mode 100644 index e853bfa..0000000 --- a/disabled_stuff/domainr.py +++ /dev/null @@ -1,18 +0,0 @@ -from util import hook, http - - -@hook.command -def domainr(inp): - """domainr - Use domain.nr's API to search for a domain, and similar domains.""" - try: - data = http.get_json('http://domai.nr/api/json/search?q=' + inp) - except (http.URLError, http.HTTPError) as e: - return "Unable to get data for some reason. Try again later." - if data['query'] == "": - return "An error occurred: {status} - {message}".format(**data['error']) - domains = "" - for domain in data['results']: - domains += ("\x034" if domain['availability'] == "taken" else ( - "\x033" if domain['availability'] == "available" else "\x031")) + domain['domain'] + "\x0f" + domain[ - 'path'] + ", " - return "Domains: " + domains diff --git a/disabled_stuff/eightball.py b/disabled_stuff/eightball.py deleted file mode 100644 index 8d91303..0000000 --- a/disabled_stuff/eightball.py +++ /dev/null @@ -1,23 +0,0 @@ -import random - -from util import hook, text - - -color_codes = { - "": "\x02\x0305", - "": "\x02\x0303", - "": "\x02" -} - -with open("plugins/data/8ball_responses.txt") as f: - responses = [line.strip() for line in - f.readlines() if not line.startswith("//")] - - -@hook.command('8ball') -def eightball(inp, action=None): - """8ball -- The all knowing magic eight ball, - in electronic form. Ask and it shall be answered!""" - - magic = text.multiword_replace(random.choice(responses), color_codes) - action("shakes the magic 8 ball... {}".format(magic)) diff --git a/disabled_stuff/encrypt.py b/disabled_stuff/encrypt.py deleted file mode 100644 index e391a04..0000000 --- a/disabled_stuff/encrypt.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -import base64 -import json -import hashlib - -from Crypto import Random -from Crypto.Cipher import AES -from Crypto.Protocol.KDF import PBKDF2 - -from util import hook - - -# helper functions to pad and unpad a string to a specified block size -# -BS = AES.block_size -pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) -unpad = lambda s: s[0:-ord(s[-1])] - -# helper functions to encrypt and encode a string with AES and base64 -encode_aes = lambda c, s: base64.b64encode(c.encrypt(pad(s))) -decode_aes = lambda c, s: unpad(c.decrypt(base64.b64decode(s))) - -db_ready = False - - -def db_init(db): - """check to see that our db has the the encryption table.""" - global db_ready - if not db_ready: - db.execute("create table if not exists encryption(encrypted, iv, " - "primary key(encrypted))") - db.commit() - db_ready = True - - -def get_salt(bot): - """generate an encryption salt if none exists, then returns the salt""" - if not bot.config.get("random_salt", False): - bot.config["random_salt"] = hashlib.md5(os.urandom(16)).hexdigest() - json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) - return bot.config["random_salt"] - - -@hook.command -def encrypt(inp, bot=None, db=None, notice=None): - """encrypt -- Encrypts with . ( can only be decrypted using this bot)""" - db_init(db) - - split = inp.split(" ") - - # if there is only one argument, return the help message - if len(split) == 1: - notice(encrypt.__doc__) - return - - # generate the key from the password and salt - password = split[0] - salt = get_salt(bot) - key = PBKDF2(password, salt) - - # generate the IV and encode it to store in the database - iv = Random.new().read(AES.block_size) - iv_encoded = base64.b64encode(iv) - - # create the AES cipher and encrypt/encode the text with it - text = " ".join(split[1:]) - cipher = AES.new(key, AES.MODE_CBC, iv) - encoded = encode_aes(cipher, text) - - # store the encoded text and IV in the DB for decoding later - db.execute("insert or replace into encryption(encrypted, iv)" - "values(?,?)", (encoded, iv_encoded)) - db.commit() - - return encoded - - -@hook.command -def decrypt(inp, bot=None, db=None, notice=None): - """decrypt -- Decrypts with . (can only decrypt strings encrypted on this bot)""" - if not db_ready: - db_init(db) - - split = inp.split(" ") - - # if there is only one argument, return the help message - if len(split) == 1: - notice(decrypt.__doc__) - return - - # generate the key from the password and salt - password = split[0] - salt = get_salt(bot) - key = PBKDF2(password, salt) - - text = " ".join(split[1:]) - - # get the encoded IV from the database and decode it - iv_encoded = db.execute("select iv from encryption where" - " encrypted=?", (text,)).fetchone()[0] - iv = base64.b64decode(iv_encoded) - - # create AES cipher, decode text, decrypt text, and unpad it - cipher = AES.new(key, AES.MODE_CBC, iv) - return decode_aes(cipher, text) diff --git a/disabled_stuff/fishbans.py b/disabled_stuff/fishbans.py deleted file mode 100644 index aa76676..0000000 --- a/disabled_stuff/fishbans.py +++ /dev/null @@ -1,57 +0,0 @@ -from urllib import quote_plus - -from util import hook, http - - -api_url = "http://api.fishbans.com/stats/{}/" - - -@hook.command("bans") -@hook.command -def fishbans(inp): - """fishbans -- Gets information on s minecraft bans from fishbans""" - user = inp.strip() - - try: - request = http.get_json(api_url.format(quote_plus(user))) - except (http.HTTPError, http.URLError) as e: - return "Could not fetch ban data from the Fishbans API: {}".format(e) - - if not request["success"]: - return "Could not fetch ban data for {}.".format(user) - - user_url = "http://fishbans.com/u/{}/".format(user) - ban_count = request["stats"]["totalbans"] - - return "The user \x02{}\x02 has \x02{}\x02 ban(s). See detailed info " \ - "at {}".format(user, ban_count, user_url) - - -@hook.command -def bancount(inp): - """bancount -- Gets a count of s minecraft bans from fishbans""" - user = inp.strip() - - try: - request = http.get_json(api_url.format(quote_plus(user))) - except (http.HTTPError, http.URLError) as e: - return "Could not fetch ban data from the Fishbans API: {}".format(e) - - if not request["success"]: - return "Could not fetch ban data for {}.".format(user) - - user_url = "http://fishbans.com/u/{}/".format(user) - services = request["stats"]["service"] - - out = [] - for service, ban_count in services.items(): - if ban_count != 0: - out.append("{}: \x02{}\x02".format(service, ban_count)) - else: - pass - - if not out: - return "The user \x02{}\x02 has no bans.".format(user) - else: - return "Bans for \x02{}\x02: ".format(user) + ", ".join(out) + ". More info " \ - "at {}".format(user_url) diff --git a/disabled_stuff/freddy.py b/disabled_stuff/freddy.py deleted file mode 100644 index c77fa5a..0000000 --- a/disabled_stuff/freddy.py +++ /dev/null @@ -1,13 +0,0 @@ -from util import hook, http, web -from subprocess import check_output, CalledProcessError - -@hook.command -def freddycode(inp): - """freddycode - Check if the Freddy Fresh code is correct.""" - - try: - return "Freddy: '%s' ist %s" % (inp, \ - check_output(["/bin/freddycheck", inp])) - except CalledProcessError as err: - return "Freddy: Skript returned %s" % (str(err)) - diff --git a/disabled_stuff/github.py b/disabled_stuff/github.py deleted file mode 100644 index 18033ef..0000000 --- a/disabled_stuff/github.py +++ /dev/null @@ -1,120 +0,0 @@ -import json -import urllib2 - -from util import hook, http - - -shortcuts = {"cloudbot": "ClouDev/CloudBot"} - - -def truncate(msg): - nmsg = msg.split() - out = None - x = 0 - for i in nmsg: - if x <= 7: - if out: - out = out + " " + nmsg[x] - else: - out = nmsg[x] - x += 1 - if x <= 7: - return out - else: - return out + "..." - - -@hook.command -def ghissues(inp): - """ghissues username/repo [number] - Get specified issue summary, or open issue count """ - args = inp.split(" ") - try: - if args[0] in shortcuts: - repo = shortcuts[args[0]] - else: - repo = args[0] - url = "https://api.github.com/repos/{}/issues".format(repo) - except IndexError: - return "Invalid syntax. .github issues username/repo [number]" - try: - url += "/%s" % args[1] - number = True - except IndexError: - number = False - try: - data = json.loads(http.open(url).read()) - print url - if not number: - try: - data = data[0] - except IndexError: - print data - return "Repo has no open issues" - except ValueError: - return "Invalid data returned. Check arguments (.github issues username/repo [number]" - fmt = "Issue: #%s (%s) by %s: %s | %s %s" # (number, state, user.login, title, truncate(body), gitio.gitio(data.url)) - fmt1 = "Issue: #%s (%s) by %s: %s %s" # (number, state, user.login, title, gitio.gitio(data.url)) - number = data["number"] - if data["state"] == "open": - state = u"\x033\x02OPEN\x02\x0f" - else: - state = u"\x034\x02CLOSED\x02\x0f by {}".format(data["closed_by"]["login"]) - user = data["user"]["login"] - title = data["title"] - summary = truncate(data["body"]) - gitiourl = gitio(data["html_url"]) - if "Failed to get URL" in gitiourl: - gitiourl = gitio(data["html_url"] + " " + repo.split("/")[1] + number) - if summary == "": - return fmt1 % (number, state, user, title, gitiourl) - else: - return fmt % (number, state, user, title, summary, gitiourl) - - -@hook.command -def gitio(inp): - """gitio [code] -- Shorten Github URLs with git.io. [code] is - a optional custom short code.""" - split = inp.split(" ") - url = split[0] - - try: - code = split[1] - except: - code = None - - # if the first 8 chars of "url" are not "https://" then append - # "https://" to the url, also convert "http://" to "https://" - if url[:8] != "https://": - if url[:7] != "http://": - url = "https://" + url - else: - url = "https://" + url[7:] - url = 'url=' + str(url) - if code: - url = url + '&code=' + str(code) - req = urllib2.Request(url='http://git.io', data=url) - - # try getting url, catch http error - try: - f = urllib2.urlopen(req) - except urllib2.HTTPError: - return "Failed to get URL!" - urlinfo = str(f.info()) - - # loop over the rows in urlinfo and pick out location and - # status (this is pretty odd code, but urllib2.Request is weird) - for row in urlinfo.split("\n"): - if row.find("Status") != -1: - status = row - if row.find("Location") != -1: - location = row - - print status - if not "201" in status: - return "Failed to get URL!" - - # this wont work for some reason, so lets ignore it ^ - - # return location, minus the first 10 chars - return location[10:] diff --git a/disabled_stuff/google_translate.py b/disabled_stuff/google_translate.py deleted file mode 100644 index a9d4ea3..0000000 --- a/disabled_stuff/google_translate.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -A Google API key is required and retrieved from the bot config file. -Since December 1, 2011, the Google Translate API is a paid service only. -""" - -import htmlentitydefs -import re - -from util import hook, http - - -max_length = 100 - - -########### from http://effbot.org/zone/re-sub.htm#unescape-html ############# - - -def unescape(text): - def fixup(m): - text = m.group(0) - if text[:2] == "&#": - # character reference - try: - if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)) - else: - return unichr(int(text[2:-1])) - except ValueError: - pass - else: - # named entity - try: - text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) - except KeyError: - pass - return text # leave as is - - return re.sub("&#?\w+;", fixup, text) - -############################################################################## - - -def goog_trans(api_key, text, slang, tlang): - url = 'https://www.googleapis.com/language/translate/v2' - - if len(text) > max_length: - return "This command only supports input of less then 100 characters." - - if slang: - parsed = http.get_json(url, key=api_key, q=text, source=slang, target=tlang, format="text") - else: - parsed = http.get_json(url, key=api_key, q=text, target=tlang, format="text") - - #if not 200 <= parsed['responseStatus'] < 300: - # raise IOError('error with the translation server: %d: %s' % ( - # parsed['responseStatus'], parsed['responseDetails'])) - if not slang: - return unescape('(%(detectedSourceLanguage)s) %(translatedText)s' % - (parsed['data']['translations'][0])) - return unescape('%(translatedText)s' % parsed['data']['translations'][0]) - - -def match_language(fragment): - fragment = fragment.lower() - for short, _ in lang_pairs: - if fragment in short.lower().split(): - return short.split()[0] - - for short, full in lang_pairs: - if fragment in full.lower(): - return short.split()[0] - - return None - - -@hook.command -def translate(inp, bot=None): - """translate [source language [target language]] -- translates - from source language (default autodetect) to target - language (default English) using Google Translate""" - - api_key = bot.config.get("api_keys", {}).get("googletranslate", None) - if not api_key: - return "This command requires a paid API key." - - args = inp.split(u' ', 2) - - try: - if len(args) >= 2: - sl = match_language(args[0]) - if not sl: - return goog_trans(api_key, inp, '', 'en') - if len(args) == 2: - return goog_trans(api_key, args[1], sl, 'en') - if len(args) >= 3: - tl = match_language(args[1]) - if not tl: - if sl == 'en': - return 'unable to determine desired target language' - return goog_trans(api_key, args[1] + ' ' + args[2], sl, 'en') - return goog_trans(api_key, args[2], sl, tl) - return goog_trans(api_key, inp, '', 'en') - except IOError, e: - return e - - -lang_pairs = [ - ("no", "Norwegian"), - ("it", "Italian"), - ("ht", "Haitian Creole"), - ("af", "Afrikaans"), - ("sq", "Albanian"), - ("ar", "Arabic"), - ("hy", "Armenian"), - ("az", "Azerbaijani"), - ("eu", "Basque"), - ("be", "Belarusian"), - ("bg", "Bulgarian"), - ("ca", "Catalan"), - ("zh-CN zh", "Chinese"), - ("hr", "Croatian"), - ("cs", "Czech"), - ("da", "Danish"), - ("nl", "Dutch"), - ("en", "English"), - ("et", "Estonian"), - ("tl", "Filipino"), - ("fi", "Finnish"), - ("fr", "French"), - ("gl", "Galician"), - ("ka", "Georgian"), - ("de", "German"), - ("el", "Greek"), - ("ht", "Haitian Creole"), - ("iw", "Hebrew"), - ("hi", "Hindi"), - ("hu", "Hungarian"), - ("is", "Icelandic"), - ("id", "Indonesian"), - ("ga", "Irish"), - ("it", "Italian"), - ("ja jp jpn", "Japanese"), - ("ko", "Korean"), - ("lv", "Latvian"), - ("lt", "Lithuanian"), - ("mk", "Macedonian"), - ("ms", "Malay"), - ("mt", "Maltese"), - ("no", "Norwegian"), - ("fa", "Persian"), - ("pl", "Polish"), - ("pt", "Portuguese"), - ("ro", "Romanian"), - ("ru", "Russian"), - ("sr", "Serbian"), - ("sk", "Slovak"), - ("sl", "Slovenian"), - ("es", "Spanish"), - ("sw", "Swahili"), - ("sv", "Swedish"), - ("th", "Thai"), - ("tr", "Turkish"), - ("uk", "Ukrainian"), - ("ur", "Urdu"), - ("vi", "Vietnamese"), - ("cy", "Welsh"), - ("yi", "Yiddish") -] diff --git a/disabled_stuff/googleurlparse.py b/disabled_stuff/googleurlparse.py deleted file mode 100644 index cbea897..0000000 --- a/disabled_stuff/googleurlparse.py +++ /dev/null @@ -1,22 +0,0 @@ -from util import hook -from urllib import unquote - -@hook.command(autohelp=False) -def googleurl(inp, db=None, nick=None): - """googleurl [nickname] - Converts Google urls (google.com/url) to normal urls - where possible, in the specified nickname's last message. If nickname isn't provided, - action will be performed on user's last message""" - if not inp: - inp = nick - last_message = db.execute("select name, quote from seen_user where name" - " like ? and chan = ?", (inp.lower(), input.chan.lower())).fetchone() - if last_message: - msg = last_message[1] - out = ", ".join([(unquote(a[4:]) if a[:4] == "url=" else "") for a in msg.split("&")])\ - .replace(", ,", "").strip() - return out if out else "No matches in your last message." - else: - if inp == nick: - return "You haven't said anything in this channel yet!" - else: - return "That user hasn't said anything in this channel yet!" diff --git a/disabled_stuff/history.py b/disabled_stuff/history.py deleted file mode 100644 index c703bcf..0000000 --- a/disabled_stuff/history.py +++ /dev/null @@ -1,89 +0,0 @@ -from collections import deque -from util import hook, timesince -import time -import re - -db_ready = [] - - -def db_init(db, conn_name): - """check to see that our db has the the seen table (connection name is for caching the result per connection)""" - global db_ready - if db_ready.count(conn_name) < 1: - db.execute("create table if not exists seen_user(name, time, quote, chan, host, " - "primary key(name, chan))") - db.commit() - db_ready.append(conn_name) - - -def track_seen(input, message_time, db, conn): - """ Tracks messages for the .seen command """ - db_init(db, conn) - # keep private messages private - if input.chan[:1] == "#" and not re.findall('^s/.*/.*/$', input.msg.lower()): - db.execute("insert or replace into seen_user(name, time, quote, chan, host)" - "values(?,?,?,?,?)", (input.nick.lower(), message_time, input.msg, - input.chan, input.mask)) - db.commit() - - -def track_history(input, message_time, conn): - try: - history = conn.history[input.chan] - except KeyError: - conn.history[input.chan] = deque(maxlen=100) - history = conn.history[input.chan] - - data = (input.nick, message_time, input.msg) - history.append(data) - - -@hook.singlethread -@hook.event('PRIVMSG', ignorebots=False) -def chat_tracker(paraml, input=None, db=None, conn=None): - message_time = time.time() - track_seen(input, message_time, db, conn) - track_history(input, message_time, conn) - - -@hook.command(autohelp=False) -def resethistory(inp, input=None, conn=None): - """resethistory - Resets chat history for the current channel""" - try: - conn.history[input.chan].clear() - return "Reset chat history for current channel." - except KeyError: - # wat - return "There is no history for this channel." - -"""seen.py: written by sklnd in about two beers July 2009""" - -@hook.command -def seen(inp, nick='', chan='', db=None, input=None, conn=None): - """seen -- Tell when a nickname was last in active in one of this bot's channels.""" - - if input.conn.nick.lower() == inp.lower(): - return "You need to get your eyes checked." - - if inp.lower() == nick.lower(): - return "Have you looked in a mirror lately?" - - if not re.match("^[A-Za-z0-9_|.\-\]\[]*$", inp.lower()): - return "I can't look up that name, its impossible to use!" - - db_init(db, conn.name) - - last_seen = db.execute("select name, time, quote from seen_user where name" - " like ? and chan = ?", (inp, chan)).fetchone() - - if last_seen: - reltime = timesince.timesince(last_seen[1]) - if last_seen[0] != inp.lower(): # for glob matching - inp = last_seen[0] - if last_seen[2][0:1] == "\x01": - return '{} was last seen {} ago: * {} {}'.format(inp, reltime, inp, - last_seen[2][8:-1]) - else: - return '{} was last seen {} ago saying: {}'.format(inp, reltime, last_seen[2]) - else: - return "I've never seen {} talking in this channel.".format(inp) diff --git a/disabled_stuff/horoscope.py b/disabled_stuff/horoscope.py deleted file mode 100644 index e4404cf..0000000 --- a/disabled_stuff/horoscope.py +++ /dev/null @@ -1,56 +0,0 @@ -# Plugin by Infinity - - -from util import hook, http, text - -db_ready = False - - -def db_init(db): - """check to see that our db has the horoscope table and return a connection.""" - global db_ready - if not db_ready: - db.execute("create table if not exists horoscope(nick primary key, sign)") - db.commit() - db_ready = True - - -@hook.command(autohelp=False) -def horoscope(inp, db=None, notice=None, nick=None): - """horoscope -- Get your horoscope.""" - db_init(db) - - # check if the user asked us not to save his details - dontsave = inp.endswith(" dontsave") - if dontsave: - sign = inp[:-9].strip().lower() - else: - sign = inp - - db.execute("create table if not exists horoscope(nick primary key, sign)") - - if not sign: - sign = db.execute("select sign from horoscope where nick=lower(?)", - (nick,)).fetchone() - if not sign: - notice("horoscope -- Get your horoscope") - return - sign = sign[0] - - url = "http://my.horoscope.com/astrology/free-daily-horoscope-{}.html".format(sign) - soup = http.get_soup(url) - - title = soup.find_all('h1', {'class': 'h1b'})[1] - horoscope_text = soup.find('div', {'class': 'fontdef1'}) - result = u"\x02%s\x02 %s" % (title, horoscope_text) - result = text.strip_html(result) - #result = unicode(result, "utf8").replace('flight ','') - - if not title: - return "Could not get the horoscope for {}.".format(inp) - - if inp and not dontsave: - db.execute("insert or replace into horoscope(nick, sign) values (?,?)", - (nick.lower(), sign)) - db.commit() - - return result diff --git a/disabled_stuff/hulu.py b/disabled_stuff/hulu.py deleted file mode 100644 index 74e6b00..0000000 --- a/disabled_stuff/hulu.py +++ /dev/null @@ -1,30 +0,0 @@ -from urllib import urlencode -import re - -from util import hook, http, timeformat - - -hulu_re = (r'(.*://)(www.hulu.com|hulu.com)(.*)', re.I) - - -@hook.regex(*hulu_re) -def hulu_url(match): - data = http.get_json("http://www.hulu.com/api/oembed.json?url=http://www.hulu.com" + match.group(3)) - showname = data['title'].split("(")[-1].split(")")[0] - title = data['title'].split(" (")[0] - return "{}: {} - {}".format(showname, title, timeformat.format_time(int(data['duration']))) - - -@hook.command('hulu') -def hulu_search(inp): - """hulu - Search Hulu""" - result = http.get_soup( - "http://m.hulu.com/search?dp_identifier=hulu&{}&items_per_page=1&page=1".format(urlencode({'query': inp}))) - data = result.find('results').find('videos').find('video') - showname = data.find('show').find('name').text - title = data.find('title').text - duration = timeformat.format_time(int(float(data.find('duration').text))) - description = data.find('description').text - rating = data.find('content-rating').text - return "{}: {} - {} - {} ({}) {}".format(showname, title, description, duration, rating, - "http://www.hulu.com/watch/" + str(data.find('id').text)) diff --git a/disabled_stuff/imdb.py b/disabled_stuff/imdb.py deleted file mode 100644 index 0272248..0000000 --- a/disabled_stuff/imdb.py +++ /dev/null @@ -1,59 +0,0 @@ -# IMDb lookup plugin by Ghetto Wizard (2011) and blha303 (2013) - -import re - -from util import hook, http, text - - -id_re = re.compile("tt\d+") -imdb_re = (r'(.*:)//(imdb.com|www.imdb.com)(:[0-9]+)?(.*)', re.I) - - -@hook.command -def imdb(inp): - """imdb -- Gets information about from IMDb.""" - - strip = inp.strip() - - if id_re.match(strip): - content = http.get_json("http://www.omdbapi.com/", i=strip) - else: - content = http.get_json("http://www.omdbapi.com/", t=strip) - - if content.get('Error', None) == 'Movie not found!': - return 'Movie not found!' - elif content['Response'] == 'True': - content['URL'] = 'http://www.imdb.com/title/{}'.format(content['imdbID']) - - out = '\x02%(Title)s\x02 (%(Year)s) (%(Genre)s): %(Plot)s' - if content['Runtime'] != 'N/A': - out += ' \x02%(Runtime)s\x02.' - if content['imdbRating'] != 'N/A' and content['imdbVotes'] != 'N/A': - out += ' \x02%(imdbRating)s/10\x02 with \x02%(imdbVotes)s\x02' \ - ' votes.' - out += ' %(URL)s' - return out % content - else: - return 'Unknown error.' - - -@hook.regex(*imdb_re) -def imdb_url(match): - imdb_id = match.group(4).split('/')[-1] - if imdb_id == "": - imdb_id = match.group(4).split('/')[-2] - content = http.get_json("http://www.omdbapi.com/", i=imdb_id) - if content.get('Error', None) == 'Movie not found!': - return 'Movie not found!' - elif content['Response'] == 'True': - content['URL'] = 'http://www.imdb.com/title/%(imdbID)s' % content - content['Plot'] = text.truncate_str(content['Plot'], 50) - out = '\x02%(Title)s\x02 (%(Year)s) (%(Genre)s): %(Plot)s' - if content['Runtime'] != 'N/A': - out += ' \x02%(Runtime)s\x02.' - if content['imdbRating'] != 'N/A' and content['imdbVotes'] != 'N/A': - out += ' \x02%(imdbRating)s/10\x02 with \x02%(imdbVotes)s\x02' \ - ' votes.' - return out % content - else: - return 'Unknown error.' diff --git a/disabled_stuff/imgur.py b/disabled_stuff/imgur.py deleted file mode 100644 index 320bc6e..0000000 --- a/disabled_stuff/imgur.py +++ /dev/null @@ -1,82 +0,0 @@ -import re -import random - -from util import hook, http, web - - -base_url = "http://reddit.com/r/{}/.json" -imgur_re = re.compile(r'http://(?:i\.)?imgur\.com/(a/)?(\w+\b(?!/))\.?\w?') - -album_api = "https://api.imgur.com/3/album/{}/images.json" - - -def is_valid(data): - if data["domain"] in ["i.imgur.com", "imgur.com"]: - return True - else: - return False - - -@hook.command(autohelp=False) -def imgur(inp): - """imgur [subreddit] -- Gets the first page of imgur images from [subreddit] and returns a link to them. - If [subreddit] is undefined, return any imgur images""" - if inp: - # see if the input ends with "nsfw" - show_nsfw = inp.endswith(" nsfw") - - # remove "nsfw" from the input string after checking for it - if show_nsfw: - inp = inp[:-5].strip().lower() - - url = base_url.format(inp.strip()) - else: - url = "http://www.reddit.com/domain/imgur.com/.json" - show_nsfw = False - - try: - data = http.get_json(url, user_agent=http.ua_chrome) - except Exception as e: - return "Error: " + str(e) - - data = data["data"]["children"] - random.shuffle(data) - - # filter list to only have imgur links - filtered_posts = [i["data"] for i in data if is_valid(i["data"])] - - if not filtered_posts: - return "No images found." - - items = [] - - headers = { - "Authorization": "Client-ID b5d127e6941b07a" - } - - # loop over the list of posts - for post in filtered_posts: - if post["over_18"] and not show_nsfw: - continue - - match = imgur_re.search(post["url"]) - if match.group(1) == 'a/': - # post is an album - url = album_api.format(match.group(2)) - images = http.get_json(url, headers=headers)["data"] - - # loop over the images in the album and add to the list - for image in images: - items.append(image["id"]) - - elif match.group(2) is not None: - # post is an image - items.append(match.group(2)) - - if not items: - return "No images found (use .imgur nsfw to show explicit content)" - - if show_nsfw: - return "{} \x02NSFW\x02".format(web.isgd("http://imgur.com/" + ','.join(items))) - else: - return web.isgd("http://imgur.com/" + ','.join(items)) diff --git a/disabled_stuff/isup.py b/disabled_stuff/isup.py deleted file mode 100644 index 5fc95d6..0000000 --- a/disabled_stuff/isup.py +++ /dev/null @@ -1,28 +0,0 @@ -import urlparse - -from util import hook, http, urlnorm - - -@hook.command -def isup(inp): - """isup -- uses isup.me to see if a site is up or not""" - - # slightly overcomplicated, esoteric URL parsing - scheme, auth, path, query, fragment = urlparse.urlsplit(inp.strip()) - - domain = auth.encode('utf-8') or path.encode('utf-8') - url = urlnorm.normalize(domain, assume_scheme="http") - - try: - soup = http.get_soup('http://isup.me/' + domain) - except http.HTTPError, http.URLError: - return "Could not get status." - - content = soup.find('div').text.strip() - - if "not just you" in content: - return "It's not just you. {} looks \x02\x034down\x02\x0f from here!".format(url) - elif "is up" in content: - return "It's just you. {} is \x02\x033up\x02\x0f.".format(url) - else: - return "Huh? That doesn't look like a site on the interweb." diff --git a/disabled_stuff/kernel.py b/disabled_stuff/kernel.py deleted file mode 100644 index 90cbed5..0000000 --- a/disabled_stuff/kernel.py +++ /dev/null @@ -1,15 +0,0 @@ -import re - -from util import hook, http - - -@hook.command(autohelp=False) -def kernel(inp, reply=None): - contents = http.get("https://www.kernel.org/finger_banner") - contents = re.sub(r'The latest(\s*)', '', contents) - contents = re.sub(r'version of the Linux kernel is:(\s*)', '- ', contents) - lines = contents.split("\n") - - message = "Linux kernel versions: " - message += ", ".join(line for line in lines[:-1]) - reply(message) diff --git a/disabled_stuff/kill.py b/disabled_stuff/kill.py deleted file mode 100644 index d25228e..0000000 --- a/disabled_stuff/kill.py +++ /dev/null @@ -1,33 +0,0 @@ -import json - -from util import hook, textgen - - -def get_generator(_json, variables): - data = json.loads(_json) - return textgen.TextGenerator(data["templates"], - data["parts"], variables=variables) - - -@hook.command -def kill(inp, action=None, nick=None, conn=None, notice=None): - """kill -- Makes the bot kill .""" - target = inp.strip() - - if " " in target: - notice("Invalid username!") - return - - # if the user is trying to make the bot kill itself, kill them - if target.lower() == conn.nick.lower() or target.lower() == "itself": - target = nick - - variables = { - "user": target - } - - with open("plugins/data/kills.json") as f: - generator = get_generator(f.read(), variables) - - # act out the message - action(generator.generate_string()) diff --git a/disabled_stuff/lyrics.py b/disabled_stuff/lyrics.py deleted file mode 100644 index eabb84a..0000000 --- a/disabled_stuff/lyrics.py +++ /dev/null @@ -1,43 +0,0 @@ -from util import hook, http, web - -url = "http://search.azlyrics.com/search.php?q=" - - -@hook.command -def lyrics(inp): - """lyrics - Search AZLyrics.com for song lyrics""" - if "pastelyrics" in inp: - dopaste = True - inp = inp.replace("pastelyrics", "").strip() - else: - dopaste = False - soup = http.get_soup(url + inp.replace(" ", "+")) - if "Try to compose less restrictive search query" in soup.find('div', {'id': 'inn'}).text: - return "No results. Check spelling." - div = None - for i in soup.findAll('div', {'class': 'sen'}): - if "/lyrics/" in i.find('a')['href']: - div = i - break - if div: - title = div.find('a').text - link = div.find('a')['href'] - if dopaste: - newsoup = http.get_soup(link) - try: - lyrics = newsoup.find('div', {'style': 'margin-left:10px;margin-right:10px;'}).text.strip() - pasteurl = " " + web.haste(lyrics) - except Exception as e: - pasteurl = " (\x02Unable to paste lyrics\x02 [{}])".format(str(e)) - else: - pasteurl = "" - artist = div.find('b').text.title() - lyricsum = div.find('div').text - if "\r\n" in lyricsum.strip(): - lyricsum = " / ".join(lyricsum.strip().split("\r\n")[0:4]) # truncate, format - else: - lyricsum = " / ".join(lyricsum.strip().split("\n")[0:4]) # truncate, format - return "\x02{}\x02 by \x02{}\x02 {}{} - {}".format(title, artist, web.try_isgd(link), pasteurl, - lyricsum[:-3]) - else: - return "No song results. " + url + inp.replace(" ", "+") diff --git a/disabled_stuff/minecraft_bukget.py b/disabled_stuff/minecraft_bukget.py deleted file mode 100644 index 496f169..0000000 --- a/disabled_stuff/minecraft_bukget.py +++ /dev/null @@ -1,154 +0,0 @@ -import time -import random - -from util import hook, http, web, text - - -## CONSTANTS - -base_url = "http://api.bukget.org/3/" - -search_url = base_url + "search/plugin_name/like/{}" -random_url = base_url + "plugins/bukkit/?start={}&size=1" -details_url = base_url + "plugins/bukkit/{}" - -categories = http.get_json("http://api.bukget.org/3/categories") - -count_total = sum([cat["count"] for cat in categories]) -count_categories = {cat["name"].lower(): int(cat["count"]) for cat in categories} # dict comps! - - -class BukgetError(Exception): - def __init__(self, code, text): - self.code = code - self.text = text - - def __str__(self): - return self.text - - -## DATA FUNCTIONS - -def plugin_search(term): - """ searches for a plugin with the bukget API and returns the slug """ - term = term.lower().strip() - - search_term = http.quote_plus(term) - - try: - results = http.get_json(search_url.format(search_term)) - except (http.HTTPError, http.URLError) as e: - raise BukgetError(500, "Error Fetching Search Page: {}".format(e)) - - if not results: - raise BukgetError(404, "No Results Found") - - for result in results: - if result["slug"] == term: - return result["slug"] - - return results[0]["slug"] - - -def plugin_random(): - """ gets a random plugin from the bukget API and returns the slug """ - results = None - - while not results: - plugin_number = random.randint(1, count_total) - print "trying {}".format(plugin_number) - try: - results = http.get_json(random_url.format(plugin_number)) - except (http.HTTPError, http.URLError) as e: - raise BukgetError(500, "Error Fetching Search Page: {}".format(e)) - - return results[0]["slug"] - - -def plugin_details(slug): - """ takes a plugin slug and returns details from the bukget API """ - slug = slug.lower().strip() - - try: - details = http.get_json(details_url.format(slug)) - except (http.HTTPError, http.URLError) as e: - raise BukgetError(500, "Error Fetching Details: {}".format(e)) - return details - - -## OTHER FUNCTIONS - -def format_output(data): - """ takes plugin data and returns two strings representing information about that plugin """ - name = data["plugin_name"] - description = text.truncate_str(data['description'], 30) - url = data['website'] - authors = data['authors'][0] - authors = authors[0] + u"\u200b" + authors[1:] - stage = data['stage'] - - current_version = data['versions'][0] - - last_update = time.strftime('%d %B %Y %H:%M', - time.gmtime(current_version['date'])) - version_number = data['versions'][0]['version'] - - bukkit_versions = ", ".join(current_version['game_versions']) - link = web.try_isgd(current_version['link']) - - if description: - line_a = u"\x02{}\x02, by \x02{}\x02 - {} - ({}) \x02{}".format(name, authors, description, stage, url) - else: - line_a = u"\x02{}\x02, by \x02{}\x02 ({}) \x02{}".format(name, authors, stage, url) - - line_b = u"Last release: \x02v{}\x02 for \x02{}\x02 at {} \x02{}\x02".format(version_number, bukkit_versions, - last_update, link) - - return line_a, line_b - - -## HOOK FUNCTIONS - -@hook.command('plugin') -@hook.command -def bukget(inp, reply=None, message=None): - """bukget - Look up a plugin on dev.bukkit.org""" - # get the plugin slug using search - try: - slug = plugin_search(inp) - except BukgetError as e: - return e - - # get the plugin info using the slug - try: - data = plugin_details(slug) - except BukgetError as e: - return e - - # format the final message and send it to IRC - line_a, line_b = format_output(data) - - reply(line_a) - message(line_b) - - -@hook.command(autohelp=None) -def randomplugin(inp, reply=None, message=None): - """randomplugin - Gets a random plugin from dev.bukkit.org""" - # get a random plugin slug - try: - slug = plugin_random() - except BukgetError as e: - return e - - # get the plugin info using the slug - try: - data = plugin_details(slug) - except BukgetError as e: - return e - - # format the final message and send it to IRC - line_a, line_b = format_output(data) - - reply(line_a) - message(line_b) \ No newline at end of file diff --git a/disabled_stuff/minecraft_ping.py b/disabled_stuff/minecraft_ping.py deleted file mode 100644 index 978ca19..0000000 --- a/disabled_stuff/minecraft_ping.py +++ /dev/null @@ -1,232 +0,0 @@ -import socket -import struct -import json -import traceback - -from util import hook - - -try: - import DNS - has_dns = True -except ImportError: - has_dns = False - - -mc_colors = [(u'\xa7f', u'\x0300'), (u'\xa70', u'\x0301'), (u'\xa71', u'\x0302'), (u'\xa72', u'\x0303'), - (u'\xa7c', u'\x0304'), (u'\xa74', u'\x0305'), (u'\xa75', u'\x0306'), (u'\xa76', u'\x0307'), - (u'\xa7e', u'\x0308'), (u'\xa7a', u'\x0309'), (u'\xa73', u'\x0310'), (u'\xa7b', u'\x0311'), - (u'\xa71', u'\x0312'), (u'\xa7d', u'\x0313'), (u'\xa78', u'\x0314'), (u'\xa77', u'\x0315'), - (u'\xa7l', u'\x02'), (u'\xa79', u'\x0310'), (u'\xa7o', u'\t'), (u'\xa7m', u'\x13'), - (u'\xa7r', u'\x0f'), (u'\xa7n', u'\x15')] - - -## EXCEPTIONS - - -class PingError(Exception): - def __init__(self, text): - self.text = text - - def __str__(self): - return self.text - - -class ParseError(Exception): - def __init__(self, text): - self.text = text - - def __str__(self): - return self.text - - -## MISC - - -def unpack_varint(s): - d = 0 - i = 0 - while True: - b = ord(s.recv(1)) - d |= (b & 0x7F) << 7 * i - i += 1 - if not b & 0x80: - return d - -pack_data = lambda d: struct.pack('>b', len(d)) + d -pack_port = lambda i: struct.pack('>H', i) - -## DATA FUNCTIONS - - -def mcping_modern(host, port): - """ pings a server using the modern (1.7+) protocol and returns data """ - try: - # connect to the server - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - s.connect((host, port)) - except socket.gaierror: - raise PingError("Invalid hostname") - except socket.timeout: - raise PingError("Request timed out") - - # send handshake + status request - s.send(pack_data("\x00\x00" + pack_data(host.encode('utf8')) + pack_port(port) + "\x01")) - s.send(pack_data("\x00")) - - # read response - unpack_varint(s) # Packet length - unpack_varint(s) # Packet ID - l = unpack_varint(s) # String length - - if not l > 1: - raise PingError("Invalid response") - - d = "" - while len(d) < l: - d += s.recv(1024) - - # Close our socket - s.close() - except socket.error: - raise PingError("Socket Error") - - # Load json and return - data = json.loads(d.decode('utf8')) - try: - version = data["version"]["name"] - try: - desc = u" ".join(data["description"]["text"].split()) - except TypeError: - desc = u" ".join(data["description"].split()) - max_players = data["players"]["max"] - online = data["players"]["online"] - except Exception as e: - # TODO: except Exception is bad - traceback.print_exc(e) - raise PingError("Unknown Error: {}".format(e)) - - output = { - "motd": format_colors(desc), - "motd_raw": desc, - "version": version, - "players": online, - "players_max": max_players - } - return output - - -def mcping_legacy(host, port): - """ pings a server using the legacy (1.6 and older) protocol and returns data """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - sock.connect((host, port)) - sock.send('\xfe\x01') - response = sock.recv(1) - except socket.gaierror: - raise PingError("Invalid hostname") - except socket.timeout: - raise PingError("Request timed out") - - if response[0] != '\xff': - raise PingError("Invalid response") - - length = struct.unpack('!h', sock.recv(2))[0] - values = sock.recv(length * 2).decode('utf-16be') - data = values.split(u'\x00') # try to decode data using new format - if len(data) == 1: - # failed to decode data, server is using old format - data = values.split(u'\xa7') - output = { - "motd": format_colors(" ".join(data[0].split())), - "motd_raw": data[0], - "version": None, - "players": data[1], - "players_max": data[2] - } - else: - # decoded data, server is using new format - output = { - "motd": format_colors(" ".join(data[3].split())), - "motd_raw": data[3], - "version": data[2], - "players": data[4], - "players_max": data[5] - } - sock.close() - return output - - -## FORMATTING/PARSING FUNCTIONS - -def check_srv(domain): - """ takes a domain and finds minecraft SRV records """ - DNS.DiscoverNameServers() - srv_req = DNS.Request(qtype='srv') - srv_result = srv_req.req('_minecraft._tcp.{}'.format(domain)) - - for getsrv in srv_result.answers: - if getsrv['typename'] == 'SRV': - data = [getsrv['data'][2], getsrv['data'][3]] - return data - - -def parse_input(inp): - """ takes the input from the mcping command and returns the host and port """ - inp = inp.strip().split(" ")[0] - if ":" in inp: - # the port is defined in the input string - host, port = inp.split(":", 1) - try: - port = int(port) - if port > 65535 or port < 0: - raise ParseError("The port '{}' is invalid.".format(port)) - except ValueError: - raise ParseError("The port '{}' is invalid.".format(port)) - return host, port - if has_dns: - # the port is not in the input string, but we have PyDNS so look for a SRV record - srv_data = check_srv(inp) - if srv_data: - return str(srv_data[1]), int(srv_data[0]) - # return default port - return inp, 25565 - - -def format_colors(motd): - for original, replacement in mc_colors: - motd = motd.replace(original, replacement) - motd = motd.replace(u"\xa7k", "") - return motd - - -def format_output(data): - if data["version"]: - return u"{motd}\x0f - {version}\x0f - {players}/{players_max}" \ - u" players.".format(**data).replace("\n", u"\x0f - ") - else: - return u"{motd}\x0f - {players}/{players_max}" \ - u" players.".format(**data).replace("\n", u"\x0f - ") - - -@hook.command -@hook.command("mcp") -def mcping(inp): - """mcping [:port] - Ping a Minecraft server to check status.""" - try: - host, port = parse_input(inp) - except ParseError as e: - return "Could not parse input ({})".format(e) - - try: - data = mcping_modern(host, port) - except PingError: - try: - data = mcping_legacy(host, port) - except PingError as e: - return "Could not ping server, is it offline? ({})".format(e) - - return format_output(data) diff --git a/disabled_stuff/minecraft_status.py b/disabled_stuff/minecraft_status.py deleted file mode 100644 index 4ca67d3..0000000 --- a/disabled_stuff/minecraft_status.py +++ /dev/null @@ -1,44 +0,0 @@ -import json - -from util import hook, http - - -@hook.command(autohelp=False) -def mcstatus(inp): - """mcstatus -- Checks the status of various Mojang (the creators of Minecraft) servers.""" - - try: - request = http.get("http://status.mojang.com/check") - except (http.URLError, http.HTTPError) as e: - return "Unable to get Minecraft server status: {}".format(e) - - # lets just reformat this data to get in a nice format - data = json.loads(request.replace("}", "").replace("{", "").replace("]", "}").replace("[", "{")) - - out = [] - - # use a loop so we don't have to update it if they add more servers - green = [] - yellow = [] - red = [] - for server, status in data.items(): - if status == "green": - green.append(server) - elif status == "yellow": - yellow.append(server) - else: - red.append(server) - - if green: - out = "\x033\x02Online\x02\x0f: " + ", ".join(green) - if yellow: - out += " " - if yellow: - out += "\x02Issues\x02: " + ", ".join(yellow) - if red: - out += " " - if red: - out += "\x034\x02Offline\x02\x0f: " + ", ".join(red) - - return "\x0f" + out.replace(".mojang.com", ".mj") \ - .replace(".minecraft.net", ".mc") diff --git a/disabled_stuff/minecraft_user.py b/disabled_stuff/minecraft_user.py deleted file mode 100644 index 4026994..0000000 --- a/disabled_stuff/minecraft_user.py +++ /dev/null @@ -1,101 +0,0 @@ -import json -from util import hook, http - -NAME_URL = "https://account.minecraft.net/buy/frame/checkName/{}" -PAID_URL = "http://www.minecraft.net/haspaid.jsp" - - -class McuError(Exception): - pass - - -def get_status(name): - """ takes a name and returns status """ - try: - name_encoded = http.quote_plus(name) - response = http.get(NAME_URL.format(name_encoded)) - except (http.URLError, http.HTTPError) as e: - raise McuError("Could not get name status: {}".format(e)) - - if "OK" in response: - return "free" - elif "TAKEN" in response: - return "taken" - elif "invalid characters" in response: - return "invalid" - - -def get_profile(name): - profile = {} - - # form the profile request - request = { - "name": name, - "agent": "minecraft" - } - - # submit the profile request - try: - headers = {"Content-Type": "application/json"} - r = http.get_json( - 'https://api.mojang.com/profiles/page/1', - post_data=json.dumps(request), - headers=headers - ) - except (http.URLError, http.HTTPError) as e: - raise McuError("Could not get profile status: {}".format(e)) - - user = r["profiles"][0] - profile["name"] = user["name"] - profile["id"] = user["id"] - - profile["legacy"] = user.get("legacy", False) - - try: - response = http.get(PAID_URL, user=name) - except (http.URLError, http.HTTPError) as e: - raise McuError("Could not get payment status: {}".format(e)) - - if "true" in response: - profile["paid"] = True - else: - profile["paid"] = False - - return profile - - -@hook.command("haspaid") -@hook.command("mcpaid") -@hook.command -def mcuser(inp): - """mcpaid -- Gets information about the Minecraft user .""" - user = inp.strip() - - try: - # get status of name (does it exist?) - name_status = get_status(user) - except McuError as e: - return e - - if name_status == "taken": - try: - # get information about user - profile = get_profile(user) - except McuError as e: - return "Error: {}".format(e) - - profile["lt"] = ", legacy" if profile["legacy"] else "" - - if profile["paid"]: - return u"The account \x02{name}\x02 ({id}{lt}) exists. It is a \x02paid\x02" \ - u" account.".format(**profile) - else: - return u"The account \x02{name}\x02 ({id}{lt}) exists. It \x034\x02is NOT\x02\x0f a paid" \ - u" account.".format(**profile) - elif name_status == "free": - return u"The account \x02{}\x02 does not exist.".format(user) - elif name_status == "invalid": - return u"The name \x02{}\x02 contains invalid characters.".format(user) - else: - # if you see this, panic - return "Unknown Error." \ No newline at end of file diff --git a/disabled_stuff/minecraft_wiki.py b/disabled_stuff/minecraft_wiki.py deleted file mode 100644 index 072a8ac..0000000 --- a/disabled_stuff/minecraft_wiki.py +++ /dev/null @@ -1,51 +0,0 @@ -import re - -from util import hook, http, text - - -api_url = "http://minecraft.gamepedia.com/api.php?action=opensearch" -mc_url = "http://minecraft.gamepedia.com/" - - -@hook.command -def mcwiki(inp): - """mcwiki -- Gets the first paragraph of - the Minecraft Wiki article on .""" - - try: - j = http.get_json(api_url, search=inp) - except (http.HTTPError, http.URLError) as e: - return "Error fetching search results: {}".format(e) - except ValueError as e: - return "Error reading search results: {}".format(e) - - if not j[1]: - return "No results found." - - # we remove items with a '/' in the name, because - # gamepedia uses sub-pages for different languages - # for some stupid reason - items = [item for item in j[1] if not "/" in item] - - if items: - article_name = items[0].replace(' ', '_').encode('utf8') - else: - # there are no items without /, just return a / one - article_name = j[1][0].replace(' ', '_').encode('utf8') - - url = mc_url + http.quote(article_name, '') - - try: - page = http.get_html(url) - except (http.HTTPError, http.URLError) as e: - return "Error fetching wiki page: {}".format(e) - - for p in page.xpath('//div[@class="mw-content-ltr"]/p'): - if p.text_content(): - summary = " ".join(p.text_content().splitlines()) - summary = re.sub("\[\d+\]", "", summary) - summary = text.truncate_str(summary, 200) - return u"{} :: {}".format(summary, url) - - # this shouldn't happen - return "Unknown Error." diff --git a/disabled_stuff/mlia.py b/disabled_stuff/mlia.py deleted file mode 100644 index feea642..0000000 --- a/disabled_stuff/mlia.py +++ /dev/null @@ -1,34 +0,0 @@ -# Plugin by Infinity - - -import random - -from util import hook, http - - -mlia_cache = [] - - -def refresh_cache(): - """gets a page of random MLIAs and puts them into a dictionary """ - url = 'http://mylifeisaverage.com/{}'.format(random.randint(1, 11000)) - soup = http.get_soup(url) - - for story in soup.find_all('div', {'class': 'story '}): - mlia_id = story.find('span', {'class': 'left'}).a.text - mlia_text = story.find('div', {'class': 'sc'}).text.strip() - mlia_cache.append((mlia_id, mlia_text)) - -# do an initial refresh of the cache -refresh_cache() - - -@hook.command(autohelp=False) -def mlia(inp, reply=None): - """mlia -- Gets a random quote from MyLifeIsAverage.com.""" - # grab the last item in the mlia cache and remove it - mlia_id, text = mlia_cache.pop() - # reply with the mlia we grabbed - reply('({}) {}'.format(mlia_id, text)) - # refresh mlia cache if its getting empty - if len(mlia_cache) < 3: - refresh_cache() diff --git a/disabled_stuff/namegen.py b/disabled_stuff/namegen.py deleted file mode 100644 index 7a1f0e6..0000000 --- a/disabled_stuff/namegen.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -import os - -from util import hook, text, textgen - - -GEN_DIR = "./plugins/data/name_files/" - - -def get_generator(_json): - data = json.loads(_json) - return textgen.TextGenerator(data["templates"], - data["parts"], default_templates=data["default_templates"]) - - -@hook.command(autohelp=False) -def namegen(inp, notice=None): - """namegen [generator] -- Generates some names using the chosen generator. - 'namegen list' will display a list of all generators.""" - - # clean up the input - inp = inp.strip().lower() - - # get a list of available name generators - files = os.listdir(GEN_DIR) - all_modules = [] - for i in files: - if os.path.splitext(i)[1] == ".json": - all_modules.append(os.path.splitext(i)[0]) - all_modules.sort() - - # command to return a list of all available generators - if inp == "list": - message = "Available generators: " - message += text.get_text_list(all_modules, 'and') - notice(message) - return - - if inp: - selected_module = inp.split()[0] - else: - # make some generic fantasy names - selected_module = "fantasy" - - # check if the selected module is valid - if not selected_module in all_modules: - return "Invalid name generator :(" - - # load the name generator - with open(os.path.join(GEN_DIR, "{}.json".format(selected_module))) as f: - try: - generator = get_generator(f.read()) - except ValueError as error: - return "Unable to read name file: {}".format(error) - - # time to generate some names - name_list = generator.generate_strings(10) - - # and finally return the final message :D - return "Some names to ponder: {}.".format(text.get_text_list(name_list, 'and')) diff --git a/disabled_stuff/newegg.py b/disabled_stuff/newegg.py deleted file mode 100644 index 68d604d..0000000 --- a/disabled_stuff/newegg.py +++ /dev/null @@ -1,95 +0,0 @@ -import json -import re - -from util import hook, http, text, web - - -## CONSTANTS - -ITEM_URL = "http://www.newegg.com/Product/Product.aspx?Item={}" - -API_PRODUCT = "http://www.ows.newegg.com/Products.egg/{}/ProductDetails" -API_SEARCH = "http://www.ows.newegg.com/Search.egg/Advanced" - -NEWEGG_RE = (r"(?:(?:www.newegg.com|newegg.com)/Product/Product\.aspx\?Item=)([-_a-zA-Z0-9]+)", re.I) - - -## OTHER FUNCTIONS - -def format_item(item, show_url=True): - """ takes a newegg API item object and returns a description """ - title = text.truncate_str(item["Title"], 50) - - # format the rating nicely if it exists - if not item["ReviewSummary"]["TotalReviews"] == "[]": - rating = "Rated {}/5 ({} ratings)".format(item["ReviewSummary"]["Rating"], - item["ReviewSummary"]["TotalReviews"][1:-1]) - else: - rating = "No Ratings" - - if not item["FinalPrice"] == item["OriginalPrice"]: - price = "{FinalPrice}, was {OriginalPrice}".format(**item) - else: - price = item["FinalPrice"] - - tags = [] - - if item["Instock"]: - tags.append("\x02Stock Available\x02") - else: - tags.append("\x02Out Of Stock\x02") - - if item["FreeShippingFlag"]: - tags.append("\x02Free Shipping\x02") - - if item["IsFeaturedItem"]: - tags.append("\x02Featured\x02") - - if item["IsShellShockerItem"]: - tags.append(u"\x02SHELL SHOCKER\u00AE\x02") - - # join all the tags together in a comma separated string ("tag1, tag2, tag3") - tag_text = u", ".join(tags) - - if show_url: - # create the item URL and shorten it - url = web.try_isgd(ITEM_URL.format(item["NeweggItemNumber"])) - return u"\x02{}\x02 ({}) - {} - {} - {}".format(title, price, rating, - tag_text, url) - else: - return u"\x02{}\x02 ({}) - {} - {}".format(title, price, rating, - tag_text) - - -## HOOK FUNCTIONS - -@hook.regex(*NEWEGG_RE) -def newegg_url(match): - item_id = match.group(1) - item = http.get_json(API_PRODUCT.format(item_id)) - return format_item(item, show_url=False) - - -@hook.command -def newegg(inp): - """newegg -- Searches newegg.com for """ - - # form the search request - request = { - "Keyword": inp, - "Sort": "FEATURED" - } - - # submit the search request - r = http.get_json( - 'http://www.ows.newegg.com/Search.egg/Advanced', - post_data=json.dumps(request) - ) - - # get the first result - if r["ProductListItems"]: - return format_item(r["ProductListItems"][0]) - else: - return "No results found." - - diff --git a/disabled_stuff/newgrounds.py b/disabled_stuff/newgrounds.py deleted file mode 100644 index b26ffe4..0000000 --- a/disabled_stuff/newgrounds.py +++ /dev/null @@ -1,59 +0,0 @@ -import re - -from util import hook, http - - -newgrounds_re = (r'(.*:)//(www.newgrounds.com|newgrounds.com)(:[0-9]+)?(.*)', re.I) -valid = set('0123456789') - - -def test(s): - return set(s) <= valid - - -@hook.regex(*newgrounds_re) -def newgrounds_url(match): - location = match.group(4).split("/")[-1] - if not test(location): - print "Not a valid Newgrounds portal ID. Example: http://www.newgrounds.com/portal/view/593993" - return None - soup = http.get_soup("http://www.newgrounds.com/portal/view/" + location) - - title = "\x02{}\x02".format(soup.find('title').text) - - # get author - try: - author_info = soup.find('ul', {'class': 'authorlinks'}).find('img')['alt'] - author = " - \x02{}\x02".format(author_info) - except: - author = "" - - # get rating - try: - rating_info = soup.find('dd', {'class': 'star-variable'})['title'].split("Stars –")[0].strip() - rating = u" - rated \x02{}\x02/\x025.0\x02".format(rating_info) - except: - rating = "" - - # get amount of ratings - try: - ratings_info = soup.find('dd', {'class': 'star-variable'})['title'].split("Stars –")[1].replace("Votes", - "").strip() - numofratings = " ({})".format(ratings_info) - except: - numofratings = "" - - # get amount of views - try: - views_info = soup.find('dl', {'class': 'contentdata'}).findAll('dd')[1].find('strong').text - views = " - \x02{}\x02 views".format(views_info) - except: - views = "" - - # get upload data - try: - date = "on \x02{}\x02".format(soup.find('dl', {'class': 'sidestats'}).find('dd').text) - except: - date = "" - - return title + rating + numofratings + views + author + date diff --git a/disabled_stuff/osrc.py b/disabled_stuff/osrc.py deleted file mode 100644 index 99cba1c..0000000 --- a/disabled_stuff/osrc.py +++ /dev/null @@ -1,29 +0,0 @@ -from bs4 import BeautifulSoup - -from util import hook, http, web - - -user_url = "http://osrc.dfm.io/{}" - - -@hook.command -def osrc(inp): - """osrc -- Gets an Open Source Report Card for """ - - user_nick = inp.strip() - url = user_url.format(user_nick) - - try: - soup = http.get_soup(url) - except (http.HTTPError, http.URLError): - return "Couldn't find any stats for this user." - - report = soup.find("div", {"id": "description"}).find("p").get_text() - - # Split and join to remove all the excess whitespace, slice the - # string to remove the trailing full stop. - report = " ".join(report.split())[:-1] - - short_url = web.try_isgd(url) - - return "{} - {}".format(report, short_url) diff --git a/disabled_stuff/plpaste.py b/disabled_stuff/plpaste.py deleted file mode 100644 index 238037d..0000000 --- a/disabled_stuff/plpaste.py +++ /dev/null @@ -1,12 +0,0 @@ -from util import hook, web - - -@hook.command(adminonly=True) -def plpaste(inp): - if "/" in inp and inp.split("/")[0] != "util": - return "Invalid input" - try: - with open("plugins/%s.py" % inp) as f: - return web.haste(f.read(), ext='py') - except IOError: - return "Plugin not found (must be in plugins folder)" diff --git a/disabled_stuff/potato.py b/disabled_stuff/potato.py deleted file mode 100644 index 9987e18..0000000 --- a/disabled_stuff/potato.py +++ /dev/null @@ -1,56 +0,0 @@ -# coding=utf-8 -import re -import random - -from util import hook - - -potatoes = ['AC Belmont', 'AC Blue Pride', 'AC Brador', 'AC Chaleur', 'AC Domino', 'AC Dubuc', 'AC Glacier Chip', - 'AC Maple Gold', 'AC Novachip', 'AC Peregrine Red', 'AC Ptarmigan', 'AC Red Island', 'AC Saguenor', - 'AC Stampede Russet', 'AC Sunbury', 'Abeille', 'Abnaki', 'Acadia', 'Acadia Russet', 'Accent', - 'Adirondack Blue', 'Adirondack Red', 'Adora', 'Agria', 'All Blue', 'All Red', 'Alpha', 'Alta Russet', - 'Alturas Russet', 'Amandine', 'Amisk', 'Andover', 'Anoka', 'Anson', 'Aquilon', 'Arran Consul', 'Asterix', - 'Atlantic', 'Austrian Crescent', 'Avalanche', 'Banana', 'Bannock Russet', 'Batoche', 'BeRus', - 'Belle De Fonteney', 'Belleisle', 'Bintje', 'Blossom', 'Blue Christie', 'Blue Mac', 'Brigus', - 'Brise du Nord', 'Butte', 'Butterfinger', 'Caesar', 'CalWhite', 'CalRed', 'Caribe', 'Carlingford', - 'Carlton', 'Carola', 'Cascade', 'Castile', 'Centennial Russet', 'Century Russet', 'Charlotte', 'Cherie', - 'Cherokee', 'Cherry Red', 'Chieftain', 'Chipeta', 'Coastal Russet', 'Colorado Rose', 'Concurrent', - 'Conestoga', 'Cowhorn', 'Crestone Russet', 'Crispin', 'Cupids', 'Daisy Gold', 'Dakota Pearl', 'Defender', - 'Delikat', 'Denali', 'Desiree', 'Divina', 'Dundrod', 'Durango Red', 'Early Rose', 'Elba', 'Envol', - 'Epicure', 'Eramosa', 'Estima', 'Eva', 'Fabula', 'Fambo', 'Fremont Russet', 'French Fingerling', - 'Frontier Russet', 'Fundy', 'Garnet Chile', 'Gem Russet', 'GemStar Russet', 'Gemchip', 'German Butterball', - 'Gigant', 'Goldrush', 'Granola', 'Green Mountain', 'Haida', 'Hertha', 'Hilite Russet', 'Huckleberry', - 'Hunter', 'Huron', 'IdaRose', 'Innovator', 'Irish Cobbler', 'Island Sunshine', 'Ivory Crisp', - 'Jacqueline Lee', 'Jemseg', 'Kanona', 'Katahdin', 'Kennebec', "Kerr's Pink", 'Keswick', 'Keuka Gold', - 'Keystone Russet', 'King Edward VII', 'Kipfel', 'Klamath Russet', 'Krantz', 'LaRatte', 'Lady Rosetta', - 'Latona', 'Lemhi Russet', 'Liberator', 'Lili', 'MaineChip', 'Marfona', 'Maris Bard', 'Maris Piper', - 'Matilda', 'Mazama', 'McIntyre', 'Michigan Purple', 'Millenium Russet', 'Mirton Pearl', 'Modoc', 'Mondial', - 'Monona', 'Morene', 'Morning Gold', 'Mouraska', 'Navan', 'Nicola', 'Nipigon', 'Niska', 'Nooksack', - 'NorValley', 'Norchip', 'Nordonna', 'Norgold Russet', 'Norking Russet', 'Norland', 'Norwis', 'Obelix', - 'Ozette', 'Peanut', 'Penta', 'Peribonka', 'Peruvian Purple', 'Pike', 'Pink Pearl', 'Prospect', 'Pungo', - 'Purple Majesty', 'Purple Viking', 'Ranger Russet', 'Reba', 'Red Cloud', 'Red Gold', 'Red La Soda', - 'Red Pontiac', 'Red Ruby', 'Red Thumb', 'Redsen', 'Rocket', 'Rose Finn Apple', 'Rose Gold', 'Roselys', - 'Rote Erstling', 'Ruby Crescent', 'Russet Burbank', 'Russet Legend', 'Russet Norkotah', 'Russet Nugget', - 'Russian Banana', 'Saginaw Gold', 'Sangre', 'Sant�', 'Satina', 'Saxon', 'Sebago', 'Shepody', 'Sierra', - 'Silverton Russet', 'Simcoe', 'Snowden', 'Spunta', "St. John's", 'Summit Russet', 'Sunrise', 'Superior', - 'Symfonia', 'Tolaas', 'Trent', 'True Blue', 'Ulla', 'Umatilla Russet', 'Valisa', 'Van Gogh', 'Viking', - 'Wallowa Russet', 'Warba', 'Western Russet', 'White Rose', 'Willamette', 'Winema', 'Yellow Finn', - 'Yukon Gold'] - - -@hook.command -def potato(inp, action=None): - """potato - Makes a tasty little potato.""" - inp = inp.strip() - - if not re.match("^[A-Za-z0-9_|.-\]\[]*$", inp.lower()): - return "I cant make a tasty potato for that user!" - - potato_type = random.choice(potatoes) - size = random.choice(['small', 'little', 'mid-sized', 'medium-sized', 'large', 'gigantic']) - flavor = random.choice(['tasty', 'delectable', 'delicious', 'yummy', 'toothsome', 'scrumptious', 'luscious']) - method = random.choice(['bakes', 'fries', 'boils', 'roasts']) - side_dish = random.choice(['side salad', 'dollop of sour cream', 'piece of chicken', 'bowl of shredded bacon']) - - action("{} a {} {} {} potato for {} and serves it with a small {}!".format(method, flavor, size, potato_type, inp, - side_dish)) diff --git a/disabled_stuff/pre.py b/disabled_stuff/pre.py deleted file mode 100644 index f4e61a3..0000000 --- a/disabled_stuff/pre.py +++ /dev/null @@ -1,38 +0,0 @@ -import datetime - -from util import hook, http, timesince - - -@hook.command("scene") -@hook.command -def pre(inp): - """pre -- searches scene releases using orlydb.com""" - - try: - h = http.get_html("http://orlydb.com/", q=inp) - except http.HTTPError as e: - return 'Unable to fetch results: {}'.format(e) - - results = h.xpath("//div[@id='releases']/div/span[@class='release']/..") - - if not results: - return "No results found." - - result = results[0] - - date = result.xpath("span[@class='timestamp']/text()")[0] - section = result.xpath("span[@class='section']//text()")[0] - name = result.xpath("span[@class='release']/text()")[0] - - # parse date/time - date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S") - date_string = date.strftime("%d %b %Y") - since = timesince.timesince(date) - - size = result.xpath("span[@class='inforight']//text()") - if size: - size = ' - ' + size[0].split()[0] - else: - size = '' - - return '{} - {}{} - {} ({} ago)'.format(section, name, size, date_string, since) diff --git a/disabled_stuff/python.py b/disabled_stuff/python.py deleted file mode 100644 index 5cdc524..0000000 --- a/disabled_stuff/python.py +++ /dev/null @@ -1,9 +0,0 @@ -from util import hook -from util.pyexec import eval_py - - -@hook.command -def python(inp): - """python -- Executes as Python code.""" - - return eval_py(inp) diff --git a/disabled_stuff/qrcode.py b/disabled_stuff/qrcode.py deleted file mode 100644 index 9d481e0..0000000 --- a/disabled_stuff/qrcode.py +++ /dev/null @@ -1,18 +0,0 @@ -# Plugin by https://github.com/Mu5tank05 -from util import hook, web, http - - -@hook.command('qr') -@hook.command -def qrcode(inp): - """qrcode [link] returns a link for a QR code.""" - - args = { - "cht": "qr", # chart type (QR) - "chs": "200x200", # dimensions - "chl": inp # data - } - - link = http.prepare_url("http://chart.googleapis.com/chart", args) - - return web.try_isgd(link) diff --git a/disabled_stuff/rdio.py b/disabled_stuff/rdio.py deleted file mode 100644 index 2677090..0000000 --- a/disabled_stuff/rdio.py +++ /dev/null @@ -1,131 +0,0 @@ -import urllib -import json -import re - -import oauth2 as oauth - -from util import hook - - -def getdata(inp, types, api_key, api_secret): - consumer = oauth.Consumer(api_key, api_secret) - client = oauth.Client(consumer) - response = client.request('http://api.rdio.com/1/', 'POST', - urllib.urlencode({'method': 'search', 'query': inp, 'types': types, 'count': '1'})) - data = json.loads(response[1]) - return data - - -@hook.command -def rdio(inp, bot=None): - """ rdio - alternatives: .rdiot (track), .rdioar (artist), .rdioal (album) """ - api_key = bot.config.get("api_keys", {}).get("rdio_key") - api_secret = bot.config.get("api_keys", {}).get("rdio_secret") - if not api_key: - return "error: no api key set" - data = getdata(inp, "Track,Album,Artist", api_key, api_secret) - try: - info = data['result']['results'][0] - except IndexError: - return "No results." - if 'name' in info: - if 'artist' in info and 'album' in info: # Track - name = info['name'] - artist = info['artist'] - album = info['album'] - url = info['shortUrl'] - return u"\x02{}\x02 by \x02{}\x02 - {} {}".format(name, artist, album, url) - elif 'artist' in info and not 'album' in info: # Album - name = info['name'] - artist = info['artist'] - url = info['shortUrl'] - return u"\x02{}\x02 by \x02{}\x02 - {}".format(name, artist, url) - else: # Artist - name = info['name'] - url = info['shortUrl'] - return u"\x02{}\x02 - {}".format(name, url) - - -@hook.command -def rdiot(inp, bot=None): - """ rdiot - Search for tracks on rdio """ - api_key = bot.config.get("api_keys", {}).get("rdio_key") - api_secret = bot.config.get("api_keys", {}).get("rdio_secret") - if not api_key: - return "error: no api key set" - data = getdata(inp, "Track", api_key, api_secret) - try: - info = data['result']['results'][0] - except IndexError: - return "No results." - name = info['name'] - artist = info['artist'] - album = info['album'] - url = info['shortUrl'] - return u"\x02{}\x02 by \x02{}\x02 - {} - {}".format(name, artist, album, url) - - -@hook.command -def rdioar(inp, bot=None): - """ rdioar - Search for artists on rdio """ - api_key = bot.config.get("api_keys", {}).get("rdio_key") - api_secret = bot.config.get("api_keys", {}).get("rdio_secret") - if not api_key: - return "error: no api key set" - data = getdata(inp, "Artist", api_key, api_secret) - try: - info = data['result']['results'][0] - except IndexError: - return "No results." - name = info['name'] - url = info['shortUrl'] - return u"\x02{}\x02 - {}".format(name, url) - - -@hook.command -def rdioal(inp, bot=None): - """ rdioal - Search for albums on rdio """ - api_key = bot.config.get("api_keys", {}).get("rdio_key") - api_secret = bot.config.get("api_keys", {}).get("rdio_secret") - if not api_key: - return "error: no api key set" - data = getdata(inp, "Album", api_key, api_secret) - try: - info = data['result']['results'][0] - except IndexError: - return "No results." - name = info['name'] - artist = info['artist'] - url = info['shortUrl'] - return u"\x02{}\x02 by \x02{}\x02 - {}".format(name, artist, url) - - -rdio_re = (r'(.*:)//(rd.io|www.rdio.com|rdio.com)(:[0-9]+)?(.*)', re.I) - - -@hook.regex(*rdio_re) -def rdio_url(match, bot=None): - api_key = bot.config.get("api_keys", {}).get("rdio_key") - api_secret = bot.config.get("api_keys", {}).get("rdio_secret") - if not api_key: - return None - url = match.group(1) + "//" + match.group(2) + match.group(4) - consumer = oauth.Consumer(api_key, api_secret) - client = oauth.Client(consumer) - response = client.request('http://api.rdio.com/1/', 'POST', - urllib.urlencode({'method': 'getObjectFromUrl', 'url': url})) - data = json.loads(response[1]) - info = data['result'] - if 'name' in info: - if 'artist' in info and 'album' in info: # Track - name = info['name'] - artist = info['artist'] - album = info['album'] - return u"Rdio track: \x02{}\x02 by \x02{}\x02 - {}".format(name, artist, album) - elif 'artist' in info and not 'album' in info: # Album - name = info['name'] - artist = info['artist'] - return u"Rdio album: \x02{}\x02 by \x02{}\x02".format(name, artist) - else: # Artist - name = info['name'] - return u"Rdio artist: \x02{}\x02".format(name) diff --git a/disabled_stuff/recipe.py b/disabled_stuff/recipe.py deleted file mode 100644 index 0e04572..0000000 --- a/disabled_stuff/recipe.py +++ /dev/null @@ -1,106 +0,0 @@ -import random - -from util import hook, http, web - -metadata_url = "http://omnidator.appspot.com/microdata/json/?url={}" - -base_url = "http://www.cookstr.com" -search_url = base_url + "/searches" -random_url = search_url + "/surprise" - -# set this to true to censor this plugin! -censor = True -phrases = [ - u"EAT SOME FUCKING \x02{}\x02", - u"YOU WON'T NOT MAKE SOME FUCKING \x02{}\x02", - u"HOW ABOUT SOME FUCKING \x02{}?\x02", - u"WHY DON'T YOU EAT SOME FUCKING \x02{}?\x02", - u"MAKE SOME FUCKING \x02{}\x02", - u"INDUCE FOOD COMA WITH SOME FUCKING \x02{}\x02" -] - -clean_key = lambda i: i.split("#")[1] - - -class ParseError(Exception): - pass - - -def get_data(url): - """ Uses the omnidator API to parse the metadata from the provided URL """ - try: - omni = http.get_json(metadata_url.format(url)) - except (http.HTTPError, http.URLError) as e: - raise ParseError(e) - schemas = omni["@"] - for d in schemas: - if d["a"] == "": - data = {clean_key(key): value for (key, value) in d.iteritems() - if key.startswith("http://schema.org/Recipe")} - return data - raise ParseError("No recipe data found") - - -@hook.command(autohelp=False) -def recipe(inp): - """recipe [term] - Gets a recipe for [term], or ets a random recipe if [term] is not provided""" - if inp: - # get the recipe URL by searching - try: - search = http.get_soup(search_url, query=inp.strip()) - except (http.HTTPError, http.URLError) as e: - return "Could not get recipe: {}".format(e) - - # find the list of results - result_list = search.find('div', {'class': 'found_results'}) - - if result_list: - results = result_list.find_all('div', {'class': 'recipe_result'}) - else: - return "No results" - - # pick a random front page result - result = random.choice(results) - - # extract the URL from the result - url = base_url + result.find('div', {'class': 'image-wrapper'}).find('a')['href'] - - else: - # get a random recipe URL - try: - page = http.open(random_url) - except (http.HTTPError, http.URLError) as e: - return "Could not get recipe: {}".format(e) - url = page.geturl() - - # use get_data() to get the recipe info from the URL - try: - data = get_data(url) - except ParseError as e: - return "Could not parse recipe: {}".format(e) - - name = data["name"].strip() - return u"Try eating \x02{}!\x02 - {}".format(name, web.try_isgd(url)) - - -@hook.command(autohelp=False) -def dinner(inp): - """dinner - WTF IS FOR DINNER""" - try: - page = http.open(random_url) - except (http.HTTPError, http.URLError) as e: - return "Could not get recipe: {}".format(e) - url = page.geturl() - - try: - data = get_data(url) - except ParseError as e: - return "Could not parse recipe: {}".format(e) - - name = data["name"].strip().upper() - text = random.choice(phrases).format(name) - - if censor: - text = text.replace("FUCK", "F**K") - - return u"{} - {}".format(text, web.try_isgd(url)) diff --git a/disabled_stuff/reddit.py b/disabled_stuff/reddit.py deleted file mode 100644 index 80fcb76..0000000 --- a/disabled_stuff/reddit.py +++ /dev/null @@ -1,79 +0,0 @@ -from datetime import datetime -import re -import random - -from util import hook, http, text, timesince - - -reddit_re = (r'.*(((www\.)?reddit\.com/r|redd\.it)[^ ]+)', re.I) - -base_url = "http://reddit.com/r/{}/.json" -short_url = "http://redd.it/{}" - - -@hook.regex(*reddit_re) -def reddit_url(match): - thread = http.get_html(match.group(0)) - - title = thread.xpath('//title/text()')[0] - upvotes = thread.xpath("//span[@class='upvotes']/span[@class='number']/text()")[0] - downvotes = thread.xpath("//span[@class='downvotes']/span[@class='number']/text()")[0] - author = thread.xpath("//div[@id='siteTable']//a[contains(@class,'author')]/text()")[0] - timeago = thread.xpath("//div[@id='siteTable']//p[@class='tagline']/time/text()")[0] - comments = thread.xpath("//div[@id='siteTable']//a[@class='comments']/text()")[0] - - return u'\x02{}\x02 - posted by \x02{}\x02 {} ago - {} upvotes, {} downvotes - {}'.format( - title, author, timeago, upvotes, downvotes, comments) - - -@hook.command(autohelp=False) -def reddit(inp): - """reddit [n] -- Gets a random post from , or gets the [n]th post in the subreddit.""" - id_num = None - - if inp: - # clean and split the input - parts = inp.lower().strip().split() - - # find the requested post number (if any) - if len(parts) > 1: - url = base_url.format(parts[0].strip()) - try: - id_num = int(parts[1]) - 1 - except ValueError: - return "Invalid post number." - else: - url = base_url.format(parts[0].strip()) - else: - url = "http://reddit.com/.json" - - try: - data = http.get_json(url, user_agent=http.ua_chrome) - except Exception as e: - return "Error: " + str(e) - data = data["data"]["children"] - - # get the requested/random post - if id_num is not None: - try: - item = data[id_num]["data"] - except IndexError: - length = len(data) - return "Invalid post number. Number must be between 1 and {}.".format(length) - else: - item = random.choice(data)["data"] - - item["title"] = text.truncate_str(item["title"], 50) - item["link"] = short_url.format(item["id"]) - - raw_time = datetime.fromtimestamp(int(item["created_utc"])) - item["timesince"] = timesince.timesince(raw_time) - - if item["over_18"]: - item["warning"] = " \x02NSFW\x02" - else: - item["warning"] = "" - - return u"\x02{title} : {subreddit}\x02 - posted by \x02{author}\x02" \ - " {timesince} ago - {ups} upvotes, {downs} downvotes -" \ - " {link}{warning}".format(**item) diff --git a/disabled_stuff/regex_chans.py b/disabled_stuff/regex_chans.py deleted file mode 100644 index c16c250..0000000 --- a/disabled_stuff/regex_chans.py +++ /dev/null @@ -1,128 +0,0 @@ -from util import hook - - -# Default value. -# If True, all channels without a setting will have regex enabled -# If False, all channels without a setting will have regex disabled -default_enabled = True - -db_ready = False - - -def db_init(db): - global db_ready - if not db_ready: - db.execute("CREATE TABLE IF NOT EXISTS regexchans(channel PRIMARY KEY, status)") - db.commit() - db_ready = True - - -def get_status(db, channel): - row = db.execute("SELECT status FROM regexchans WHERE channel = ?", [channel]).fetchone() - if row: - return row[0] - else: - return None - - -def set_status(db, channel, status): - row = db.execute("REPLACE INTO regexchans (channel, status) VALUES(?, ?)", [channel, status]) - db.commit() - - -def delete_status(db, channel): - row = db.execute("DELETE FROM regexchans WHERE channel = ?", [channel]) - db.commit() - - -def list_status(db): - row = db.execute("SELECT * FROM regexchans").fetchall() - result = None - for values in row: - if result: - result += u", {}: {}".format(values[0], values[1]) - else: - result = u"{}: {}".format(values[0], values[1]) - return result - - -@hook.sieve -def sieve_regex(bot, inp, func, kind, args): - db = bot.get_db_connection(inp.conn) - db_init(db) - if kind == 'regex' and inp.chan.startswith("#") and func.__name__ != 'factoid': - chanstatus = get_status(db, inp.chan) - if chanstatus != "ENABLED" and (chanstatus == "DISABLED" or not default_enabled): - print u"Denying input.raw={}, kind={}, args={} from {}".format(inp.raw, kind, args, inp.chan) - return None - print u"Allowing input.raw={}, kind={}, args={} from {}".format(inp.raw, kind, args, inp.chan) - - return inp - - -@hook.command(permissions=["botcontrol"]) -def enableregex(inp, db=None, message=None, notice=None, chan=None, nick=None): - db_init(db) - inp = inp.strip().lower() - if not inp: - channel = chan - elif inp.startswith("#"): - channel = inp - else: - channel = u"#{}".format(inp) - - message(u"Enabling regex matching (youtube, etc) (issued by {})".format(nick), target=channel) - notice(u"Enabling regex matching (youtube, etc) in channel {}".format(channel)) - set_status(db, channel, "ENABLED") - - -@hook.command(permissions=["botcontrol"]) -def disableregex(inp, db=None, message=None, notice=None, chan=None, nick=None): - db_init(db) - inp = inp.strip().lower() - if not inp: - channel = chan - elif inp.startswith("#"): - channel = inp - else: - channel = u"#{}".format(inp) - - message(u"Disabling regex matching (youtube, etc) (issued by {})".format(nick), target=channel) - notice(u"Disabling regex matching (youtube, etc) in channel {}".format(channel)) - set_status(db, channel, "DISABLED") - - -@hook.command(permissions=["botcontrol"]) -def resetregex(inp, db=None, message=None, notice=None, chan=None, nick=None): - db_init(db) - inp = inp.strip().lower() - if not inp: - channel = chan - elif inp.startswith("#"): - channel = inp - else: - channel = u"#{}".format(inp) - - message(u"Resetting regex matching setting (youtube, etc) (issued by {})".format(nick), target=channel) - notice(u"Resetting regex matching setting (youtube, etc) in channel {}".format(channel)) - delete_status(db, channel) - - -@hook.command(permissions=["botcontrol"]) -def regexstatus(inp, db=None, chan=None): - db_init(db) - inp = inp.strip().lower() - if not inp: - channel = chan - elif inp.startswith("#"): - channel = inp - else: - channel = u"#{}".format(inp) - - return u"Regex status for {}: {}".format(channel, get_status(db, channel)) - - -@hook.command(permissions=["botcontrol"]) -def listregex(inp, db=None): - db_init(db) - return list_status(db) diff --git a/disabled_stuff/religion.py b/disabled_stuff/religion.py deleted file mode 100644 index 552b23f..0000000 --- a/disabled_stuff/religion.py +++ /dev/null @@ -1,38 +0,0 @@ -from util import hook, http - - -@hook.command('god') -@hook.command -def bible(inp): - """.bible -- gets from the Bible (ESV)""" - - base_url = ('http://www.esvapi.org/v2/rest/passageQuery?key=IP&' - 'output-format=plain-text&include-heading-horizontal-lines&' - 'include-headings=false&include-passage-horizontal-lines=false&' - 'include-passage-references=false&include-short-copyright=false&' - 'include-footnotes=false&line-length=0&' - 'include-heading-horizontal-lines=false') - - text = http.get(base_url, passage=inp) - - text = ' '.join(text.split()) - - if len(text) > 400: - text = text[:text.rfind(' ', 0, 400)] + '...' - - return text - - -@hook.command('allah') -@hook.command -def koran(inp): # Koran look-up plugin by Ghetto Wizard - """.koran -- gets from the Koran""" - - url = 'http://quod.lib.umich.edu/cgi/k/koran/koran-idx?type=simple' - - results = http.get_html(url, q1=inp).xpath('//li') - - if not results: - return 'No results for ' + inp - - return results[0].text_content() diff --git a/disabled_stuff/slap.py b/disabled_stuff/slap.py deleted file mode 100644 index 37dfbbd..0000000 --- a/disabled_stuff/slap.py +++ /dev/null @@ -1,33 +0,0 @@ -import json - -from util import hook, textgen - - -def get_generator(_json, variables): - data = json.loads(_json) - return textgen.TextGenerator(data["templates"], - data["parts"], variables=variables) - - -@hook.command -def slap(inp, action=None, nick=None, conn=None, notice=None): - """slap -- Makes the bot slap .""" - target = inp.strip() - - if " " in target: - notice("Invalid username!") - return - - # if the user is trying to make the bot slap itself, slap them - if target.lower() == conn.nick.lower() or target.lower() == "itself": - target = nick - - variables = { - "user": target - } - - with open("plugins/data/slaps.json") as f: - generator = get_generator(f.read(), variables) - - # act out the message - action(generator.generate_string()) diff --git a/disabled_stuff/soundcloud.py b/disabled_stuff/soundcloud.py deleted file mode 100644 index d31f103..0000000 --- a/disabled_stuff/soundcloud.py +++ /dev/null @@ -1,50 +0,0 @@ -from urllib import urlencode -import re - -from util import hook, http, web, text - - -sc_re = (r'(.*:)//(www.)?(soundcloud.com)(.*)', re.I) -api_url = "http://api.soundcloud.com" -sndsc_re = (r'(.*:)//(www.)?(snd.sc)(.*)', re.I) - - -def soundcloud(url, api_key): - data = http.get_json(api_url + '/resolve.json?' + urlencode({'url': url, 'client_id': api_key})) - - if data['description']: - desc = u": {} ".format(text.truncate_str(data['description'], 50)) - else: - desc = "" - if data['genre']: - genre = u"- Genre: \x02{}\x02 ".format(data['genre']) - else: - genre = "" - - url = web.try_isgd(data['permalink_url']) - - return u"SoundCloud track: \x02{}\x02 by \x02{}\x02 {}{}- {} plays, {} downloads, {} comments - {}".format( - data['title'], data['user']['username'], desc, genre, data['playback_count'], data['download_count'], - data['comment_count'], url) - - -@hook.regex(*sc_re) -def soundcloud_url(match, bot=None): - api_key = bot.config.get("api_keys", {}).get("soundcloud") - if not api_key: - print "Error: no api key set" - return None - url = match.group(1).split(' ')[-1] + "//" + (match.group(2) if match.group(2) else "") + match.group(3) + \ - match.group(4).split(' ')[0] - return soundcloud(url, api_key) - - -@hook.regex(*sndsc_re) -def sndsc_url(match, bot=None): - api_key = bot.config.get("api_keys", {}).get("soundcloud") - if not api_key: - print "Error: no api key set" - return None - url = match.group(1).split(' ')[-1] + "//" + (match.group(2) if match.group(2) else "") + match.group(3) + \ - match.group(4).split(' ')[0] - return soundcloud(http.open(url).url, api_key) diff --git a/disabled_stuff/spellcheck.py b/disabled_stuff/spellcheck.py deleted file mode 100644 index 1630a0d..0000000 --- a/disabled_stuff/spellcheck.py +++ /dev/null @@ -1,47 +0,0 @@ -from enchant.checker import SpellChecker -import enchant - -from util import hook - - -locale = "en_US" - - -@hook.command -def spell(inp): - """spell -- Check spelling of a word or sentence.""" - - if not enchant.dict_exists(locale): - return "Could not find dictionary: {}".format(locale) - - if len(inp.split(" ")) > 1: - # input is a sentence - checker = SpellChecker(locale) - checker.set_text(inp) - - offset = 0 - for err in checker: - # find the location of the incorrect word - start = err.wordpos + offset - finish = start + len(err.word) - # get some suggestions for it - suggestions = err.suggest() - s_string = '/'.join(suggestions[:3]) - s_string = "\x02{}\x02".format(s_string) - # calculate the offset for the next word - offset = (offset + len(s_string)) - len(err.word) - # replace the word with the suggestions - inp = inp[:start] + s_string + inp[finish:] - return inp - else: - # input is a word - dictionary = enchant.Dict(locale) - is_correct = dictionary.check(inp) - suggestions = dictionary.suggest(inp) - s_string = ', '.join(suggestions[:10]) - if is_correct: - return '"{}" appears to be \x02valid\x02! ' \ - '(suggestions: {})'.format(inp, s_string) - else: - return '"{}" appears to be \x02invalid\x02! ' \ - '(suggestions: {})'.format(inp, s_string) diff --git a/disabled_stuff/spotify.py b/disabled_stuff/spotify.py deleted file mode 100644 index 9897235..0000000 --- a/disabled_stuff/spotify.py +++ /dev/null @@ -1,106 +0,0 @@ -import re -from urllib import urlencode - -from util import hook, http, web - -gateway = 'http://open.spotify.com/{}/{}' # http spotify gw address -spuri = 'spotify:{}:{}' - -spotify_re = (r'(spotify:(track|album|artist|user):([a-zA-Z0-9]+))', re.I) -http_re = (r'(open\.spotify\.com\/(track|album|artist|user)\/' - '([a-zA-Z0-9]+))', re.I) - - -def sptfy(inp, sptfy=False): - if sptfy: - shortenurl = "http://sptfy.com/index.php" - data = urlencode({'longUrl': inp, 'shortUrlDomain': 1, 'submitted': 1, "shortUrlFolder": 6, "customUrl": "", - "shortUrlPassword": "", "shortUrlExpiryDate": "", "shortUrlUses": 0, "shortUrlType": 0}) - try: - soup = http.get_soup(shortenurl, post_data=data, cookies=True) - except: - return inp - try: - link = soup.find('div', {'class': 'resultLink'}).text.strip() - return link - except: - message = "Unable to shorten URL: %s" % \ - soup.find('div', {'class': 'messagebox_text'}).find('p').text.split("
")[0] - return message - else: - return web.try_isgd(inp) - - -@hook.command('sptrack') -@hook.command -def spotify(inp): - """spotify -- Search Spotify for """ - try: - data = http.get_json("http://ws.spotify.com/search/1/track.json", q=inp.strip()) - except Exception as e: - return "Could not get track information: {}".format(e) - - try: - type, id = data["tracks"][0]["href"].split(":")[1:] - except IndexError: - return "Could not find track." - url = sptfy(gateway.format(type, id)) - return u"\x02{}\x02 by \x02{}\x02 - {}".format(data["tracks"][0]["name"], - data["tracks"][0]["artists"][0]["name"], url) - - -@hook.command -def spalbum(inp): - """spalbum -- Search Spotify for """ - try: - data = http.get_json("http://ws.spotify.com/search/1/album.json", q=inp.strip()) - except Exception as e: - return "Could not get album information: {}".format(e) - - try: - type, id = data["albums"][0]["href"].split(":")[1:] - except IndexError: - return "Could not find album." - url = sptfy(gateway.format(type, id)) - return u"\x02{}\x02 by \x02{}\x02 - {}".format(data["albums"][0]["name"], - data["albums"][0]["artists"][0]["name"], url) - - -@hook.command -def spartist(inp): - """spartist -- Search Spotify for """ - try: - data = http.get_json("http://ws.spotify.com/search/1/artist.json", q=inp.strip()) - except Exception as e: - return "Could not get artist information: {}".format(e) - - try: - type, id = data["artists"][0]["href"].split(":")[1:] - except IndexError: - return "Could not find artist." - url = sptfy(gateway.format(type, id)) - return u"\x02{}\x02 - {}".format(data["artists"][0]["name"], url) - - -@hook.regex(*http_re) -@hook.regex(*spotify_re) -def spotify_url(match): - type = match.group(2) - spotify_id = match.group(3) - url = spuri.format(type, spotify_id) - # no error catching here, if the API is down fail silently - data = http.get_json("http://ws.spotify.com/lookup/1/.json", uri=url) - if type == "track": - name = data["track"]["name"] - artist = data["track"]["artists"][0]["name"] - album = data["track"]["album"]["name"] - return u"Spotify Track: \x02{}\x02 by \x02{}\x02 from the album \x02{}\x02 - {}".format(name, artist, - album, sptfy( - gateway.format(type, spotify_id))) - elif type == "artist": - return u"Spotify Artist: \x02{}\x02 - {}".format(data["artist"]["name"], - sptfy(gateway.format(type, spotify_id))) - elif type == "album": - return u"Spotify Album: \x02{}\x02 - \x02{}\x02 - {}".format(data["album"]["artist"], - data["album"]["name"], - sptfy(gateway.format(type, spotify_id))) diff --git a/disabled_stuff/status.py b/disabled_stuff/status.py deleted file mode 100644 index 977ac8e..0000000 --- a/disabled_stuff/status.py +++ /dev/null @@ -1,53 +0,0 @@ -from util import hook -import re -import time -from subprocess import check_output - -def getstatus(): - try: - return check_output("sudo /bin/chch-status", shell=True).strip("\n").decode("utf-8") - except: - return "unbekannt" - -@hook.command("status", autohelp=False) -def cmd_status(inp, reply=None): - """status - Return the door status""" - reply("Chaostreff Status: %s" % (getstatus())) - -@hook.event("TOPIC") -def topic_update(info, conn=None, chan=None): - """topic_update -- Update the topic on TOPIC command""" - status = getstatus() - - topic = info[-1] - - sstr = "Status: %s" % (status) - if sstr in topic: - return - - if 'Status: ' in topic: - new_topic = re.sub("Status: [^ ]*", sstr, topic) - else: - new_topic = "%s | %s" % (topic.rstrip(' |'), sstr) - - if new_topic != topic: - conn.send("TOPIC %s :%s" % (chan, new_topic)) - -@hook.event("332") -def e332_update(info, conn=None, chan=None): - """e332_update -- run after current topic was requested""" - chan = info[1] - topic_update(info, conn=conn, chan=chan) - -@hook.singlethread -@hook.event("353") -def e353_update(info, conn=None, chan=None): - """e353_update -- runs after a channel was joined""" - chan = info[2] - if chan.lower() == "#chaoschemnitz": - conn.send("PRIVMSG Chanserv :op #chaoschemnitz") - - while True: - conn.send("TOPIC %s" % (chan)) - time.sleep(60) - diff --git a/disabled_stuff/steam.py b/disabled_stuff/steam.py deleted file mode 100644 index f3814db..0000000 --- a/disabled_stuff/steam.py +++ /dev/null @@ -1,75 +0,0 @@ -import re - -from bs4 import BeautifulSoup, NavigableString, Tag - -from util import hook, http, web -from util.text import truncate_str - - -steam_re = (r'(.*:)//(store.steampowered.com)(:[0-9]+)?(.*)', re.I) - - -def get_steam_info(url): - page = http.get(url) - soup = BeautifulSoup(page, 'lxml', from_encoding="utf-8") - - data = {} - - data["name"] = soup.find('div', {'class': 'apphub_AppName'}).text - data["desc"] = truncate_str(soup.find('meta', {'name': 'description'})['content'].strip(), 80) - - # get the element details_block - details = soup.find('div', {'class': 'details_block'}) - - # loop over every tag in details_block - for b in details.findAll('b'): - # get the contents of the tag, which is our title - title = b.text.lower().replace(":", "") - if title == "languages": - # we have all we need! - break - - # find the next element directly after the tag - next_element = b.nextSibling - if next_element: - # if the element is some text - if isinstance(next_element, NavigableString): - text = next_element.string.strip() - if text: - # we found valid text, save it and continue the loop - data[title] = text - continue - else: - # the text is blank - sometimes this means there are - # useless spaces or tabs between the and tags. - # so we find the next tag and carry on to the next - # bit of code below - next_element = next_element.find_next('a', href=True) - - # if the element is an tag - if isinstance(next_element, Tag) and next_element.name == 'a': - text = next_element.string.strip() - if text: - # we found valid text (in the tag), - # save it and continue the loop - data[title] = text - continue - - data["price"] = soup.find('div', {'class': 'game_purchase_price price'}).text.strip() - - return u"\x02{name}\x02: {desc}, \x02Genre\x02: {genre}, \x02Release Date\x02: {release date}," \ - u" \x02Price\x02: {price}".format(**data) - - -@hook.regex(*steam_re) -def steam_url(match): - return get_steam_info("http://store.steampowered.com" + match.group(4)) - - -@hook.command -def steam(inp): - """steam [search] - Search for specified game/trailer/DLC""" - page = http.get("http://store.steampowered.com/search/?term=" + inp) - soup = BeautifulSoup(page, 'lxml', from_encoding="utf-8") - result = soup.find('a', {'class': 'search_result_row'}) - return get_steam_info(result['href']) + " - " + web.isgd(result['href']) diff --git a/disabled_stuff/steam_calc.py b/disabled_stuff/steam_calc.py deleted file mode 100644 index 6684eba..0000000 --- a/disabled_stuff/steam_calc.py +++ /dev/null @@ -1,120 +0,0 @@ -import csv -import StringIO - -from util import hook, http, text - - -gauge_url = "http://www.mysteamgauge.com/search?username={}" - -api_url = "http://mysteamgauge.com/user/{}.csv" -steam_api_url = "http://steamcommunity.com/id/{}/?xml=1" - - -def refresh_data(name): - http.get(gauge_url.format(name), timeout=25, get_method='HEAD') - - -def get_data(name): - return http.get(api_url.format(name)) - - -def is_number(s): - try: - float(s) - return True - except ValueError: - return False - - -def unicode_dictreader(utf8_data, **kwargs): - csv_reader = csv.DictReader(utf8_data, **kwargs) - for row in csv_reader: - yield dict([(key.lower(), unicode(value, 'utf-8')) for key, value in row.iteritems()]) - - -@hook.command('sc') -@hook.command -def steamcalc(inp, reply=None): - """steamcalc [currency] - Gets value of steam account and - total hours played. Uses steamcommunity.com/id/. """ - - # check if the user asked us to force reload - force_reload = inp.endswith(" forcereload") - if force_reload: - name = inp[:-12].strip().lower() - else: - name = inp.strip() - - if force_reload: - try: - reply("Collecting data, this may take a while.") - refresh_data(name) - request = get_data(name) - do_refresh = False - except (http.HTTPError, http.URLError): - return "Could not get data for this user." - else: - try: - request = get_data(name) - do_refresh = True - except (http.HTTPError, http.URLError): - try: - reply("Collecting data, this may take a while.") - refresh_data(name) - request = get_data(name) - do_refresh = False - except (http.HTTPError, http.URLError): - return "Could not get data for this user." - - csv_data = StringIO.StringIO(request) # we use StringIO because CSV can't read a string - reader = unicode_dictreader(csv_data) - - # put the games in a list - games = [] - for row in reader: - games.append(row) - - data = {} - - # basic information - steam_profile = http.get_xml(steam_api_url.format(name)) - try: - data["name"] = steam_profile.find('steamID').text - online_state = steam_profile.find('stateMessage').text - except AttributeError: - return "Could not get data for this user." - - online_state = online_state.replace("
", ": ") # will make this pretty later - data["state"] = text.strip_html(online_state) - - # work out the average metascore for all games - ms = [float(game["metascore"]) for game in games if is_number(game["metascore"])] - metascore = float(sum(ms)) / len(ms) if len(ms) > 0 else float('nan') - data["average_metascore"] = "{0:.1f}".format(metascore) - - # work out the totals - data["games"] = len(games) - - total_value = sum([float(game["value"]) for game in games if is_number(game["value"])]) - data["value"] = str(int(round(total_value))) - - # work out the total size - total_size = 0.0 - - for game in games: - if not is_number(game["size"]): - continue - - if game["unit"] == "GB": - total_size += float(game["size"]) - else: - total_size += float(game["size"]) / 1024 - - data["size"] = "{0:.1f}".format(total_size) - - reply("{name} ({state}) has {games} games with a total value of ${value}" - " and a total size of {size}GB! The average metascore for these" - " games is {average_metascore}.".format(**data)) - - if do_refresh: - refresh_data(name) diff --git a/disabled_stuff/stock.py b/disabled_stuff/stock.py deleted file mode 100644 index aedf051..0000000 --- a/disabled_stuff/stock.py +++ /dev/null @@ -1,30 +0,0 @@ -from util import hook, web - - -@hook.command -def stock(inp): - """stock -- gets stock information""" - sym = inp.strip().lower() - - query = "SELECT * FROM yahoo.finance.quote WHERE symbol=@symbol LIMIT 1" - quote = web.query(query, {"symbol": sym}).one() - - # if we don't get a company name back, the symbol doesn't match a company - if quote['Change'] is None: - return "Unknown ticker symbol: {}".format(sym) - - change = float(quote['Change']) - price = float(quote['LastTradePriceOnly']) - - if change < 0: - quote['color'] = "5" - else: - quote['color'] = "3" - - quote['PercentChange'] = 100 * change / (price - change) - print quote - - return u"\x02{Name}\x02 (\x02{symbol}\x02) - {LastTradePriceOnly} " \ - "\x03{color}{Change} ({PercentChange:.2f}%)\x03 " \ - "Day Range: {DaysRange} " \ - "MCAP: {MarketCapitalization}".format(**quote) diff --git a/disabled_stuff/suggest.py b/disabled_stuff/suggest.py deleted file mode 100644 index ec66144..0000000 --- a/disabled_stuff/suggest.py +++ /dev/null @@ -1,19 +0,0 @@ -from util import hook, http, text -from bs4 import BeautifulSoup - - -@hook.command -def suggest(inp): - """suggest -- Gets suggested phrases for a google search""" - suggestions = http.get_json('http://suggestqueries.google.com/complete/search', client='firefox', q=inp)[1] - - if not suggestions: - return 'no suggestions found' - - out = u", ".join(suggestions) - - # defuckify text (might not be needed now, but I'll keep it) - soup = BeautifulSoup(out) - out = soup.get_text() - - return text.truncate_str(out, 200) diff --git a/disabled_stuff/tell.py b/disabled_stuff/tell.py deleted file mode 100644 index 52a0aa1..0000000 --- a/disabled_stuff/tell.py +++ /dev/null @@ -1,121 +0,0 @@ -""" tell.py: written by sklnd in July 2009 - 2010.01.25 - modified by Scaevolus""" - -import time -import re - -from util import hook, timesince - -db_ready = [] - - -def db_init(db, conn): - """Check that our db has the tell table, create it if not.""" - global db_ready - if not conn.name in db_ready: - db.execute("create table if not exists tell" - "(user_to, user_from, message, chan, time," - "primary key(user_to, message))") - db.commit() - db_ready.append(conn.name) - - -def get_tells(db, user_to): - return db.execute("select user_from, message, time, chan from tell where" - " user_to=lower(?) order by time", - (user_to.lower(),)).fetchall() - - -@hook.singlethread -@hook.event('PRIVMSG') -def tellinput(inp, input=None, notice=None, db=None, nick=None, conn=None): - if 'showtells' in input.msg.lower(): - return - - db_init(db, conn) - - tells = get_tells(db, nick) - - if tells: - user_from, message, time, chan = tells[0] - reltime = timesince.timesince(time) - - reply = "{} sent you a message {} ago from {}: {}".format(user_from, reltime, chan, - message) - if len(tells) > 1: - reply += " (+{} more, {}showtells to view)".format(len(tells) - 1, conn.conf["command_prefix"]) - - db.execute("delete from tell where user_to=lower(?) and message=?", - (nick, message)) - db.commit() - notice(reply) - - -@hook.command(autohelp=False) -def showtells(inp, nick='', chan='', notice=None, db=None, conn=None): - """showtells -- View all pending tell messages (sent in a notice).""" - - db_init(db, conn) - - tells = get_tells(db, nick) - - if not tells: - notice("You have no pending tells.") - return - - for tell in tells: - user_from, message, time, chan = tell - past = timesince.timesince(time) - notice("{} sent you a message {} ago from {}: {}".format(user_from, past, chan, message)) - - db.execute("delete from tell where user_to=lower(?)", - (nick,)) - db.commit() - - -@hook.command -def tell(inp, nick='', chan='', db=None, input=None, notice=None, conn=None): - """tell -- Relay to when is around.""" - query = inp.split(' ', 1) - - if len(query) != 2: - notice(tell.__doc__) - return - - user_to = query[0].lower() - message = query[1].strip() - user_from = nick - - if chan.lower() == user_from.lower(): - chan = 'a pm' - - if user_to == user_from.lower(): - notice("Have you looked in a mirror lately?") - return - - if user_to.lower() == input.conn.nick.lower(): - # user is looking for us, being a smart-ass - notice("Thanks for the message, {}!".format(user_from)) - return - - if not re.match("^[A-Za-z0-9_|.\-\]\[]*$", user_to.lower()): - notice("I can't send a message to that user!") - return - - db_init(db, conn) - - if db.execute("select count() from tell where user_to=?", - (user_to,)).fetchone()[0] >= 10: - notice("That person has too many messages queued.") - return - - try: - db.execute("insert into tell(user_to, user_from, message, chan," - "time) values(?,?,?,?,?)", (user_to, user_from, message, - chan, time.time())) - db.commit() - except db.IntegrityError: - notice("Message has already been queued.") - return - - notice("Your message has been sent!") diff --git a/disabled_stuff/time_plugin.py b/disabled_stuff/time_plugin.py deleted file mode 100644 index 885208b..0000000 --- a/disabled_stuff/time_plugin.py +++ /dev/null @@ -1,62 +0,0 @@ -import time - -from util import hook, http -from util.text import capitalize_first - - -api_url = 'http://api.wolframalpha.com/v2/query?format=plaintext' - - -@hook.command("time") -def time_command(inp, bot=None): - """time -- Gets the time in """ - - query = "current time in {}".format(inp) - - api_key = bot.config.get("api_keys", {}).get("wolframalpha", None) - if not api_key: - return "error: no wolfram alpha api key set" - - request = http.get_xml(api_url, input=query, appid=api_key) - current_time = " ".join(request.xpath("//pod[@title='Result']/subpod/plaintext/text()")) - current_time = current_time.replace(" | ", ", ") - - if current_time: - # nice place name for UNIX time - if inp.lower() == "unix": - place = "Unix Epoch" - else: - place = capitalize_first(" ".join(request.xpath("//pod[@" - "title='Input interpretation']/subpod/plaintext/text()"))[ - 16:]) - return "{} - \x02{}\x02".format(current_time, place) - else: - return "Could not get the time for '{}'.".format(inp) - - -@hook.command(autohelp=False) -def beats(inp): - """beats -- Gets the current time in .beats (Swatch Internet Time). """ - - if inp.lower() == "wut": - return "Instead of hours and minutes, the mean solar day is divided " \ - "up into 1000 parts called \".beats\". Each .beat lasts 1 minute and" \ - " 26.4 seconds. Times are notated as a 3-digit number out of 1000 af" \ - "ter midnight. So, @248 would indicate a time 248 .beats after midni" \ - "ght representing 248/1000 of a day, just over 5 hours and 57 minute" \ - "s. There are no timezones." - elif inp.lower() == "guide": - return "1 day = 1000 .beats, 1 hour = 41.666 .beats, 1 min = 0.6944 .beats, 1 second = 0.01157 .beats" - - t = time.gmtime() - h, m, s = t.tm_hour, t.tm_min, t.tm_sec - - utc = 3600 * h + 60 * m + s - bmt = utc + 3600 # Biel Mean Time (BMT) - - beat = bmt / 86.4 - - if beat > 1000: - beat -= 1000 - - return "Swatch Internet Time: @%06.2f" % beat diff --git a/disabled_stuff/twitch.py b/disabled_stuff/twitch.py deleted file mode 100644 index 7e1a56a..0000000 --- a/disabled_stuff/twitch.py +++ /dev/null @@ -1,115 +0,0 @@ -import re -from HTMLParser import HTMLParser - -from util import hook, http - - -twitch_re = (r'(.*:)//(twitch.tv|www.twitch.tv)(:[0-9]+)?(.*)', re.I) -multitwitch_re = (r'(.*:)//(www.multitwitch.tv|multitwitch.tv)/(.*)', re.I) - - -def test(s): - valid = set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/') - return set(s) <= valid - - -def truncate(msg): - nmsg = msg.split(" ") - out = None - x = 0 - for i in nmsg: - if x <= 7: - if out: - out = out + " " + nmsg[x] - else: - out = nmsg[x] - x += 1 - if x <= 7: - return out - else: - return out + "..." - - -@hook.regex(*multitwitch_re) -def multitwitch_url(match): - usernames = match.group(3).split("/") - out = "" - for i in usernames: - if not test(i): - print "Not a valid username" - return None - if out == "": - out = twitch_lookup(i) - else: - out = out + " \x02|\x02 " + twitch_lookup(i) - return out - - -@hook.regex(*twitch_re) -def twitch_url(match): - bit = match.group(4).split("#")[0] - location = "/".join(bit.split("/")[1:]) - if not test(location): - print "Not a valid username" - return None - return twitch_lookup(location) - - -@hook.command('twitchviewers') -@hook.command -def twviewers(inp): - inp = inp.split("/")[-1] - if test(inp): - location = inp - else: - return "Not a valid channel name." - return twitch_lookup(location).split("(")[-1].split(")")[0].replace("Online now! ", "") - - -def twitch_lookup(location): - locsplit = location.split("/") - if len(locsplit) > 1 and len(locsplit) == 3: - channel = locsplit[0] - type = locsplit[1] # should be b or c - id = locsplit[2] - else: - channel = locsplit[0] - type = None - id = None - h = HTMLParser() - fmt = "{}: {} playing {} ({})" # Title: nickname playing Game (x views) - if type and id: - if type == "b": # I haven't found an API to retrieve broadcast info - soup = http.get_soup("http://twitch.tv/" + location) - title = soup.find('span', {'class': 'real_title js-title'}).text - playing = soup.find('a', {'class': 'game js-game'}).text - views = soup.find('span', {'id': 'views-count'}).text + " view" - views = views + "s" if not views[0:2] == "1 " else views - return h.unescape(fmt.format(title, channel, playing, views)) - elif type == "c": - data = http.get_json("https://api.twitch.tv/kraken/videos/" + type + id) - title = data['title'] - playing = data['game'] - views = str(data['views']) + " view" - views = views + "s" if not views[0:2] == "1 " else views - return h.unescape(fmt.format(title, channel, playing, views)) - else: - data = http.get_json("http://api.justin.tv/api/stream/list.json?channel=" + channel) - if data and len(data) >= 1: - data = data[0] - title = data['title'] - playing = data['meta_game'] - viewers = "\x033\x02Online now!\x02\x0f " + str(data["channel_count"]) + " viewer" - print viewers - viewers = viewers + "s" if not " 1 view" in viewers else viewers - print viewers - return h.unescape(fmt.format(title, channel, playing, viewers)) - else: - try: - data = http.get_json("https://api.twitch.tv/kraken/channels/" + channel) - except: - return - title = data['status'] - playing = data['game'] - viewers = "\x034\x02Offline\x02\x0f" - return h.unescape(fmt.format(title, channel, playing, viewers)) diff --git a/disabled_stuff/twitter.py b/disabled_stuff/twitter.py deleted file mode 100644 index c83ea67..0000000 --- a/disabled_stuff/twitter.py +++ /dev/null @@ -1,178 +0,0 @@ -import re -import random -from datetime import datetime - -import tweepy - -from util import hook, timesince - - -TWITTER_RE = (r"(?:(?:www.twitter.com|twitter.com)/(?:[-_a-zA-Z0-9]+)/status/)([0-9]+)", re.I) - - -def get_api(bot): - consumer_key = bot.config.get("api_keys", {}).get("twitter_consumer_key") - consumer_secret = bot.config.get("api_keys", {}).get("twitter_consumer_secret") - - oauth_token = bot.config.get("api_keys", {}).get("twitter_access_token") - oauth_secret = bot.config.get("api_keys", {}).get("twitter_access_secret") - - if not consumer_key: - return False - - auth = tweepy.OAuthHandler(consumer_key, consumer_secret) - auth.set_access_token(oauth_token, oauth_secret) - - return tweepy.API(auth) - - -@hook.regex(*TWITTER_RE) -def twitter_url(match, bot=None): - # Find the tweet ID from the URL - tweet_id = match.group(1) - - # Get the tweet using the tweepy API - api = get_api(bot) - if not api: - return - try: - tweet = api.get_status(tweet_id) - user = tweet.user - except tweepy.error.TweepError: - return - - # Format the return the text of the tweet - text = " ".join(tweet.text.split()) - - if user.verified: - prefix = u"\u2713" - else: - prefix = "" - - time = timesince.timesince(tweet.created_at, datetime.utcnow()) - - return u"{}@\x02{}\x02 ({}): {} ({} ago)".format(prefix, user.screen_name, user.name, text, time) - - -@hook.command("tw") -@hook.command("twatter") -@hook.command -def twitter(inp, bot=None): - """twitter [n] -- Gets last/[n]th tweet from """ - - api = get_api(bot) - if not api: - return "Error: No Twitter API details." - - if re.match(r'^\d+$', inp): - # user is getting a tweet by id - - try: - # get tweet by id - tweet = api.get_status(inp) - except tweepy.error.TweepError as e: - if e[0][0]['code'] == 34: - return "Could not find tweet." - else: - return u"Error {}: {}".format(e[0][0]['code'], e[0][0]['message']) - - user = tweet.user - - elif re.match(r'^\w{1,15}$', inp) or re.match(r'^\w{1,15}\s+\d+$', inp): - # user is getting a tweet by name - - if inp.find(' ') == -1: - username = inp - tweet_number = 0 - else: - username, tweet_number = inp.split() - tweet_number = int(tweet_number) - 1 - - if tweet_number > 300: - return "This command can only find the last \x02300\x02 tweets." - - try: - # try to get user by username - user = api.get_user(username) - except tweepy.error.TweepError as e: - if e[0][0]['code'] == 34: - return "Could not find user." - else: - return u"Error {}: {}".format(e[0][0]['code'], e[0][0]['message']) - - # get the users tweets - user_timeline = api.user_timeline(id=user.id, count=tweet_number + 1) - - # if the timeline is empty, return an error - if not user_timeline: - return u"The user \x02{}\x02 has no tweets.".format(user.screen_name) - - # grab the newest tweet from the users timeline - try: - tweet = user_timeline[tweet_number] - except IndexError: - tweet_count = len(user_timeline) - return u"The user \x02{}\x02 only has \x02{}\x02 tweets.".format(user.screen_name, tweet_count) - - elif re.match(r'^#\w+$', inp): - # user is searching by hashtag - search = api.search(inp) - - if not search: - return "No tweets found." - - tweet = random.choice(search) - user = tweet.user - else: - # ??? - return "Invalid Input" - - # Format the return the text of the tweet - text = " ".join(tweet.text.split()) - - if user.verified: - prefix = u"\u2713" - else: - prefix = "" - - time = timesince.timesince(tweet.created_at, datetime.utcnow()) - - return u"{}@\x02{}\x02 ({}): {} ({} ago)".format(prefix, user.screen_name, user.name, text, time) - - -@hook.command("twinfo") -@hook.command -def twuser(inp, bot=None): - """twuser -- Get info on the Twitter user """ - - api = get_api(bot) - if not api: - return "Error: No Twitter API details." - - try: - # try to get user by username - user = api.get_user(inp) - except tweepy.error.TweepError as e: - if e[0][0]['code'] == 34: - return "Could not find user." - else: - return "Unknown error" - - if user.verified: - prefix = u"\u2713" - else: - prefix = "" - - if user.location: - loc_str = u" is located in \x02{}\x02 and".format(user.location) - else: - loc_str = "" - - if user.description: - desc_str = u" The users description is \"{}\"".format(user.description) - else: - desc_str = "" - - return u"{}@\x02{}\x02 ({}){} has \x02{:,}\x02 tweets and \x02{:,}\x02 followers.{}" \ - "".format(prefix, user.screen_name, user.name, loc_str, user.statuses_count, user.followers_count, - desc_str) diff --git a/disabled_stuff/update.py b/disabled_stuff/update.py deleted file mode 100644 index 67e55f2..0000000 --- a/disabled_stuff/update.py +++ /dev/null @@ -1,43 +0,0 @@ -from git import Repo - - -from util import hook, web - -@hook.command -def update(inp, bot=None): - repo = Repo() - git = repo.git - try: - pull = git.pull() - except Exception as e: - return e - if "\n" in pull: - return web.haste(pull) - else: - return pull - - -@hook.command -def version(inp, bot=None): - repo = Repo() - - # get origin and fetch it - origin = repo.remotes.origin - info = origin.fetch() - - # get objects - head = repo.head - origin_head = info[0] - current_commit = head.commit - remote_commit = origin_head.commit - - if current_commit == remote_commit: - in_sync = True - else: - in_sync = False - - # output - return "Local \x02{}\x02 is at commit \x02{}\x02, remote \x02{}\x02 is at commit \x02{}\x02." \ - " You {} running the latest version.".format(head, current_commit.name_rev[:7], - origin_head, remote_commit.name_rev[:7], - "are" if in_sync else "are not") diff --git a/disabled_stuff/urban.py b/disabled_stuff/urban.py deleted file mode 100644 index 48da433..0000000 --- a/disabled_stuff/urban.py +++ /dev/null @@ -1,66 +0,0 @@ -import re -import random - -from util import hook, http, text - - -base_url = 'http://api.urbandictionary.com/v0' -define_url = base_url + "/define" -random_url = base_url + "/random" - -@hook.command('u', autohelp=False) -@hook.command(autohelp=False) -def urban(inp): - """urban [id] -- Looks up on urbandictionary.com.""" - - if inp: - # clean and split the input - inp = inp.lower().strip() - parts = inp.split() - - # if the last word is a number, set the ID to that number - if parts[-1].isdigit(): - id_num = int(parts[-1]) - # remove the ID from the input string - del parts[-1] - inp = " ".join(parts) - else: - id_num = 1 - - # fetch the definitions - page = http.get_json(define_url, term=inp, referer="http://m.urbandictionary.com") - - if page['result_type'] == 'no_results': - return 'Not found.' - else: - # get a random definition! - page = http.get_json(random_url, referer="http://m.urbandictionary.com") - id_num = None - - definitions = page['list'] - - if id_num: - # try getting the requested definition - try: - definition = definitions[id_num - 1] - - def_text = " ".join(definition['definition'].split()) # remove excess spaces - def_text = text.truncate_str(def_text, 200) - except IndexError: - return 'Not found.' - - url = definition['permalink'] - output = u"[%i/%i] %s :: %s" % \ - (id_num, len(definitions), def_text, url) - - else: - definition = random.choice(definitions) - - def_text = " ".join(definition['definition'].split()) # remove excess spaces - def_text = text.truncate_str(def_text, 200) - - name = definition['word'] - url = definition['permalink'] - output = u"\x02{}\x02: {} :: {}".format(name, def_text, url) - - return output diff --git a/disabled_stuff/utility.py b/disabled_stuff/utility.py deleted file mode 100644 index b0afa5b..0000000 --- a/disabled_stuff/utility.py +++ /dev/null @@ -1,197 +0,0 @@ -import hashlib -import collections -import re - -from util import hook, text - - -# variables - -colors = collections.OrderedDict([ - ('red', '\x0304'), - ('orange', '\x0307'), - ('yellow', '\x0308'), - ('green', '\x0309'), - ('cyan', '\x0303'), - ('ltblue', '\x0310'), - ('rylblue', '\x0312'), - ('blue', '\x0302'), - ('magenta', '\x0306'), - ('pink', '\x0313'), - ('maroon', '\x0305') -]) - -# helper functions - -strip_re = re.compile("(\x03|\x02|\x1f)(?:,?\d{1,2}(?:,\d{1,2})?)?", re.UNICODE) - - -def strip(string): - return strip_re.sub('', string) - - -# basic text tools - - -## TODO: make this capitalize sentences correctly -@hook.command("capitalise") -@hook.command -def capitalize(inp): - """capitalize -- Capitalizes .""" - return inp.capitalize() - - -@hook.command -def upper(inp): - """upper -- Convert string to uppercase.""" - return inp.upper() - - -@hook.command -def lower(inp): - """lower -- Convert string to lowercase.""" - return inp.lower() - - -@hook.command -def titlecase(inp): - """title -- Convert string to title case.""" - return inp.title() - - -@hook.command -def swapcase(inp): - """swapcase -- Swaps the capitalization of .""" - return inp.swapcase() - - -# encoding - - -@hook.command -def rot13(inp): - """rot13 -- Encode with rot13.""" - return inp.encode('rot13') - - -@hook.command -def base64(inp): - """base64 -- Encode with base64.""" - return inp.encode('base64') - - -@hook.command -def unbase64(inp): - """unbase64 -- Decode with base64.""" - return inp.decode('base64') - - -@hook.command -def checkbase64(inp): - try: - decoded = inp.decode('base64') - recoded = decoded.encode('base64').strip() - is_base64 = recoded == inp - except: - return '"{}" is not base64 encoded'.format(inp) - - if is_base64: - return '"{}" is base64 encoded'.format(recoded) - else: - return '"{}" is not base64 encoded'.format(inp) - - -@hook.command -def unescape(inp): - """unescape -- Unescapes .""" - try: - return inp.decode('unicode-escape') - except Exception as e: - return "Error: {}".format(e) - - -@hook.command -def escape(inp): - """escape -- Escapes .""" - try: - return inp.encode('unicode-escape') - except Exception as e: - return "Error: {}".format(e) - - -# length - - -@hook.command -def length(inp): - """length -- gets the length of """ - return "The length of that string is {} characters.".format(len(inp)) - - -# reverse - - -@hook.command -def reverse(inp): - """reverse -- reverses .""" - return inp[::-1] - - -# hashing - - -@hook.command("hash") -def hash_command(inp): - """hash -- Returns hashes of .""" - return ', '.join(x + ": " + getattr(hashlib, x)(inp).hexdigest() - for x in ['md5', 'sha1', 'sha256']) - - -# novelty - - -@hook.command -def munge(inp): - """munge -- Munges up .""" - return text.munge(inp) - - -# colors - based on code by Reece Selwood - - - -@hook.command -def rainbow(inp): - inp = unicode(inp) - inp = strip(inp) - col = colors.items() - out = "" - l = len(colors) - for i, t in enumerate(inp): - if t == " ": - out += t - else: - out += col[i % l][1] + t - return out - - -@hook.command -def wrainbow(inp): - inp = unicode(inp) - col = colors.items() - inp = strip(inp).split(' ') - out = [] - l = len(colors) - for i, t in enumerate(inp): - out.append(col[i % l][1] + t) - return ' '.join(out) - - -@hook.command -def usa(inp): - inp = strip(inp) - c = [colors['red'], '\x0300', colors['blue']] - l = len(c) - out = '' - for i, t in enumerate(inp): - out += c[i % l] + t - return out diff --git a/disabled_stuff/valvesounds.py b/disabled_stuff/valvesounds.py deleted file mode 100644 index 88bc8ce..0000000 --- a/disabled_stuff/valvesounds.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -import urllib2 - -from util import hook, http, web - - -def get_sound_info(game, search): - search = search.replace(" ", "+") - try: - data = http.get_json("http://p2sounds.blha303.com.au/search/%s/%s?format=json" % (game, search)) - except urllib2.HTTPError as e: - return "Error: " + json.loads(e.read())["error"] - items = [] - for item in data["items"]: - if "music" in game: - textsplit = item["text"].split('"') - text = "" - for i in xrange(len(textsplit)): - if i % 2 != 0 and i < 6: - if text: - text += " / " + textsplit[i] - else: - text = textsplit[i] - else: - text = item["text"] - items.append("{} - {} {}".format(item["who"], - text if len(text) < 325 else text[:325] + "...", - item["listen"])) - if len(items) == 1: - return items[0] - else: - return "{} (and {} others: {})".format(items[0], len(items) - 1, web.haste("\n".join(items))) - - -@hook.command -def portal2(inp): - """portal2 - Look up Portal 2 quote. - Example: .portal2 demand to see life's manager""" - return get_sound_info("portal2", inp) - - -@hook.command -def portal2dlc(inp): - """portal2dlc - Look up Portal 2 DLC quote. - Example: .portal2dlc1 these exhibits are interactive""" - return get_sound_info("portal2dlc1", inp) - - -@hook.command("portal2pti") -@hook.command -def portal2dlc2(inp): - """portal2dlc2 - Look up Portal 2 Perpetual Testing Inititive quote. - Example: .portal2 Cave here.""" - return get_sound_info("portal2dlc2", inp) - - -@hook.command -def portal2music(inp): - """portal2music - Look up Portal 2 music. - Example: .portal2music turret opera""" - return get_sound_info("portal2music", inp) - - -@hook.command('portal1') -@hook.command -def portal(inp): - """portal <quote> - Look up Portal quote. - Example: .portal The last thing you want to do is hurt me""" - return get_sound_info("portal1", inp) - - -@hook.command('portal1music') -@hook.command -def portalmusic(inp): - """portalmusic <title> - Look up Portal music. - Example: .portalmusic still alive""" - return get_sound_info("portal1music", inp) - - -@hook.command('tf2sound') -@hook.command -def tf2(inp): - """tf2 [who - ]<quote> - Look up TF2 quote. - Example: .tf2 may i borrow your earpiece""" - return get_sound_info("tf2", inp) - - -@hook.command -def tf2music(inp): - """tf2music title - Look up TF2 music lyrics. - Example: .tf2music rocket jump waltz""" - return get_sound_info("tf2music", inp) diff --git a/disabled_stuff/vimeo.py b/disabled_stuff/vimeo.py deleted file mode 100644 index 0a55549..0000000 --- a/disabled_stuff/vimeo.py +++ /dev/null @@ -1,20 +0,0 @@ -from util import hook, http, timeformat - - -@hook.regex(r'vimeo.com/([0-9]+)') -def vimeo_url(match): - """vimeo <url> -- returns information on the Vimeo video at <url>""" - info = http.get_json('http://vimeo.com/api/v2/video/%s.json' - % match.group(1)) - - if info: - info[0]["duration"] = timeformat.format_time(info[0]["duration"]) - info[0]["stats_number_of_likes"] = format( - info[0]["stats_number_of_likes"], ",d") - info[0]["stats_number_of_plays"] = format( - info[0]["stats_number_of_plays"], ",d") - return ("\x02%(title)s\x02 - length \x02%(duration)s\x02 - " - "\x02%(stats_number_of_likes)s\x02 likes - " - "\x02%(stats_number_of_plays)s\x02 plays - " - "\x02%(user_name)s\x02 on \x02%(upload_date)s\x02" - % info[0]) diff --git a/disabled_stuff/weather.py b/disabled_stuff/weather.py deleted file mode 100644 index 8a56046..0000000 --- a/disabled_stuff/weather.py +++ /dev/null @@ -1,99 +0,0 @@ -from util import hook, http, web - -base_url = "http://api.wunderground.com/api/{}/{}/q/{}.json" - - -@hook.command(autohelp=None) -def weather(inp, reply=None, db=None, nick=None, bot=None, notice=None): - """weather <location> [dontsave] -- Gets weather data - for <location> from Wunderground.""" - - api_key = bot.config.get("api_keys", {}).get("wunderground") - - if not api_key: - return "Error: No wunderground API details." - - # initialise weather DB - db.execute("create table if not exists weather(nick primary key, loc)") - - # if there is no input, try getting the users last location from the DB - if not inp: - location = db.execute("select loc from weather where nick=lower(?)", - [nick]).fetchone() - if not location: - # no location saved in the database, send the user help text - notice(weather.__doc__) - return - loc = location[0] - - # no need to save a location, we already have it - dontsave = True - else: - # see if the input ends with "dontsave" - dontsave = inp.endswith(" dontsave") - - # remove "dontsave" from the input string after checking for it - if dontsave: - loc = inp[:-9].strip().lower() - else: - loc = inp - - location = http.quote_plus(loc) - - request_url = base_url.format(api_key, "geolookup/forecast/conditions", location) - response = http.get_json(request_url) - - if 'location' not in response: - try: - location_id = response['response']['results'][0]['zmw'] - except KeyError: - return "Could not get weather for that location." - - # get the weather again, using the closest match - request_url = base_url.format(api_key, "geolookup/forecast/conditions", "zmw:" + location_id) - response = http.get_json(request_url) - - if response['location']['state']: - place_name = "\x02{}\x02, \x02{}\x02 (\x02{}\x02)".format(response['location']['city'], - response['location']['state'], - response['location']['country']) - else: - place_name = "\x02{}\x02 (\x02{}\x02)".format(response['location']['city'], - response['location']['country']) - - forecast_today = response["forecast"]["simpleforecast"]["forecastday"][0] - forecast_tomorrow = response["forecast"]["simpleforecast"]["forecastday"][1] - - # put all the stuff we want to use in a dictionary for easy formatting of the output - weather_data = { - "place": place_name, - "conditions": response['current_observation']['weather'], - "temp_f": response['current_observation']['temp_f'], - "temp_c": response['current_observation']['temp_c'], - "humidity": response['current_observation']['relative_humidity'], - "wind_kph": response['current_observation']['wind_kph'], - "wind_mph": response['current_observation']['wind_mph'], - "wind_direction": response['current_observation']['wind_dir'], - "today_conditions": forecast_today['conditions'], - "today_high_f": forecast_today['high']['fahrenheit'], - "today_high_c": forecast_today['high']['celsius'], - "today_low_f": forecast_today['low']['fahrenheit'], - "today_low_c": forecast_today['low']['celsius'], - "tomorrow_conditions": forecast_tomorrow['conditions'], - "tomorrow_high_f": forecast_tomorrow['high']['fahrenheit'], - "tomorrow_high_c": forecast_tomorrow['high']['celsius'], - "tomorrow_low_f": forecast_tomorrow['low']['fahrenheit'], - "tomorrow_low_c": forecast_tomorrow['low']['celsius'], - "url": web.isgd(response["current_observation"]['forecast_url'] + "?apiref=e535207ff4757b18") - } - - reply("{place} - \x02Current:\x02 {conditions}, {temp_f}F/{temp_c}C, {humidity}, " - "Wind: {wind_kph}KPH/{wind_mph}MPH {wind_direction}, \x02Today:\x02 {today_conditions}, " - "High: {today_high_f}F/{today_high_c}C, Low: {today_low_f}F/{today_low_c}C. " - "\x02Tomorrow:\x02 {tomorrow_conditions}, High: {tomorrow_high_f}F/{tomorrow_high_c}C, " - "Low: {tomorrow_low_f}F/{tomorrow_low_c}C - {url}".format(**weather_data)) - - if location and not dontsave: - db.execute("insert or replace into weather(nick, loc) values (?,?)", - (nick.lower(), location)) - db.commit() diff --git a/disabled_stuff/xkcd.py b/disabled_stuff/xkcd.py deleted file mode 100644 index d7fad59..0000000 --- a/disabled_stuff/xkcd.py +++ /dev/null @@ -1,43 +0,0 @@ -import re - -from util import hook, http - - -xkcd_re = (r'(.*:)//(www.xkcd.com|xkcd.com)(.*)', re.I) -months = {1: 'January', 2: 'February', 3: 'March', 4: 'April', 5: 'May', 6: 'June', 7: 'July', 8: 'August', - 9: 'September', 10: 'October', 11: 'November', 12: 'December'} - - -def xkcd_info(xkcd_id, url=False): - """ takes an XKCD entry ID and returns a formatted string """ - data = http.get_json("http://www.xkcd.com/" + xkcd_id + "/info.0.json") - date = "%s %s %s" % (data['day'], months[int(data['month'])], data['year']) - if url: - url = " | http://xkcd.com/" + xkcd_id.replace("/", "") - return "xkcd: \x02%s\x02 (%s)%s" % (data['title'], date, url if url else "") - - -def xkcd_search(term): - search_term = http.quote_plus(term) - soup = http.get_soup("http://www.ohnorobot.com/index.pl?s={}&Search=Search&" - "comic=56&e=0&n=0&b=0&m=0&d=0&t=0".format(search_term)) - result = soup.find('li') - if result: - url = result.find('div', {'class': 'tinylink'}).text - xkcd_id = url[:-1].split("/")[-1] - print xkcd_id - return xkcd_info(xkcd_id, url=True) - else: - return "No results found!" - - -@hook.regex(*xkcd_re) -def xkcd_url(match): - xkcd_id = match.group(3).split(" ")[0].split("/")[1] - return xkcd_info(xkcd_id) - - -@hook.command -def xkcd(inp): - """xkcd <search term> - Search for xkcd comic matching <search term>""" - return xkcd_search(inp) diff --git a/disabled_stuff/youtube.py b/disabled_stuff/youtube.py deleted file mode 100644 index e63bca3..0000000 --- a/disabled_stuff/youtube.py +++ /dev/null @@ -1,136 +0,0 @@ -import re -import time - -from util import hook, http, timeformat - - -youtube_re = (r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)' - '([-_a-zA-Z0-9]+)', re.I) - -base_url = 'http://gdata.youtube.com/feeds/api/' -api_url = base_url + 'videos/{}?v=2&alt=jsonc' -search_api_url = base_url + 'videos?v=2&alt=jsonc&max-results=1' -video_url = "http://youtu.be/%s" - - -def plural(num=0, text=''): - return "{:,} {}{}".format(num, text, "s"[num == 1:]) - - -def get_video_description(video_id): - request = http.get_json(api_url.format(video_id)) - - if request.get('error'): - return - - data = request['data'] - - out = u'\x02{}\x02'.format(data['title']) - - if not data.get('duration'): - return out - - length = data['duration'] - out += u' - length \x02{}\x02'.format(timeformat.format_time(length, simple=True)) - - if 'ratingCount' in data: - likes = plural(int(data['likeCount']), "like") - dislikes = plural(data['ratingCount'] - int(data['likeCount']), "dislike") - - percent = 100 * float(data['likeCount']) / float(data['ratingCount']) - out += u' - {}, {} (\x02{:.1f}\x02%)'.format(likes, - dislikes, percent) - - if 'viewCount' in data: - views = data['viewCount'] - out += u' - \x02{:,}\x02 view{}'.format(views, "s"[views == 1:]) - - try: - uploader = http.get_json(base_url + "users/{}?alt=json".format(data["uploader"]))["entry"]["author"][0]["name"][ - "$t"] - except: - uploader = data["uploader"] - - upload_time = time.strptime(data['uploaded'], "%Y-%m-%dT%H:%M:%S.000Z") - out += u' - \x02{}\x02 on \x02{}\x02'.format(uploader, - time.strftime("%Y.%m.%d", upload_time)) - - if 'contentRating' in data: - out += u' - \x034NSFW\x02' - - return out - - -@hook.regex(*youtube_re) -def youtube_url(match): - return get_video_description(match.group(1)) - - -@hook.command('you') -@hook.command('yt') -@hook.command('y') -@hook.command -def youtube(inp): - """youtube <query> -- Returns the first YouTube search result for <query>.""" - request = http.get_json(search_api_url, q=inp) - - if 'error' in request: - return 'error performing search' - - if request['data']['totalItems'] == 0: - return 'no results found' - - video_id = request['data']['items'][0]['id'] - - return get_video_description(video_id) + u" - " + video_url % video_id - - -@hook.command('ytime') -@hook.command -def youtime(inp): - """youtime <query> -- Gets the total run time of the first YouTube search result for <query>.""" - request = http.get_json(search_api_url, q=inp) - - if 'error' in request: - return 'error performing search' - - if request['data']['totalItems'] == 0: - return 'no results found' - - video_id = request['data']['items'][0]['id'] - request = http.get_json(api_url.format(video_id)) - - if request.get('error'): - return - data = request['data'] - - if not data.get('duration'): - return - - length = data['duration'] - views = data['viewCount'] - total = int(length * views) - - length_text = timeformat.format_time(length, simple=True) - total_text = timeformat.format_time(total, accuracy=8) - - return u'The video \x02{}\x02 has a length of {} and has been viewed {:,} times for ' \ - u'a total run time of {}!'.format(data['title'], length_text, views, - total_text) - - -ytpl_re = (r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)', re.I) - - -@hook.regex(*ytpl_re) -def ytplaylist_url(match): - location = match.group(4).split("=")[-1] - try: - soup = http.get_soup("https://www.youtube.com/playlist?list=" + location) - except Exception: - return "\x034\x02Invalid response." - title = soup.find('title').text.split('-')[0].strip() - author = soup.find('img', {'class': 'channel-header-profile-image'})['title'] - num_videos = soup.find('ul', {'class': 'header-stats'}).findAll('li')[0].text.split(' ')[0] - views = soup.find('ul', {'class': 'header-stats'}).findAll('li')[1].text.split(' ')[0] - return u"\x02%s\x02 - \x02%s\x02 views - \x02%s\x02 videos - \x02%s\x02" % (title, views, num_videos, author) diff --git a/lib/bs4/AUTHORS.txt b/lib/bs4/AUTHORS.txt deleted file mode 100644 index 2ac8fcc..0000000 --- a/lib/bs4/AUTHORS.txt +++ /dev/null @@ -1,43 +0,0 @@ -Behold, mortal, the origins of Beautiful Soup... -================================================ - -Leonard Richardson is the primary programmer. - -Aaron DeVore is awesome. - -Mark Pilgrim provided the encoding detection code that forms the base -of UnicodeDammit. - -Thomas Kluyver and Ezio Melotti finished the work of getting Beautiful -Soup 4 working under Python 3. - -Simon Willison wrote soupselect, which was used to make Beautiful Soup -support CSS selectors. - -Sam Ruby helped with a lot of edge cases. - -Jonathan Ellis was awarded the prestigous Beau Potage D'Or for his -work in solving the nestable tags conundrum. - -An incomplete list of people have contributed patches to Beautiful -Soup: - - Istvan Albert, Andrew Lin, Anthony Baxter, Andrew Boyko, Tony Chang, - Zephyr Fang, Fuzzy, Roman Gaufman, Yoni Gilad, Richie Hindle, Peteris - Krumins, Kent Johnson, Ben Last, Robert Leftwich, Staffan Malmgren, - Ksenia Marasanova, JP Moins, Adam Monsen, John Nagle, "Jon", Ed - Oskiewicz, Greg Phillips, Giles Radford, Arthur Rudolph, Marko - Samastur, Jouni Seppänen, Alexander Schmolck, Andy Theyers, Glyn - Webster, Paul Wright, Danny Yoo - -An incomplete list of people who made suggestions or found bugs or -found ways to break Beautiful Soup: - - Hanno Böck, Matteo Bertini, Chris Curvey, Simon Cusack, Bruce Eckel, - Matt Ernst, Michael Foord, Tom Harris, Bill de hOra, Donald Howes, - Matt Patterson, Scott Roberts, Steve Strassmann, Mike Williams, - warchild at redho dot com, Sami Kuisma, Carlos Rocha, Bob Hutchison, - Joren Mc, Michal Migurski, John Kleven, Tim Heaney, Tripp Lilley, Ed - Summers, Dennis Sutch, Chris Smith, Aaron Sweep^W Swartz, Stuart - Turner, Greg Edwards, Kevin J Kalupson, Nikos Kouremenos, Artur de - Sousa Rocha, Yichun Wei, Per Vognsen diff --git a/lib/bs4/__init__.py b/lib/bs4/__init__.py index 03b2416..80f6f68 100644 --- a/lib/bs4/__init__.py +++ b/lib/bs4/__init__.py @@ -17,8 +17,8 @@ http://www.crummy.com/software/BeautifulSoup/bs4/doc/ """ __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.2.1" -__copyright__ = "Copyright (c) 2004-2013 Leonard Richardson" +__version__ = "4.1.3" +__copyright__ = "Copyright (c) 2004-2012 Leonard Richardson" __license__ = "MIT" __all__ = ['BeautifulSoup'] @@ -201,9 +201,9 @@ class BeautifulSoup(Tag): """Create a new tag associated with this soup.""" return Tag(None, self.builder, name, namespace, nsprefix, attrs) - def new_string(self, s, subclass=NavigableString): + def new_string(self, s): """Create a new NavigableString associated with this soup.""" - navigable = subclass(s) + navigable = NavigableString(s) navigable.setup() return navigable @@ -245,15 +245,13 @@ class BeautifulSoup(Tag): o = containerClass(currentData) self.object_was_parsed(o) - def object_was_parsed(self, o, parent=None, most_recent_element=None): + def object_was_parsed(self, o): """Add an object to the parse tree.""" - parent = parent or self.currentTag - most_recent_element = most_recent_element or self._most_recent_element - o.setup(parent, most_recent_element) - if most_recent_element is not None: - most_recent_element.next_element = o - self._most_recent_element = o - parent.contents.append(o) + o.setup(self.currentTag, self.previous_element) + if self.previous_element: + self.previous_element.next_element = o + self.previous_element = o + self.currentTag.contents.append(o) def _popToTag(self, name, nsprefix=None, inclusivePop=True): """Pops the tag stack up to and including the most recent @@ -297,12 +295,12 @@ class BeautifulSoup(Tag): return None tag = Tag(self, self.builder, name, namespace, nsprefix, attrs, - self.currentTag, self._most_recent_element) + self.currentTag, self.previous_element) if tag is None: return tag - if self._most_recent_element: - self._most_recent_element.next_element = tag - self._most_recent_element = tag + if self.previous_element: + self.previous_element.next_element = tag + self.previous_element = tag self.pushTag(tag) return tag @@ -335,10 +333,6 @@ class BeautifulSoup(Tag): return prefix + super(BeautifulSoup, self).decode( indent_level, eventual_encoding, formatter) -# Alias to make it easier to type import: 'from bs4 import _soup' -_s = BeautifulSoup -_soup = BeautifulSoup - class BeautifulStoneSoup(BeautifulSoup): """Deprecated interface to an XML parser.""" diff --git a/lib/bs4/builder/__init__.py b/lib/bs4/builder/__init__.py index bae453e..dc7deb9 100644 --- a/lib/bs4/builder/__init__.py +++ b/lib/bs4/builder/__init__.py @@ -152,7 +152,7 @@ class TreeBuilder(object): tag_specific = self.cdata_list_attributes.get( tag_name.lower(), []) for cdata_list_attr in itertools.chain(universal, tag_specific): - if cdata_list_attr in attrs: + if cdata_list_attr in dict(attrs): # Basically, we have a "class" attribute whose # value is a whitespace-separated list of CSS # classes. Split it into a list. diff --git a/lib/bs4/builder/_html5lib.py b/lib/bs4/builder/_html5lib.py index e439ac8..6001e38 100644 --- a/lib/bs4/builder/_html5lib.py +++ b/lib/bs4/builder/_html5lib.py @@ -131,9 +131,9 @@ class Element(html5lib.treebuilders._base.Node): old_element = self.element.contents[-1] new_element = self.soup.new_string(old_element + node.element) old_element.replace_with(new_element) - self.soup._most_recent_element = new_element else: - self.soup.object_was_parsed(node.element, parent=self.element) + self.element.append(node.element) + node.parent = self def getAttributes(self): return AttrList(self.element) diff --git a/lib/bs4/builder/_htmlparser.py b/lib/bs4/builder/_htmlparser.py index 65ee618..ede5cec 100644 --- a/lib/bs4/builder/_htmlparser.py +++ b/lib/bs4/builder/_htmlparser.py @@ -58,8 +58,6 @@ class BeautifulSoupHTMLParser(HTMLParser): # it's fixed. if name.startswith('x'): real_name = int(name.lstrip('x'), 16) - elif name.startswith('X'): - real_name = int(name.lstrip('X'), 16) else: real_name = int(name) @@ -87,9 +85,6 @@ class BeautifulSoupHTMLParser(HTMLParser): self.soup.endData() if data.startswith("DOCTYPE "): data = data[len("DOCTYPE "):] - elif data == 'DOCTYPE': - # i.e. "<!DOCTYPE>" - data = '' self.soup.handle_data(data) self.soup.endData(Doctype) diff --git a/lib/bs4/builder/_lxml.py b/lib/bs4/builder/_lxml.py index be35d70..f6b91ff 100644 --- a/lib/bs4/builder/_lxml.py +++ b/lib/bs4/builder/_lxml.py @@ -3,7 +3,6 @@ __all__ = [ 'LXMLTreeBuilder', ] -from io import BytesIO from StringIO import StringIO import collections from lxml import etree @@ -29,10 +28,6 @@ class LXMLTreeBuilderForXML(TreeBuilder): CHUNK_SIZE = 512 - # This namespace mapping is specified in the XML Namespace - # standard. - DEFAULT_NSMAPS = {'http://www.w3.org/XML/1998/namespace' : "xml"} - @property def default_parser(self): # This can either return a parser object or a class, which @@ -50,7 +45,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): parser = parser(target=self, strip_cdata=False) self.parser = parser self.soup = None - self.nsmaps = [self.DEFAULT_NSMAPS] + self.nsmaps = None def _getNsTag(self, tag): # Split the namespace URL out of a fully-qualified lxml tag @@ -76,9 +71,7 @@ class LXMLTreeBuilderForXML(TreeBuilder): dammit.contains_replacement_characters) def feed(self, markup): - if isinstance(markup, bytes): - markup = BytesIO(markup) - elif isinstance(markup, unicode): + if isinstance(markup, basestring): markup = StringIO(markup) # Call feed() at least once, even if the markup is empty, # or the parser won't be initialized. @@ -92,20 +85,23 @@ class LXMLTreeBuilderForXML(TreeBuilder): self.parser.close() def close(self): - self.nsmaps = [self.DEFAULT_NSMAPS] + self.nsmaps = None def start(self, name, attrs, nsmap={}): # Make sure attrs is a mutable dict--lxml may send an immutable dictproxy. attrs = dict(attrs) + nsprefix = None # Invert each namespace map as it comes in. - if len(self.nsmaps) > 1: - # There are no new namespaces for this tag, but - # non-default namespaces are in play, so we need a - # separate tag stack to know when they end. + if len(nsmap) == 0 and self.nsmaps != None: + # There are no new namespaces for this tag, but namespaces + # are in play, so we need a separate tag stack to know + # when they end. self.nsmaps.append(None) elif len(nsmap) > 0: # A new namespace mapping has come into play. + if self.nsmaps is None: + self.nsmaps = [] inverted_nsmap = dict((value, key) for key, value in nsmap.items()) self.nsmaps.append(inverted_nsmap) # Also treat the namespace mapping as a set of attributes on the @@ -116,19 +112,20 @@ class LXMLTreeBuilderForXML(TreeBuilder): "xmlns", prefix, "http://www.w3.org/2000/xmlns/") attrs[attribute] = namespace - # Namespaces are in play. Find any attributes that came in - # from lxml with namespaces attached to their names, and - # turn then into NamespacedAttribute objects. - new_attrs = {} - for attr, value in attrs.items(): - namespace, attr = self._getNsTag(attr) - if namespace is None: - new_attrs[attr] = value - else: - nsprefix = self._prefix_for_namespace(namespace) - attr = NamespacedAttribute(nsprefix, attr, namespace) - new_attrs[attr] = value - attrs = new_attrs + if self.nsmaps is not None and len(self.nsmaps) > 0: + # Namespaces are in play. Find any attributes that came in + # from lxml with namespaces attached to their names, and + # turn then into NamespacedAttribute objects. + new_attrs = {} + for attr, value in attrs.items(): + namespace, attr = self._getNsTag(attr) + if namespace is None: + new_attrs[attr] = value + else: + nsprefix = self._prefix_for_namespace(namespace) + attr = NamespacedAttribute(nsprefix, attr, namespace) + new_attrs[attr] = value + attrs = new_attrs namespace, name = self._getNsTag(name) nsprefix = self._prefix_for_namespace(namespace) @@ -141,7 +138,6 @@ class LXMLTreeBuilderForXML(TreeBuilder): for inverted_nsmap in reversed(self.nsmaps): if inverted_nsmap is not None and namespace in inverted_nsmap: return inverted_nsmap[namespace] - return None def end(self, name): self.soup.endData() @@ -154,10 +150,14 @@ class LXMLTreeBuilderForXML(TreeBuilder): nsprefix = inverted_nsmap[namespace] break self.soup.handle_endtag(name, nsprefix) - if len(self.nsmaps) > 1: + if self.nsmaps != None: # This tag, or one of its parents, introduced a namespace # mapping, so pop it off the stack. self.nsmaps.pop() + if len(self.nsmaps) == 0: + # Namespaces are no longer in play, so don't bother keeping + # track of the namespace stack. + self.nsmaps = None def pi(self, target, data): pass diff --git a/lib/bs4/dammit.py b/lib/bs4/dammit.py index a733cad..983ade0 100644 --- a/lib/bs4/dammit.py +++ b/lib/bs4/dammit.py @@ -81,8 +81,6 @@ class EntitySubstitution(object): "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" ")") - AMPERSAND_OR_BRACKET = re.compile("([<>&])") - @classmethod def _substitute_html_entity(cls, matchobj): entity = cls.CHARACTER_TO_HTML_ENTITY.get(matchobj.group(0)) @@ -136,28 +134,6 @@ class EntitySubstitution(object): def substitute_xml(cls, value, make_quoted_attribute=False): """Substitute XML entities for special XML characters. - :param value: A string to be substituted. The less-than sign - will become <, the greater-than sign will become >, - and any ampersands will become &. If you want ampersands - that appear to be part of an entity definition to be left - alone, use substitute_xml_containing_entities() instead. - - :param make_quoted_attribute: If True, then the string will be - quoted, as befits an attribute value. - """ - # Escape angle brackets and ampersands. - value = cls.AMPERSAND_OR_BRACKET.sub( - cls._substitute_xml_entity, value) - - if make_quoted_attribute: - value = cls.quoted_attribute_value(value) - return value - - @classmethod - def substitute_xml_containing_entities( - cls, value, make_quoted_attribute=False): - """Substitute XML entities for special XML characters. - :param value: A string to be substituted. The less-than sign will become <, the greater-than sign will become >, and any ampersands that are not part of an entity defition will @@ -175,7 +151,6 @@ class EntitySubstitution(object): value = cls.quoted_attribute_value(value) return value - @classmethod def substitute_html(cls, s): """Replace certain Unicode characters with named HTML entities. @@ -298,6 +273,7 @@ class UnicodeDammit: return None self.tried_encodings.append((proposed, errors)) markup = self.markup + # Convert smart quotes to HTML if coming from an encoding # that might have them. if (self.smart_quotes_to is not None diff --git a/lib/bs4/diagnose.py b/lib/bs4/diagnose.py deleted file mode 100644 index 25fda5c..0000000 --- a/lib/bs4/diagnose.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Diagnostic functions, mainly for use when doing tech support.""" -from StringIO import StringIO -from HTMLParser import HTMLParser -from bs4 import BeautifulSoup, __version__ -from bs4.builder import builder_registry -import os -import random -import time -import traceback -import sys -import cProfile - -def diagnose(data): - """Diagnostic suite for isolating common problems.""" - print "Diagnostic running on Beautiful Soup %s" % __version__ - print "Python version %s" % sys.version - - basic_parsers = ["html.parser", "html5lib", "lxml"] - for name in basic_parsers: - for builder in builder_registry.builders: - if name in builder.features: - break - else: - basic_parsers.remove(name) - print ( - "I noticed that %s is not installed. Installing it may help." % - name) - - if 'lxml' in basic_parsers: - basic_parsers.append(["lxml", "xml"]) - from lxml import etree - print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION)) - - if 'html5lib' in basic_parsers: - import html5lib - print "Found html5lib version %s" % html5lib.__version__ - - if hasattr(data, 'read'): - data = data.read() - elif os.path.exists(data): - print '"%s" looks like a filename. Reading data from the file.' % data - data = open(data).read() - elif data.startswith("http:") or data.startswith("https:"): - print '"%s" looks like a URL. Beautiful Soup is not an HTTP client.' % data - print "You need to use some other library to get the document behind the URL, and feed that document to Beautiful Soup." - return - print - - for parser in basic_parsers: - print "Trying to parse your markup with %s" % parser - success = False - try: - soup = BeautifulSoup(data, parser) - success = True - except Exception, e: - print "%s could not parse the markup." % parser - traceback.print_exc() - if success: - print "Here's what %s did with the markup:" % parser - print soup.prettify() - - print "-" * 80 - -def lxml_trace(data, html=True): - """Print out the lxml events that occur during parsing. - - This lets you see how lxml parses a document when no Beautiful - Soup code is running. - """ - from lxml import etree - for event, element in etree.iterparse(StringIO(data), html=html): - print("%s, %4s, %s" % (event, element.tag, element.text)) - -class AnnouncingParser(HTMLParser): - """Announces HTMLParser parse events, without doing anything else.""" - - def _p(self, s): - print(s) - - def handle_starttag(self, name, attrs): - self._p("%s START" % name) - - def handle_endtag(self, name): - self._p("%s END" % name) - - def handle_data(self, data): - self._p("%s DATA" % data) - - def handle_charref(self, name): - self._p("%s CHARREF" % name) - - def handle_entityref(self, name): - self._p("%s ENTITYREF" % name) - - def handle_comment(self, data): - self._p("%s COMMENT" % data) - - def handle_decl(self, data): - self._p("%s DECL" % data) - - def unknown_decl(self, data): - self._p("%s UNKNOWN-DECL" % data) - - def handle_pi(self, data): - self._p("%s PI" % data) - -def htmlparser_trace(data): - """Print out the HTMLParser events that occur during parsing. - - This lets you see how HTMLParser parses a document when no - Beautiful Soup code is running. - """ - parser = AnnouncingParser() - parser.feed(data) - -_vowels = "aeiou" -_consonants = "bcdfghjklmnpqrstvwxyz" - -def rword(length=5): - "Generate a random word-like string." - s = '' - for i in range(length): - if i % 2 == 0: - t = _consonants - else: - t = _vowels - s += random.choice(t) - return s - -def rsentence(length=4): - "Generate a random sentence-like string." - return " ".join(rword(random.randint(4,9)) for i in range(length)) - -def rdoc(num_elements=1000): - """Randomly generate an invalid HTML document.""" - tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table'] - elements = [] - for i in range(num_elements): - choice = random.randint(0,3) - if choice == 0: - # New tag. - tag_name = random.choice(tag_names) - elements.append("<%s>" % tag_name) - elif choice == 1: - elements.append(rsentence(random.randint(1,4))) - elif choice == 2: - # Close a tag. - tag_name = random.choice(tag_names) - elements.append("</%s>" % tag_name) - return "<html>" + "\n".join(elements) + "</html>" - -def benchmark_parsers(num_elements=100000): - """Very basic head-to-head performance benchmark.""" - print "Comparative parser benchmark on Beautiful Soup %s" % __version__ - data = rdoc(num_elements) - print "Generated a large invalid HTML document (%d bytes)." % len(data) - - for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]: - success = False - try: - a = time.time() - soup = BeautifulSoup(data, parser) - b = time.time() - success = True - except Exception, e: - print "%s could not parse the markup." % parser - traceback.print_exc() - if success: - print "BS4+%s parsed the markup in %.2fs." % (parser, b-a) - - from lxml import etree - a = time.time() - etree.HTML(data) - b = time.time() - print "Raw lxml parsed the markup in %.2fs." % (b-a) - -if __name__ == '__main__': - diagnose(sys.stdin.read()) diff --git a/lib/bs4/element.py b/lib/bs4/element.py index f6864f2..26422fd 100644 --- a/lib/bs4/element.py +++ b/lib/bs4/element.py @@ -26,9 +26,6 @@ class NamespacedAttribute(unicode): def __new__(cls, prefix, name, namespace=None): if name is None: obj = unicode.__new__(cls, prefix) - elif prefix is None: - # Not really namespaced. - obj = unicode.__new__(cls, name) else: obj = unicode.__new__(cls, prefix + ":" + name) obj.prefix = prefix @@ -81,40 +78,6 @@ class ContentMetaAttributeValue(AttributeValueWithCharsetSubstitution): return match.group(1) + encoding return self.CHARSET_RE.sub(rewrite, self.original_value) -class HTMLAwareEntitySubstitution(EntitySubstitution): - - """Entity substitution rules that are aware of some HTML quirks. - - Specifically, the contents of <script> and <style> tags should not - undergo entity substitution. - - Incoming NavigableString objects are checked to see if they're the - direct children of a <script> or <style> tag. - """ - - cdata_containing_tags = set(["script", "style"]) - - preformatted_tags = set(["pre"]) - - @classmethod - def _substitute_if_appropriate(cls, ns, f): - if (isinstance(ns, NavigableString) - and ns.parent is not None - and ns.parent.name in cls.cdata_containing_tags): - # Do nothing. - return ns - # Substitute. - return f(ns) - - @classmethod - def substitute_html(cls, ns): - return cls._substitute_if_appropriate( - ns, EntitySubstitution.substitute_html) - - @classmethod - def substitute_xml(cls, ns): - return cls._substitute_if_appropriate( - ns, EntitySubstitution.substitute_xml) class PageElement(object): """Contains the navigational information for some part of the page @@ -131,60 +94,25 @@ class PageElement(object): # converted to entities. This is not recommended, but it's # faster than "minimal". # A function - This function will be called on every string that - # needs to undergo entity substitution. - # - - # In an HTML document, the default "html" and "minimal" functions - # will leave the contents of <script> and <style> tags alone. For - # an XML document, all tags will be given the same treatment. - - HTML_FORMATTERS = { - "html" : HTMLAwareEntitySubstitution.substitute_html, - "minimal" : HTMLAwareEntitySubstitution.substitute_xml, - None : None - } - - XML_FORMATTERS = { + # needs to undergo entity substition + FORMATTERS = { "html" : EntitySubstitution.substitute_html, "minimal" : EntitySubstitution.substitute_xml, None : None } + @classmethod def format_string(self, s, formatter='minimal'): """Format the given string using the given formatter.""" if not callable(formatter): - formatter = self._formatter_for_name(formatter) + formatter = self.FORMATTERS.get( + formatter, EntitySubstitution.substitute_xml) if formatter is None: output = s else: output = formatter(s) return output - @property - def _is_xml(self): - """Is this element part of an XML tree or an HTML tree? - - This is used when mapping a formatter name ("minimal") to an - appropriate function (one that performs entity-substitution on - the contents of <script> and <style> tags, or not). It's - inefficient, but it should be called very rarely. - """ - if self.parent is None: - # This is the top-level object. It should have .is_xml set - # from tree creation. If not, take a guess--BS is usually - # used on HTML markup. - return getattr(self, 'is_xml', False) - return self.parent._is_xml - - def _formatter_for_name(self, name): - "Look up a formatter function based on its name and the tree." - if self._is_xml: - return self.XML_FORMATTERS.get( - name, EntitySubstitution.substitute_xml) - else: - return self.HTML_FORMATTERS.get( - name, HTMLAwareEntitySubstitution.substitute_xml) - def setup(self, parent=None, previous_element=None): """Sets up the initial relations between this element and other elements.""" @@ -438,7 +366,7 @@ class PageElement(object): # NOTE: We can't use _find_one because findParents takes a different # set of arguments. r = None - l = self.find_parents(name, attrs, 1, **kwargs) + l = self.find_parents(name, attrs, 1) if l: r = l[0] return r @@ -567,14 +495,6 @@ class PageElement(object): value =" ".join(value) return value - def _tag_name_matches_and(self, function, tag_name): - if not tag_name: - return function - else: - def _match(tag): - return tag.name == tag_name and function(tag) - return _match - def _attribute_checker(self, operator, attribute, value=''): """Create a function that performs a CSS selector operation. @@ -616,6 +536,87 @@ class PageElement(object): else: return lambda el: el.has_attr(attribute) + def select(self, selector): + """Perform a CSS selection operation on the current element.""" + tokens = selector.split() + current_context = [self] + for index, token in enumerate(tokens): + if tokens[index - 1] == '>': + # already found direct descendants in last step. skip this + # step. + continue + m = self.attribselect_re.match(token) + if m is not None: + # Attribute selector + tag, attribute, operator, value = m.groups() + if not tag: + tag = True + checker = self._attribute_checker(operator, attribute, value) + found = [] + for context in current_context: + found.extend( + [el for el in context.find_all(tag) if checker(el)]) + current_context = found + continue + + if '#' in token: + # ID selector + tag, id = token.split('#', 1) + if tag == "": + tag = True + el = current_context[0].find(tag, {'id': id}) + if el is None: + return [] # No match + current_context = [el] + continue + + if '.' in token: + # Class selector + tag_name, klass = token.split('.', 1) + if not tag_name: + tag_name = True + classes = set(klass.split('.')) + found = [] + def classes_match(tag): + if tag_name is not True and tag.name != tag_name: + return False + if not tag.has_attr('class'): + return False + return classes.issubset(tag['class']) + for context in current_context: + found.extend(context.find_all(classes_match)) + current_context = found + continue + + if token == '*': + # Star selector + found = [] + for context in current_context: + found.extend(context.findAll(True)) + current_context = found + continue + + if token == '>': + # Child selector + tag = tokens[index + 1] + if not tag: + tag = True + + found = [] + for context in current_context: + found.extend(context.find_all(tag, recursive=False)) + current_context = found + continue + + # Here we should just have a regular tag + if not self.tag_name_re.match(token): + return [] + found = [] + for context in current_context: + found.extend(context.findAll(token)) + current_context = found + return current_context + # Old non-property versions of the generators, for backwards # compatibility with BS3. def nextGenerator(self): @@ -651,9 +652,6 @@ class NavigableString(unicode, PageElement): return unicode.__new__(cls, value) return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) - def __copy__(self): - return self - def __getnewargs__(self): return (unicode(self),) @@ -711,7 +709,7 @@ class Doctype(PreformattedString): @classmethod def for_name_and_ids(cls, name, pub_id, system_id): - value = name or '' + value = name if pub_id is not None: value += ' PUBLIC "%s"' % pub_id if system_id is not None: @@ -805,24 +803,16 @@ class Tag(PageElement): self.clear() self.append(string.__class__(string)) - def _all_strings(self, strip=False, types=(NavigableString, CData)): - """Yield all strings of certain classes, possibly stripping them. - - By default, yields only NavigableString and CData objects. So - no comments, processing instructions, etc. - """ + def _all_strings(self, strip=False): + """Yield all child strings, possibly stripping them.""" for descendant in self.descendants: - if ( - (types is None and not isinstance(descendant, NavigableString)) - or - (types is not None and type(descendant) not in types)): + if not isinstance(descendant, NavigableString): continue if strip: descendant = descendant.strip() if len(descendant) == 0: continue yield descendant - strings = property(_all_strings) @property @@ -830,13 +820,11 @@ class Tag(PageElement): for string in self._all_strings(True): yield string - def get_text(self, separator=u"", strip=False, - types=(NavigableString, CData)): + def get_text(self, separator=u"", strip=False): """ Get all child strings, concatenated using the given separator. """ - return separator.join([s for s in self._all_strings( - strip, types=types)]) + return separator.join([s for s in self._all_strings(strip)]) getText = get_text text = property(get_text) @@ -847,7 +835,6 @@ class Tag(PageElement): while i is not None: next = i.next_element i.__dict__.clear() - i.contents = [] i = next def clear(self, decompose=False): @@ -979,13 +966,6 @@ class Tag(PageElement): u = self.decode(indent_level, encoding, formatter) return u.encode(encoding, errors) - def _should_pretty_print(self, indent_level): - """Should this tag be pretty-printed?""" - return ( - indent_level is not None and - (self.name not in HTMLAwareEntitySubstitution.preformatted_tags - or self._is_xml)) - def decode(self, indent_level=None, eventual_encoding=DEFAULT_OUTPUT_ENCODING, formatter="minimal"): @@ -998,12 +978,6 @@ class Tag(PageElement): document contains a <META> tag that mentions the document's encoding. """ - - # First off, turn a string formatter into a function. This - # will stop the lookup from happening over and over again. - if not callable(formatter): - formatter = self._formatter_for_name(formatter) - attrs = [] if self.attrs: for key, val in sorted(self.attrs.items()): @@ -1036,15 +1010,12 @@ class Tag(PageElement): else: closeTag = '</%s%s>' % (prefix, self.name) - pretty_print = self._should_pretty_print(indent_level) - space = '' - indent_space = '' - if indent_level is not None: - indent_space = (' ' * (indent_level - 1)) + pretty_print = (indent_level is not None) if pretty_print: - space = indent_space + space = (' ' * (indent_level - 1)) indent_contents = indent_level + 1 else: + space = '' indent_contents = None contents = self.decode_contents( indent_contents, eventual_encoding, formatter) @@ -1057,10 +1028,8 @@ class Tag(PageElement): attribute_string = '' if attrs: attribute_string = ' ' + ' '.join(attrs) - if indent_level is not None: - # Even if this particular tag is not pretty-printed, - # we should indent up to the start of the tag. - s.append(indent_space) + if pretty_print: + s.append(space) s.append('<%s%s%s%s>' % ( prefix, self.name, attribute_string, close)) if pretty_print: @@ -1071,10 +1040,7 @@ class Tag(PageElement): if pretty_print and closeTag: s.append(space) s.append(closeTag) - if indent_level is not None and closeTag and self.next_sibling: - # Even if this particular tag is not pretty-printed, - # we're now done with the tag, and we should add a - # newline if appropriate. + if pretty_print and closeTag and self.next_sibling: s.append("\n") s = ''.join(s) return s @@ -1097,11 +1063,6 @@ class Tag(PageElement): document contains a <META> tag that mentions the document's encoding. """ - # First off, turn a string formatter into a function. This - # will stop the lookup from happening over and over again. - if not callable(formatter): - formatter = self._formatter_for_name(formatter) - pretty_print = (indent_level is not None) s = [] for c in self: @@ -1111,13 +1072,13 @@ class Tag(PageElement): elif isinstance(c, Tag): s.append(c.decode(indent_level, eventual_encoding, formatter)) - if text and indent_level and not self.name == 'pre': + if text and indent_level: text = text.strip() if text: - if pretty_print and not self.name == 'pre': + if pretty_print: s.append(" " * (indent_level - 1)) s.append(text) - if pretty_print and not self.name == 'pre': + if pretty_print: s.append("\n") return ''.join(s) @@ -1184,207 +1145,6 @@ class Tag(PageElement): yield current current = current.next_element - # CSS selector code - - _selector_combinators = ['>', '+', '~'] - _select_debug = False - def select(self, selector, _candidate_generator=None): - """Perform a CSS selection operation on the current element.""" - tokens = selector.split() - current_context = [self] - - if tokens[-1] in self._selector_combinators: - raise ValueError( - 'Final combinator "%s" is missing an argument.' % tokens[-1]) - if self._select_debug: - print 'Running CSS selector "%s"' % selector - for index, token in enumerate(tokens): - if self._select_debug: - print ' Considering token "%s"' % token - recursive_candidate_generator = None - tag_name = None - if tokens[index-1] in self._selector_combinators: - # This token was consumed by the previous combinator. Skip it. - if self._select_debug: - print ' Token was consumed by the previous combinator.' - continue - # Each operation corresponds to a checker function, a rule - # for determining whether a candidate matches the - # selector. Candidates are generated by the active - # iterator. - checker = None - - m = self.attribselect_re.match(token) - if m is not None: - # Attribute selector - tag_name, attribute, operator, value = m.groups() - checker = self._attribute_checker(operator, attribute, value) - - elif '#' in token: - # ID selector - tag_name, tag_id = token.split('#', 1) - def id_matches(tag): - return tag.get('id', None) == tag_id - checker = id_matches - - elif '.' in token: - # Class selector - tag_name, klass = token.split('.', 1) - classes = set(klass.split('.')) - def classes_match(candidate): - return classes.issubset(candidate.get('class', [])) - checker = classes_match - - elif ':' in token: - # Pseudo-class - tag_name, pseudo = token.split(':', 1) - if tag_name == '': - raise ValueError( - "A pseudo-class must be prefixed with a tag name.") - pseudo_attributes = re.match('([a-zA-Z\d-]+)\(([a-zA-Z\d]+)\)', pseudo) - found = [] - if pseudo_attributes is not None: - pseudo_type, pseudo_value = pseudo_attributes.groups() - if pseudo_type == 'nth-of-type': - try: - pseudo_value = int(pseudo_value) - except: - raise NotImplementedError( - 'Only numeric values are currently supported for the nth-of-type pseudo-class.') - if pseudo_value < 1: - raise ValueError( - 'nth-of-type pseudo-class value must be at least 1.') - class Counter(object): - def __init__(self, destination): - self.count = 0 - self.destination = destination - - def nth_child_of_type(self, tag): - self.count += 1 - if self.count == self.destination: - return True - if self.count > self.destination: - # Stop the generator that's sending us - # these things. - raise StopIteration() - return False - checker = Counter(pseudo_value).nth_child_of_type - else: - raise NotImplementedError( - 'Only the following pseudo-classes are implemented: nth-of-type.') - - elif token == '*': - # Star selector -- matches everything - pass - elif token == '>': - # Run the next token as a CSS selector against the - # direct children of each tag in the current context. - recursive_candidate_generator = lambda tag: tag.children - elif token == '~': - # Run the next token as a CSS selector against the - # siblings of each tag in the current context. - recursive_candidate_generator = lambda tag: tag.next_siblings - elif token == '+': - # For each tag in the current context, run the next - # token as a CSS selector against the tag's next - # sibling that's a tag. - def next_tag_sibling(tag): - yield tag.find_next_sibling(True) - recursive_candidate_generator = next_tag_sibling - - elif self.tag_name_re.match(token): - # Just a tag name. - tag_name = token - else: - raise ValueError( - 'Unsupported or invalid CSS selector: "%s"' % token) - - if recursive_candidate_generator: - # This happens when the selector looks like "> foo". - # - # The generator calls select() recursively on every - # member of the current context, passing in a different - # candidate generator and a different selector. - # - # In the case of "> foo", the candidate generator is - # one that yields a tag's direct children (">"), and - # the selector is "foo". - next_token = tokens[index+1] - def recursive_select(tag): - if self._select_debug: - print ' Calling select("%s") recursively on %s %s' % (next_token, tag.name, tag.attrs) - print '-' * 40 - for i in tag.select(next_token, recursive_candidate_generator): - if self._select_debug: - print '(Recursive select picked up candidate %s %s)' % (i.name, i.attrs) - yield i - if self._select_debug: - print '-' * 40 - _use_candidate_generator = recursive_select - elif _candidate_generator is None: - # By default, a tag's candidates are all of its - # children. If tag_name is defined, only yield tags - # with that name. - if self._select_debug: - if tag_name: - check = "[any]" - else: - check = tag_name - print ' Default candidate generator, tag name="%s"' % check - if self._select_debug: - # This is redundant with later code, but it stops - # a bunch of bogus tags from cluttering up the - # debug log. - def default_candidate_generator(tag): - for child in tag.descendants: - if not isinstance(child, Tag): - continue - if tag_name and not child.name == tag_name: - continue - yield child - _use_candidate_generator = default_candidate_generator - else: - _use_candidate_generator = lambda tag: tag.descendants - else: - _use_candidate_generator = _candidate_generator - - new_context = [] - new_context_ids = set([]) - for tag in current_context: - if self._select_debug: - print " Running candidate generator on %s %s" % ( - tag.name, repr(tag.attrs)) - for candidate in _use_candidate_generator(tag): - if not isinstance(candidate, Tag): - continue - if tag_name and candidate.name != tag_name: - continue - if checker is not None: - try: - result = checker(candidate) - except StopIteration: - # The checker has decided we should no longer - # run the generator. - break - if checker is None or result: - if self._select_debug: - print " SUCCESS %s %s" % (candidate.name, repr(candidate.attrs)) - if id(candidate) not in new_context_ids: - # If a tag matches a selector more than once, - # don't include it in the context more than once. - new_context.append(candidate) - new_context_ids.add(id(candidate)) - elif self._select_debug: - print " FAILURE %s %s" % (candidate.name, repr(candidate.attrs)) - - current_context = new_context - - if self._select_debug: - print "Final verdict:" - for i in current_context: - print " %s %s" % (i.name, i.attrs) - return current_context - # Old names for backwards compatibility def childGenerator(self): return self.children @@ -1392,13 +1152,10 @@ class Tag(PageElement): def recursiveChildGenerator(self): return self.descendants - def has_key(self, key): - """This was kind of misleading because has_key() (attributes) - was different from __in__ (contents). has_key() is gone in - Python 3, anyway.""" - warnings.warn('has_key is deprecated. Use has_attr("%s") instead.' % ( - key)) - return self.has_attr(key) + # This was kind of misleading because has_key() (attributes) was + # different from __in__ (contents). has_key() is gone in Python 3, + # anyway. + has_key = has_attr # Next, a couple classes to represent queries and their results. class SoupStrainer(object): diff --git a/lib/bs4/testing.py b/lib/bs4/testing.py index 23b26f1..30e74f4 100644 --- a/lib/bs4/testing.py +++ b/lib/bs4/testing.py @@ -81,11 +81,6 @@ class HTMLTreeBuilderSmokeTest(object): self.assertDoctypeHandled( 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"') - def test_empty_doctype(self): - soup = self.soup("<!DOCTYPE>") - doctype = soup.contents[0] - self.assertEqual("", doctype.strip()) - def test_public_doctype_with_url(self): doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"' self.assertDoctypeHandled(doctype) @@ -164,12 +159,6 @@ class HTMLTreeBuilderSmokeTest(object): comment = soup.find(text="foobar") self.assertEqual(comment.__class__, Comment) - # The comment is properly integrated into the tree. - foo = soup.find(text="foo") - self.assertEqual(comment, foo.next_element) - baz = soup.find(text="baz") - self.assertEqual(comment, baz.previous_element) - def test_preserved_whitespace_in_pre_and_textarea(self): """Whitespace must be preserved in <pre> and <textarea> tags.""" self.assertSoupEquals("<pre> </pre>") @@ -228,14 +217,12 @@ class HTMLTreeBuilderSmokeTest(object): expect = u'<p id="pi\N{LATIN SMALL LETTER N WITH TILDE}ata"></p>' self.assertSoupEquals('<p id="piñata"></p>', expect) self.assertSoupEquals('<p id="piñata"></p>', expect) - self.assertSoupEquals('<p id="piñata"></p>', expect) self.assertSoupEquals('<p id="piñata"></p>', expect) def test_entities_in_text_converted_to_unicode(self): expect = u'<p>pi\N{LATIN SMALL LETTER N WITH TILDE}ata</p>' self.assertSoupEquals("<p>piñata</p>", expect) self.assertSoupEquals("<p>piñata</p>", expect) - self.assertSoupEquals("<p>piñata</p>", expect) self.assertSoupEquals("<p>piñata</p>", expect) def test_quot_entity_converted_to_quotation_mark(self): @@ -248,12 +235,6 @@ class HTMLTreeBuilderSmokeTest(object): self.assertSoupEquals("�", expect) self.assertSoupEquals("�", expect) - def test_multipart_strings(self): - "Mostly to prevent a recurrence of a bug in the html5lib treebuilder." - soup = self.soup("<html><h2>\nfoo</h2><p></p></html>") - self.assertEqual("p", soup.h2.string.next_element.name) - self.assertEqual("p", soup.p.name) - def test_basic_namespaces(self): """Parsers don't need to *understand* namespaces, but at the very least they should not choke on namespaces or lose @@ -472,18 +453,6 @@ class XMLTreeBuilderSmokeTest(object): self.assertEqual( soup.encode("utf-8"), markup) - def test_formatter_processes_script_tag_for_xml_documents(self): - doc = """ - <script type="text/javascript"> - </script> -""" - soup = BeautifulSoup(doc, "xml") - # lxml would have stripped this while parsing, but we can add - # it later. - soup.script.string = 'console.log("< < hey > > ");' - encoded = soup.encode() - self.assertTrue(b"< < hey > >" in encoded) - def test_popping_namespaced_tag(self): markup = '<rss xmlns:dc="foo"><dc:creator>b</dc:creator><dc:date>2012-07-02T20:33:42Z</dc:date><dc:rights>c</dc:rights><image>d</image></rss>' soup = self.soup(markup) @@ -526,11 +495,6 @@ class XMLTreeBuilderSmokeTest(object): soup = self.soup(markup) self.assertEqual(unicode(soup.foo), markup) - def test_namespaced_attributes_xml_namespace(self): - markup = '<foo xml:lang="fr">bar</foo>' - soup = self.soup(markup) - self.assertEqual(unicode(soup.foo), markup) - class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest): """Smoke test for a tree builder that supports HTML5.""" @@ -559,12 +523,6 @@ class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest): self.assertEqual(namespace, soup.math.namespace) self.assertEqual(namespace, soup.msqrt.namespace) - def test_xml_declaration_becomes_comment(self): - markup = '<?xml version="1.0" encoding="utf-8"?><html></html>' - soup = self.soup(markup) - self.assertTrue(isinstance(soup.contents[0], Comment)) - self.assertEqual(soup.contents[0], '?xml version="1.0" encoding="utf-8"?') - self.assertEqual("html", soup.contents[0].next_element.name) def skipIf(condition, reason): def nothing(test, *args, **kwargs): diff --git a/lib/bs4/tests/test_html5lib.py b/lib/bs4/tests/test_html5lib.py index 2a3b41e..f195f7d 100644 --- a/lib/bs4/tests/test_html5lib.py +++ b/lib/bs4/tests/test_html5lib.py @@ -56,17 +56,3 @@ class HTML5LibBuilderSmokeTest(SoupTest, HTML5TreeBuilderSmokeTest): "<table><thead><tr><td>Foo</td></tr></thead>" "<tbody><tr><td>Bar</td></tr></tbody>" "<tfoot><tr><td>Baz</td></tr></tfoot></table>") - - def test_xml_declaration_followed_by_doctype(self): - markup = '''<?xml version="1.0" encoding="utf-8"?> -<!DOCTYPE html> -<html> - <head> - </head> - <body> - <p>foo</p> - </body> -</html>''' - soup = self.soup(markup) - # Verify that we can reach the <p> tag; this means the tree is connected. - self.assertEqual(b"<p>foo</p>", soup.p.encode()) diff --git a/lib/bs4/tests/test_lxml.py b/lib/bs4/tests/test_lxml.py index 80458de..39e26bf 100644 --- a/lib/bs4/tests/test_lxml.py +++ b/lib/bs4/tests/test_lxml.py @@ -6,11 +6,8 @@ import warnings try: from bs4.builder import LXMLTreeBuilder, LXMLTreeBuilderForXML LXML_PRESENT = True - import lxml.etree - LXML_VERSION = lxml.etree.LXML_VERSION except ImportError, e: LXML_PRESENT = False - LXML_VERSION = (0,) from bs4 import ( BeautifulSoup, @@ -44,17 +41,6 @@ class LXMLTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest): self.assertSoupEquals( "<p>foo�bar</p>", "<p>foobar</p>") - # In lxml < 2.3.5, an empty doctype causes a segfault. Skip this - # test if an old version of lxml is installed. - - @skipIf( - not LXML_PRESENT or LXML_VERSION < (2,3,5,0), - "Skipping doctype test for old version of lxml to avoid segfault.") - def test_empty_doctype(self): - soup = self.soup("<!DOCTYPE>") - doctype = soup.contents[0] - self.assertEqual("", doctype.strip()) - def test_beautifulstonesoup_is_xml_parser(self): # Make sure that the deprecated BSS class uses an xml builder # if one is installed. @@ -86,3 +72,4 @@ class LXMLXMLTreeBuilderSmokeTest(SoupTest, XMLTreeBuilderSmokeTest): @property def default_builder(self): return LXMLTreeBuilderForXML() + diff --git a/lib/bs4/tests/test_soup.py b/lib/bs4/tests/test_soup.py index b127716..dd636d8 100644 --- a/lib/bs4/tests/test_soup.py +++ b/lib/bs4/tests/test_soup.py @@ -125,14 +125,9 @@ class TestEntitySubstitution(unittest.TestCase): def test_xml_quoting_handles_ampersands(self): self.assertEqual(self.sub.substitute_xml("AT&T"), "AT&T") - def test_xml_quoting_including_ampersands_when_they_are_part_of_an_entity(self): + def test_xml_quoting_ignores_ampersands_when_they_are_part_of_an_entity(self): self.assertEqual( self.sub.substitute_xml("ÁT&T"), - "&Aacute;T&T") - - def test_xml_quoting_ignoring_ampersands_when_they_are_part_of_an_entity(self): - self.assertEqual( - self.sub.substitute_xml_containing_entities("ÁT&T"), "ÁT&T") def test_quotes_not_html_substituted(self): diff --git a/lib/bs4/tests/test_tree.py b/lib/bs4/tests/test_tree.py index 2d09f96..5f3395b 100644 --- a/lib/bs4/tests/test_tree.py +++ b/lib/bs4/tests/test_tree.py @@ -20,7 +20,6 @@ from bs4.builder import ( ) from bs4.element import ( CData, - Comment, Doctype, NavigableString, SoupStrainer, @@ -426,7 +425,6 @@ class TestParentOperations(TreeTest): def test_find_parent(self): self.assertEqual(self.start.find_parent('ul')['id'], 'bottom') - self.assertEqual(self.start.find_parent('ul', id='top')['id'], 'top') def test_parent_of_text_element(self): text = self.tree.find(text="Start here") @@ -689,12 +687,6 @@ class TestTagCreation(SoupTest): self.assertEqual("foo", s) self.assertTrue(isinstance(s, NavigableString)) - def test_new_string_can_create_navigablestring_subclass(self): - soup = self.soup("") - s = soup.new_string("foo", Comment) - self.assertEqual("foo", s) - self.assertTrue(isinstance(s, Comment)) - class TestTreeModification(SoupTest): def test_attribute_modification(self): @@ -1056,7 +1048,7 @@ class TestTreeModification(SoupTest): # clear using decompose() em = a.em a.clear(decompose=True) - self.assertEqual(0, len(em.contents)) + self.assertFalse(hasattr(em, "contents")) def test_string_set(self): """Tag.string = 'string'""" @@ -1174,19 +1166,6 @@ class TestElementObjects(SoupTest): self.assertEqual(soup.a.get_text(","), "a,r, , t ") self.assertEqual(soup.a.get_text(",", strip=True), "a,r,t") - def test_get_text_ignores_comments(self): - soup = self.soup("foo<!--IGNORE-->bar") - self.assertEqual(soup.get_text(), "foobar") - - self.assertEqual( - soup.get_text(types=(NavigableString, Comment)), "fooIGNOREbar") - self.assertEqual( - soup.get_text(types=None), "fooIGNOREbar") - - def test_all_strings_ignores_comments(self): - soup = self.soup("foo<!--IGNORE-->bar") - self.assertEqual(['foo', 'bar'], list(soup.strings)) - class TestCDAtaListAttributes(SoupTest): """Testing cdata-list attributes like 'class'. @@ -1331,32 +1310,6 @@ class TestSubstitutions(SoupTest): expect_upper = u'<a href="HTTP://A.COM?A=B&C=É">E</a>' self.assertEqual(expect_upper, a.decode(formatter=lambda x: x.upper())) - def test_formatter_skips_script_tag_for_html_documents(self): - doc = """ - <script type="text/javascript"> - console.log("< < hey > > "); - </script> -""" - encoded = BeautifulSoup(doc).encode() - self.assertTrue(b"< < hey > >" in encoded) - - def test_formatter_skips_style_tag_for_html_documents(self): - doc = """ - <style type="text/css"> - console.log("< < hey > > "); - </style> -""" - encoded = BeautifulSoup(doc).encode() - self.assertTrue(b"< < hey > >" in encoded) - - def test_prettify_leaves_preformatted_text_alone(self): - soup = self.soup("<div> foo <pre> \tbar\n \n </pre> baz ") - # Everything outside the <pre> tag is reformatted, but everything - # inside is left alone. - self.assertEqual( - u'<div>\n foo\n <pre> \tbar\n \n </pre>\n baz\n</div>', - soup.div.prettify()) - def test_prettify_accepts_formatter(self): soup = BeautifulSoup("<html><body>foo</body></html>") pretty = soup.prettify(formatter = lambda x: x.upper()) @@ -1506,7 +1459,7 @@ class TestSoupSelector(TreeTest): </head> <body> -<div id="main" class="fancy"> +<div id="main"> <div id="inner"> <h1 id="header1">An H1</h1> <p>Some text</p> @@ -1578,7 +1531,7 @@ class TestSoupSelector(TreeTest): self.assertEqual(len(self.soup.select('del')), 0) def test_invalid_tag(self): - self.assertRaises(ValueError, self.soup.select, 'tag%t') + self.assertEqual(len(self.soup.select('tag%t')), 0) def test_header_tags(self): self.assertSelectMultiple( @@ -1611,7 +1564,7 @@ class TestSoupSelector(TreeTest): for el in els: self.assertEqual(el.name, 'p') self.assertEqual(els[1]['class'], ['onep']) - self.assertFalse(els[0].has_attr('class')) + self.assertFalse(els[0].has_key('class')) def test_a_bunch_of_emptys(self): for selector in ('div#main del', 'div#main div.oops', 'div div#main'): @@ -1631,9 +1584,6 @@ class TestSoupSelector(TreeTest): self.assertSelects('.s1 > a', ['s1a1', 's1a2']) self.assertSelects('.s1 > a span', ['s1a2s1']) - def test_child_selector_id(self): - self.assertSelects('.s1 > a#s1a2 span', ['s1a2s1']) - def test_attribute_equals(self): self.assertSelectMultiple( ('p[class="onep"]', ['p1']), @@ -1740,33 +1690,6 @@ class TestSoupSelector(TreeTest): ('p[blah]', []), ) - def test_nth_of_type(self): - # Try to select first paragraph - els = self.soup.select('div#inner p:nth-of-type(1)') - self.assertEqual(len(els), 1) - self.assertEqual(els[0].string, u'Some text') - - # Try to select third paragraph - els = self.soup.select('div#inner p:nth-of-type(3)') - self.assertEqual(len(els), 1) - self.assertEqual(els[0].string, u'Another') - - # Try to select (non-existent!) fourth paragraph - els = self.soup.select('div#inner p:nth-of-type(4)') - self.assertEqual(len(els), 0) - - # Pass in an invalid value. - self.assertRaises( - ValueError, self.soup.select, 'div p:nth-of-type(0)') - - def test_nth_of_type_direct_descendant(self): - els = self.soup.select('div#inner > p:nth-of-type(1)') - self.assertEqual(len(els), 1) - self.assertEqual(els[0].string, u'Some text') - - def test_id_child_selector_nth_of_type(self): - self.assertSelects('#inner > p:nth-of-type(2)', ['p1']) - def test_select_on_element(self): # Other tests operate on the tree; this operates on an element # within the tree. @@ -1775,26 +1698,3 @@ class TestSoupSelector(TreeTest): # The <div id="inner"> tag was selected. The <div id="footer"> # tag was not. self.assertSelectsIDs(selected, ['inner']) - - def test_overspecified_child_id(self): - self.assertSelects(".fancy #inner", ['inner']) - self.assertSelects(".normal #inner", []) - - def test_adjacent_sibling_selector(self): - self.assertSelects('#p1 + h2', ['header2']) - self.assertSelects('#p1 + h2 + p', ['pmulti']) - self.assertSelects('#p1 + #header2 + .class1', ['pmulti']) - self.assertEqual([], self.soup.select('#p1 + p')) - - def test_general_sibling_selector(self): - self.assertSelects('#p1 ~ h2', ['header2', 'header3']) - self.assertSelects('#p1 ~ #header2', ['header2']) - self.assertSelects('#p1 ~ h2 + a', ['me']) - self.assertSelects('#p1 ~ h2 + [rel="me"]', ['me']) - self.assertEqual([], self.soup.select('#inner ~ h2')) - - def test_dangling_combinator(self): - self.assertRaises(ValueError, self.soup.select, 'h1 >') - - def test_sibling_combinator_wont_select_same_tag_twice(self): - self.assertSelects('p[lang] ~ p', ['lang-en-gb', 'lang-en-us', 'lang-fr']) diff --git a/lib/pygeoip/COPYING b/lib/pygeoip/COPYING deleted file mode 100644 index 02bbb60..0000000 --- a/lib/pygeoip/COPYING +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file diff --git a/lib/pygeoip/DEVELOPER b/lib/pygeoip/DEVELOPER deleted file mode 100644 index 6f81d45..0000000 --- a/lib/pygeoip/DEVELOPER +++ /dev/null @@ -1,21 +0,0 @@ -Bootstrap manual for developers of pygeoip - -Dependencies: tox, nose, epydoc - -For testing we are using tox virtualenv-based Python version testing -and nose as test framwork. - -Tox will create virtualenvs for all Python version pygeoip supports -and installs the current working tree using the setup.py install script. -Running the tests requires a couple of sample databases found on the -link below. - -Maxmind sample databases for testing can be downloaded here: -http://www.defunct.cc/maxmind-geoip-samples.tar.gz (58 MB) - -Extract the tarball in the tests directory and run tox from the root directory. - -Please make sure your code passes all tests before opening pull requests. - -All the best, -William Tisäter diff --git a/lib/pygeoip/__init__.py b/lib/pygeoip/__init__.py index b073088..3e89422 100644 --- a/lib/pygeoip/__init__.py +++ b/lib/pygeoip/__init__.py @@ -1,13 +1,17 @@ -# -*- coding: utf-8 -*- """ -Pure Python GeoIP API +Pure Python GeoIP API. The API is based off of U{MaxMind's C-based Python API<http://www.maxmind.com/app/python>}, +but the code itself is based on the U{pure PHP5 API<http://pear.php.net/package/Net_GeoIP/>} +by Jim Winstead and Hans Lellelid. -The API is based on MaxMind's C-based Python API, but the code itself is -ported from the Pure PHP GeoIP API by Jim Winstead and Hans Lellelid. +It is mostly a drop-in replacement, except the +C{new} and C{open} methods are gone. You should instantiate the L{GeoIP} class yourself: -@author: Jennifer Ennis <zaylea@gmail.com> +C{gi = GeoIP('/path/to/GeoIP.dat', pygeoip.MEMORY_CACHE)} -@license: Copyright(C) 2004 MaxMind LLC +@author: Jennifer Ennis <zaylea at gmail dot com> + +@license: +Copyright(C) 2004 MaxMind LLC This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by @@ -23,43 +27,39 @@ You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>. """ +from __future__ import with_statement, absolute_import, division import os import math import socket import mmap +import gzip import codecs -from threading import Lock +from StringIO import StringIO -try: - from StringIO import StringIO -except ImportError: - from io import StringIO, BytesIO +from . import const +from .util import ip2long +from .timezone import time_zone_by_country_and_region -from pygeoip import util, const -from pygeoip.const import PY2, PY3 -from pygeoip.timezone import time_zone_by_country_and_region +import six - -STANDARD = const.STANDARD MMAP_CACHE = const.MMAP_CACHE MEMORY_CACHE = const.MEMORY_CACHE - -ENCODING = const.ENCODING - +STANDARD = const.STANDARD class GeoIPError(Exception): pass - class GeoIPMetaclass(type): + def __new__(cls, *args, **kwargs): """ Singleton method to gets an instance without reparsing the db. Unique instances are instantiated based on the filename of the db. Flags are - ignored for this, i.e. if you initialize one with STANDARD - flag (default) and then try later to initialize with MEMORY_CACHE, it - will still return the STANDARD one. + ignored for this, i.e. if you initialize one with STANDARD flag (default) + and then try later to initialize with MEMORY_CACHE, it will still + return the STANDARD one. """ + if not hasattr(cls, '_instances'): cls._instances = {} @@ -68,25 +68,25 @@ class GeoIPMetaclass(type): elif 'filename' in kwargs: filename = kwargs['filename'] - if filename not in cls._instances: + if not filename in cls._instances: cls._instances[filename] = type.__new__(cls, *args, **kwargs) return cls._instances[filename] - GeoIPBase = GeoIPMetaclass('GeoIPBase', (object,), {}) - class GeoIP(GeoIPBase): + def __init__(self, filename, flags=0): """ Initialize the class. - @param filename: Path to a geoip database. + @param filename: path to a geoip database. If MEMORY_CACHE is used, + the file can be gzipped. @type filename: str - @param flags: Flags that affect how the database is processed. - Currently supported flags are STANDARD (the default), - MEMORY_CACHE (preload the whole file into memory) and + @param flags: flags that affect how the database is processed. + Currently the only supported flags are STANDARD (the default), + MEMORY_CACHE (preload the whole file into memory), and MMAP_CACHE (access the file via mmap). @type flags: int """ @@ -94,71 +94,42 @@ class GeoIP(GeoIPBase): self._flags = flags if self._flags & const.MMAP_CACHE: - f = open(filename, 'rb') - access = mmap.ACCESS_READ - self._filehandle = mmap.mmap(f.fileno(), 0, access=access) - f.close() + with open(filename, 'rb') as f: + self._filehandle = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) elif self._flags & const.MEMORY_CACHE: - f = open(filename, 'rb') - self._memoryBuffer = f.read() - iohandle = BytesIO if PY3 else StringIO - self._filehandle = iohandle(self._memoryBuffer) - f.close() + if filename.endswith('.gz'): + opener = gzip.open + else: + opener = open + with opener(filename, 'rb') as f: + self._memoryBuffer = f.read() + self._filehandle = StringIO(self._memoryBuffer) else: - self._filehandle = codecs.open(filename, 'rb', ENCODING) + self._filehandle = codecs.open(filename, 'rb','latin_1') - self._lock = Lock() self._setup_segments() def _setup_segments(self): """ - Parses the database file to determine what kind of database is - being used and setup segment sizes and start points that will - be used by the seek*() methods later. - - Supported databases: - - * COUNTRY_EDITION - * COUNTRY_EDITION_V6 - * REGION_EDITION_REV0 - * REGION_EDITION_REV1 - * CITY_EDITION_REV0 - * CITY_EDITION_REV1 - * CITY_EDITION_REV1_V6 - * ORG_EDITION - * ISP_EDITION - * ASNUM_EDITION - * ASNUM_EDITION_V6 - + Parses the database file to determine what kind of database is being used and setup + segment sizes and start points that will be used by the seek*() methods later. """ self._databaseType = const.COUNTRY_EDITION self._recordLength = const.STANDARD_RECORD_LENGTH - self._databaseSegments = const.COUNTRY_BEGIN - self._lock.acquire() filepos = self._filehandle.tell() self._filehandle.seek(-3, os.SEEK_END) for i in range(const.STRUCTURE_INFO_MAX_SIZE): - chars = chr(255) * 3 delim = self._filehandle.read(3) - if PY3 and type(delim) is bytes: - delim = delim.decode(ENCODING) + if delim == six.u(chr(255) * 3): + self._databaseType = ord(self._filehandle.read(1)) - if PY2: - chars = chars.decode(ENCODING) - if type(delim) is str: - delim = delim.decode(ENCODING) - - if delim == chars: - byte = self._filehandle.read(1) - self._databaseType = ord(byte) - - # Compatibility with databases from April 2003 and earlier if (self._databaseType >= 106): + # backwards compatibility with databases from April 2003 and earlier self._databaseType -= 105 if self._databaseType == const.REGION_EDITION_REV0: @@ -169,29 +140,51 @@ class GeoIP(GeoIPBase): elif self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1, - const.CITY_EDITION_REV1_V6, const.ORG_EDITION, const.ISP_EDITION, - const.ASNUM_EDITION, - const.ASNUM_EDITION_V6): + const.ASNUM_EDITION): self._databaseSegments = 0 buf = self._filehandle.read(const.SEGMENT_RECORD_LENGTH) - if PY3 and type(buf) is bytes: - buf = buf.decode(ENCODING) - for j in range(const.SEGMENT_RECORD_LENGTH): self._databaseSegments += (ord(buf[j]) << (j * 8)) - LONG_RECORDS = (const.ORG_EDITION, const.ISP_EDITION) - if self._databaseType in LONG_RECORDS: + if self._databaseType in (const.ORG_EDITION, const.ISP_EDITION): self._recordLength = const.ORG_RECORD_LENGTH + break else: self._filehandle.seek(-4, os.SEEK_CUR) + if self._databaseType == const.COUNTRY_EDITION: + self._databaseSegments = const.COUNTRY_BEGIN + self._filehandle.seek(filepos, os.SEEK_SET) - self._lock.release() + + def _lookup_country_id(self, addr): + """ + Get the country index. + + This method is called by the _lookupCountryCode and _lookupCountryName + methods. It looks up the index ('id') for the country which is the key + for the code and name. + + @param addr: The IP address + @type addr: str + @return: network byte order 32-bit integer + @rtype: int + """ + + ipnum = ip2long(addr) + + if not ipnum: + raise ValueError("Invalid IP address: %s" % addr) + + if self._databaseType != const.COUNTRY_EDITION: + raise GeoIPError('Invalid database type; country_* methods expect '\ + 'Country database') + + return self._seek_country(ipnum) - const.COUNTRY_BEGIN def _seek_country(self, ipnum): """ @@ -203,119 +196,117 @@ class GeoIP(GeoIPBase): @return: offset of start of record @rtype: int """ - try: - offset = 0 - seek_depth = 127 if len(str(ipnum)) > 10 else 31 + offset = 0 - for depth in range(seek_depth, -1, -1): - if self._flags & const.MEMORY_CACHE: - startIndex = 2 * self._recordLength * offset - endIndex = startIndex + (2 * self._recordLength) - buf = self._memoryBuffer[startIndex:endIndex] - else: - startIndex = 2 * self._recordLength * offset - readLength = 2 * self._recordLength - self._lock.acquire() - self._filehandle.seek(startIndex, os.SEEK_SET) - buf = self._filehandle.read(readLength) - self._lock.release() + for depth in range(31, -1, -1): - if PY3 and type(buf) is bytes: - buf = buf.decode(ENCODING) + if self._flags & const.MEMORY_CACHE: + startIndex = 2 * self._recordLength * offset + length = 2 * self._recordLength + endIndex = startIndex + length + buf = self._memoryBuffer[startIndex:endIndex] + else: + self._filehandle.seek(2 * self._recordLength * offset, os.SEEK_SET) + buf = self._filehandle.read(2 * self._recordLength) - x = [0, 0] - for i in range(2): - for j in range(self._recordLength): - byte = buf[self._recordLength * i + j] - x[i] += ord(byte) << (j * 8) - if ipnum & (1 << depth): - if x[1] >= self._databaseSegments: - return x[1] - offset = x[1] - else: - if x[0] >= self._databaseSegments: - return x[0] - offset = x[0] - except: - pass + x = [0,0] - raise GeoIPError('Corrupt database') + for i in range(2): + for j in range(self._recordLength): + x[i] += ord(buf[self._recordLength * i + j]) << (j * 8) + + if ipnum & (1 << depth): + + if x[1] >= self._databaseSegments: + return x[1] + + offset = x[1] + + else: + + if x[0] >= self._databaseSegments: + return x[0] + + offset = x[0] + + + raise Exception('Error traversing database - perhaps it is corrupt?') def _get_org(self, ipnum): """ - Seek and return organization or ISP name for ipnum. + Seek and return organization (or ISP) name for converted IP addr. @param ipnum: Converted IP address @type ipnum: int @return: org/isp name @rtype: str """ + seek_org = self._seek_country(ipnum) if seek_org == self._databaseSegments: return None - read_length = (2 * self._recordLength - 1) * self._databaseSegments - self._lock.acquire() - self._filehandle.seek(seek_org + read_length, os.SEEK_SET) - buf = self._filehandle.read(const.MAX_ORG_RECORD_LENGTH) - self._lock.release() + record_pointer = seek_org + (2 * self._recordLength - 1) * self._databaseSegments - if PY3 and type(buf) is bytes: - buf = buf.decode(ENCODING) + self._filehandle.seek(record_pointer, os.SEEK_SET) - return buf[:buf.index(chr(0))] + org_buf = self._filehandle.read(const.MAX_ORG_RECORD_LENGTH) + + return org_buf[:org_buf.index(chr(0))] def _get_region(self, ipnum): """ - Seek and return the region info (dict containing country_code - and region_name). + Seek and return the region info (dict containing country_code and region_name). - @param ipnum: Converted IP address + @param ipnum: converted IP address @type ipnum: int @return: dict containing country_code and region_name @rtype: dict """ - region = '' country_code = '' - seek_country = self._seek_country(ipnum) - - def get_region_name(offset): - region1 = chr(offset // 26 + 65) - region2 = chr(offset % 26 + 65) - return ''.join([region1, region2]) + region = '' if self._databaseType == const.REGION_EDITION_REV0: + seek_country = self._seek_country(ipnum) seek_region = seek_country - const.STATE_BEGIN_REV0 if seek_region >= 1000: country_code = 'US' - region = get_region_name(seek_region - 1000) + region = ''.join([chr((seek_region // 1000) // 26 + 65), chr((seek_region // 1000) % 26 + 65)]) else: country_code = const.COUNTRY_CODES[seek_region] + region = '' elif self._databaseType == const.REGION_EDITION_REV1: + seek_country = self._seek_country(ipnum) seek_region = seek_country - const.STATE_BEGIN_REV1 if seek_region < const.US_OFFSET: - pass + country_code = ''; + region = '' elif seek_region < const.CANADA_OFFSET: country_code = 'US' - region = get_region_name(seek_region - const.US_OFFSET) - elif seek_region < const.WORLD_OFFSET: + region = ''.join([chr((seek_region - const.US_OFFSET) // 26 + 65), chr((seek_region - const.US_OFFSET) % 26 + 65)]) + elif seek_region < const.WORLD_OFFSET: country_code = 'CA' - region = get_region_name(seek_region - const.CANADA_OFFSET) + region = ''.join([chr((seek_region - const.CANADA_OFFSET) // 26 + 65), chr((seek_region - const.CANADA_OFFSET) % 26 + 65)]) else: - index = (seek_region - const.WORLD_OFFSET) // const.FIPS_RANGE - if index in const.COUNTRY_CODES: - country_code = const.COUNTRY_CODES[index] - elif self._databaseType in const.CITY_EDITIONS: - rec = self._get_record(ipnum) - region = rec.get('region_name', '') - country_code = rec.get('country_code', '') + i = (seek_region - const.WORLD_OFFSET) // const.FIPS_RANGE + if i < len(const.COUNTRY_CODES): + #country_code = const.COUNTRY_CODES[(seek_region - const.WORLD_OFFSET) // const.FIPS_RANGE] + country_code = const.COUNTRY_CODES[i] + else: + country_code = '' + region = '' - return {'country_code': country_code, 'region_name': region} + elif self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): + rec = self._get_record(ipnum) + country_code = rec['country_code'] if 'country_code' in rec else '' + region = rec['region_name'] if 'region_name' in rec else '' + + return {'country_code' : country_code, 'region_name' : region } def _get_record(self, ipnum): """ Populate location dict for converted IP. - @param ipnum: Converted IP address + @param ipnum: converted IP address @type ipnum: int @return: dict with country_code, country_code3, country_name, region, city, postal_code, latitude, longitude, @@ -324,115 +315,107 @@ class GeoIP(GeoIPBase): """ seek_country = self._seek_country(ipnum) if seek_country == self._databaseSegments: - return {} + return None - read_length = (2 * self._recordLength - 1) * self._databaseSegments - self._lock.acquire() - self._filehandle.seek(seek_country + read_length, os.SEEK_SET) - buf = self._filehandle.read(const.FULL_RECORD_LENGTH) - self._lock.release() + record_pointer = seek_country + (2 * self._recordLength - 1) * self._databaseSegments - if PY3 and type(buf) is bytes: - buf = buf.decode(ENCODING) + self._filehandle.seek(record_pointer, os.SEEK_SET) + record_buf = self._filehandle.read(const.FULL_RECORD_LENGTH) - record = { - 'dma_code': 0, - 'area_code': 0, - 'metro_code': '', - 'postal_code': '' - } + record = {} - latitude = 0 - longitude = 0 - buf_pos = 0 - - # Get country - char = ord(buf[buf_pos]) + record_buf_pos = 0 + char = ord(record_buf[record_buf_pos]) + #char = record_buf[record_buf_pos] if six.PY3 else ord(record_buf[record_buf_pos]) record['country_code'] = const.COUNTRY_CODES[char] record['country_code3'] = const.COUNTRY_CODES3[char] record['country_name'] = const.COUNTRY_NAMES[char] - record['continent'] = const.CONTINENT_NAMES[char] + record_buf_pos += 1 + str_length = 0 - buf_pos += 1 - def get_data(buf, buf_pos): - offset = buf_pos - char = ord(buf[offset]) - while (char != 0): - offset += 1 - char = ord(buf[offset]) - if offset > buf_pos: - return (offset, buf[buf_pos:offset]) - return (offset, '') + # get region + char = ord(record_buf[record_buf_pos+str_length]) + while (char != 0): + str_length += 1 + char = ord(record_buf[record_buf_pos+str_length]) - offset, record['region_name'] = get_data(buf, buf_pos) - offset, record['city'] = get_data(buf, offset + 1) - offset, record['postal_code'] = get_data(buf, offset + 1) - buf_pos = offset + 1 + if str_length > 0: + record['region_name'] = record_buf[record_buf_pos:record_buf_pos+str_length] + record_buf_pos += str_length + 1 + str_length = 0 + + # get city + char = ord(record_buf[record_buf_pos+str_length]) + while (char != 0): + str_length += 1 + char = ord(record_buf[record_buf_pos+str_length]) + + if str_length > 0: + record['city'] = record_buf[record_buf_pos:record_buf_pos+str_length] + else: + record['city'] = '' + + record_buf_pos += str_length + 1 + str_length = 0 + + # get the postal code + char = ord(record_buf[record_buf_pos+str_length]) + while (char != 0): + str_length += 1 + char = ord(record_buf[record_buf_pos+str_length]) + + if str_length > 0: + record['postal_code'] = record_buf[record_buf_pos:record_buf_pos+str_length] + else: + record['postal_code'] = None + + record_buf_pos += str_length + 1 + str_length = 0 + + latitude = 0 + longitude = 0 for j in range(3): - char = ord(buf[buf_pos]) - buf_pos += 1 + char = ord(record_buf[record_buf_pos]) + record_buf_pos += 1 latitude += (char << (j * 8)) + record['latitude'] = (latitude/10000.0) - 180.0 + for j in range(3): - char = ord(buf[buf_pos]) - buf_pos += 1 + char = ord(record_buf[record_buf_pos]) + record_buf_pos += 1 longitude += (char << (j * 8)) - record['latitude'] = (latitude / 10000.0) - 180.0 - record['longitude'] = (longitude / 10000.0) - 180.0 + record['longitude'] = (longitude/10000.0) - 180.0 - if self._databaseType in (const.CITY_EDITION_REV1, const.CITY_EDITION_REV1_V6): + if self._databaseType == const.CITY_EDITION_REV1: dmaarea_combo = 0 if record['country_code'] == 'US': for j in range(3): - char = ord(buf[buf_pos]) - dmaarea_combo += (char << (j * 8)) - buf_pos += 1 + char = ord(record_buf[record_buf_pos]) + record_buf_pos += 1 + dmaarea_combo += (char << (j*8)) - record['dma_code'] = int(math.floor(dmaarea_combo / 1000)) - record['area_code'] = dmaarea_combo % 1000 + record['dma_code'] = int(math.floor(dmaarea_combo/1000)) + record['area_code'] = dmaarea_combo%1000 + else: + record['dma_code'] = 0 + record['area_code'] = 0 - record['metro_code'] = const.DMA_MAP.get(record['dma_code']) - params = (record['country_code'], record['region_name']) - record['time_zone'] = time_zone_by_country_and_region(*params) + if 'dma_code' in record and record['dma_code'] in const.DMA_MAP: + record['metro_code'] = const.DMA_MAP[record['dma_code']] + else: + record['metro_code'] = '' + + if 'country_code' in record: + record['time_zone'] = time_zone_by_country_and_region( + record['country_code'], record.get('region_name')) or '' + else: + record['time_zone'] = '' return record - def _gethostbyname(self, hostname): - if self._databaseType in const.IPV6_EDITIONS: - try: - response = socket.getaddrinfo(hostname, 0, socket.AF_INET6) - family, socktype, proto, canonname, sockaddr = response[0] - address, port, flow, scope = sockaddr - return address - except socket.gaierror: - return '' - else: - return socket.gethostbyname(hostname) - - def id_by_addr(self, addr): - """ - Get the country index. - Looks up the index for the country which is the key for - the code and name. - - @param addr: The IP address - @type addr: str - @return: network byte order 32-bit integer - @rtype: int - """ - ipnum = util.ip2long(addr) - if not ipnum: - raise ValueError("Invalid IP address: %s" % addr) - - COUNTY_EDITIONS = (const.COUNTRY_EDITION, const.COUNTRY_EDITION_V6) - if self._databaseType not in COUNTY_EDITIONS: - message = 'Invalid database type, expected Country' - raise GeoIPError(message) - - return self._seek_country(ipnum) - const.COUNTRY_BEGIN - def country_code_by_addr(self, addr): """ Returns 2-letter country code (e.g. 'US') for specified IP address. @@ -444,38 +427,31 @@ class GeoIP(GeoIPBase): @rtype: str """ try: - VALID_EDITIONS = (const.COUNTRY_EDITION, const.COUNTRY_EDITION_V6) - if self._databaseType in VALID_EDITIONS: - ipv = 6 if addr.find(':') >= 0 else 4 - - if ipv == 4 and self._databaseType != const.COUNTRY_EDITION: - message = 'Invalid database type; expected IPv6 address' - raise ValueError(message) - if ipv == 6 and self._databaseType != const.COUNTRY_EDITION_V6: - message = 'Invalid database type; expected IPv4 address' - raise ValueError(message) - - country_id = self.id_by_addr(addr) + if self._databaseType == const.COUNTRY_EDITION: + country_id = self._lookup_country_id(addr) return const.COUNTRY_CODES[country_id] - elif self._databaseType in const.REGION_CITY_EDITIONS: - return self.region_by_addr(addr).get('country_code') + elif self._databaseType in (const.REGION_EDITION_REV0, const.REGION_EDITION_REV1, + const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): + return self.region_by_addr(addr)['country_code'] + else: + raise GeoIPError('Invalid database type; country_* methods expect '\ + 'Country, City, or Region database') - message = 'Invalid database type, expected Country, City or Region' - raise GeoIPError(message) except ValueError: - raise GeoIPError('Failed to lookup address %s' % addr) + raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def country_code_by_name(self, hostname): """ Returns 2-letter country code (e.g. 'US') for specified hostname. Use this method if you have a Country, Region, or City database. - @param hostname: Hostname + @param hostname: host name @type hostname: str @return: 2-letter country code @rtype: str """ - addr = self._gethostbyname(hostname) + addr = socket.gethostbyname(hostname) + return self.country_code_by_addr(addr) def country_name_by_addr(self, addr): @@ -489,35 +465,34 @@ class GeoIP(GeoIPBase): @rtype: str """ try: - VALID_EDITIONS = (const.COUNTRY_EDITION, const.COUNTRY_EDITION_V6) - if self._databaseType in VALID_EDITIONS: - country_id = self.id_by_addr(addr) + if self._databaseType == const.COUNTRY_EDITION: + country_id = self._lookup_country_id(addr) return const.COUNTRY_NAMES[country_id] - elif self._databaseType in const.CITY_EDITIONS: - return self.record_by_addr(addr).get('country_name') + elif self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): + return self.record_by_addr(addr)['country_name'] else: - message = 'Invalid database type, expected Country or City' - raise GeoIPError(message) + raise GeoIPError('Invalid database type; country_* methods expect '\ + 'Country or City database') except ValueError: - raise GeoIPError('Failed to lookup address %s' % addr) + raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def country_name_by_name(self, hostname): """ Returns full country name for specified hostname. Use this method if you have a Country database. - @param hostname: Hostname + @param hostname: host name @type hostname: str @return: country name @rtype: str """ - addr = self._gethostbyname(hostname) + addr = socket.gethostbyname(hostname) return self.country_name_by_addr(addr) def org_by_addr(self, addr): """ - Lookup Organization, ISP or ASNum for given IP address. - Use this method if you have an Organization, ISP or ASNum database. + Lookup the organization (or ISP) for given IP address. + Use this method if you have an Organization/ISP database. @param addr: IP address @type addr: str @@ -525,30 +500,31 @@ class GeoIP(GeoIPBase): @rtype: str """ try: - ipnum = util.ip2long(addr) - if not ipnum: - raise ValueError('Invalid IP address') + ipnum = ip2long(addr) - valid = (const.ORG_EDITION, const.ISP_EDITION, const.ASNUM_EDITION, const.ASNUM_EDITION_V6) - if self._databaseType not in valid: - message = 'Invalid database type, expected Org, ISP or ASNum' - raise GeoIPError(message) + if not ipnum: + raise ValueError("Invalid IP address: %s" % addr) + + if self._databaseType not in (const.ORG_EDITION, const.ISP_EDITION, const.ASNUM_EDITION): + raise GeoIPError('Invalid database type; org_* methods expect '\ + 'Org/ISP database') return self._get_org(ipnum) except ValueError: - raise GeoIPError('Failed to lookup address %s' % addr) + raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def org_by_name(self, hostname): """ Lookup the organization (or ISP) for hostname. Use this method if you have an Organization/ISP database. - @param hostname: Hostname + @param hostname: host name @type hostname: str - @return: Organization or ISP name + @return: organization or ISP name @rtype: str """ - addr = self._gethostbyname(hostname) + addr = socket.gethostbyname(hostname) + return self.org_by_addr(addr) def record_by_addr(self, addr): @@ -558,41 +534,38 @@ class GeoIP(GeoIPBase): @param addr: IP address @type addr: str - @return: Dictionary with country_code, country_code3, country_name, - region, city, postal_code, latitude, longitude, dma_code, - metro_code, area_code, region_name, time_zone + @return: dict with country_code, country_code3, country_name, + region, city, postal_code, latitude, longitude, + dma_code, metro_code, area_code, region_name, time_zone @rtype: dict """ try: - ipnum = util.ip2long(addr) + ipnum = ip2long(addr) + if not ipnum: - raise ValueError('Invalid IP address') + raise ValueError("Invalid IP address: %s" % addr) - if self._databaseType not in const.CITY_EDITIONS: - message = 'Invalid database type, expected City' - raise GeoIPError(message) + if not self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): + raise GeoIPError('Invalid database type; record_* methods expect City database') - rec = self._get_record(ipnum) - if not rec: - return None - - return rec + return self._get_record(ipnum) except ValueError: - raise GeoIPError('Failed to lookup address %s' % addr) + raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def record_by_name(self, hostname): """ Look up the record for a given hostname. Use this method if you have a City database. - @param hostname: Hostname + @param hostname: host name @type hostname: str - @return: Dictionary with country_code, country_code3, country_name, - region, city, postal_code, latitude, longitude, dma_code, - metro_code, area_code, region_name, time_zone + @return: dict with country_code, country_code3, country_name, + region, city, postal_code, latitude, longitude, + dma_code, metro_code, area_code, region_name, time_zone @rtype: dict """ - addr = self._gethostbyname(hostname) + addr = socket.gethostbyname(hostname) + return self.record_by_addr(addr) def region_by_addr(self, addr): @@ -602,33 +575,37 @@ class GeoIP(GeoIPBase): @param addr: IP address @type addr: str - @return: Dictionary containing country_code, region and region_name + @return: dict containing country_code, region, + and region_name @rtype: dict """ try: - ipnum = util.ip2long(addr) - if not ipnum: - raise ValueError('Invalid IP address') + ipnum = ip2long(addr) - if self._databaseType not in const.REGION_CITY_EDITIONS: - message = 'Invalid database type, expected Region or City' - raise GeoIPError(message) + if not ipnum: + raise ValueError("Invalid IP address: %s" % addr) + + if not self._databaseType in (const.REGION_EDITION_REV0, const.REGION_EDITION_REV1, + const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): + raise GeoIPError('Invalid database type; region_* methods expect '\ + 'Region or City database') return self._get_region(ipnum) except ValueError: - raise GeoIPError('Failed to lookup address %s' % addr) + raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def region_by_name(self, hostname): """ Lookup the region for given hostname. Use this method if you have a Region database. - @param hostname: Hostname + @param hostname: host name @type hostname: str - @return: Dictionary containing country_code, region, and region_name + @return: dict containing country_code, region, + and region_name @rtype: dict """ - addr = self._gethostbyname(hostname) + addr = socket.gethostbyname(hostname) return self.region_by_addr(addr) def time_zone_by_addr(self, addr): @@ -636,33 +613,35 @@ class GeoIP(GeoIPBase): Look up the time zone for a given IP address. Use this method if you have a Region or City database. - @param addr: IP address - @type addr: str + @param hostname: IP address + @type hostname: str @return: Time zone @rtype: str """ try: - ipnum = util.ip2long(addr) + ipnum = ip2long(addr) + if not ipnum: - raise ValueError('Invalid IP address') + raise ValueError("Invalid IP address: %s" % addr) - if self._databaseType not in const.CITY_EDITIONS: - message = 'Invalid database type, expected City' - raise GeoIPError(message) + if not self._databaseType in (const.REGION_EDITION_REV0, const.REGION_EDITION_REV1, + const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): + raise GeoIPError('Invalid database type; region_* methods expect '\ + 'Region or City database') - return self._get_record(ipnum).get('time_zone') + return self._get_record(ipnum)['time_zone'] except ValueError: - raise GeoIPError('Failed to lookup address %s' % addr) + raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def time_zone_by_name(self, hostname): """ Look up the time zone for a given hostname. Use this method if you have a Region or City database. - @param hostname: Hostname + @param hostname: host name @type hostname: str @return: Time zone @rtype: str """ - addr = self._gethostbyname(hostname) + addr = socket.gethostbyname(hostname) return self.time_zone_by_addr(addr) diff --git a/lib/pygeoip/const.py b/lib/pygeoip/const.py index b37b045..a215226 100644 --- a/lib/pygeoip/const.py +++ b/lib/pygeoip/const.py @@ -1,431 +1,382 @@ -# -*- coding: utf-8 -*- -""" -Constants needed for the binary parser. Part of the pygeoip package. - -@author: Jennifer Ennis <zaylea@gmail.com> - -@license: Copyright(C) 2004 MaxMind LLC - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>. -""" - -from platform import python_version_tuple - -PY2 = python_version_tuple()[0] == '2' -PY3 = python_version_tuple()[0] == '3' - -GEOIP_STANDARD = 0 -GEOIP_MEMORY_CACHE = 1 - -DMA_MAP = { - 500: 'Portland-Auburn, ME', - 501: 'New York, NY', - 502: 'Binghamton, NY', - 503: 'Macon, GA', - 504: 'Philadelphia, PA', - 505: 'Detroit, MI', - 506: 'Boston, MA', - 507: 'Savannah, GA', - 508: 'Pittsburgh, PA', - 509: 'Ft Wayne, IN', - 510: 'Cleveland, OH', - 511: 'Washington, DC', - 512: 'Baltimore, MD', - 513: 'Flint, MI', - 514: 'Buffalo, NY', - 515: 'Cincinnati, OH', - 516: 'Erie, PA', - 517: 'Charlotte, NC', - 518: 'Greensboro, NC', - 519: 'Charleston, SC', - 520: 'Augusta, GA', - 521: 'Providence, RI', - 522: 'Columbus, GA', - 523: 'Burlington, VT', - 524: 'Atlanta, GA', - 525: 'Albany, GA', - 526: 'Utica-Rome, NY', - 527: 'Indianapolis, IN', - 528: 'Miami, FL', - 529: 'Louisville, KY', - 530: 'Tallahassee, FL', - 531: 'Tri-Cities, TN', - 532: 'Albany-Schenectady-Troy, NY', - 533: 'Hartford, CT', - 534: 'Orlando, FL', - 535: 'Columbus, OH', - 536: 'Youngstown-Warren, OH', - 537: 'Bangor, ME', - 538: 'Rochester, NY', - 539: 'Tampa, FL', - 540: 'Traverse City-Cadillac, MI', - 541: 'Lexington, KY', - 542: 'Dayton, OH', - 543: 'Springfield-Holyoke, MA', - 544: 'Norfolk-Portsmouth, VA', - 545: 'Greenville-New Bern-Washington, NC', - 546: 'Columbia, SC', - 547: 'Toledo, OH', - 548: 'West Palm Beach, FL', - 549: 'Watertown, NY', - 550: 'Wilmington, NC', - 551: 'Lansing, MI', - 552: 'Presque Isle, ME', - 553: 'Marquette, MI', - 554: 'Wheeling, WV', - 555: 'Syracuse, NY', - 556: 'Richmond-Petersburg, VA', - 557: 'Knoxville, TN', - 558: 'Lima, OH', - 559: 'Bluefield-Beckley-Oak Hill, WV', - 560: 'Raleigh-Durham, NC', - 561: 'Jacksonville, FL', - 563: 'Grand Rapids, MI', - 564: 'Charleston-Huntington, WV', - 565: 'Elmira, NY', - 566: 'Harrisburg-Lancaster-Lebanon-York, PA', - 567: 'Greenville-Spartenburg, SC', - 569: 'Harrisonburg, VA', - 570: 'Florence-Myrtle Beach, SC', - 571: 'Ft Myers, FL', - 573: 'Roanoke-Lynchburg, VA', - 574: 'Johnstown-Altoona, PA', - 575: 'Chattanooga, TN', - 576: 'Salisbury, MD', - 577: 'Wilkes Barre-Scranton, PA', - 581: 'Terre Haute, IN', - 582: 'Lafayette, IN', - 583: 'Alpena, MI', - 584: 'Charlottesville, VA', - 588: 'South Bend, IN', - 592: 'Gainesville, FL', - 596: 'Zanesville, OH', - 597: 'Parkersburg, WV', - 598: 'Clarksburg-Weston, WV', - 600: 'Corpus Christi, TX', - 602: 'Chicago, IL', - 603: 'Joplin-Pittsburg, MO', - 604: 'Columbia-Jefferson City, MO', - 605: 'Topeka, KS', - 606: 'Dothan, AL', - 609: 'St Louis, MO', - 610: 'Rockford, IL', - 611: 'Rochester-Mason City-Austin, MN', - 612: 'Shreveport, LA', - 613: 'Minneapolis-St Paul, MN', - 616: 'Kansas City, MO', - 617: 'Milwaukee, WI', - 618: 'Houston, TX', - 619: 'Springfield, MO', - 620: 'Tuscaloosa, AL', - 622: 'New Orleans, LA', - 623: 'Dallas-Fort Worth, TX', - 624: 'Sioux City, IA', - 625: 'Waco-Temple-Bryan, TX', - 626: 'Victoria, TX', - 627: 'Wichita Falls, TX', - 628: 'Monroe, LA', - 630: 'Birmingham, AL', - 631: 'Ottumwa-Kirksville, IA', - 632: 'Paducah, KY', - 633: 'Odessa-Midland, TX', - 634: 'Amarillo, TX', - 635: 'Austin, TX', - 636: 'Harlingen, TX', - 637: 'Cedar Rapids-Waterloo, IA', - 638: 'St Joseph, MO', - 639: 'Jackson, TN', - 640: 'Memphis, TN', - 641: 'San Antonio, TX', - 642: 'Lafayette, LA', - 643: 'Lake Charles, LA', - 644: 'Alexandria, LA', - 646: 'Anniston, AL', - 647: 'Greenwood-Greenville, MS', - 648: 'Champaign-Springfield-Decatur, IL', - 649: 'Evansville, IN', - 650: 'Oklahoma City, OK', - 651: 'Lubbock, TX', - 652: 'Omaha, NE', - 656: 'Panama City, FL', - 657: 'Sherman, TX', - 658: 'Green Bay-Appleton, WI', - 659: 'Nashville, TN', - 661: 'San Angelo, TX', - 662: 'Abilene-Sweetwater, TX', - 669: 'Madison, WI', - 670: 'Ft Smith-Fay-Springfield, AR', - 671: 'Tulsa, OK', - 673: 'Columbus-Tupelo-West Point, MS', - 675: 'Peoria-Bloomington, IL', - 676: 'Duluth, MN', - 678: 'Wichita, KS', - 679: 'Des Moines, IA', - 682: 'Davenport-Rock Island-Moline, IL', - 686: 'Mobile, AL', - 687: 'Minot-Bismarck-Dickinson, ND', - 691: 'Huntsville, AL', - 692: 'Beaumont-Port Author, TX', - 693: 'Little Rock-Pine Bluff, AR', - 698: 'Montgomery, AL', - 702: 'La Crosse-Eau Claire, WI', - 705: 'Wausau-Rhinelander, WI', - 709: 'Tyler-Longview, TX', - 710: 'Hattiesburg-Laurel, MS', - 711: 'Meridian, MS', - 716: 'Baton Rouge, LA', - 717: 'Quincy, IL', - 718: 'Jackson, MS', - 722: 'Lincoln-Hastings, NE', - 724: 'Fargo-Valley City, ND', - 725: 'Sioux Falls, SD', - 734: 'Jonesboro, AR', - 736: 'Bowling Green, KY', - 737: 'Mankato, MN', - 740: 'North Platte, NE', - 743: 'Anchorage, AK', - 744: 'Honolulu, HI', - 745: 'Fairbanks, AK', - 746: 'Biloxi-Gulfport, MS', - 747: 'Juneau, AK', - 749: 'Laredo, TX', - 751: 'Denver, CO', - 752: 'Colorado Springs, CO', - 753: 'Phoenix, AZ', - 754: 'Butte-Bozeman, MT', - 755: 'Great Falls, MT', - 756: 'Billings, MT', - 757: 'Boise, ID', - 758: 'Idaho Falls-Pocatello, ID', - 759: 'Cheyenne, WY', - 760: 'Twin Falls, ID', - 762: 'Missoula, MT', - 764: 'Rapid City, SD', - 765: 'El Paso, TX', - 766: 'Helena, MT', - 767: 'Casper-Riverton, WY', - 770: 'Salt Lake City, UT', - 771: 'Yuma, AZ', - 773: 'Grand Junction, CO', - 789: 'Tucson, AZ', - 790: 'Albuquerque, NM', - 798: 'Glendive, MT', - 800: 'Bakersfield, CA', - 801: 'Eugene, OR', - 802: 'Eureka, CA', - 803: 'Los Angeles, CA', - 804: 'Palm Springs, CA', - 807: 'San Francisco, CA', - 810: 'Yakima-Pasco, WA', - 811: 'Reno, NV', - 813: 'Medford-Klamath Falls, OR', - 819: 'Seattle-Tacoma, WA', - 820: 'Portland, OR', - 821: 'Bend, OR', - 825: 'San Diego, CA', - 828: 'Monterey-Salinas, CA', - 839: 'Las Vegas, NV', - 855: 'Santa Barbara, CA', - 862: 'Sacramento, CA', - 866: 'Fresno, CA', - 868: 'Chico-Redding, CA', - 881: 'Spokane, WA' -} - -COUNTRY_CODES = ( - '', - 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', - 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', - 'BH', 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', - 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', - 'CO', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', - 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', - 'FO', 'FR', 'FX', 'GA', 'GB', 'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', - 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', - 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', - 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', - 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', - 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', - 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', - 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', - 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', - 'PY', 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', - 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', - 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', - 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', - 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', - 'ZM', 'ME', 'ZW', 'A1', 'A2', 'O1', 'AX', 'GG', 'IM', 'JE', 'BL', 'MF', - 'BQ', 'SS' -) - -COUNTRY_CODES3 = ( - '', 'AP', 'EU', 'AND', 'ARE', 'AFG', 'ATG', 'AIA', 'ALB', 'ARM', 'ANT', - 'AGO', 'AQ', 'ARG', 'ASM', 'AUT', 'AUS', 'ABW', 'AZE', 'BIH', 'BRB', 'BGD', - 'BEL', 'BFA', 'BGR', 'BHR', 'BDI', 'BEN', 'BMU', 'BRN', 'BOL', 'BRA', - 'BHS', 'BTN', 'BV', 'BWA', 'BLR', 'BLZ', 'CAN', 'CC', 'COD', 'CAF', 'COG', - 'CHE', 'CIV', 'COK', 'CHL', 'CMR', 'CHN', 'COL', 'CRI', 'CUB', 'CPV', 'CX', - 'CYP', 'CZE', 'DEU', 'DJI', 'DNK', 'DMA', 'DOM', 'DZA', 'ECU', 'EST', - 'EGY', 'ESH', 'ERI', 'ESP', 'ETH', 'FIN', 'FJI', 'FLK', 'FSM', 'FRO', - 'FRA', 'FX', 'GAB', 'GBR', 'GRD', 'GEO', 'GUF', 'GHA', 'GIB', 'GRL', 'GMB', - 'GIN', 'GLP', 'GNQ', 'GRC', 'GS', 'GTM', 'GUM', 'GNB', 'GUY', 'HKG', 'HM', - 'HND', 'HRV', 'HTI', 'HUN', 'IDN', 'IRL', 'ISR', 'IND', 'IO', 'IRQ', 'IRN', - 'ISL', 'ITA', 'JAM', 'JOR', 'JPN', 'KEN', 'KGZ', 'KHM', 'KIR', 'COM', - 'KNA', 'PRK', 'KOR', 'KWT', 'CYM', 'KAZ', 'LAO', 'LBN', 'LCA', 'LIE', - 'LKA', 'LBR', 'LSO', 'LTU', 'LUX', 'LVA', 'LBY', 'MAR', 'MCO', 'MDA', - 'MDG', 'MHL', 'MKD', 'MLI', 'MMR', 'MNG', 'MAC', 'MNP', 'MTQ', 'MRT', - 'MSR', 'MLT', 'MUS', 'MDV', 'MWI', 'MEX', 'MYS', 'MOZ', 'NAM', 'NCL', - 'NER', 'NFK', 'NGA', 'NIC', 'NLD', 'NOR', 'NPL', 'NRU', 'NIU', 'NZL', - 'OMN', 'PAN', 'PER', 'PYF', 'PNG', 'PHL', 'PAK', 'POL', 'SPM', 'PCN', - 'PRI', 'PSE', 'PRT', 'PLW', 'PRY', 'QAT', 'REU', 'ROU', 'RUS', 'RWA', - 'SAU', 'SLB', 'SYC', 'SDN', 'SWE', 'SGP', 'SHN', 'SVN', 'SJM', 'SVK', - 'SLE', 'SMR', 'SEN', 'SOM', 'SUR', 'STP', 'SLV', 'SYR', 'SWZ', 'TCA', - 'TCD', 'TF', 'TGO', 'THA', 'TJK', 'TKL', 'TLS', 'TKM', 'TUN', 'TON', 'TUR', - 'TTO', 'TUV', 'TWN', 'TZA', 'UKR', 'UGA', 'UM', 'USA', 'URY', 'UZB', 'VAT', - 'VCT', 'VEN', 'VGB', 'VIR', 'VNM', 'VUT', 'WLF', 'WSM', 'YEM', 'YT', 'SRB', - 'ZAF', 'ZMB', 'MNE', 'ZWE', 'A1', 'A2', 'O1', 'ALA', 'GGY', 'IMN', 'JEY', - 'BLM', 'MAF', 'BES', 'SSD' -) - -COUNTRY_NAMES = ( - '', 'Asia/Pacific Region', 'Europe', 'Andorra', 'United Arab Emirates', - 'Afghanistan', 'Antigua and Barbuda', 'Anguilla', 'Albania', 'Armenia', - 'Netherlands Antilles', 'Angola', 'Antarctica', 'Argentina', - 'American Samoa', 'Austria', 'Australia', 'Aruba', 'Azerbaijan', - 'Bosnia and Herzegovina', 'Barbados', 'Bangladesh', 'Belgium', - 'Burkina Faso', 'Bulgaria', 'Bahrain', 'Burundi', 'Benin', 'Bermuda', - 'Brunei Darussalam', 'Bolivia', 'Brazil', 'Bahamas', 'Bhutan', - 'Bouvet Island', 'Botswana', 'Belarus', 'Belize', 'Canada', - 'Cocos (Keeling) Islands', 'Congo, The Democratic Republic of the', - 'Central African Republic', 'Congo', 'Switzerland', 'Cote D\'Ivoire', - 'Cook Islands', 'Chile', 'Cameroon', 'China', 'Colombia', 'Costa Rica', - 'Cuba', 'Cape Verde', 'Christmas Island', 'Cyprus', 'Czech Republic', - 'Germany', 'Djibouti', 'Denmark', 'Dominica', 'Dominican Republic', - 'Algeria', 'Ecuador', 'Estonia', 'Egypt', 'Western Sahara', 'Eritrea', - 'Spain', 'Ethiopia', 'Finland', 'Fiji', 'Falkland Islands (Malvinas)', - 'Micronesia, Federated States of', 'Faroe Islands', 'France', - 'France, Metropolitan', 'Gabon', 'United Kingdom', 'Grenada', 'Georgia', - 'French Guiana', 'Ghana', 'Gibraltar', 'Greenland', 'Gambia', 'Guinea', - 'Guadeloupe', 'Equatorial Guinea', 'Greece', - 'South Georgia and the South Sandwich Islands', 'Guatemala', 'Guam', - 'Guinea-Bissau', 'Guyana', 'Hong Kong', - 'Heard Island and McDonald Islands', 'Honduras', 'Croatia', 'Haiti', - 'Hungary', 'Indonesia', 'Ireland', 'Israel', 'India', - 'British Indian Ocean Territory', 'Iraq', 'Iran, Islamic Republic of', - 'Iceland', 'Italy', 'Jamaica', 'Jordan', 'Japan', 'Kenya', 'Kyrgyzstan', - 'Cambodia', 'Kiribati', 'Comoros', 'Saint Kitts and Nevis', - 'Korea, Democratic People\'s Republic of', 'Korea, Republic of', 'Kuwait', - 'Cayman Islands', 'Kazakhstan', 'Lao People\'s Democratic Republic', - 'Lebanon', 'Saint Lucia', 'Liechtenstein', 'Sri Lanka', 'Liberia', - 'Lesotho', 'Lithuania', 'Luxembourg', 'Latvia', 'Libya', 'Morocco', - 'Monaco', 'Moldova, Republic of', 'Madagascar', 'Marshall Islands', - 'Macedonia', 'Mali', 'Myanmar', 'Mongolia', 'Macau', - 'Northern Mariana Islands', 'Martinique', 'Mauritania', 'Montserrat', - 'Malta', 'Mauritius', 'Maldives', 'Malawi', 'Mexico', 'Malaysia', - 'Mozambique', 'Namibia', 'New Caledonia', 'Niger', 'Norfolk Island', - 'Nigeria', 'Nicaragua', 'Netherlands', 'Norway', 'Nepal', 'Nauru', 'Niue', - 'New Zealand', 'Oman', 'Panama', 'Peru', 'French Polynesia', - 'Papua New Guinea', 'Philippines', 'Pakistan', 'Poland', - 'Saint Pierre and Miquelon', 'Pitcairn Islands', 'Puerto Rico', - 'Palestinian Territory', 'Portugal', 'Palau', 'Paraguay', 'Qatar', - 'Reunion', 'Romania', 'Russian Federation', 'Rwanda', 'Saudi Arabia', - 'Solomon Islands', 'Seychelles', 'Sudan', 'Sweden', 'Singapore', - 'Saint Helena', 'Slovenia', 'Svalbard and Jan Mayen', 'Slovakia', - 'Sierra Leone', 'San Marino', 'Senegal', 'Somalia', 'Suriname', - 'Sao Tome and Principe', 'El Salvador', 'Syrian Arab Republic', - 'Swaziland', 'Turks and Caicos Islands', 'Chad', - 'French Southern Territories', 'Togo', 'Thailand', 'Tajikistan', 'Tokelau', - 'Turkmenistan', 'Tunisia', 'Tonga', 'Timor-Leste', 'Turkey', - 'Trinidad and Tobago', 'Tuvalu', 'Taiwan', 'Tanzania, United Republic of', - 'Ukraine', 'Uganda', 'United States Minor Outlying Islands', - 'United States', 'Uruguay', 'Uzbekistan', 'Holy See (Vatican City State)', - 'Saint Vincent and the Grenadines', 'Venezuela', 'Virgin Islands, British', - 'Virgin Islands, U.S.', 'Vietnam', 'Vanuatu', 'Wallis and Futuna', 'Samoa', - 'Yemen', 'Mayotte', 'Serbia', 'South Africa', 'Zambia', 'Montenegro', - 'Zimbabwe', 'Anonymous Proxy', 'Satellite Provider', 'Other', - 'Aland Islands', 'Guernsey', 'Isle of Man', 'Jersey', 'Saint Barthelemy', - 'Saint Martin', 'Bonaire, Sint Eustatius and Saba', 'South Sudan' -) - -CONTINENT_NAMES = ( - '--', 'AS', 'EU', 'EU', 'AS', 'AS', 'NA', 'NA', 'EU', 'AS', 'NA', 'AF', - 'AN', 'SA', 'OC', 'EU', 'OC', 'NA', 'AS', 'EU', 'NA', 'AS', 'EU', 'AF', - 'EU', 'AS', 'AF', 'AF', 'NA', 'AS', 'SA', 'SA', 'NA', 'AS', 'AN', 'AF', - 'EU', 'NA', 'NA', 'AS', 'AF', 'AF', 'AF', 'EU', 'AF', 'OC', 'SA', 'AF', - 'AS', 'SA', 'NA', 'NA', 'AF', 'AS', 'AS', 'EU', 'EU', 'AF', 'EU', 'NA', - 'NA', 'AF', 'SA', 'EU', 'AF', 'AF', 'AF', 'EU', 'AF', 'EU', 'OC', 'SA', - 'OC', 'EU', 'EU', 'NA', 'AF', 'EU', 'NA', 'AS', 'SA', 'AF', 'EU', 'NA', - 'AF', 'AF', 'NA', 'AF', 'EU', 'AN', 'NA', 'OC', 'AF', 'SA', 'AS', 'AN', - 'NA', 'EU', 'NA', 'EU', 'AS', 'EU', 'AS', 'AS', 'AS', 'AS', 'AS', 'EU', - 'EU', 'NA', 'AS', 'AS', 'AF', 'AS', 'AS', 'OC', 'AF', 'NA', 'AS', 'AS', - 'AS', 'NA', 'AS', 'AS', 'AS', 'NA', 'EU', 'AS', 'AF', 'AF', 'EU', 'EU', - 'EU', 'AF', 'AF', 'EU', 'EU', 'AF', 'OC', 'EU', 'AF', 'AS', 'AS', 'AS', - 'OC', 'NA', 'AF', 'NA', 'EU', 'AF', 'AS', 'AF', 'NA', 'AS', 'AF', 'AF', - 'OC', 'AF', 'OC', 'AF', 'NA', 'EU', 'EU', 'AS', 'OC', 'OC', 'OC', 'AS', - 'NA', 'SA', 'OC', 'OC', 'AS', 'AS', 'EU', 'NA', 'OC', 'NA', 'AS', 'EU', - 'OC', 'SA', 'AS', 'AF', 'EU', 'EU', 'AF', 'AS', 'OC', 'AF', 'AF', 'EU', - 'AS', 'AF', 'EU', 'EU', 'EU', 'AF', 'EU', 'AF', 'AF', 'SA', 'AF', 'NA', - 'AS', 'AF', 'NA', 'AF', 'AN', 'AF', 'AS', 'AS', 'OC', 'AS', 'AF', 'OC', - 'AS', 'EU', 'NA', 'OC', 'AS', 'AF', 'EU', 'AF', 'OC', 'NA', 'SA', 'AS', - 'EU', 'NA', 'SA', 'NA', 'NA', 'AS', 'OC', 'OC', 'OC', 'AS', 'AF', 'EU', - 'AF', 'AF', 'EU', 'AF', '--', '--', '--', 'EU', 'EU', 'EU', 'EU', 'NA', - 'NA', 'NA', 'AF' -) - -# storage / caching flags -STANDARD = 0 -MEMORY_CACHE = 1 -MMAP_CACHE = 8 - -# Database structure constants -COUNTRY_BEGIN = 16776960 -STATE_BEGIN_REV0 = 16700000 -STATE_BEGIN_REV1 = 16000000 - -STRUCTURE_INFO_MAX_SIZE = 20 -DATABASE_INFO_MAX_SIZE = 100 - -# Database editions -COUNTRY_EDITION = 1 -COUNTRY_EDITION_V6 = 12 -REGION_EDITION_REV0 = 7 -REGION_EDITION_REV1 = 3 -CITY_EDITION_REV0 = 6 -CITY_EDITION_REV1 = 2 -CITY_EDITION_REV1_V6 = 30 -ORG_EDITION = 5 -ISP_EDITION = 4 -ASNUM_EDITION = 9 -ASNUM_EDITION_V6 = 21 -# Not yet supported databases -PROXY_EDITION = 8 -NETSPEED_EDITION = 11 - -# Collection of databases -IPV6_EDITIONS = (COUNTRY_EDITION_V6, ASNUM_EDITION_V6, CITY_EDITION_REV1_V6) -CITY_EDITIONS = (CITY_EDITION_REV0, CITY_EDITION_REV1, CITY_EDITION_REV1_V6) -REGION_EDITIONS = (REGION_EDITION_REV0, REGION_EDITION_REV1) -REGION_CITY_EDITIONS = REGION_EDITIONS + CITY_EDITIONS - -SEGMENT_RECORD_LENGTH = 3 -STANDARD_RECORD_LENGTH = 3 -ORG_RECORD_LENGTH = 4 -MAX_RECORD_LENGTH = 4 -MAX_ORG_RECORD_LENGTH = 300 -FULL_RECORD_LENGTH = 50 - -US_OFFSET = 1 -CANADA_OFFSET = 677 -WORLD_OFFSET = 1353 -FIPS_RANGE = 360 -ENCODING = 'iso-8859-1' +""" +Constants needed for parsing binary GeoIP databases. It is part of the pygeoip +package. + +@author: Jennifer Ennis <zaylea at gmail dot com> + +@license: +Copyright(C) 2004 MaxMind LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>. +""" + +GEOIP_STANDARD = 0 +GEOIP_MEMORY_CACHE = 1 + +DMA_MAP = { + 500 : 'Portland-Auburn, ME', + 501 : 'New York, NY', + 502 : 'Binghamton, NY', + 503 : 'Macon, GA', + 504 : 'Philadelphia, PA', + 505 : 'Detroit, MI', + 506 : 'Boston, MA', + 507 : 'Savannah, GA', + 508 : 'Pittsburgh, PA', + 509 : 'Ft Wayne, IN', + 510 : 'Cleveland, OH', + 511 : 'Washington, DC', + 512 : 'Baltimore, MD', + 513 : 'Flint, MI', + 514 : 'Buffalo, NY', + 515 : 'Cincinnati, OH', + 516 : 'Erie, PA', + 517 : 'Charlotte, NC', + 518 : 'Greensboro, NC', + 519 : 'Charleston, SC', + 520 : 'Augusta, GA', + 521 : 'Providence, RI', + 522 : 'Columbus, GA', + 523 : 'Burlington, VT', + 524 : 'Atlanta, GA', + 525 : 'Albany, GA', + 526 : 'Utica-Rome, NY', + 527 : 'Indianapolis, IN', + 528 : 'Miami, FL', + 529 : 'Louisville, KY', + 530 : 'Tallahassee, FL', + 531 : 'Tri-Cities, TN', + 532 : 'Albany-Schenectady-Troy, NY', + 533 : 'Hartford, CT', + 534 : 'Orlando, FL', + 535 : 'Columbus, OH', + 536 : 'Youngstown-Warren, OH', + 537 : 'Bangor, ME', + 538 : 'Rochester, NY', + 539 : 'Tampa, FL', + 540 : 'Traverse City-Cadillac, MI', + 541 : 'Lexington, KY', + 542 : 'Dayton, OH', + 543 : 'Springfield-Holyoke, MA', + 544 : 'Norfolk-Portsmouth, VA', + 545 : 'Greenville-New Bern-Washington, NC', + 546 : 'Columbia, SC', + 547 : 'Toledo, OH', + 548 : 'West Palm Beach, FL', + 549 : 'Watertown, NY', + 550 : 'Wilmington, NC', + 551 : 'Lansing, MI', + 552 : 'Presque Isle, ME', + 553 : 'Marquette, MI', + 554 : 'Wheeling, WV', + 555 : 'Syracuse, NY', + 556 : 'Richmond-Petersburg, VA', + 557 : 'Knoxville, TN', + 558 : 'Lima, OH', + 559 : 'Bluefield-Beckley-Oak Hill, WV', + 560 : 'Raleigh-Durham, NC', + 561 : 'Jacksonville, FL', + 563 : 'Grand Rapids, MI', + 564 : 'Charleston-Huntington, WV', + 565 : 'Elmira, NY', + 566 : 'Harrisburg-Lancaster-Lebanon-York, PA', + 567 : 'Greenville-Spartenburg, SC', + 569 : 'Harrisonburg, VA', + 570 : 'Florence-Myrtle Beach, SC', + 571 : 'Ft Myers, FL', + 573 : 'Roanoke-Lynchburg, VA', + 574 : 'Johnstown-Altoona, PA', + 575 : 'Chattanooga, TN', + 576 : 'Salisbury, MD', + 577 : 'Wilkes Barre-Scranton, PA', + 581 : 'Terre Haute, IN', + 582 : 'Lafayette, IN', + 583 : 'Alpena, MI', + 584 : 'Charlottesville, VA', + 588 : 'South Bend, IN', + 592 : 'Gainesville, FL', + 596 : 'Zanesville, OH', + 597 : 'Parkersburg, WV', + 598 : 'Clarksburg-Weston, WV', + 600 : 'Corpus Christi, TX', + 602 : 'Chicago, IL', + 603 : 'Joplin-Pittsburg, MO', + 604 : 'Columbia-Jefferson City, MO', + 605 : 'Topeka, KS', + 606 : 'Dothan, AL', + 609 : 'St Louis, MO', + 610 : 'Rockford, IL', + 611 : 'Rochester-Mason City-Austin, MN', + 612 : 'Shreveport, LA', + 613 : 'Minneapolis-St Paul, MN', + 616 : 'Kansas City, MO', + 617 : 'Milwaukee, WI', + 618 : 'Houston, TX', + 619 : 'Springfield, MO', + 620 : 'Tuscaloosa, AL', + 622 : 'New Orleans, LA', + 623 : 'Dallas-Fort Worth, TX', + 624 : 'Sioux City, IA', + 625 : 'Waco-Temple-Bryan, TX', + 626 : 'Victoria, TX', + 627 : 'Wichita Falls, TX', + 628 : 'Monroe, LA', + 630 : 'Birmingham, AL', + 631 : 'Ottumwa-Kirksville, IA', + 632 : 'Paducah, KY', + 633 : 'Odessa-Midland, TX', + 634 : 'Amarillo, TX', + 635 : 'Austin, TX', + 636 : 'Harlingen, TX', + 637 : 'Cedar Rapids-Waterloo, IA', + 638 : 'St Joseph, MO', + 639 : 'Jackson, TN', + 640 : 'Memphis, TN', + 641 : 'San Antonio, TX', + 642 : 'Lafayette, LA', + 643 : 'Lake Charles, LA', + 644 : 'Alexandria, LA', + 646 : 'Anniston, AL', + 647 : 'Greenwood-Greenville, MS', + 648 : 'Champaign-Springfield-Decatur, IL', + 649 : 'Evansville, IN', + 650 : 'Oklahoma City, OK', + 651 : 'Lubbock, TX', + 652 : 'Omaha, NE', + 656 : 'Panama City, FL', + 657 : 'Sherman, TX', + 658 : 'Green Bay-Appleton, WI', + 659 : 'Nashville, TN', + 661 : 'San Angelo, TX', + 662 : 'Abilene-Sweetwater, TX', + 669 : 'Madison, WI', + 670 : 'Ft Smith-Fay-Springfield, AR', + 671 : 'Tulsa, OK', + 673 : 'Columbus-Tupelo-West Point, MS', + 675 : 'Peoria-Bloomington, IL', + 676 : 'Duluth, MN', + 678 : 'Wichita, KS', + 679 : 'Des Moines, IA', + 682 : 'Davenport-Rock Island-Moline, IL', + 686 : 'Mobile, AL', + 687 : 'Minot-Bismarck-Dickinson, ND', + 691 : 'Huntsville, AL', + 692 : 'Beaumont-Port Author, TX', + 693 : 'Little Rock-Pine Bluff, AR', + 698 : 'Montgomery, AL', + 702 : 'La Crosse-Eau Claire, WI', + 705 : 'Wausau-Rhinelander, WI', + 709 : 'Tyler-Longview, TX', + 710 : 'Hattiesburg-Laurel, MS', + 711 : 'Meridian, MS', + 716 : 'Baton Rouge, LA', + 717 : 'Quincy, IL', + 718 : 'Jackson, MS', + 722 : 'Lincoln-Hastings, NE', + 724 : 'Fargo-Valley City, ND', + 725 : 'Sioux Falls, SD', + 734 : 'Jonesboro, AR', + 736 : 'Bowling Green, KY', + 737 : 'Mankato, MN', + 740 : 'North Platte, NE', + 743 : 'Anchorage, AK', + 744 : 'Honolulu, HI', + 745 : 'Fairbanks, AK', + 746 : 'Biloxi-Gulfport, MS', + 747 : 'Juneau, AK', + 749 : 'Laredo, TX', + 751 : 'Denver, CO', + 752 : 'Colorado Springs, CO', + 753 : 'Phoenix, AZ', + 754 : 'Butte-Bozeman, MT', + 755 : 'Great Falls, MT', + 756 : 'Billings, MT', + 757 : 'Boise, ID', + 758 : 'Idaho Falls-Pocatello, ID', + 759 : 'Cheyenne, WY', + 760 : 'Twin Falls, ID', + 762 : 'Missoula, MT', + 764 : 'Rapid City, SD', + 765 : 'El Paso, TX', + 766 : 'Helena, MT', + 767 : 'Casper-Riverton, WY', + 770 : 'Salt Lake City, UT', + 771 : 'Yuma, AZ', + 773 : 'Grand Junction, CO', + 789 : 'Tucson, AZ', + 790 : 'Albuquerque, NM', + 798 : 'Glendive, MT', + 800 : 'Bakersfield, CA', + 801 : 'Eugene, OR', + 802 : 'Eureka, CA', + 803 : 'Los Angeles, CA', + 804 : 'Palm Springs, CA', + 807 : 'San Francisco, CA', + 810 : 'Yakima-Pasco, WA', + 811 : 'Reno, NV', + 813 : 'Medford-Klamath Falls, OR', + 819 : 'Seattle-Tacoma, WA', + 820 : 'Portland, OR', + 821 : 'Bend, OR', + 825 : 'San Diego, CA', + 828 : 'Monterey-Salinas, CA', + 839 : 'Las Vegas, NV', + 855 : 'Santa Barbara, CA', + 862 : 'Sacramento, CA', + 866 : 'Fresno, CA', + 868 : 'Chico-Redding, CA', + 881 : 'Spokane, WA' + } + +COUNTRY_CODES = ( + '', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', + 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', + 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', + 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', + 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', + 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'FX', 'GA', 'GB', + 'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', + 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN', + 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', + 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', + 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', + 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', + 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', + 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', + 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', + 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD', + 'TF', 'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', 'TR', 'TT', 'TV', 'TW', + 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', + 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', 'ZM', 'ME', 'ZW', 'A1', 'A2', 'O1', + 'AX', 'GG', 'IM', 'JE', 'BL', 'MF' + ) + +COUNTRY_CODES3 = ( + '','AP','EU','AND','ARE','AFG','ATG','AIA','ALB','ARM','ANT','AGO','AQ','ARG', + 'ASM','AUT','AUS','ABW','AZE','BIH','BRB','BGD','BEL','BFA','BGR','BHR','BDI', + 'BEN','BMU','BRN','BOL','BRA','BHS','BTN','BV','BWA','BLR','BLZ','CAN','CC', + 'COD','CAF','COG','CHE','CIV','COK','CHL','CMR','CHN','COL','CRI','CUB','CPV', + 'CX','CYP','CZE','DEU','DJI','DNK','DMA','DOM','DZA','ECU','EST','EGY','ESH', + 'ERI','ESP','ETH','FIN','FJI','FLK','FSM','FRO','FRA','FX','GAB','GBR','GRD', + 'GEO','GUF','GHA','GIB','GRL','GMB','GIN','GLP','GNQ','GRC','GS','GTM','GUM', + 'GNB','GUY','HKG','HM','HND','HRV','HTI','HUN','IDN','IRL','ISR','IND','IO', + 'IRQ','IRN','ISL','ITA','JAM','JOR','JPN','KEN','KGZ','KHM','KIR','COM','KNA', + 'PRK','KOR','KWT','CYM','KAZ','LAO','LBN','LCA','LIE','LKA','LBR','LSO','LTU', + 'LUX','LVA','LBY','MAR','MCO','MDA','MDG','MHL','MKD','MLI','MMR','MNG','MAC', + 'MNP','MTQ','MRT','MSR','MLT','MUS','MDV','MWI','MEX','MYS','MOZ','NAM','NCL', + 'NER','NFK','NGA','NIC','NLD','NOR','NPL','NRU','NIU','NZL','OMN','PAN','PER', + 'PYF','PNG','PHL','PAK','POL','SPM','PCN','PRI','PSE','PRT','PLW','PRY','QAT', + 'REU','ROU','RUS','RWA','SAU','SLB','SYC','SDN','SWE','SGP','SHN','SVN','SJM', + 'SVK','SLE','SMR','SEN','SOM','SUR','STP','SLV','SYR','SWZ','TCA','TCD','TF', + 'TGO','THA','TJK','TKL','TLS','TKM','TUN','TON','TUR','TTO','TUV','TWN','TZA', + 'UKR','UGA','UM','USA','URY','UZB','VAT','VCT','VEN','VGB','VIR','VNM','VUT', + 'WLF','WSM','YEM','YT','SRB','ZAF','ZMB','MNE','ZWE','A1','A2','O1', + 'ALA','GGY','IMN','JEY','BLM','MAF' + ) + +COUNTRY_NAMES = ( + "", "Asia/Pacific Region", "Europe", "Andorra", "United Arab Emirates", + "Afghanistan", "Antigua and Barbuda", "Anguilla", "Albania", "Armenia", + "Netherlands Antilles", "Angola", "Antarctica", "Argentina", "American Samoa", + "Austria", "Australia", "Aruba", "Azerbaijan", "Bosnia and Herzegovina", + "Barbados", "Bangladesh", "Belgium", "Burkina Faso", "Bulgaria", "Bahrain", + "Burundi", "Benin", "Bermuda", "Brunei Darussalam", "Bolivia", "Brazil", + "Bahamas", "Bhutan", "Bouvet Island", "Botswana", "Belarus", "Belize", + "Canada", "Cocos (Keeling) Islands", "Congo, The Democratic Republic of the", + "Central African Republic", "Congo", "Switzerland", "Cote D'Ivoire", "Cook Islands", + "Chile", "Cameroon", "China", "Colombia", "Costa Rica", "Cuba", "Cape Verde", + "Christmas Island", "Cyprus", "Czech Republic", "Germany", "Djibouti", + "Denmark", "Dominica", "Dominican Republic", "Algeria", "Ecuador", "Estonia", + "Egypt", "Western Sahara", "Eritrea", "Spain", "Ethiopia", "Finland", "Fiji", + "Falkland Islands (Malvinas)", "Micronesia, Federated States of", "Faroe Islands", + "France", "France, Metropolitan", "Gabon", "United Kingdom", + "Grenada", "Georgia", "French Guiana", "Ghana", "Gibraltar", "Greenland", + "Gambia", "Guinea", "Guadeloupe", "Equatorial Guinea", "Greece", + "South Georgia and the South Sandwich Islands", + "Guatemala", "Guam", "Guinea-Bissau", + "Guyana", "Hong Kong", "Heard Island and McDonald Islands", "Honduras", + "Croatia", "Haiti", "Hungary", "Indonesia", "Ireland", "Israel", "India", + "British Indian Ocean Territory", "Iraq", "Iran, Islamic Republic of", + "Iceland", "Italy", "Jamaica", "Jordan", "Japan", "Kenya", "Kyrgyzstan", + "Cambodia", "Kiribati", "Comoros", "Saint Kitts and Nevis", + "Korea, Democratic People's Republic of", + "Korea, Republic of", "Kuwait", "Cayman Islands", + "Kazakstan", "Lao People's Democratic Republic", "Lebanon", "Saint Lucia", + "Liechtenstein", "Sri Lanka", "Liberia", "Lesotho", "Lithuania", "Luxembourg", + "Latvia", "Libyan Arab Jamahiriya", "Morocco", "Monaco", "Moldova, Republic of", + "Madagascar", "Marshall Islands", "Macedonia", + "Mali", "Myanmar", "Mongolia", "Macau", "Northern Mariana Islands", + "Martinique", "Mauritania", "Montserrat", "Malta", "Mauritius", "Maldives", + "Malawi", "Mexico", "Malaysia", "Mozambique", "Namibia", "New Caledonia", + "Niger", "Norfolk Island", "Nigeria", "Nicaragua", "Netherlands", "Norway", + "Nepal", "Nauru", "Niue", "New Zealand", "Oman", "Panama", "Peru", "French Polynesia", + "Papua New Guinea", "Philippines", "Pakistan", "Poland", "Saint Pierre and Miquelon", + "Pitcairn Islands", "Puerto Rico", "Palestinian Territory", + "Portugal", "Palau", "Paraguay", "Qatar", "Reunion", "Romania", + "Russian Federation", "Rwanda", "Saudi Arabia", "Solomon Islands", + "Seychelles", "Sudan", "Sweden", "Singapore", "Saint Helena", "Slovenia", + "Svalbard and Jan Mayen", "Slovakia", "Sierra Leone", "San Marino", "Senegal", + "Somalia", "Suriname", "Sao Tome and Principe", "El Salvador", "Syrian Arab Republic", + "Swaziland", "Turks and Caicos Islands", "Chad", "French Southern Territories", + "Togo", "Thailand", "Tajikistan", "Tokelau", "Turkmenistan", + "Tunisia", "Tonga", "Timor-Leste", "Turkey", "Trinidad and Tobago", "Tuvalu", + "Taiwan", "Tanzania, United Republic of", "Ukraine", + "Uganda", "United States Minor Outlying Islands", "United States", "Uruguay", + "Uzbekistan", "Holy See (Vatican City State)", "Saint Vincent and the Grenadines", + "Venezuela", "Virgin Islands, British", "Virgin Islands, U.S.", + "Vietnam", "Vanuatu", "Wallis and Futuna", "Samoa", "Yemen", "Mayotte", + "Serbia", "South Africa", "Zambia", "Montenegro", "Zimbabwe", + "Anonymous Proxy","Satellite Provider","Other", + "Aland Islands","Guernsey","Isle of Man","Jersey","Saint Barthelemy","Saint Martin" + ) + +# storage / caching flags +STANDARD = 0 +MEMORY_CACHE = 1 +MMAP_CACHE = 8 + +# Database structure constants +COUNTRY_BEGIN = 16776960 +STATE_BEGIN_REV0 = 16700000 +STATE_BEGIN_REV1 = 16000000 + +STRUCTURE_INFO_MAX_SIZE = 20 +DATABASE_INFO_MAX_SIZE = 100 + +# Database editions +COUNTRY_EDITION = 1 +REGION_EDITION_REV0 = 7 +REGION_EDITION_REV1 = 3 +CITY_EDITION_REV0 = 6 +CITY_EDITION_REV1 = 2 +ORG_EDITION = 5 +ISP_EDITION = 4 +PROXY_EDITION = 8 +ASNUM_EDITION = 9 +NETSPEED_EDITION = 11 +COUNTRY_EDITION_V6 = 12 + +SEGMENT_RECORD_LENGTH = 3 +STANDARD_RECORD_LENGTH = 3 +ORG_RECORD_LENGTH = 4 +MAX_RECORD_LENGTH = 4 +MAX_ORG_RECORD_LENGTH = 300 +FULL_RECORD_LENGTH = 50 + +US_OFFSET = 1 +CANADA_OFFSET = 677 +WORLD_OFFSET = 1353 +FIPS_RANGE = 360 + + diff --git a/lib/pygeoip/timezone.py b/lib/pygeoip/timezone.py index 6346709..033c8d6 100644 --- a/lib/pygeoip/timezone.py +++ b/lib/pygeoip/timezone.py @@ -1,760 +1,714 @@ -# -*- coding: utf-8 -*- -""" -Time zone functions. Part of the pygeoip package. - -@author: Jennifer Ennis <zaylea@gmail.com> - -@license: Copyright(C) 2004 MaxMind LLC - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>. -""" - __all__ = ['time_zone_by_country_and_region'] -_country = { - 'AD': 'Europe/Andorra', - 'AE': 'Asia/Dubai', - 'AF': 'Asia/Kabul', - 'AG': 'America/Antigua', - 'AI': 'America/Anguilla', - 'AL': 'Europe/Tirane', - 'AM': 'Asia/Yerevan', - 'AN': 'America/Curacao', - 'AO': 'Africa/Luanda', - 'AR': { - '01': 'America/Argentina/Buenos_Aires', - '02': 'America/Argentina/Catamarca', - '03': 'America/Argentina/Tucuman', - '04': 'America/Argentina/Rio_Gallegos', - '05': 'America/Argentina/Cordoba', - '06': 'America/Argentina/Tucuman', - '07': 'America/Argentina/Buenos_Aires', - '08': 'America/Argentina/Buenos_Aires', - '09': 'America/Argentina/Tucuman', - '10': 'America/Argentina/Jujuy', - '11': 'America/Argentina/San_Luis', - '12': 'America/Argentina/La_Rioja', - '13': 'America/Argentina/Mendoza', - '14': 'America/Argentina/Buenos_Aires', - '15': 'America/Argentina/San_Luis', - '16': 'America/Argentina/Buenos_Aires', - '17': 'America/Argentina/Salta', - '18': 'America/Argentina/San_Juan', - '19': 'America/Argentina/San_Luis', - '20': 'America/Argentina/Rio_Gallegos', - '21': 'America/Argentina/Buenos_Aires', - '22': 'America/Argentina/Catamarca', - '23': 'America/Argentina/Ushuaia', - '24': 'America/Argentina/Tucuman' - }, - 'AS': 'US/Samoa', - 'AT': 'Europe/Vienna', - 'AU': { - '01': 'Australia/Canberra', - '02': 'Australia/NSW', - '03': 'Australia/North', - '04': 'Australia/Queensland', - '05': 'Australia/South', - '06': 'Australia/Tasmania', - '07': 'Australia/Victoria', - '08': 'Australia/West' - }, - 'AW': 'America/Aruba', - 'AX': 'Europe/Mariehamn', - 'AZ': 'Asia/Baku', - 'BA': 'Europe/Sarajevo', - 'BB': 'America/Barbados', - 'BD': 'Asia/Dhaka', - 'BE': 'Europe/Brussels', - 'BF': 'Africa/Ouagadougou', - 'BG': 'Europe/Sofia', - 'BH': 'Asia/Bahrain', - 'BI': 'Africa/Bujumbura', - 'BJ': 'Africa/Porto-Novo', - 'BL': 'America/St_Barthelemy', - 'BM': 'Atlantic/Bermuda', - 'BN': 'Asia/Brunei', - 'BO': 'America/La_Paz', - 'BQ': 'America/Curacao', - 'BR': { - '01': 'America/Rio_Branco', - '02': 'America/Maceio', - '03': 'America/Sao_Paulo', - '04': 'America/Manaus', - '05': 'America/Bahia', - '06': 'America/Fortaleza', - '07': 'America/Sao_Paulo', - '08': 'America/Sao_Paulo', - '11': 'America/Campo_Grande', - '13': 'America/Belem', - '14': 'America/Cuiaba', - '15': 'America/Sao_Paulo', - '16': 'America/Belem', - '17': 'America/Recife', - '18': 'America/Sao_Paulo', - '20': 'America/Fortaleza', - '21': 'America/Sao_Paulo', - '22': 'America/Recife', - '23': 'America/Sao_Paulo', - '24': 'America/Porto_Velho', - '25': 'America/Boa_Vista', - '26': 'America/Sao_Paulo', - '27': 'America/Sao_Paulo', - '28': 'America/Maceio', - '29': 'America/Sao_Paulo', - '30': 'America/Recife', - '31': 'America/Araguaina' - }, - 'BS': 'America/Nassau', - 'BT': 'Asia/Thimphu', - 'BW': 'Africa/Gaborone', - 'BY': 'Europe/Minsk', - 'BZ': 'America/Belize', - 'CA': { - 'AB': 'America/Edmonton', - 'BC': 'America/Vancouver', - 'MB': 'America/Winnipeg', - 'NB': 'America/Halifax', - 'NL': 'America/St_Johns', - 'NS': 'America/Halifax', - 'NT': 'America/Yellowknife', - 'NU': 'America/Rankin_Inlet', - 'ON': 'America/Toronto', - 'PE': 'America/Halifax', - 'QC': 'America/Montreal', - 'SK': 'America/Regina', - 'YT': 'America/Whitehorse' - }, - 'CC': 'Indian/Cocos', - 'CD': { - '02': 'Africa/Kinshasa', - '05': 'Africa/Lubumbashi', - '06': 'Africa/Kinshasa', - '08': 'Africa/Kinshasa', - '10': 'Africa/Lubumbashi', - '11': 'Africa/Lubumbashi', - '12': 'Africa/Lubumbashi' - }, - 'CF': 'Africa/Bangui', - 'CG': 'Africa/Brazzaville', - 'CH': 'Europe/Zurich', - 'CI': 'Africa/Abidjan', - 'CK': 'Pacific/Rarotonga', - 'CL': 'Chile/Continental', - 'CM': 'Africa/Lagos', - 'CN': { - '01': 'Asia/Shanghai', - '02': 'Asia/Shanghai', - '03': 'Asia/Shanghai', - '04': 'Asia/Shanghai', - '05': 'Asia/Harbin', - '06': 'Asia/Chongqing', - '07': 'Asia/Shanghai', - '08': 'Asia/Harbin', - '09': 'Asia/Shanghai', - '10': 'Asia/Shanghai', - '11': 'Asia/Chongqing', - '12': 'Asia/Shanghai', - '13': 'Asia/Urumqi', - '14': 'Asia/Chongqing', - '15': 'Asia/Chongqing', - '16': 'Asia/Chongqing', - '18': 'Asia/Chongqing', - '19': 'Asia/Harbin', - '20': 'Asia/Harbin', - '21': 'Asia/Chongqing', - '22': 'Asia/Harbin', - '23': 'Asia/Shanghai', - '24': 'Asia/Chongqing', - '25': 'Asia/Shanghai', - '26': 'Asia/Chongqing', - '28': 'Asia/Shanghai', - '29': 'Asia/Chongqing', - '30': 'Asia/Chongqing', - '31': 'Asia/Chongqing', - '32': 'Asia/Chongqing', - '33': 'Asia/Chongqing' - }, - 'CO': 'America/Bogota', - 'CR': 'America/Costa_Rica', - 'CU': 'America/Havana', - 'CV': 'Atlantic/Cape_Verde', - 'CW': 'America/Curacao', - 'CX': 'Indian/Christmas', - 'CY': 'Asia/Nicosia', - 'CZ': 'Europe/Prague', - 'DE': 'Europe/Berlin', - 'DJ': 'Africa/Djibouti', - 'DK': 'Europe/Copenhagen', - 'DM': 'America/Dominica', - 'DO': 'America/Santo_Domingo', - 'DZ': 'Africa/Algiers', - 'EC': { - '01': 'Pacific/Galapagos', - '02': 'America/Guayaquil', - '03': 'America/Guayaquil', - '04': 'America/Guayaquil', - '05': 'America/Guayaquil', - '06': 'America/Guayaquil', - '07': 'America/Guayaquil', - '08': 'America/Guayaquil', - '09': 'America/Guayaquil', - '10': 'America/Guayaquil', - '11': 'America/Guayaquil', - '12': 'America/Guayaquil', - '13': 'America/Guayaquil', - '14': 'America/Guayaquil', - '15': 'America/Guayaquil', - '17': 'America/Guayaquil', - '18': 'America/Guayaquil', - '19': 'America/Guayaquil', - '20': 'America/Guayaquil', - '22': 'America/Guayaquil' - }, - 'EE': 'Europe/Tallinn', - 'EG': 'Africa/Cairo', - 'EH': 'Africa/El_Aaiun', - 'ER': 'Africa/Asmera', - 'ES': { - '07': 'Europe/Madrid', - '27': 'Europe/Madrid', - '29': 'Europe/Madrid', - '31': 'Europe/Madrid', - '32': 'Europe/Madrid', - '34': 'Europe/Madrid', - '39': 'Europe/Madrid', - '51': 'Africa/Ceuta', - '52': 'Europe/Madrid', - '53': 'Atlantic/Canary', - '54': 'Europe/Madrid', - '55': 'Europe/Madrid', - '56': 'Europe/Madrid', - '57': 'Europe/Madrid', - '58': 'Europe/Madrid', - '59': 'Europe/Madrid', - '60': 'Europe/Madrid' - }, - 'ET': 'Africa/Addis_Ababa', - 'FI': 'Europe/Helsinki', - 'FJ': 'Pacific/Fiji', - 'FK': 'Atlantic/Stanley', - 'FO': 'Atlantic/Faeroe', - 'FR': 'Europe/Paris', - 'FX': 'Europe/Paris', - 'GA': 'Africa/Libreville', - 'GB': 'Europe/London', - 'GD': 'America/Grenada', - 'GE': 'Asia/Tbilisi', - 'GF': 'America/Cayenne', - 'GG': 'Europe/Guernsey', - 'GH': 'Africa/Accra', - 'GI': 'Europe/Gibraltar', - 'GL': { - '01': 'America/Thule', - '02': 'America/Godthab', - '03': 'America/Godthab' - }, - 'GM': 'Africa/Banjul', - 'GN': 'Africa/Conakry', - 'GP': 'America/Guadeloupe', - 'GQ': 'Africa/Malabo', - 'GR': 'Europe/Athens', - 'GS': 'Atlantic/South_Georgia', - 'GT': 'America/Guatemala', - 'GU': 'Pacific/Guam', - 'GW': 'Africa/Bissau', - 'GY': 'America/Guyana', - 'HK': 'Asia/Hong_Kong', - 'HN': 'America/Tegucigalpa', - 'HR': 'Europe/Zagreb', - 'HT': 'America/Port-au-Prince', - 'HU': 'Europe/Budapest', - 'ID': { - '01': 'Asia/Pontianak', - '02': 'Asia/Makassar', - '03': 'Asia/Jakarta', - '04': 'Asia/Jakarta', - '05': 'Asia/Jakarta', - '06': 'Asia/Jakarta', - '07': 'Asia/Jakarta', - '08': 'Asia/Jakarta', - '09': 'Asia/Jayapura', - '10': 'Asia/Jakarta', - '11': 'Asia/Pontianak', - '12': 'Asia/Makassar', - '13': 'Asia/Makassar', - '14': 'Asia/Makassar', - '15': 'Asia/Jakarta', - '16': 'Asia/Makassar', - '17': 'Asia/Makassar', - '18': 'Asia/Makassar', - '19': 'Asia/Pontianak', - '20': 'Asia/Makassar', - '21': 'Asia/Makassar', - '22': 'Asia/Makassar', - '23': 'Asia/Makassar', - '24': 'Asia/Jakarta', - '25': 'Asia/Pontianak', - '26': 'Asia/Pontianak', - '30': 'Asia/Jakarta', - '31': 'Asia/Makassar', - '33': 'Asia/Jakarta' - }, - 'IE': 'Europe/Dublin', - 'IL': 'Asia/Jerusalem', - 'IM': 'Europe/Isle_of_Man', - 'IN': 'Asia/Calcutta', - 'IO': 'Indian/Chagos', - 'IQ': 'Asia/Baghdad', - 'IR': 'Asia/Tehran', - 'IS': 'Atlantic/Reykjavik', - 'IT': 'Europe/Rome', - 'JE': 'Europe/Jersey', - 'JM': 'America/Jamaica', - 'JO': 'Asia/Amman', - 'JP': 'Asia/Tokyo', - 'KE': 'Africa/Nairobi', - 'KG': 'Asia/Bishkek', - 'KH': 'Asia/Phnom_Penh', - 'KI': 'Pacific/Tarawa', - 'KM': 'Indian/Comoro', - 'KN': 'America/St_Kitts', - 'KP': 'Asia/Pyongyang', - 'KR': 'Asia/Seoul', - 'KW': 'Asia/Kuwait', - 'KY': 'America/Cayman', - 'KZ': { - '01': 'Asia/Almaty', - '02': 'Asia/Almaty', - '03': 'Asia/Qyzylorda', - '04': 'Asia/Aqtobe', - '05': 'Asia/Qyzylorda', - '06': 'Asia/Aqtau', - '07': 'Asia/Oral', - '08': 'Asia/Qyzylorda', - '09': 'Asia/Aqtau', - '10': 'Asia/Qyzylorda', - '11': 'Asia/Almaty', - '12': 'Asia/Qyzylorda', - '13': 'Asia/Aqtobe', - '14': 'Asia/Qyzylorda', - '15': 'Asia/Almaty', - '16': 'Asia/Aqtobe', - '17': 'Asia/Almaty' - }, - 'LA': 'Asia/Vientiane', - 'LB': 'Asia/Beirut', - 'LC': 'America/St_Lucia', - 'LI': 'Europe/Vaduz', - 'LK': 'Asia/Colombo', - 'LR': 'Africa/Monrovia', - 'LS': 'Africa/Maseru', - 'LT': 'Europe/Vilnius', - 'LU': 'Europe/Luxembourg', - 'LV': 'Europe/Riga', - 'LY': 'Africa/Tripoli', - 'MA': 'Africa/Casablanca', - 'MC': 'Europe/Monaco', - 'MD': 'Europe/Chisinau', - 'ME': 'Europe/Podgorica', - 'MF': 'America/Marigot', - 'MG': 'Indian/Antananarivo', - 'MK': 'Europe/Skopje', - 'ML': 'Africa/Bamako', - 'MM': 'Asia/Rangoon', - 'MN': 'Asia/Choibalsan', - 'MO': 'Asia/Macao', - 'MP': 'Pacific/Saipan', - 'MQ': 'America/Martinique', - 'MR': 'Africa/Nouakchott', - 'MS': 'America/Montserrat', - 'MT': 'Europe/Malta', - 'MU': 'Indian/Mauritius', - 'MV': 'Indian/Maldives', - 'MW': 'Africa/Blantyre', - 'MX': { - '01': 'America/Mexico_City', - '02': 'America/Tijuana', - '03': 'America/Hermosillo', - '04': 'America/Merida', - '05': 'America/Mexico_City', - '06': 'America/Chihuahua', - '07': 'America/Monterrey', - '08': 'America/Mexico_City', - '09': 'America/Mexico_City', - '10': 'America/Mazatlan', - '11': 'America/Mexico_City', - '12': 'America/Mexico_City', - '13': 'America/Mexico_City', - '14': 'America/Mazatlan', - '15': 'America/Chihuahua', - '16': 'America/Mexico_City', - '17': 'America/Mexico_City', - '18': 'America/Mazatlan', - '19': 'America/Monterrey', - '20': 'America/Mexico_City', - '21': 'America/Mexico_City', - '22': 'America/Mexico_City', - '23': 'America/Cancun', - '24': 'America/Mexico_City', - '25': 'America/Mazatlan', - '26': 'America/Hermosillo', - '27': 'America/Merida', - '28': 'America/Monterrey', - '29': 'America/Mexico_City', - '30': 'America/Mexico_City', - '31': 'America/Merida', - '32': 'America/Monterrey' - }, - 'MY': { - '01': 'Asia/Kuala_Lumpur', - '02': 'Asia/Kuala_Lumpur', - '03': 'Asia/Kuala_Lumpur', - '04': 'Asia/Kuala_Lumpur', - '05': 'Asia/Kuala_Lumpur', - '06': 'Asia/Kuala_Lumpur', - '07': 'Asia/Kuala_Lumpur', - '08': 'Asia/Kuala_Lumpur', - '09': 'Asia/Kuala_Lumpur', - '11': 'Asia/Kuching', - '12': 'Asia/Kuala_Lumpur', - '13': 'Asia/Kuala_Lumpur', - '14': 'Asia/Kuala_Lumpur', - '15': 'Asia/Kuching', - '16': 'Asia/Kuching' - }, - 'MZ': 'Africa/Maputo', - 'NA': 'Africa/Windhoek', - 'NC': 'Pacific/Noumea', - 'NE': 'Africa/Niamey', - 'NF': 'Pacific/Norfolk', - 'NG': 'Africa/Lagos', - 'NI': 'America/Managua', - 'NL': 'Europe/Amsterdam', - 'NO': 'Europe/Oslo', - 'NP': 'Asia/Katmandu', - 'NR': 'Pacific/Nauru', - 'NU': 'Pacific/Niue', - 'NZ': { - '85': 'Pacific/Auckland', - 'E7': 'Pacific/Auckland', - 'E8': 'Pacific/Auckland', - 'E9': 'Pacific/Auckland', - 'F1': 'Pacific/Auckland', - 'F2': 'Pacific/Auckland', - 'F3': 'Pacific/Auckland', - 'F4': 'Pacific/Auckland', - 'F5': 'Pacific/Auckland', - 'F7': 'Pacific/Chatham', - 'F8': 'Pacific/Auckland', - 'F9': 'Pacific/Auckland', - 'G1': 'Pacific/Auckland', - 'G2': 'Pacific/Auckland', - 'G3': 'Pacific/Auckland' - }, - 'OM': 'Asia/Muscat', - 'PA': 'America/Panama', - 'PE': 'America/Lima', - 'PF': 'Pacific/Marquesas', - 'PG': 'Pacific/Port_Moresby', - 'PH': 'Asia/Manila', - 'PK': 'Asia/Karachi', - 'PL': 'Europe/Warsaw', - 'PM': 'America/Miquelon', - 'PN': 'Pacific/Pitcairn', - 'PR': 'America/Puerto_Rico', - 'PS': 'Asia/Gaza', - 'PT': { - '02': 'Europe/Lisbon', - '03': 'Europe/Lisbon', - '04': 'Europe/Lisbon', - '05': 'Europe/Lisbon', - '06': 'Europe/Lisbon', - '07': 'Europe/Lisbon', - '08': 'Europe/Lisbon', - '09': 'Europe/Lisbon', - '10': 'Atlantic/Madeira', - '11': 'Europe/Lisbon', - '13': 'Europe/Lisbon', - '14': 'Europe/Lisbon', - '16': 'Europe/Lisbon', - '17': 'Europe/Lisbon', - '18': 'Europe/Lisbon', - '19': 'Europe/Lisbon', - '20': 'Europe/Lisbon', - '21': 'Europe/Lisbon', - '22': 'Europe/Lisbon' - }, - 'PW': 'Pacific/Palau', - 'PY': 'America/Asuncion', - 'QA': 'Asia/Qatar', - 'RE': 'Indian/Reunion', - 'RO': 'Europe/Bucharest', - 'RS': 'Europe/Belgrade', - 'RU': { - '01': 'Europe/Volgograd', - '02': 'Asia/Irkutsk', - '03': 'Asia/Novokuznetsk', - '04': 'Asia/Novosibirsk', - '05': 'Asia/Vladivostok', - '06': 'Europe/Moscow', - '07': 'Europe/Volgograd', - '08': 'Europe/Samara', - '09': 'Europe/Moscow', - '10': 'Europe/Moscow', - '11': 'Asia/Irkutsk', - '13': 'Asia/Yekaterinburg', - '14': 'Asia/Irkutsk', - '15': 'Asia/Anadyr', - '16': 'Europe/Samara', - '17': 'Europe/Volgograd', - '18': 'Asia/Krasnoyarsk', - '20': 'Asia/Irkutsk', - '21': 'Europe/Moscow', - '22': 'Europe/Volgograd', - '23': 'Europe/Kaliningrad', - '24': 'Europe/Volgograd', - '25': 'Europe/Moscow', - '26': 'Asia/Kamchatka', - '27': 'Europe/Volgograd', - '28': 'Europe/Moscow', - '29': 'Asia/Novokuznetsk', - '30': 'Asia/Vladivostok', - '31': 'Asia/Krasnoyarsk', - '32': 'Asia/Omsk', - '33': 'Asia/Yekaterinburg', - '34': 'Asia/Yekaterinburg', - '35': 'Asia/Yekaterinburg', - '36': 'Asia/Anadyr', - '37': 'Europe/Moscow', - '38': 'Europe/Volgograd', - '39': 'Asia/Krasnoyarsk', - '40': 'Asia/Yekaterinburg', - '41': 'Europe/Moscow', - '42': 'Europe/Moscow', - '43': 'Europe/Moscow', - '44': 'Asia/Magadan', - '45': 'Europe/Samara', - '46': 'Europe/Samara', - '47': 'Europe/Moscow', - '48': 'Europe/Moscow', - '49': 'Europe/Moscow', - '50': 'Asia/Yekaterinburg', - '51': 'Europe/Moscow', - '52': 'Europe/Moscow', - '53': 'Asia/Novosibirsk', - '54': 'Asia/Omsk', - '55': 'Europe/Samara', - '56': 'Europe/Moscow', - '57': 'Europe/Samara', - '58': 'Asia/Yekaterinburg', - '59': 'Asia/Vladivostok', - '60': 'Europe/Kaliningrad', - '61': 'Europe/Volgograd', - '62': 'Europe/Moscow', - '63': 'Asia/Yakutsk', - '64': 'Asia/Sakhalin', - '65': 'Europe/Samara', - '66': 'Europe/Moscow', - '67': 'Europe/Samara', - '68': 'Europe/Volgograd', - '69': 'Europe/Moscow', - '70': 'Europe/Volgograd', - '71': 'Asia/Yekaterinburg', - '72': 'Europe/Moscow', - '73': 'Europe/Samara', - '74': 'Asia/Krasnoyarsk', - '75': 'Asia/Novosibirsk', - '76': 'Europe/Moscow', - '77': 'Europe/Moscow', - '78': 'Asia/Yekaterinburg', - '79': 'Asia/Irkutsk', - '80': 'Asia/Yekaterinburg', - '81': 'Europe/Samara', - '82': 'Asia/Irkutsk', - '83': 'Europe/Moscow', - '84': 'Europe/Volgograd', - '85': 'Europe/Moscow', - '86': 'Europe/Moscow', - '87': 'Asia/Novosibirsk', - '88': 'Europe/Moscow', - '89': 'Asia/Vladivostok' - }, - 'RW': 'Africa/Kigali', - 'SA': 'Asia/Riyadh', - 'SB': 'Pacific/Guadalcanal', - 'SC': 'Indian/Mahe', - 'SD': 'Africa/Khartoum', - 'SE': 'Europe/Stockholm', - 'SG': 'Asia/Singapore', - 'SH': 'Atlantic/St_Helena', - 'SI': 'Europe/Ljubljana', - 'SJ': 'Arctic/Longyearbyen', - 'SK': 'Europe/Bratislava', - 'SL': 'Africa/Freetown', - 'SM': 'Europe/San_Marino', - 'SN': 'Africa/Dakar', - 'SO': 'Africa/Mogadishu', - 'SR': 'America/Paramaribo', - 'SS': 'Africa/Juba', - 'ST': 'Africa/Sao_Tome', - 'SV': 'America/El_Salvador', - 'SX': 'America/Curacao', - 'SY': 'Asia/Damascus', - 'SZ': 'Africa/Mbabane', - 'TC': 'America/Grand_Turk', - 'TD': 'Africa/Ndjamena', - 'TF': 'Indian/Kerguelen', - 'TG': 'Africa/Lome', - 'TH': 'Asia/Bangkok', - 'TJ': 'Asia/Dushanbe', - 'TK': 'Pacific/Fakaofo', - 'TL': 'Asia/Dili', - 'TM': 'Asia/Ashgabat', - 'TN': 'Africa/Tunis', - 'TO': 'Pacific/Tongatapu', - 'TR': 'Asia/Istanbul', - 'TT': 'America/Port_of_Spain', - 'TV': 'Pacific/Funafuti', - 'TW': 'Asia/Taipei', - 'TZ': 'Africa/Dar_es_Salaam', - 'UA': { - '01': 'Europe/Kiev', - '02': 'Europe/Kiev', - '03': 'Europe/Uzhgorod', - '04': 'Europe/Zaporozhye', - '05': 'Europe/Zaporozhye', - '06': 'Europe/Uzhgorod', - '07': 'Europe/Zaporozhye', - '08': 'Europe/Simferopol', - '09': 'Europe/Kiev', - '10': 'Europe/Zaporozhye', - '11': 'Europe/Simferopol', - '13': 'Europe/Kiev', - '14': 'Europe/Zaporozhye', - '15': 'Europe/Uzhgorod', - '16': 'Europe/Zaporozhye', - '17': 'Europe/Simferopol', - '18': 'Europe/Zaporozhye', - '19': 'Europe/Kiev', - '20': 'Europe/Simferopol', - '21': 'Europe/Kiev', - '22': 'Europe/Uzhgorod', - '23': 'Europe/Kiev', - '24': 'Europe/Uzhgorod', - '25': 'Europe/Uzhgorod', - '26': 'Europe/Zaporozhye', - '27': 'Europe/Kiev' - }, - 'UG': 'Africa/Kampala', - 'US': { - 'AK': 'America/Anchorage', - 'AL': 'America/Chicago', - 'AR': 'America/Chicago', - 'AZ': 'America/Phoenix', - 'CA': 'America/Los_Angeles', - 'CO': 'America/Denver', - 'CT': 'America/New_York', - 'DC': 'America/New_York', - 'DE': 'America/New_York', - 'FL': 'America/New_York', - 'GA': 'America/New_York', - 'HI': 'Pacific/Honolulu', - 'IA': 'America/Chicago', - 'ID': 'America/Denver', - 'IL': 'America/Chicago', - 'IN': 'America/Indianapolis', - 'KS': 'America/Chicago', - 'KY': 'America/New_York', - 'LA': 'America/Chicago', - 'MA': 'America/New_York', - 'MD': 'America/New_York', - 'ME': 'America/New_York', - 'MI': 'America/New_York', - 'MN': 'America/Chicago', - 'MO': 'America/Chicago', - 'MS': 'America/Chicago', - 'MT': 'America/Denver', - 'NC': 'America/New_York', - 'ND': 'America/Chicago', - 'NE': 'America/Chicago', - 'NH': 'America/New_York', - 'NJ': 'America/New_York', - 'NM': 'America/Denver', - 'NV': 'America/Los_Angeles', - 'NY': 'America/New_York', - 'OH': 'America/New_York', - 'OK': 'America/Chicago', - 'OR': 'America/Los_Angeles', - 'PA': 'America/New_York', - 'RI': 'America/New_York', - 'SC': 'America/New_York', - 'SD': 'America/Chicago', - 'TN': 'America/Chicago', - 'TX': 'America/Chicago', - 'UT': 'America/Denver', - 'VA': 'America/New_York', - 'VT': 'America/New_York', - 'WA': 'America/Los_Angeles', - 'WI': 'America/Chicago', - 'WV': 'America/New_York', - 'WY': 'America/Denver' - }, - 'UY': 'America/Montevideo', - 'UZ': { - '01': 'Asia/Tashkent', - '02': 'Asia/Samarkand', - '03': 'Asia/Tashkent', - '06': 'Asia/Tashkent', - '07': 'Asia/Samarkand', - '08': 'Asia/Samarkand', - '09': 'Asia/Samarkand', - '10': 'Asia/Samarkand', - '12': 'Asia/Samarkand', - '13': 'Asia/Tashkent', - '14': 'Asia/Tashkent' - }, - 'VA': 'Europe/Vatican', - 'VC': 'America/St_Vincent', - 'VE': 'America/Caracas', - 'VG': 'America/Tortola', - 'VI': 'America/St_Thomas', - 'VN': 'Asia/Phnom_Penh', - 'VU': 'Pacific/Efate', - 'WF': 'Pacific/Wallis', - 'WS': 'Pacific/Samoa', - 'YE': 'Asia/Aden', - 'YT': 'Indian/Mayotte', - 'YU': 'Europe/Belgrade', - 'ZA': 'Africa/Johannesburg', - 'ZM': 'Africa/Lusaka', - 'ZW': 'Africa/Harare' - } - +_country = {} +_country["AD"] = "Europe/Andorra" +_country["AE"] = "Asia/Dubai" +_country["AF"] = "Asia/Kabul" +_country["AG"] = "America/Antigua" +_country["AI"] = "America/Anguilla" +_country["AL"] = "Europe/Tirane" +_country["AM"] = "Asia/Yerevan" +_country["AO"] = "Africa/Luanda" +_country["AR"] = {} +_country["AR"]["01"] = "America/Argentina/Buenos_Aires" +_country["AR"]["02"] = "America/Argentina/Catamarca" +_country["AR"]["03"] = "America/Argentina/Tucuman" +_country["AR"]["04"] = "America/Argentina/Rio_Gallegos" +_country["AR"]["05"] = "America/Argentina/Cordoba" +_country["AR"]["06"] = "America/Argentina/Tucuman" +_country["AR"]["07"] = "America/Argentina/Buenos_Aires" +_country["AR"]["08"] = "America/Argentina/Buenos_Aires" +_country["AR"]["09"] = "America/Argentina/Tucuman" +_country["AR"]["10"] = "America/Argentina/Jujuy" +_country["AR"]["11"] = "America/Argentina/San_Luis" +_country["AR"]["12"] = "America/Argentina/La_Rioja" +_country["AR"]["13"] = "America/Argentina/Mendoza" +_country["AR"]["14"] = "America/Argentina/Buenos_Aires" +_country["AR"]["15"] = "America/Argentina/San_Luis" +_country["AR"]["16"] = "America/Argentina/Buenos_Aires" +_country["AR"]["17"] = "America/Argentina/Salta" +_country["AR"]["18"] = "America/Argentina/San_Juan" +_country["AR"]["19"] = "America/Argentina/San_Luis" +_country["AR"]["20"] = "America/Argentina/Rio_Gallegos" +_country["AR"]["21"] = "America/Argentina/Buenos_Aires" +_country["AR"]["22"] = "America/Argentina/Catamarca" +_country["AR"]["23"] = "America/Argentina/Ushuaia" +_country["AR"]["24"] = "America/Argentina/Tucuman" +_country["AS"] = "US/Samoa" +_country["AT"] = "Europe/Vienna" +_country["AU"] = {} +_country["AU"]["01"] = "Australia/Canberra" +_country["AU"]["02"] = "Australia/NSW" +_country["AU"]["03"] = "Australia/North" +_country["AU"]["04"] = "Australia/Queensland" +_country["AU"]["05"] = "Australia/South" +_country["AU"]["06"] = "Australia/Tasmania" +_country["AU"]["07"] = "Australia/Victoria" +_country["AU"]["08"] = "Australia/West" +_country["AW"] = "America/Aruba" +_country["AX"] = "Europe/Mariehamn" +_country["AZ"] = "Asia/Baku" +_country["BA"] = "Europe/Sarajevo" +_country["BB"] = "America/Barbados" +_country["BD"] = "Asia/Dhaka" +_country["BE"] = "Europe/Brussels" +_country["BF"] = "Africa/Ouagadougou" +_country["BG"] = "Europe/Sofia" +_country["BH"] = "Asia/Bahrain" +_country["BI"] = "Africa/Bujumbura" +_country["BJ"] = "Africa/Porto-Novo" +_country["BL"] = "America/St_Barthelemy" +_country["BM"] = "Atlantic/Bermuda" +_country["BN"] = "Asia/Brunei" +_country["BO"] = "America/La_Paz" +_country["BQ"] = "America/Curacao" +_country["BR"] = {} +_country["BR"]["01"] = "America/Rio_Branco" +_country["BR"]["02"] = "America/Maceio" +_country["BR"]["03"] = "America/Sao_Paulo" +_country["BR"]["04"] = "America/Manaus" +_country["BR"]["05"] = "America/Bahia" +_country["BR"]["06"] = "America/Fortaleza" +_country["BR"]["07"] = "America/Sao_Paulo" +_country["BR"]["08"] = "America/Sao_Paulo" +_country["BR"]["11"] = "America/Campo_Grande" +_country["BR"]["13"] = "America/Belem" +_country["BR"]["14"] = "America/Cuiaba" +_country["BR"]["15"] = "America/Sao_Paulo" +_country["BR"]["16"] = "America/Belem" +_country["BR"]["17"] = "America/Recife" +_country["BR"]["18"] = "America/Sao_Paulo" +_country["BR"]["20"] = "America/Fortaleza" +_country["BR"]["21"] = "America/Sao_Paulo" +_country["BR"]["22"] = "America/Recife" +_country["BR"]["23"] = "America/Sao_Paulo" +_country["BR"]["24"] = "America/Porto_Velho" +_country["BR"]["25"] = "America/Boa_Vista" +_country["BR"]["26"] = "America/Sao_Paulo" +_country["BR"]["27"] = "America/Sao_Paulo" +_country["BR"]["28"] = "America/Maceio" +_country["BR"]["29"] = "America/Sao_Paulo" +_country["BR"]["30"] = "America/Recife" +_country["BR"]["31"] = "America/Araguaina" +_country["BS"] = "America/Nassau" +_country["BT"] = "Asia/Thimphu" +_country["BW"] = "Africa/Gaborone" +_country["BY"] = "Europe/Minsk" +_country["BZ"] = "America/Belize" +_country["CA"] = {} +_country["CA"]["AB"] = "America/Edmonton" +_country["CA"]["BC"] = "America/Vancouver" +_country["CA"]["MB"] = "America/Winnipeg" +_country["CA"]["NB"] = "America/Halifax" +_country["CA"]["NL"] = "America/St_Johns" +_country["CA"]["NS"] = "America/Halifax" +_country["CA"]["NT"] = "America/Yellowknife" +_country["CA"]["NU"] = "America/Rankin_Inlet" +_country["CA"]["ON"] = "America/Rainy_River" +_country["CA"]["PE"] = "America/Halifax" +_country["CA"]["QC"] = "America/Montreal" +_country["CA"]["SK"] = "America/Regina" +_country["CA"]["YT"] = "America/Whitehorse" +_country["CC"] = "Indian/Cocos" +_country["CD"] = {} +_country["CD"]["02"] = "Africa/Kinshasa" +_country["CD"]["05"] = "Africa/Lubumbashi" +_country["CD"]["06"] = "Africa/Kinshasa" +_country["CD"]["08"] = "Africa/Kinshasa" +_country["CD"]["10"] = "Africa/Lubumbashi" +_country["CD"]["11"] = "Africa/Lubumbashi" +_country["CD"]["12"] = "Africa/Lubumbashi" +_country["CF"] = "Africa/Bangui" +_country["CG"] = "Africa/Brazzaville" +_country["CH"] = "Europe/Zurich" +_country["CI"] = "Africa/Abidjan" +_country["CK"] = "Pacific/Rarotonga" +_country["CL"] = "Chile/Continental" +_country["CM"] = "Africa/Lagos" +_country["CN"] = {} +_country["CN"]["01"] = "Asia/Shanghai" +_country["CN"]["02"] = "Asia/Shanghai" +_country["CN"]["03"] = "Asia/Shanghai" +_country["CN"]["04"] = "Asia/Shanghai" +_country["CN"]["05"] = "Asia/Harbin" +_country["CN"]["06"] = "Asia/Chongqing" +_country["CN"]["07"] = "Asia/Shanghai" +_country["CN"]["08"] = "Asia/Harbin" +_country["CN"]["09"] = "Asia/Shanghai" +_country["CN"]["10"] = "Asia/Shanghai" +_country["CN"]["11"] = "Asia/Chongqing" +_country["CN"]["12"] = "Asia/Shanghai" +_country["CN"]["13"] = "Asia/Urumqi" +_country["CN"]["14"] = "Asia/Chongqing" +_country["CN"]["15"] = "Asia/Chongqing" +_country["CN"]["16"] = "Asia/Chongqing" +_country["CN"]["18"] = "Asia/Chongqing" +_country["CN"]["19"] = "Asia/Harbin" +_country["CN"]["20"] = "Asia/Harbin" +_country["CN"]["21"] = "Asia/Chongqing" +_country["CN"]["22"] = "Asia/Harbin" +_country["CN"]["23"] = "Asia/Shanghai" +_country["CN"]["24"] = "Asia/Chongqing" +_country["CN"]["25"] = "Asia/Shanghai" +_country["CN"]["26"] = "Asia/Chongqing" +_country["CN"]["28"] = "Asia/Shanghai" +_country["CN"]["29"] = "Asia/Chongqing" +_country["CN"]["30"] = "Asia/Chongqing" +_country["CN"]["31"] = "Asia/Chongqing" +_country["CN"]["32"] = "Asia/Chongqing" +_country["CN"]["33"] = "Asia/Chongqing" +_country["CO"] = "America/Bogota" +_country["CR"] = "America/Costa_Rica" +_country["CU"] = "America/Havana" +_country["CV"] = "Atlantic/Cape_Verde" +_country["CW"] = "America/Curacao" +_country["CX"] = "Indian/Christmas" +_country["CY"] = "Asia/Nicosia" +_country["CZ"] = "Europe/Prague" +_country["DE"] = "Europe/Berlin" +_country["DJ"] = "Africa/Djibouti" +_country["DK"] = "Europe/Copenhagen" +_country["DM"] = "America/Dominica" +_country["DO"] = "America/Santo_Domingo" +_country["DZ"] = "Africa/Algiers" +_country["EC"] = {} +_country["EC"]["01"] = "Pacific/Galapagos" +_country["EC"]["02"] = "America/Guayaquil" +_country["EC"]["03"] = "America/Guayaquil" +_country["EC"]["04"] = "America/Guayaquil" +_country["EC"]["05"] = "America/Guayaquil" +_country["EC"]["06"] = "America/Guayaquil" +_country["EC"]["07"] = "America/Guayaquil" +_country["EC"]["08"] = "America/Guayaquil" +_country["EC"]["09"] = "America/Guayaquil" +_country["EC"]["10"] = "America/Guayaquil" +_country["EC"]["11"] = "America/Guayaquil" +_country["EC"]["12"] = "America/Guayaquil" +_country["EC"]["13"] = "America/Guayaquil" +_country["EC"]["14"] = "America/Guayaquil" +_country["EC"]["15"] = "America/Guayaquil" +_country["EC"]["17"] = "America/Guayaquil" +_country["EC"]["18"] = "America/Guayaquil" +_country["EC"]["19"] = "America/Guayaquil" +_country["EC"]["20"] = "America/Guayaquil" +_country["EC"]["22"] = "America/Guayaquil" +_country["EE"] = "Europe/Tallinn" +_country["EG"] = "Africa/Cairo" +_country["EH"] = "Africa/El_Aaiun" +_country["ER"] = "Africa/Asmera" +_country["ES"] = {} +_country["ES"]["07"] = "Europe/Madrid" +_country["ES"]["27"] = "Europe/Madrid" +_country["ES"]["29"] = "Europe/Madrid" +_country["ES"]["31"] = "Europe/Madrid" +_country["ES"]["32"] = "Europe/Madrid" +_country["ES"]["34"] = "Europe/Madrid" +_country["ES"]["39"] = "Europe/Madrid" +_country["ES"]["51"] = "Africa/Ceuta" +_country["ES"]["52"] = "Europe/Madrid" +_country["ES"]["53"] = "Atlantic/Canary" +_country["ES"]["54"] = "Europe/Madrid" +_country["ES"]["55"] = "Europe/Madrid" +_country["ES"]["56"] = "Europe/Madrid" +_country["ES"]["57"] = "Europe/Madrid" +_country["ES"]["58"] = "Europe/Madrid" +_country["ES"]["59"] = "Europe/Madrid" +_country["ES"]["60"] = "Europe/Madrid" +_country["ET"] = "Africa/Addis_Ababa" +_country["FI"] = "Europe/Helsinki" +_country["FJ"] = "Pacific/Fiji" +_country["FK"] = "Atlantic/Stanley" +_country["FO"] = "Atlantic/Faeroe" +_country["FR"] = "Europe/Paris" +_country["GA"] = "Africa/Libreville" +_country["GB"] = "Europe/London" +_country["GD"] = "America/Grenada" +_country["GE"] = "Asia/Tbilisi" +_country["GF"] = "America/Cayenne" +_country["GG"] = "Europe/Guernsey" +_country["GH"] = "Africa/Accra" +_country["GI"] = "Europe/Gibraltar" +_country["GL"] = {} +_country["GL"]["01"] = "America/Thule" +_country["GL"]["02"] = "America/Godthab" +_country["GL"]["03"] = "America/Godthab" +_country["GM"] = "Africa/Banjul" +_country["GN"] = "Africa/Conakry" +_country["GP"] = "America/Guadeloupe" +_country["GQ"] = "Africa/Malabo" +_country["GR"] = "Europe/Athens" +_country["GS"] = "Atlantic/South_Georgia" +_country["GT"] = "America/Guatemala" +_country["GU"] = "Pacific/Guam" +_country["GW"] = "Africa/Bissau" +_country["GY"] = "America/Guyana" +_country["HK"] = "Asia/Hong_Kong" +_country["HN"] = "America/Tegucigalpa" +_country["HR"] = "Europe/Zagreb" +_country["HT"] = "America/Port-au-Prince" +_country["HU"] = "Europe/Budapest" +_country["ID"] = {} +_country["ID"]["01"] = "Asia/Pontianak" +_country["ID"]["02"] = "Asia/Makassar" +_country["ID"]["03"] = "Asia/Jakarta" +_country["ID"]["04"] = "Asia/Jakarta" +_country["ID"]["05"] = "Asia/Jakarta" +_country["ID"]["06"] = "Asia/Jakarta" +_country["ID"]["07"] = "Asia/Jakarta" +_country["ID"]["08"] = "Asia/Jakarta" +_country["ID"]["09"] = "Asia/Jayapura" +_country["ID"]["10"] = "Asia/Jakarta" +_country["ID"]["11"] = "Asia/Pontianak" +_country["ID"]["12"] = "Asia/Makassar" +_country["ID"]["13"] = "Asia/Makassar" +_country["ID"]["14"] = "Asia/Makassar" +_country["ID"]["15"] = "Asia/Jakarta" +_country["ID"]["16"] = "Asia/Makassar" +_country["ID"]["17"] = "Asia/Makassar" +_country["ID"]["18"] = "Asia/Makassar" +_country["ID"]["19"] = "Asia/Pontianak" +_country["ID"]["20"] = "Asia/Makassar" +_country["ID"]["21"] = "Asia/Makassar" +_country["ID"]["22"] = "Asia/Makassar" +_country["ID"]["23"] = "Asia/Makassar" +_country["ID"]["24"] = "Asia/Jakarta" +_country["ID"]["25"] = "Asia/Pontianak" +_country["ID"]["26"] = "Asia/Pontianak" +_country["ID"]["30"] = "Asia/Jakarta" +_country["ID"]["31"] = "Asia/Makassar" +_country["ID"]["33"] = "Asia/Jakarta" +_country["IE"] = "Europe/Dublin" +_country["IL"] = "Asia/Jerusalem" +_country["IM"] = "Europe/Isle_of_Man" +_country["IN"] = "Asia/Calcutta" +_country["IO"] = "Indian/Chagos" +_country["IQ"] = "Asia/Baghdad" +_country["IR"] = "Asia/Tehran" +_country["IS"] = "Atlantic/Reykjavik" +_country["IT"] = "Europe/Rome" +_country["JE"] = "Europe/Jersey" +_country["JM"] = "America/Jamaica" +_country["JO"] = "Asia/Amman" +_country["JP"] = "Asia/Tokyo" +_country["KE"] = "Africa/Nairobi" +_country["KG"] = "Asia/Bishkek" +_country["KH"] = "Asia/Phnom_Penh" +_country["KI"] = "Pacific/Tarawa" +_country["KM"] = "Indian/Comoro" +_country["KN"] = "America/St_Kitts" +_country["KP"] = "Asia/Pyongyang" +_country["KR"] = "Asia/Seoul" +_country["KW"] = "Asia/Kuwait" +_country["KY"] = "America/Cayman" +_country["KZ"] = {} +_country["KZ"]["01"] = "Asia/Almaty" +_country["KZ"]["02"] = "Asia/Almaty" +_country["KZ"]["03"] = "Asia/Qyzylorda" +_country["KZ"]["04"] = "Asia/Aqtobe" +_country["KZ"]["05"] = "Asia/Qyzylorda" +_country["KZ"]["06"] = "Asia/Aqtau" +_country["KZ"]["07"] = "Asia/Oral" +_country["KZ"]["08"] = "Asia/Qyzylorda" +_country["KZ"]["09"] = "Asia/Aqtau" +_country["KZ"]["10"] = "Asia/Qyzylorda" +_country["KZ"]["11"] = "Asia/Almaty" +_country["KZ"]["12"] = "Asia/Qyzylorda" +_country["KZ"]["13"] = "Asia/Aqtobe" +_country["KZ"]["14"] = "Asia/Qyzylorda" +_country["KZ"]["15"] = "Asia/Almaty" +_country["KZ"]["16"] = "Asia/Aqtobe" +_country["KZ"]["17"] = "Asia/Almaty" +_country["LA"] = "Asia/Vientiane" +_country["LB"] = "Asia/Beirut" +_country["LC"] = "America/St_Lucia" +_country["LI"] = "Europe/Vaduz" +_country["LK"] = "Asia/Colombo" +_country["LR"] = "Africa/Monrovia" +_country["LS"] = "Africa/Maseru" +_country["LT"] = "Europe/Vilnius" +_country["LU"] = "Europe/Luxembourg" +_country["LV"] = "Europe/Riga" +_country["LY"] = "Africa/Tripoli" +_country["MA"] = "Africa/Casablanca" +_country["MC"] = "Europe/Monaco" +_country["MD"] = "Europe/Chisinau" +_country["ME"] = "Europe/Podgorica" +_country["MF"] = "America/Marigot" +_country["MG"] = "Indian/Antananarivo" +_country["MK"] = "Europe/Skopje" +_country["ML"] = "Africa/Bamako" +_country["MM"] = "Asia/Rangoon" +_country["MN"] = "Asia/Choibalsan" +_country["MO"] = "Asia/Macao" +_country["MP"] = "Pacific/Saipan" +_country["MQ"] = "America/Martinique" +_country["MR"] = "Africa/Nouakchott" +_country["MS"] = "America/Montserrat" +_country["MT"] = "Europe/Malta" +_country["MU"] = "Indian/Mauritius" +_country["MV"] = "Indian/Maldives" +_country["MW"] = "Africa/Blantyre" +_country["MX"] = {} +_country["MX"]["01"] = "America/Mexico_City" +_country["MX"]["02"] = "America/Tijuana" +_country["MX"]["03"] = "America/Hermosillo" +_country["MX"]["04"] = "America/Merida" +_country["MX"]["05"] = "America/Mexico_City" +_country["MX"]["06"] = "America/Chihuahua" +_country["MX"]["07"] = "America/Monterrey" +_country["MX"]["08"] = "America/Mexico_City" +_country["MX"]["09"] = "America/Mexico_City" +_country["MX"]["10"] = "America/Mazatlan" +_country["MX"]["11"] = "America/Mexico_City" +_country["MX"]["12"] = "America/Mexico_City" +_country["MX"]["13"] = "America/Mexico_City" +_country["MX"]["14"] = "America/Mazatlan" +_country["MX"]["15"] = "America/Chihuahua" +_country["MX"]["16"] = "America/Mexico_City" +_country["MX"]["17"] = "America/Mexico_City" +_country["MX"]["18"] = "America/Mazatlan" +_country["MX"]["19"] = "America/Monterrey" +_country["MX"]["20"] = "America/Mexico_City" +_country["MX"]["21"] = "America/Mexico_City" +_country["MX"]["22"] = "America/Mexico_City" +_country["MX"]["23"] = "America/Cancun" +_country["MX"]["24"] = "America/Mexico_City" +_country["MX"]["25"] = "America/Mazatlan" +_country["MX"]["26"] = "America/Hermosillo" +_country["MX"]["27"] = "America/Merida" +_country["MX"]["28"] = "America/Monterrey" +_country["MX"]["29"] = "America/Mexico_City" +_country["MX"]["30"] = "America/Mexico_City" +_country["MX"]["31"] = "America/Merida" +_country["MX"]["32"] = "America/Monterrey" +_country["MY"] = {} +_country["MY"]["01"] = "Asia/Kuala_Lumpur" +_country["MY"]["02"] = "Asia/Kuala_Lumpur" +_country["MY"]["03"] = "Asia/Kuala_Lumpur" +_country["MY"]["04"] = "Asia/Kuala_Lumpur" +_country["MY"]["05"] = "Asia/Kuala_Lumpur" +_country["MY"]["06"] = "Asia/Kuala_Lumpur" +_country["MY"]["07"] = "Asia/Kuala_Lumpur" +_country["MY"]["08"] = "Asia/Kuala_Lumpur" +_country["MY"]["09"] = "Asia/Kuala_Lumpur" +_country["MY"]["11"] = "Asia/Kuching" +_country["MY"]["12"] = "Asia/Kuala_Lumpur" +_country["MY"]["13"] = "Asia/Kuala_Lumpur" +_country["MY"]["14"] = "Asia/Kuala_Lumpur" +_country["MY"]["15"] = "Asia/Kuching" +_country["MY"]["16"] = "Asia/Kuching" +_country["MZ"] = "Africa/Maputo" +_country["NA"] = "Africa/Windhoek" +_country["NC"] = "Pacific/Noumea" +_country["NE"] = "Africa/Niamey" +_country["NF"] = "Pacific/Norfolk" +_country["NG"] = "Africa/Lagos" +_country["NI"] = "America/Managua" +_country["NL"] = "Europe/Amsterdam" +_country["NO"] = "Europe/Oslo" +_country["NP"] = "Asia/Katmandu" +_country["NR"] = "Pacific/Nauru" +_country["NU"] = "Pacific/Niue" +_country["NZ"] = {} +_country["NZ"]["85"] = "Pacific/Auckland" +_country["NZ"]["E7"] = "Pacific/Auckland" +_country["NZ"]["E8"] = "Pacific/Auckland" +_country["NZ"]["E9"] = "Pacific/Auckland" +_country["NZ"]["F1"] = "Pacific/Auckland" +_country["NZ"]["F2"] = "Pacific/Auckland" +_country["NZ"]["F3"] = "Pacific/Auckland" +_country["NZ"]["F4"] = "Pacific/Auckland" +_country["NZ"]["F5"] = "Pacific/Auckland" +_country["NZ"]["F7"] = "Pacific/Chatham" +_country["NZ"]["F8"] = "Pacific/Auckland" +_country["NZ"]["F9"] = "Pacific/Auckland" +_country["NZ"]["G1"] = "Pacific/Auckland" +_country["NZ"]["G2"] = "Pacific/Auckland" +_country["NZ"]["G3"] = "Pacific/Auckland" +_country["OM"] = "Asia/Muscat" +_country["PA"] = "America/Panama" +_country["PE"] = "America/Lima" +_country["PF"] = "Pacific/Marquesas" +_country["PG"] = "Pacific/Port_Moresby" +_country["PH"] = "Asia/Manila" +_country["PK"] = "Asia/Karachi" +_country["PL"] = "Europe/Warsaw" +_country["PM"] = "America/Miquelon" +_country["PN"] = "Pacific/Pitcairn" +_country["PR"] = "America/Puerto_Rico" +_country["PS"] = "Asia/Gaza" +_country["PT"] = {} +_country["PT"]["02"] = "Europe/Lisbon" +_country["PT"]["03"] = "Europe/Lisbon" +_country["PT"]["04"] = "Europe/Lisbon" +_country["PT"]["05"] = "Europe/Lisbon" +_country["PT"]["06"] = "Europe/Lisbon" +_country["PT"]["07"] = "Europe/Lisbon" +_country["PT"]["08"] = "Europe/Lisbon" +_country["PT"]["09"] = "Europe/Lisbon" +_country["PT"]["10"] = "Atlantic/Madeira" +_country["PT"]["11"] = "Europe/Lisbon" +_country["PT"]["13"] = "Europe/Lisbon" +_country["PT"]["14"] = "Europe/Lisbon" +_country["PT"]["16"] = "Europe/Lisbon" +_country["PT"]["17"] = "Europe/Lisbon" +_country["PT"]["18"] = "Europe/Lisbon" +_country["PT"]["19"] = "Europe/Lisbon" +_country["PT"]["20"] = "Europe/Lisbon" +_country["PT"]["21"] = "Europe/Lisbon" +_country["PT"]["22"] = "Europe/Lisbon" +_country["PW"] = "Pacific/Palau" +_country["PY"] = "America/Asuncion" +_country["QA"] = "Asia/Qatar" +_country["RE"] = "Indian/Reunion" +_country["RO"] = "Europe/Bucharest" +_country["RS"] = "Europe/Belgrade" +_country["RU"] = {} +_country["RU"]["01"] = "Europe/Volgograd" +_country["RU"]["02"] = "Asia/Irkutsk" +_country["RU"]["03"] = "Asia/Novokuznetsk" +_country["RU"]["04"] = "Asia/Novosibirsk" +_country["RU"]["05"] = "Asia/Vladivostok" +_country["RU"]["06"] = "Europe/Moscow" +_country["RU"]["07"] = "Europe/Volgograd" +_country["RU"]["08"] = "Europe/Samara" +_country["RU"]["09"] = "Europe/Moscow" +_country["RU"]["10"] = "Europe/Moscow" +_country["RU"]["11"] = "Asia/Irkutsk" +_country["RU"]["13"] = "Asia/Yekaterinburg" +_country["RU"]["14"] = "Asia/Irkutsk" +_country["RU"]["15"] = "Asia/Anadyr" +_country["RU"]["16"] = "Europe/Samara" +_country["RU"]["17"] = "Europe/Volgograd" +_country["RU"]["18"] = "Asia/Krasnoyarsk" +_country["RU"]["20"] = "Asia/Irkutsk" +_country["RU"]["21"] = "Europe/Moscow" +_country["RU"]["22"] = "Europe/Volgograd" +_country["RU"]["23"] = "Europe/Kaliningrad" +_country["RU"]["24"] = "Europe/Volgograd" +_country["RU"]["25"] = "Europe/Moscow" +_country["RU"]["26"] = "Asia/Kamchatka" +_country["RU"]["27"] = "Europe/Volgograd" +_country["RU"]["28"] = "Europe/Moscow" +_country["RU"]["29"] = "Asia/Novokuznetsk" +_country["RU"]["30"] = "Asia/Vladivostok" +_country["RU"]["31"] = "Asia/Krasnoyarsk" +_country["RU"]["32"] = "Asia/Omsk" +_country["RU"]["33"] = "Asia/Yekaterinburg" +_country["RU"]["34"] = "Asia/Yekaterinburg" +_country["RU"]["35"] = "Asia/Yekaterinburg" +_country["RU"]["36"] = "Asia/Anadyr" +_country["RU"]["37"] = "Europe/Moscow" +_country["RU"]["38"] = "Europe/Volgograd" +_country["RU"]["39"] = "Asia/Krasnoyarsk" +_country["RU"]["40"] = "Asia/Yekaterinburg" +_country["RU"]["41"] = "Europe/Moscow" +_country["RU"]["42"] = "Europe/Moscow" +_country["RU"]["43"] = "Europe/Moscow" +_country["RU"]["44"] = "Asia/Magadan" +_country["RU"]["45"] = "Europe/Samara" +_country["RU"]["46"] = "Europe/Samara" +_country["RU"]["47"] = "Europe/Moscow" +_country["RU"]["48"] = "Europe/Moscow" +_country["RU"]["49"] = "Europe/Moscow" +_country["RU"]["50"] = "Asia/Yekaterinburg" +_country["RU"]["51"] = "Europe/Moscow" +_country["RU"]["52"] = "Europe/Moscow" +_country["RU"]["53"] = "Asia/Novosibirsk" +_country["RU"]["54"] = "Asia/Omsk" +_country["RU"]["55"] = "Europe/Samara" +_country["RU"]["56"] = "Europe/Moscow" +_country["RU"]["57"] = "Europe/Samara" +_country["RU"]["58"] = "Asia/Yekaterinburg" +_country["RU"]["59"] = "Asia/Vladivostok" +_country["RU"]["60"] = "Europe/Kaliningrad" +_country["RU"]["61"] = "Europe/Volgograd" +_country["RU"]["62"] = "Europe/Moscow" +_country["RU"]["63"] = "Asia/Yakutsk" +_country["RU"]["64"] = "Asia/Sakhalin" +_country["RU"]["65"] = "Europe/Samara" +_country["RU"]["66"] = "Europe/Moscow" +_country["RU"]["67"] = "Europe/Samara" +_country["RU"]["68"] = "Europe/Volgograd" +_country["RU"]["69"] = "Europe/Moscow" +_country["RU"]["70"] = "Europe/Volgograd" +_country["RU"]["71"] = "Asia/Yekaterinburg" +_country["RU"]["72"] = "Europe/Moscow" +_country["RU"]["73"] = "Europe/Samara" +_country["RU"]["74"] = "Asia/Krasnoyarsk" +_country["RU"]["75"] = "Asia/Novosibirsk" +_country["RU"]["76"] = "Europe/Moscow" +_country["RU"]["77"] = "Europe/Moscow" +_country["RU"]["78"] = "Asia/Yekaterinburg" +_country["RU"]["79"] = "Asia/Irkutsk" +_country["RU"]["80"] = "Asia/Yekaterinburg" +_country["RU"]["81"] = "Europe/Samara" +_country["RU"]["82"] = "Asia/Irkutsk" +_country["RU"]["83"] = "Europe/Moscow" +_country["RU"]["84"] = "Europe/Volgograd" +_country["RU"]["85"] = "Europe/Moscow" +_country["RU"]["86"] = "Europe/Moscow" +_country["RU"]["87"] = "Asia/Novosibirsk" +_country["RU"]["88"] = "Europe/Moscow" +_country["RU"]["89"] = "Asia/Vladivostok" +_country["RW"] = "Africa/Kigali" +_country["SA"] = "Asia/Riyadh" +_country["SB"] = "Pacific/Guadalcanal" +_country["SC"] = "Indian/Mahe" +_country["SD"] = "Africa/Khartoum" +_country["SE"] = "Europe/Stockholm" +_country["SG"] = "Asia/Singapore" +_country["SH"] = "Atlantic/St_Helena" +_country["SI"] = "Europe/Ljubljana" +_country["SJ"] = "Arctic/Longyearbyen" +_country["SK"] = "Europe/Bratislava" +_country["SL"] = "Africa/Freetown" +_country["SM"] = "Europe/San_Marino" +_country["SN"] = "Africa/Dakar" +_country["SO"] = "Africa/Mogadishu" +_country["SR"] = "America/Paramaribo" +_country["ST"] = "Africa/Sao_Tome" +_country["SV"] = "America/El_Salvador" +_country["SX"] = "America/Curacao" +_country["SY"] = "Asia/Damascus" +_country["SZ"] = "Africa/Mbabane" +_country["TC"] = "America/Grand_Turk" +_country["TD"] = "Africa/Ndjamena" +_country["TF"] = "Indian/Kerguelen" +_country["TG"] = "Africa/Lome" +_country["TH"] = "Asia/Bangkok" +_country["TJ"] = "Asia/Dushanbe" +_country["TK"] = "Pacific/Fakaofo" +_country["TL"] = "Asia/Dili" +_country["TM"] = "Asia/Ashgabat" +_country["TN"] = "Africa/Tunis" +_country["TO"] = "Pacific/Tongatapu" +_country["TR"] = "Asia/Istanbul" +_country["TT"] = "America/Port_of_Spain" +_country["TV"] = "Pacific/Funafuti" +_country["TW"] = "Asia/Taipei" +_country["TZ"] = "Africa/Dar_es_Salaam" +_country["UA"] = {} +_country["UA"]["01"] = "Europe/Kiev" +_country["UA"]["02"] = "Europe/Kiev" +_country["UA"]["03"] = "Europe/Uzhgorod" +_country["UA"]["04"] = "Europe/Zaporozhye" +_country["UA"]["05"] = "Europe/Zaporozhye" +_country["UA"]["06"] = "Europe/Uzhgorod" +_country["UA"]["07"] = "Europe/Zaporozhye" +_country["UA"]["08"] = "Europe/Simferopol" +_country["UA"]["09"] = "Europe/Kiev" +_country["UA"]["10"] = "Europe/Zaporozhye" +_country["UA"]["11"] = "Europe/Simferopol" +_country["UA"]["13"] = "Europe/Kiev" +_country["UA"]["14"] = "Europe/Zaporozhye" +_country["UA"]["15"] = "Europe/Uzhgorod" +_country["UA"]["16"] = "Europe/Zaporozhye" +_country["UA"]["17"] = "Europe/Simferopol" +_country["UA"]["18"] = "Europe/Zaporozhye" +_country["UA"]["19"] = "Europe/Kiev" +_country["UA"]["20"] = "Europe/Simferopol" +_country["UA"]["21"] = "Europe/Kiev" +_country["UA"]["22"] = "Europe/Uzhgorod" +_country["UA"]["23"] = "Europe/Kiev" +_country["UA"]["24"] = "Europe/Uzhgorod" +_country["UA"]["25"] = "Europe/Uzhgorod" +_country["UA"]["26"] = "Europe/Zaporozhye" +_country["UA"]["27"] = "Europe/Kiev" +_country["UG"] = "Africa/Kampala" +_country["US"] = {} +_country["US"]["AK"] = "America/Anchorage" +_country["US"]["AL"] = "America/Chicago" +_country["US"]["AR"] = "America/Chicago" +_country["US"]["AZ"] = "America/Phoenix" +_country["US"]["CA"] = "America/Los_Angeles" +_country["US"]["CO"] = "America/Denver" +_country["US"]["CT"] = "America/New_York" +_country["US"]["DC"] = "America/New_York" +_country["US"]["DE"] = "America/New_York" +_country["US"]["FL"] = "America/New_York" +_country["US"]["GA"] = "America/New_York" +_country["US"]["HI"] = "Pacific/Honolulu" +_country["US"]["IA"] = "America/Chicago" +_country["US"]["ID"] = "America/Denver" +_country["US"]["IL"] = "America/Chicago" +_country["US"]["IN"] = "America/Indianapolis" +_country["US"]["KS"] = "America/Chicago" +_country["US"]["KY"] = "America/New_York" +_country["US"]["LA"] = "America/Chicago" +_country["US"]["MA"] = "America/New_York" +_country["US"]["MD"] = "America/New_York" +_country["US"]["ME"] = "America/New_York" +_country["US"]["MI"] = "America/New_York" +_country["US"]["MN"] = "America/Chicago" +_country["US"]["MO"] = "America/Chicago" +_country["US"]["MS"] = "America/Chicago" +_country["US"]["MT"] = "America/Denver" +_country["US"]["NC"] = "America/New_York" +_country["US"]["ND"] = "America/Chicago" +_country["US"]["NE"] = "America/Chicago" +_country["US"]["NH"] = "America/New_York" +_country["US"]["NJ"] = "America/New_York" +_country["US"]["NM"] = "America/Denver" +_country["US"]["NV"] = "America/Los_Angeles" +_country["US"]["NY"] = "America/New_York" +_country["US"]["OH"] = "America/New_York" +_country["US"]["OK"] = "America/Chicago" +_country["US"]["OR"] = "America/Los_Angeles" +_country["US"]["PA"] = "America/New_York" +_country["US"]["RI"] = "America/New_York" +_country["US"]["SC"] = "America/New_York" +_country["US"]["SD"] = "America/Chicago" +_country["US"]["TN"] = "America/Chicago" +_country["US"]["TX"] = "America/Chicago" +_country["US"]["UT"] = "America/Denver" +_country["US"]["VA"] = "America/New_York" +_country["US"]["VT"] = "America/New_York" +_country["US"]["WA"] = "America/Los_Angeles" +_country["US"]["WI"] = "America/Chicago" +_country["US"]["WV"] = "America/New_York" +_country["US"]["WY"] = "America/Denver" +_country["UY"] = "America/Montevideo" +_country["UZ"] = {} +_country["UZ"]["01"] = "Asia/Tashkent" +_country["UZ"]["02"] = "Asia/Samarkand" +_country["UZ"]["03"] = "Asia/Tashkent" +_country["UZ"]["06"] = "Asia/Tashkent" +_country["UZ"]["07"] = "Asia/Samarkand" +_country["UZ"]["08"] = "Asia/Samarkand" +_country["UZ"]["09"] = "Asia/Samarkand" +_country["UZ"]["10"] = "Asia/Samarkand" +_country["UZ"]["12"] = "Asia/Samarkand" +_country["UZ"]["13"] = "Asia/Tashkent" +_country["UZ"]["14"] = "Asia/Tashkent" +_country["VA"] = "Europe/Vatican" +_country["VC"] = "America/St_Vincent" +_country["VE"] = "America/Caracas" +_country["VG"] = "America/Tortola" +_country["VI"] = "America/St_Thomas" +_country["VN"] = "Asia/Phnom_Penh" +_country["VU"] = "Pacific/Efate" +_country["WF"] = "Pacific/Wallis" +_country["WS"] = "Pacific/Samoa" +_country["YE"] = "Asia/Aden" +_country["YT"] = "Indian/Mayotte" +_country["YU"] = "Europe/Belgrade" +_country["ZA"] = "Africa/Johannesburg" +_country["ZM"] = "Africa/Lusaka" +_country["ZW"] = "Africa/Harare" def time_zone_by_country_and_region(country_code, region_name=None): + if country_code not in _country: - return '' + return None if not region_name or region_name == '00': region_name = None timezones = _country[country_code] + if isinstance(timezones, str): return timezones - if not region_name: - return '' - - return timezones.get(region_name) + if region_name: + return timezones.get(region_name) diff --git a/lib/pygeoip/util.py b/lib/pygeoip/util.py index cca3fec..f2873f3 100644 --- a/lib/pygeoip/util.py +++ b/lib/pygeoip/util.py @@ -1,36 +1,42 @@ -# -*- coding: utf-8 -*- -""" -Utility functions. Part of the pygeoip package. - -@author: Jennifer Ennis <zaylea@gmail.com> - -@license: Copyright(C) 2004 MaxMind LLC - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>. -""" - -import socket -import binascii - - -def ip2long(ip): - """ - Wrapper function for IPv4 and IPv6 converters - @param ip: IPv4 or IPv6 address - @type ip: str - """ - try: - return int(binascii.hexlify(socket.inet_aton(ip)), 16) - except socket.error: - return int(binascii.hexlify(socket.inet_pton(socket.AF_INET6, ip)), 16) +""" +Misc. utility functions. It is part of the pygeoip package. + +@author: Jennifer Ennis <zaylea at gmail dot com> + +@license: +Copyright(C) 2004 MaxMind LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>. +""" + +import six + +def ip2long(ip): + """ + Convert a IPv4 address into a 32-bit integer. + + @param ip: quad-dotted IPv4 address + @type ip: str + @return: network byte order 32-bit integer + @rtype: int + """ + ip_array = ip.split('.') + + if six.PY3: + # int and long are unified in py3 + ip_long = int(ip_array[0]) * 16777216 + int(ip_array[1]) * 65536 + int(ip_array[2]) * 256 + int(ip_array[3]) + else: + ip_long = long(ip_array[0]) * 16777216 + long(ip_array[1]) * 65536 + long(ip_array[2]) * 256 + long(ip_array[3]) + return ip_long + diff --git a/lib/spotimeta/__init__.py b/lib/spotimeta/__init__.py new file mode 100644 index 0000000..fe40d73 --- /dev/null +++ b/lib/spotimeta/__init__.py @@ -0,0 +1,270 @@ +""""Library for querying the Spotify metadata service""" + +__version__ = "0.2" +__author__ = "Rune Halvorsen <runefh@gmail.com>" +__homepage__ = "http://bitbucket.org/runeh/spotimeta/" +__docformat__ = "restructuredtext" + + +import sys +import urllib2 +import time + +try: + from email.utils import parsedate_tz, mktime_tz, formatdate +except ImportError: # utils module name was lowercased after 2.4 + from email.Utils import parsedate_tz, mktime_tz, formatdate + + +from urllib import urlencode +from parser import parse_lookup_doc, parse_search_doc + + +API_VERSION = "1" +USER_AGENT = "Spotimeta %s" % __version__ + + +class SpotimetaError(Exception): + """Superclass for all spotimeta exceptions. Adds no functionality. Only + there so it's possible to set up try blocks that catch all spotimeta + errors, regardless of class""" + pass + + +class RequestTimeout(SpotimetaError): + """Raised when the timeout flag is in use and a request did not finish + within the allotted time.""" + pass + + +class NotFound(SpotimetaError): + """Raised when doing lookup on something that does not exist. Triggered + by the 404 http status code""" + pass + + +class RateLimiting(SpotimetaError): + """Raised when the request was not completed due to rate limiting + restrictions""" + pass + + +class ServiceUnavailable(SpotimetaError): + """Raised when the metadata service is not available (that is, the server + is up, but not accepting API requests at this time""" + pass + + +class ServerError(SpotimetaError): + """Raised when an internal server error occurs. According to the spotify + documentation, this "should not happen".""" + pass + + +def canonical(url_or_uri): + """returns a spotify uri, regardless if a url or uri is passed in""" + if url_or_uri.startswith("http"): # assume it's a url + parts = url_or_uri.split("/") + return "spotify:%s:%s" % (parts[-2], parts[-1]) + else: + return url_or_uri + + +def entrytype(url_or_uri): + """Return "album", "artist" or "track" based on the type of entry the uri + or url refers to.""" + uri = canonical(url_or_uri) + try: + return uri.split(":")[1] + except IndexError: + return None + + +class Metadata(object): + + def __init__(self, cache=None, rate=10, timeout=None, user_agent=None): + self.cache = cache # not implemented yet + self.rate = rate # not implemented yet + self.timeout = timeout + self.user_agent = user_agent or USER_AGENT + self._timeout_supported = True + self._port = "80" + self._host = "ws.spotify.com" + self._detailtypes = { + "artist": {1: "album", 2: "albumdetail"}, + "album": {1: "track", 2: "trackdetail"} + } + + + major, minor = sys.version_info[:2] + if self.timeout and major == 2 and minor <6: + self._timeout_supported = False + import warnings + warnings.warn("Timeouts in urllib not supported in this version" + + " of python. timeout argument will be ignored!") + + + def _do_request(self, url, headers): + """Perform an actual response. Deal with 200 and 304 responses + correctly. If another error occurs, raise the appropriate + exception""" + try: + req = urllib2.Request(url, None, headers) + if self.timeout and self._timeout_supported: + return urllib2.urlopen(req, timeout=self.timeout) + else: + return urllib2.urlopen(req) + + except urllib2.HTTPError, e: + if e.code == 304: + return e # looks wrong but isnt't. On non fatal errors the + # exception behaves like the retval from urlopen + elif e.code == 404: + raise NotFound() + elif e.code == 403: + raise RateLimiting() + elif e.code == 500: + raise ServerError() + elif e.code == 503: + raise ServiceUnavailable() + else: + raise # this should never happen + except urllib2.URLError, e: + """Probably timeout. should do a better check. FIXME""" + raise RequestTimeout() + except: + raise + # all the exceptions we don't know about yet. Probably + # some socket errors will come up here. + + def _get_url(self, url, query, if_modified_since=None): + """Perform an http requests and return the open file-like object, if + there is one, as well as the expiry time and last-modified-time + if they were present in the reply. + If the if_modified_since variable is passed in, send it as the value + of the If-Modified-Since header.""" + if query: + url = "%s?%s" %(url, urlencode(query)) + + headers = {'User-Agent': self.user_agent} + if if_modified_since: + headers["If-Modified-Since"] = formatdate(if_modified_since, False, True) + + fp = self._do_request(url, headers) + + # at this point we have something file like after the request + # finished with a 200 or 304. + + headers = fp.info() + if fp.code == 304: + fp = None + + expires = None + if "Expires" in headers: + expires = mktime_tz(parsedate_tz(headers.get("Expires"))) + + modified = None + if "Last-Modified" in headers: + modified = mktime_tz(parsedate_tz(headers.get("Last-Modified"))) + + return fp, modified, expires + + + def lookup(self, uri, detail=0): + """Lookup metadata for a URI. Optionally ask for extra details. + The details argument is an int: 0 for normal ammount of detauls, 1 + for extra details, and 2 for most details. For tracks the details + argument is ignored, as the Spotify api only has one level of detail + for tracks. For the meaning of the detail levels, look at the + Spotify api docs""" + + key = "%s:%s" % (uri, detail) + res, modified, expires = self._cache_get(key) + + if res and time.time() < expires: + return res + # else, cache is outdated or entry not in it. Normal request cycle + + url = "http://%s:%s/lookup/%s/" % (self._host, self._port, API_VERSION) + uri = canonical(uri) + query = {"uri": uri} + kind = entrytype(uri) + + if detail in (1,2) and kind in self._detailtypes.keys(): + query["extras"] = self._detailtypes[kind][detail] + + fp, new_modified, new_expires = self._get_url(url, query, modified) + + if fp: # We got data, sweet + res = parse_lookup_doc(fp, uri=uri) + + self._cache_put(key, res, new_modified or modified, new_expires or expires) + return res + + def search_album(self, term, page=None): + """The first page is numbered 1!""" + url = "http://%s:%s/search/%s/album" % ( + self._host, self._port, API_VERSION) + + return self._do_search(url, term, page) + + def search_artist(self, term, page=None): + """The first page is numbered 1!""" + url = "http://%s:%s/search/%s/artist" % ( + self._host, self._port, API_VERSION) + + return self._do_search(url, term, page) + + def search_track(self, term, page=None): + """The first page is numbered 1!""" + url = "http://%s:%s/search/%s/track" % ( + self._host, self._port, API_VERSION) + + return self._do_search(url, term, page) + + def _do_search(self, url, term, page): + key = "%s:%s" % (term, page) + + res, modified, expires = self._cache_get(key) + if res and time.time() < expires: + return res + + query = {"q": term.encode('UTF-8')} + + if page is not None: + query["page"] = str(page) + + fp, new_modified, new_expires = self._get_url(url, query, modified) + + if fp: # We got data, sweet + res = parse_search_doc(fp) + + self._cache_put(key, res, new_modified or modified, new_expires or expires) + + return res + + def _cache_get(self, key): + """Get a tuple containing data, last-modified, expires. + If entry is not in cache return None, 0, 0 + """ + entry = None + if self.cache is not None: + entry = self.cache.get(key) + + return entry or (None, 0, 0) + + def _cache_put(self, key, value, modified, expires): + """Inverse of _cache_put""" + if self.cache is not None: + self.cache[key] = value, modified, expires + +# This is an instance of the metadata module used for module level +# operations. Only suitable for simple stuff. Normally one should +# instanciate Metadata manually with appropriate options, especially +# with regards to caching +_module_meta_instance = Metadata() + +lookup = _module_meta_instance.lookup +search_album = _module_meta_instance.search_album +search_artist = _module_meta_instance.search_artist +search_track = _module_meta_instance.search_track diff --git a/lib/spotimeta/parser.py b/lib/spotimeta/parser.py new file mode 100644 index 0000000..98e66e6 --- /dev/null +++ b/lib/spotimeta/parser.py @@ -0,0 +1,196 @@ +from xml.dom import minidom + +# extremely boring dom parsing ahead. Consider yourself warned. + + +# The reason for the uri arg is that the xml returned from lookups do not +# contain the href uri of the thing that was looked up. However, when an +# element is encountered that is NOT the root of a query, it DOES contain +# the href. We pass it in so the returned data will have the same format +# always +def parse_lookup_doc(src, uri=None): + doc = minidom.parse(src) + root = doc.documentElement + + if root.nodeName == "artist": + return {"type": "artist", "result": parse_artist(root, uri)} + elif root.nodeName == "album": + return {"type": "album", "result": parse_album(root, uri)} + elif root.nodeName == "track": + return {"type": "track", "result": parse_track(root, uri)} + else: + raise Exception("unknown node type! " + root.nodeName) # fixme: proper exception here + + +def parse_search_doc(src): + doc = minidom.parse(src) + root = doc.documentElement + + if root.nodeName == "artists": + return parse_artist_search(root) + elif root.nodeName == "albums": + return parse_album_search(root) + elif root.nodeName == "tracks": + return parse_track_search(root) + else: + raise Exception("unknown node type! " + root.nodeName) # fixme: proper exception here + + +def parse_artist(root, uri=None): + ret = {} + if uri or root.hasAttribute("href"): + ret["href"] = uri or root.getAttribute("href") + + for name, elem in _nodes(root): + if name == "name": + ret["name"] = _text(elem) + elif name == "albums": + ret["albums"] = parse_albumlist(elem) + + return ret + + +def parse_artistlist(root): + return map(parse_artist, _filter(root, "artist")) + + +def parse_albumlist(root): + return map(parse_album, _filter(root, "album")) + + +def parse_tracklist(root): + return map(parse_track, _filter(root, "track")) + + +def parse_album(root, uri=None): + ret = {} + if uri or root.hasAttribute("href"): + ret["href"] = uri or root.getAttribute("href") + + for name, elem in _nodes(root): + if name == "name": + ret["name"] = _text(elem) + elif name == "released": + released = _text(elem) + if released: + ret["released"] = int(_text(elem)) + elif name == "id": + if not "ids" in ret: + ret["ids"] = [] + ret["ids"].append(parse_id(elem)) + elif name == "tracks": + ret["tracks"] = parse_tracklist(elem) + + ret["artists"] = parse_artistlist(root) + if len(ret["artists"]) == 1: + ret["artist"] = ret["artists"][0] + else: + ret["artist"] = None + + + # todo: availability stuff. RFH + return ret + + +def parse_id(elem): + ret = {"type": elem.getAttribute("type"), + "id": _text(elem)} + if elem.hasAttribute("href"): + ret["href"] = elem.getAttribute("href") + return ret + + +def parse_track(root, uri=None): + ret = {} + if uri or root.hasAttribute("href"): + ret["href"] = uri or root.getAttribute("href") + + for name, elem in _nodes(root): + if name == "name": + ret["name"] = _text(elem) + elif name == "disc-number": + ret["disc-number"] = int(_text(elem)) + elif name == "track-number": + ret["track-number"] = int(_text(elem)) + elif name == "length": + ret["length"] = float(_text(elem)) + elif name == "popularity": + ret["popularity"] = float(_text(elem)) + elif name == "album": + ret["album"] = parse_album(elem) + elif name == "id": + if not "ids" in ret: + ret["ids"] = [] + ret["ids"].append(parse_id(elem)) + + ret["artists"] = parse_artistlist(root) + + # Following prop is there for backwards compat. It may be dropped in a + # future version + if ret["artists"]: + ret["artist"] = ret["artists"][0] + + return ret + + +def parse_opensearch(root): + ret = {} + elems = root.getElementsByTagNameNS("http://a9.com/-/spec/opensearch/1.1/", "*") + + for name, elem in ((e.localName, e) for e in elems): + if name == "Query": + ret["term"] = elem.getAttribute("searchTerms") + ret["start_page"] = int(elem.getAttribute("startPage")) + elif name == "totalResults": + ret["total_results"] = int(_text(elem)) + elif name == "startIndex": + ret["start_index"] = int(_text(elem)) + elif name == "itemsPerPage": + ret["items_per_page"] = int(_text(elem)) + + return ret + + +def parse_album_search(root): + # Note that the search result tags are not <search> tags or similar. + # Instead they are normal <artists|albums|tracks> tags with extra + # stuff from the opensearch namespace. That's why we cant just directly + # return the result from parse_albumlist + ret = parse_opensearch(root) + ret["result"] = parse_albumlist(root) + return ret + + +def parse_artist_search(root): + ret = parse_opensearch(root) + ret["result"] = parse_artistlist(root) + return ret + + +def parse_track_search(root): + ret = parse_opensearch(root) + ret["result"] = parse_tracklist(root) + return ret + + +def _nodes(elem): + """return an generator yielding element nodes that are children + of elem.""" + return ((e.nodeName, e) for e + in elem.childNodes + if e.nodeType==e.ELEMENT_NODE) + + +def _text(elem): + """Returns a concatenation of all text nodes that are children + of elem (roughly what elem.textContent does in web dom""" + return "".join((e.nodeValue for e + in elem.childNodes + if e.nodeType==e.TEXT_NODE)) + + +def _filter(elem, filtername): + """Returns a generator yielding all child nodes with the nodeName name""" + return (elem for (name, elem) + in _nodes(elem) + if name == filtername) diff --git a/lib/tweepy/CONTRIBUTORS.txt b/lib/tweepy/CONTRIBUTORS.txt deleted file mode 100644 index 12956df..0000000 --- a/lib/tweepy/CONTRIBUTORS.txt +++ /dev/null @@ -1,31 +0,0 @@ -Thank you to all who have contributed to this project! -If you contributed and not listed below please let me know. - -Aaron Swartz -Adam Miskiewicz -AlanBell -Arthur Debert -Bas Westerbaan -Chris Kelly -Clay McClure -Ferenc Szalai -Gergely Imreh -Guan Yang -Ivo Wetzel -James Rowe -Jenny Loomis -Johannes Faigle -Kumar Appaiah -Michael (Doc) Norton -Pascal Jürgens -Robin Houston -Sam Kaufman -Thomas Bohmbach, Jr -Wayne Moore -Will McCutchen -gilles -Can Duruk -Jan Schaumann (@jschauma) -Stuart Powers -Jeff Hull (@jsh2134) -Mike (mikeandmore) diff --git a/lib/tweepy/LICENSE.txt b/lib/tweepy/LICENSE.txt deleted file mode 100644 index 8a91f2c..0000000 --- a/lib/tweepy/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -MIT License -Copyright (c) 2009-2010 Joshua Roesslein - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/lib/tweepy/__init__.py b/lib/tweepy/__init__.py deleted file mode 100644 index 4a45b54..0000000 --- a/lib/tweepy/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -""" -Tweepy Twitter API library -""" -__version__ = '2.1' -__author__ = 'Joshua Roesslein' -__license__ = 'MIT' - -from tweepy.models import Status, User, DirectMessage, Friendship, SavedSearch, SearchResults, ModelFactory, Category -from tweepy.error import TweepError -from tweepy.api import API -from tweepy.cache import Cache, MemoryCache, FileCache -from tweepy.auth import BasicAuthHandler, OAuthHandler -from tweepy.streaming import Stream, StreamListener -from tweepy.cursor import Cursor - -# Global, unauthenticated instance of API -api = API() - -def debug(enable=True, level=1): - - import httplib - httplib.HTTPConnection.debuglevel = level - diff --git a/lib/tweepy/api.py b/lib/tweepy/api.py deleted file mode 100644 index 7418809..0000000 --- a/lib/tweepy/api.py +++ /dev/null @@ -1,718 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -import os -import mimetypes - -from tweepy.binder import bind_api -from tweepy.error import TweepError -from tweepy.parsers import ModelParser -from tweepy.utils import list_to_csv - - -class API(object): - """Twitter API""" - - def __init__(self, auth_handler=None, - host='api.twitter.com', search_host='search.twitter.com', - cache=None, secure=True, api_root='/1.1', search_root='', - retry_count=0, retry_delay=0, retry_errors=None, timeout=60, - parser=None, compression=False): - self.auth = auth_handler - self.host = host - self.search_host = search_host - self.api_root = api_root - self.search_root = search_root - self.cache = cache - self.secure = secure - self.compression = compression - self.retry_count = retry_count - self.retry_delay = retry_delay - self.retry_errors = retry_errors - self.timeout = timeout - self.parser = parser or ModelParser() - - """ statuses/home_timeline """ - home_timeline = bind_api( - path = '/statuses/home_timeline.json', - payload_type = 'status', payload_list = True, - allowed_param = ['since_id', 'max_id', 'count'], - require_auth = True - ) - - """ statuses/user_timeline """ - user_timeline = bind_api( - path = '/statuses/user_timeline.json', - payload_type = 'status', payload_list = True, - allowed_param = ['id', 'user_id', 'screen_name', 'since_id', - 'max_id', 'count', 'include_rts'] - ) - - """ statuses/mentions """ - mentions_timeline = bind_api( - path = '/statuses/mentions_timeline.json', - payload_type = 'status', payload_list = True, - allowed_param = ['since_id', 'max_id', 'count'], - require_auth = True - ) - - """/statuses/:id/retweeted_by.format""" - retweeted_by = bind_api( - path = '/statuses/{id}/retweeted_by.json', - payload_type = 'status', payload_list = True, - allowed_param = ['id', 'count', 'page'], - require_auth = True - ) - - """/related_results/show/:id.format""" - related_results = bind_api( - path = '/related_results/show/{id}.json', - payload_type = 'relation', payload_list = True, - allowed_param = ['id'], - require_auth = False - ) - - """/statuses/:id/retweeted_by/ids.format""" - retweeted_by_ids = bind_api( - path = '/statuses/{id}/retweeted_by/ids.json', - payload_type = 'ids', - allowed_param = ['id', 'count', 'page'], - require_auth = True - ) - - """ statuses/retweets_of_me """ - retweets_of_me = bind_api( - path = '/statuses/retweets_of_me.json', - payload_type = 'status', payload_list = True, - allowed_param = ['since_id', 'max_id', 'count'], - require_auth = True - ) - - """ statuses/show """ - get_status = bind_api( - path = '/statuses/show.json', - payload_type = 'status', - allowed_param = ['id'] - ) - - """ statuses/update """ - update_status = bind_api( - path = '/statuses/update.json', - method = 'POST', - payload_type = 'status', - allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'], - require_auth = True - ) - - """ statuses/destroy """ - destroy_status = bind_api( - path = '/statuses/destroy/{id}.json', - method = 'POST', - payload_type = 'status', - allowed_param = ['id'], - require_auth = True - ) - - """ statuses/retweet """ - retweet = bind_api( - path = '/statuses/retweet/{id}.json', - method = 'POST', - payload_type = 'status', - allowed_param = ['id'], - require_auth = True - ) - - """ statuses/retweets """ - retweets = bind_api( - path = '/statuses/retweets/{id}.json', - payload_type = 'status', payload_list = True, - allowed_param = ['id', 'count'], - require_auth = True - ) - - """ users/show """ - get_user = bind_api( - path = '/users/show.json', - payload_type = 'user', - allowed_param = ['id', 'user_id', 'screen_name'] - ) - - ''' statuses/oembed ''' - get_oembed = bind_api( - path = '/statuses/oembed.json', - payload_type = 'json', - allowed_param = ['id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang'] - ) - - """ Perform bulk look up of users from user ID or screenname """ - def lookup_users(self, user_ids=None, screen_names=None): - return self._lookup_users(list_to_csv(user_ids), list_to_csv(screen_names)) - - _lookup_users = bind_api( - path = '/users/lookup.json', - payload_type = 'user', payload_list = True, - allowed_param = ['user_id', 'screen_name'], - ) - - """ Get the authenticated user """ - def me(self): - return self.get_user(screen_name=self.auth.get_username()) - - """ users/search """ - search_users = bind_api( - path = '/users/search.json', - payload_type = 'user', payload_list = True, - require_auth = True, - allowed_param = ['q', 'per_page', 'page'] - ) - - """ users/suggestions/:slug """ - suggested_users = bind_api( - path = '/users/suggestions/{slug}.json', - payload_type = 'user', payload_list = True, - require_auth = True, - allowed_param = ['slug', 'lang'] - ) - - """ users/suggestions """ - suggested_categories = bind_api( - path = '/users/suggestions.json', - payload_type = 'category', payload_list = True, - allowed_param = ['lang'], - require_auth = True - ) - - """ users/suggestions/:slug/members """ - suggested_users_tweets = bind_api( - path = '/users/suggestions/{slug}/members.json', - payload_type = 'status', payload_list = True, - allowed_param = ['slug'], - require_auth = True - ) - - """ direct_messages """ - direct_messages = bind_api( - path = '/direct_messages.json', - payload_type = 'direct_message', payload_list = True, - allowed_param = ['since_id', 'max_id', 'count'], - require_auth = True - ) - - """ direct_messages/show """ - get_direct_message = bind_api( - path = '/direct_messages/show/{id}.json', - payload_type = 'direct_message', - allowed_param = ['id'], - require_auth = True - ) - - """ direct_messages/sent """ - sent_direct_messages = bind_api( - path = '/direct_messages/sent.json', - payload_type = 'direct_message', payload_list = True, - allowed_param = ['since_id', 'max_id', 'count', 'page'], - require_auth = True - ) - - """ direct_messages/new """ - send_direct_message = bind_api( - path = '/direct_messages/new.json', - method = 'POST', - payload_type = 'direct_message', - allowed_param = ['user', 'screen_name', 'user_id', 'text'], - require_auth = True - ) - - """ direct_messages/destroy """ - destroy_direct_message = bind_api( - path = '/direct_messages/destroy.json', - method = 'DELETE', - payload_type = 'direct_message', - allowed_param = ['id'], - require_auth = True - ) - - """ friendships/create """ - create_friendship = bind_api( - path = '/friendships/create.json', - method = 'POST', - payload_type = 'user', - allowed_param = ['id', 'user_id', 'screen_name', 'follow'], - require_auth = True - ) - - """ friendships/destroy """ - destroy_friendship = bind_api( - path = '/friendships/destroy.json', - method = 'DELETE', - payload_type = 'user', - allowed_param = ['id', 'user_id', 'screen_name'], - require_auth = True - ) - - """ friendships/show """ - show_friendship = bind_api( - path = '/friendships/show.json', - payload_type = 'friendship', - allowed_param = ['source_id', 'source_screen_name', - 'target_id', 'target_screen_name'] - ) - - """ Perform bulk look up of friendships from user ID or screenname """ - def lookup_friendships(self, user_ids=None, screen_names=None): - return self._lookup_friendships(list_to_csv(user_ids), list_to_csv(screen_names)) - - _lookup_friendships = bind_api( - path = '/friendships/lookup.json', - payload_type = 'relationship', payload_list = True, - allowed_param = ['user_id', 'screen_name'], - require_auth = True - ) - - - """ friends/ids """ - friends_ids = bind_api( - path = '/friends/ids.json', - payload_type = 'ids', - allowed_param = ['id', 'user_id', 'screen_name', 'cursor'] - ) - - """ friends/list """ - friends = bind_api( - path = '/friends/list.json', - payload_type = 'user', payload_list = True, - allowed_param = ['id', 'user_id', 'screen_name', 'cursor'] - ) - - """ friendships/incoming """ - friendships_incoming = bind_api( - path = '/friendships/incoming.json', - payload_type = 'ids', - allowed_param = ['cursor'] - ) - - """ friendships/outgoing""" - friendships_outgoing = bind_api( - path = '/friendships/outgoing.json', - payload_type = 'ids', - allowed_param = ['cursor'] - ) - - """ followers/ids """ - followers_ids = bind_api( - path = '/followers/ids.json', - payload_type = 'ids', - allowed_param = ['id', 'user_id', 'screen_name', 'cursor'] - ) - - """ followers/list """ - followers = bind_api( - path = '/followers/list.json', - payload_type = 'user', payload_list = True, - allowed_param = ['id', 'user_id', 'screen_name', 'cursor'] - ) - - """ account/verify_credentials """ - def verify_credentials(self, **kargs): - try: - return bind_api( - path = '/account/verify_credentials.json', - payload_type = 'user', - require_auth = True, - allowed_param = ['include_entities', 'skip_status'], - )(self, **kargs) - except TweepError, e: - if e.response and e.response.status == 401: - return False - raise - - """ account/rate_limit_status """ - rate_limit_status = bind_api( - path = '/application/rate_limit_status.json', - payload_type = 'json', - allowed_param = ['resources'], - use_cache = False - ) - - """ account/update_delivery_device """ - set_delivery_device = bind_api( - path = '/account/update_delivery_device.json', - method = 'POST', - allowed_param = ['device'], - payload_type = 'user', - require_auth = True - ) - - """ account/update_profile_colors """ - update_profile_colors = bind_api( - path = '/account/update_profile_colors.json', - method = 'POST', - payload_type = 'user', - allowed_param = ['profile_background_color', 'profile_text_color', - 'profile_link_color', 'profile_sidebar_fill_color', - 'profile_sidebar_border_color'], - require_auth = True - ) - - """ account/update_profile_image """ - def update_profile_image(self, filename): - headers, post_data = API._pack_image(filename, 700) - return bind_api( - path = '/account/update_profile_image.json', - method = 'POST', - payload_type = 'user', - require_auth = True - )(self, post_data=post_data, headers=headers) - - """ account/update_profile_background_image """ - def update_profile_background_image(self, filename, *args, **kargs): - headers, post_data = API._pack_image(filename, 800) - bind_api( - path = '/account/update_profile_background_image.json', - method = 'POST', - payload_type = 'user', - allowed_param = ['tile'], - require_auth = True - )(self, post_data=post_data, headers=headers) - - """ account/update_profile """ - update_profile = bind_api( - path = '/account/update_profile.json', - method = 'POST', - payload_type = 'user', - allowed_param = ['name', 'url', 'location', 'description'], - require_auth = True - ) - - """ favorites """ - favorites = bind_api( - path = '/favorites/list.json', - payload_type = 'status', payload_list = True, - allowed_param = ['screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id'] - ) - - """ favorites/create """ - create_favorite = bind_api( - path = '/favorites/create.json', - method = 'POST', - payload_type = 'status', - allowed_param = ['id'], - require_auth = True - ) - - """ favorites/destroy """ - destroy_favorite = bind_api( - path = '/favorites/destroy.json', - method = 'POST', - payload_type = 'status', - allowed_param = ['id'], - require_auth = True - ) - - """ blocks/create """ - create_block = bind_api( - path = '/blocks/create.json', - method = 'POST', - payload_type = 'user', - allowed_param = ['id', 'user_id', 'screen_name'], - require_auth = True - ) - - """ blocks/destroy """ - destroy_block = bind_api( - path = '/blocks/destroy.json', - method = 'DELETE', - payload_type = 'user', - allowed_param = ['id', 'user_id', 'screen_name'], - require_auth = True - ) - - """ blocks/blocking """ - blocks = bind_api( - path = '/blocks/list.json', - payload_type = 'user', payload_list = True, - allowed_param = ['cursor'], - require_auth = True - ) - - """ blocks/blocking/ids """ - blocks_ids = bind_api( - path = '/blocks/ids.json', - payload_type = 'json', - require_auth = True - ) - - """ report_spam """ - report_spam = bind_api( - path = '/users/report_spam.json', - method = 'POST', - payload_type = 'user', - allowed_param = ['user_id', 'screen_name'], - require_auth = True - ) - - """ saved_searches """ - saved_searches = bind_api( - path = '/saved_searches/list.json', - payload_type = 'saved_search', payload_list = True, - require_auth = True - ) - - """ saved_searches/show """ - get_saved_search = bind_api( - path = '/saved_searches/show/{id}.json', - payload_type = 'saved_search', - allowed_param = ['id'], - require_auth = True - ) - - """ saved_searches/create """ - create_saved_search = bind_api( - path = '/saved_searches/create.json', - method = 'POST', - payload_type = 'saved_search', - allowed_param = ['query'], - require_auth = True - ) - - """ saved_searches/destroy """ - destroy_saved_search = bind_api( - path = '/saved_searches/destroy/{id}.json', - method = 'POST', - payload_type = 'saved_search', - allowed_param = ['id'], - require_auth = True - ) - - """ help/test """ - def test(self): - try: - bind_api( - path = '/help/test.json', - )(self) - except TweepError: - return False - return True - - create_list = bind_api( - path = '/lists/create.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['name', 'mode', 'description'], - require_auth = True - ) - - destroy_list = bind_api( - path = '/lists/destroy.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['owner_screen_name', 'owner_id', 'list_id', 'slug'], - require_auth = True - ) - - update_list = bind_api( - path = '/lists/update.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'], - require_auth = True - ) - - lists_all = bind_api( - path = '/lists/list.json', - payload_type = 'list', payload_list = True, - allowed_param = ['screen_name', 'user_id'], - require_auth = True - ) - - lists_memberships = bind_api( - path = '/lists/memberships.json', - payload_type = 'list', payload_list = True, - allowed_param = ['screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'], - require_auth = True - ) - - lists_subscriptions = bind_api( - path = '/lists/subscriptions.json', - payload_type = 'list', payload_list = True, - allowed_param = ['screen_name', 'user_id', 'cursor'], - require_auth = True - ) - - list_timeline = bind_api( - path = '/lists/statuses.json', - payload_type = 'status', payload_list = True, - allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'since_id', 'max_id', 'count'] - ) - - get_list = bind_api( - path = '/lists/show.json', - payload_type = 'list', - allowed_param = ['owner_screen_name', 'owner_id', 'slug', 'list_id'] - ) - - add_list_member = bind_api( - path = '/lists/members/create.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['screen_name', 'user_id', 'owner_screen_name', 'owner_id', 'slug', 'list_id'], - require_auth = True - ) - - remove_list_member = bind_api( - path = '/lists/members/destroy.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['screen_name', 'user_id', 'owner_screen_name', 'owner_id', 'slug', 'list_id'], - require_auth = True - ) - - list_members = bind_api( - path = '/lists/members.json', - payload_type = 'user', payload_list = True, - allowed_param = ['owner_screen_name', 'slug', 'list_id', 'owner_id', 'cursor'] - ) - - show_list_member = bind_api( - path = '/lists/members/show.json', - payload_type = 'user', - allowed_param = ['list_id', 'slug', 'user_id', 'screen_name', 'owner_screen_name', 'owner_id'] - ) - - subscribe_list = bind_api( - path = '/lists/subscribers/create.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id'], - require_auth = True - ) - - unsubscribe_list = bind_api( - path = '/lists/subscribers/destroy.json', - method = 'POST', - payload_type = 'list', - allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id'], - require_auth = True - ) - - list_subscribers = bind_api( - path = '/lists/subscribers.json', - payload_type = 'user', payload_list = True, - allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'cursor'] - ) - - show_list_subscriber = bind_api( - path = '/lists/subscribers/show.json', - payload_type = 'user', - allowed_param = ['owner_screen_name', 'slug', 'screen_name', 'owner_id', 'list_id', 'user_id'] - ) - - """ trends/available """ - trends_available = bind_api( - path = '/trends/available.json', - payload_type = 'json' - ) - - trends_place = bind_api( - path = '/trends/place.json', - payload_type = 'json', - allowed_param = ['id', 'exclude'] - ) - - trends_closest = bind_api( - path = '/trends/closest.json', - payload_type = 'json', - allowed_param = ['lat', 'long'] - ) - - """ search """ - search = bind_api( - path = '/search/tweets.json', - payload_type = 'search_results', - allowed_param = ['q', 'lang', 'locale', 'since_id', 'geocode', 'show_user', 'max_id', 'since', 'until', 'result_type'] - ) - - """ trends/daily """ - trends_daily = bind_api( - path = '/trends/daily.json', - payload_type = 'json', - allowed_param = ['date', 'exclude'] - ) - - """ trends/weekly """ - trends_weekly = bind_api( - path = '/trends/weekly.json', - payload_type = 'json', - allowed_param = ['date', 'exclude'] - ) - - """ geo/reverse_geocode """ - reverse_geocode = bind_api( - path = '/geo/reverse_geocode.json', - payload_type = 'place', payload_list = True, - allowed_param = ['lat', 'long', 'accuracy', 'granularity', 'max_results'] - ) - - """ geo/id """ - geo_id = bind_api( - path = '/geo/id/{id}.json', - payload_type = 'place', - allowed_param = ['id'] - ) - - """ geo/search """ - geo_search = bind_api( - path = '/geo/search.json', - payload_type = 'place', payload_list = True, - allowed_param = ['lat', 'long', 'query', 'ip', 'granularity', 'accuracy', 'max_results', 'contained_within'] - ) - - """ geo/similar_places """ - geo_similar_places = bind_api( - path = '/geo/similar_places.json', - payload_type = 'place', payload_list = True, - allowed_param = ['lat', 'long', 'name', 'contained_within'] - ) - - """ Internal use only """ - @staticmethod - def _pack_image(filename, max_size): - """Pack image from file into multipart-formdata post body""" - # image must be less than 700kb in size - try: - if os.path.getsize(filename) > (max_size * 1024): - raise TweepError('File is too big, must be less than 700kb.') - except os.error: - raise TweepError('Unable to access file') - - # image must be gif, jpeg, or png - file_type = mimetypes.guess_type(filename) - if file_type is None: - raise TweepError('Could not determine file type') - file_type = file_type[0] - if file_type not in ['image/gif', 'image/jpeg', 'image/png']: - raise TweepError('Invalid file type for image: %s' % file_type) - - # build the mulitpart-formdata body - fp = open(filename, 'rb') - BOUNDARY = 'Tw3ePy' - body = [] - body.append('--' + BOUNDARY) - body.append('Content-Disposition: form-data; name="image"; filename="%s"' % filename) - body.append('Content-Type: %s' % file_type) - body.append('') - body.append(fp.read()) - body.append('--' + BOUNDARY + '--') - body.append('') - fp.close() - body = '\r\n'.join(body) - - # build headers - headers = { - 'Content-Type': 'multipart/form-data; boundary=Tw3ePy', - 'Content-Length': str(len(body)) - } - - return headers, body - diff --git a/lib/tweepy/auth.py b/lib/tweepy/auth.py deleted file mode 100644 index 27890aa..0000000 --- a/lib/tweepy/auth.py +++ /dev/null @@ -1,163 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -from urllib2 import Request, urlopen -import base64 - -from tweepy import oauth -from tweepy.error import TweepError -from tweepy.api import API - - -class AuthHandler(object): - - def apply_auth(self, url, method, headers, parameters): - """Apply authentication headers to request""" - raise NotImplementedError - - def get_username(self): - """Return the username of the authenticated user""" - raise NotImplementedError - - -class BasicAuthHandler(AuthHandler): - - def __init__(self, username, password): - self.username = username - self._b64up = base64.b64encode('%s:%s' % (username, password)) - - def apply_auth(self, url, method, headers, parameters): - headers['Authorization'] = 'Basic %s' % self._b64up - - def get_username(self): - return self.username - - -class OAuthHandler(AuthHandler): - """OAuth authentication handler""" - - OAUTH_HOST = 'api.twitter.com' - OAUTH_ROOT = '/oauth/' - - def __init__(self, consumer_key, consumer_secret, callback=None, secure=False): - self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) - self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1() - self.request_token = None - self.access_token = None - self.callback = callback - self.username = None - self.secure = secure - - def _get_oauth_url(self, endpoint, secure=False): - if self.secure or secure: - prefix = 'https://' - else: - prefix = 'http://' - - return prefix + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint - - def apply_auth(self, url, method, headers, parameters): - request = oauth.OAuthRequest.from_consumer_and_token( - self._consumer, http_url=url, http_method=method, - token=self.access_token, parameters=parameters - ) - request.sign_request(self._sigmethod, self._consumer, self.access_token) - headers.update(request.to_header()) - - def _get_request_token(self): - try: - url = self._get_oauth_url('request_token') - request = oauth.OAuthRequest.from_consumer_and_token( - self._consumer, http_url=url, callback=self.callback - ) - request.sign_request(self._sigmethod, self._consumer, None) - resp = urlopen(Request(url, headers=request.to_header())) - return oauth.OAuthToken.from_string(resp.read()) - except Exception, e: - raise TweepError(e) - - def set_request_token(self, key, secret): - self.request_token = oauth.OAuthToken(key, secret) - - def set_access_token(self, key, secret): - self.access_token = oauth.OAuthToken(key, secret) - - def get_authorization_url(self, signin_with_twitter=False): - """Get the authorization URL to redirect the user""" - try: - # get the request token - self.request_token = self._get_request_token() - - # build auth request and return as url - if signin_with_twitter: - url = self._get_oauth_url('authenticate') - else: - url = self._get_oauth_url('authorize') - request = oauth.OAuthRequest.from_token_and_callback( - token=self.request_token, http_url=url - ) - - return request.to_url() - except Exception, e: - raise TweepError(e) - - def get_access_token(self, verifier=None): - """ - After user has authorized the request token, get access token - with user supplied verifier. - """ - try: - url = self._get_oauth_url('access_token') - - # build request - request = oauth.OAuthRequest.from_consumer_and_token( - self._consumer, - token=self.request_token, http_url=url, - verifier=str(verifier) - ) - request.sign_request(self._sigmethod, self._consumer, self.request_token) - - # send request - resp = urlopen(Request(url, headers=request.to_header())) - self.access_token = oauth.OAuthToken.from_string(resp.read()) - return self.access_token - except Exception, e: - raise TweepError(e) - - def get_xauth_access_token(self, username, password): - """ - Get an access token from an username and password combination. - In order to get this working you need to create an app at - http://twitter.com/apps, after that send a mail to api@twitter.com - and request activation of xAuth for it. - """ - try: - url = self._get_oauth_url('access_token', secure=True) # must use HTTPS - request = oauth.OAuthRequest.from_consumer_and_token( - oauth_consumer=self._consumer, - http_method='POST', http_url=url, - parameters = { - 'x_auth_mode': 'client_auth', - 'x_auth_username': username, - 'x_auth_password': password - } - ) - request.sign_request(self._sigmethod, self._consumer, None) - - resp = urlopen(Request(url, data=request.to_postdata())) - self.access_token = oauth.OAuthToken.from_string(resp.read()) - return self.access_token - except Exception, e: - raise TweepError(e) - - def get_username(self): - if self.username is None: - api = API(self) - user = api.verify_credentials() - if user: - self.username = user.screen_name - else: - raise TweepError("Unable to get username, invalid oauth token!") - return self.username - diff --git a/lib/tweepy/binder.py b/lib/tweepy/binder.py deleted file mode 100644 index 0797215..0000000 --- a/lib/tweepy/binder.py +++ /dev/null @@ -1,210 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -import httplib -import urllib -import time -import re -from StringIO import StringIO -import gzip - -from tweepy.error import TweepError -from tweepy.utils import convert_to_utf8_str -from tweepy.models import Model - -re_path_template = re.compile('{\w+}') - - -def bind_api(**config): - - class APIMethod(object): - - path = config['path'] - payload_type = config.get('payload_type', None) - payload_list = config.get('payload_list', False) - allowed_param = config.get('allowed_param', []) - method = config.get('method', 'GET') - require_auth = config.get('require_auth', False) - search_api = config.get('search_api', False) - use_cache = config.get('use_cache', True) - - def __init__(self, api, args, kargs): - # If authentication is required and no credentials - # are provided, throw an error. - if self.require_auth and not api.auth: - raise TweepError('Authentication required!') - - self.api = api - self.post_data = kargs.pop('post_data', None) - self.retry_count = kargs.pop('retry_count', api.retry_count) - self.retry_delay = kargs.pop('retry_delay', api.retry_delay) - self.retry_errors = kargs.pop('retry_errors', api.retry_errors) - self.headers = kargs.pop('headers', {}) - self.build_parameters(args, kargs) - - # Pick correct URL root to use - if self.search_api: - self.api_root = api.search_root - else: - self.api_root = api.api_root - - # Perform any path variable substitution - self.build_path() - - if api.secure: - self.scheme = 'https://' - else: - self.scheme = 'http://' - - if self.search_api: - self.host = api.search_host - else: - self.host = api.host - - # Manually set Host header to fix an issue in python 2.5 - # or older where Host is set including the 443 port. - # This causes Twitter to issue 301 redirect. - # See Issue https://github.com/tweepy/tweepy/issues/12 - self.headers['Host'] = self.host - - def build_parameters(self, args, kargs): - self.parameters = {} - for idx, arg in enumerate(args): - if arg is None: - continue - - try: - self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg) - except IndexError: - raise TweepError('Too many parameters supplied!') - - for k, arg in kargs.items(): - if arg is None: - continue - if k in self.parameters: - raise TweepError('Multiple values for parameter %s supplied!' % k) - - self.parameters[k] = convert_to_utf8_str(arg) - - def build_path(self): - for variable in re_path_template.findall(self.path): - name = variable.strip('{}') - - if name == 'user' and 'user' not in self.parameters and self.api.auth: - # No 'user' parameter provided, fetch it from Auth instead. - value = self.api.auth.get_username() - else: - try: - value = urllib.quote(self.parameters[name]) - except KeyError: - raise TweepError('No parameter value found for path variable: %s' % name) - del self.parameters[name] - - self.path = self.path.replace(variable, value) - - def execute(self): - # Build the request URL - url = self.api_root + self.path - if len(self.parameters): - url = '%s?%s' % (url, urllib.urlencode(self.parameters)) - - # Query the cache if one is available - # and this request uses a GET method. - if self.use_cache and self.api.cache and self.method == 'GET': - cache_result = self.api.cache.get(url) - # if cache result found and not expired, return it - if cache_result: - # must restore api reference - if isinstance(cache_result, list): - for result in cache_result: - if isinstance(result, Model): - result._api = self.api - else: - if isinstance(cache_result, Model): - cache_result._api = self.api - return cache_result - - # Continue attempting request until successful - # or maximum number of retries is reached. - retries_performed = 0 - while retries_performed < self.retry_count + 1: - # Open connection - if self.api.secure: - conn = httplib.HTTPSConnection(self.host, timeout=self.api.timeout) - else: - conn = httplib.HTTPConnection(self.host, timeout=self.api.timeout) - - # Apply authentication - if self.api.auth: - self.api.auth.apply_auth( - self.scheme + self.host + url, - self.method, self.headers, self.parameters - ) - - # Request compression if configured - if self.api.compression: - self.headers['Accept-encoding'] = 'gzip' - - # Execute request - try: - conn.request(self.method, url, headers=self.headers, body=self.post_data) - resp = conn.getresponse() - except Exception, e: - raise TweepError('Failed to send request: %s' % e) - - # Exit request loop if non-retry error code - if self.retry_errors: - if resp.status not in self.retry_errors: break - else: - if resp.status == 200: break - - # Sleep before retrying request again - time.sleep(self.retry_delay) - retries_performed += 1 - - # If an error was returned, throw an exception - self.api.last_response = resp - if resp.status != 200: - try: - error_msg = self.api.parser.parse_error(resp.read()) - except Exception: - error_msg = "Twitter error response: status code = %s" % resp.status - raise TweepError(error_msg, resp) - - # Parse the response payload - body = resp.read() - if resp.getheader('Content-Encoding', '') == 'gzip': - try: - zipper = gzip.GzipFile(fileobj=StringIO(body)) - body = zipper.read() - except Exception, e: - raise TweepError('Failed to decompress data: %s' % e) - result = self.api.parser.parse(self, body) - - conn.close() - - # Store result into cache if one is available. - if self.use_cache and self.api.cache and self.method == 'GET' and result: - self.api.cache.store(url, result) - - return result - - - def _call(api, *args, **kargs): - - method = APIMethod(api, args, kargs) - return method.execute() - - - # Set pagination mode - if 'cursor' in APIMethod.allowed_param: - _call.pagination_mode = 'cursor' - elif 'max_id' in APIMethod.allowed_param and \ - 'since_id' in APIMethod.allowed_param: - _call.pagination_mode = 'id' - elif 'page' in APIMethod.allowed_param: - _call.pagination_mode = 'page' - - return _call - diff --git a/lib/tweepy/cache.py b/lib/tweepy/cache.py deleted file mode 100644 index 25564a3..0000000 --- a/lib/tweepy/cache.py +++ /dev/null @@ -1,424 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -import time -import datetime -import threading -import os - -try: - import cPickle as pickle -except ImportError: - import pickle - -try: - import hashlib -except ImportError: - # python 2.4 - import md5 as hashlib - -try: - import fcntl -except ImportError: - # Probably on a windows system - # TODO: use win32file - pass - - -class Cache(object): - """Cache interface""" - - def __init__(self, timeout=60): - """Initialize the cache - timeout: number of seconds to keep a cached entry - """ - self.timeout = timeout - - def store(self, key, value): - """Add new record to cache - key: entry key - value: data of entry - """ - raise NotImplementedError - - def get(self, key, timeout=None): - """Get cached entry if exists and not expired - key: which entry to get - timeout: override timeout with this value [optional] - """ - raise NotImplementedError - - def count(self): - """Get count of entries currently stored in cache""" - raise NotImplementedError - - def cleanup(self): - """Delete any expired entries in cache.""" - raise NotImplementedError - - def flush(self): - """Delete all cached entries""" - raise NotImplementedError - - -class MemoryCache(Cache): - """In-memory cache""" - - def __init__(self, timeout=60): - Cache.__init__(self, timeout) - self._entries = {} - self.lock = threading.Lock() - - def __getstate__(self): - # pickle - return {'entries': self._entries, 'timeout': self.timeout} - - def __setstate__(self, state): - # unpickle - self.lock = threading.Lock() - self._entries = state['entries'] - self.timeout = state['timeout'] - - def _is_expired(self, entry, timeout): - return timeout > 0 and (time.time() - entry[0]) >= timeout - - def store(self, key, value): - self.lock.acquire() - self._entries[key] = (time.time(), value) - self.lock.release() - - def get(self, key, timeout=None): - self.lock.acquire() - try: - # check to see if we have this key - entry = self._entries.get(key) - if not entry: - # no hit, return nothing - return None - - # use provided timeout in arguments if provided - # otherwise use the one provided during init. - if timeout is None: - timeout = self.timeout - - # make sure entry is not expired - if self._is_expired(entry, timeout): - # entry expired, delete and return nothing - del self._entries[key] - return None - - # entry found and not expired, return it - return entry[1] - finally: - self.lock.release() - - def count(self): - return len(self._entries) - - def cleanup(self): - self.lock.acquire() - try: - for k, v in self._entries.items(): - if self._is_expired(v, self.timeout): - del self._entries[k] - finally: - self.lock.release() - - def flush(self): - self.lock.acquire() - self._entries.clear() - self.lock.release() - - -class FileCache(Cache): - """File-based cache""" - - # locks used to make cache thread-safe - cache_locks = {} - - def __init__(self, cache_dir, timeout=60): - Cache.__init__(self, timeout) - if os.path.exists(cache_dir) is False: - os.mkdir(cache_dir) - self.cache_dir = cache_dir - if cache_dir in FileCache.cache_locks: - self.lock = FileCache.cache_locks[cache_dir] - else: - self.lock = threading.Lock() - FileCache.cache_locks[cache_dir] = self.lock - - if os.name == 'posix': - self._lock_file = self._lock_file_posix - self._unlock_file = self._unlock_file_posix - elif os.name == 'nt': - self._lock_file = self._lock_file_win32 - self._unlock_file = self._unlock_file_win32 - else: - print 'Warning! FileCache locking not supported on this system!' - self._lock_file = self._lock_file_dummy - self._unlock_file = self._unlock_file_dummy - - def _get_path(self, key): - md5 = hashlib.md5() - md5.update(key) - return os.path.join(self.cache_dir, md5.hexdigest()) - - def _lock_file_dummy(self, path, exclusive=True): - return None - - def _unlock_file_dummy(self, lock): - return - - def _lock_file_posix(self, path, exclusive=True): - lock_path = path + '.lock' - if exclusive is True: - f_lock = open(lock_path, 'w') - fcntl.lockf(f_lock, fcntl.LOCK_EX) - else: - f_lock = open(lock_path, 'r') - fcntl.lockf(f_lock, fcntl.LOCK_SH) - if os.path.exists(lock_path) is False: - f_lock.close() - return None - return f_lock - - def _unlock_file_posix(self, lock): - lock.close() - - def _lock_file_win32(self, path, exclusive=True): - # TODO: implement - return None - - def _unlock_file_win32(self, lock): - # TODO: implement - return - - def _delete_file(self, path): - os.remove(path) - if os.path.exists(path + '.lock'): - os.remove(path + '.lock') - - def store(self, key, value): - path = self._get_path(key) - self.lock.acquire() - try: - # acquire lock and open file - f_lock = self._lock_file(path) - datafile = open(path, 'wb') - - # write data - pickle.dump((time.time(), value), datafile) - - # close and unlock file - datafile.close() - self._unlock_file(f_lock) - finally: - self.lock.release() - - def get(self, key, timeout=None): - return self._get(self._get_path(key), timeout) - - def _get(self, path, timeout): - if os.path.exists(path) is False: - # no record - return None - self.lock.acquire() - try: - # acquire lock and open - f_lock = self._lock_file(path, False) - datafile = open(path, 'rb') - - # read pickled object - created_time, value = pickle.load(datafile) - datafile.close() - - # check if value is expired - if timeout is None: - timeout = self.timeout - if timeout > 0 and (time.time() - created_time) >= timeout: - # expired! delete from cache - value = None - self._delete_file(path) - - # unlock and return result - self._unlock_file(f_lock) - return value - finally: - self.lock.release() - - def count(self): - c = 0 - for entry in os.listdir(self.cache_dir): - if entry.endswith('.lock'): - continue - c += 1 - return c - - def cleanup(self): - for entry in os.listdir(self.cache_dir): - if entry.endswith('.lock'): - continue - self._get(os.path.join(self.cache_dir, entry), None) - - def flush(self): - for entry in os.listdir(self.cache_dir): - if entry.endswith('.lock'): - continue - self._delete_file(os.path.join(self.cache_dir, entry)) - -class MemCacheCache(Cache): - """Cache interface""" - - def __init__(self, client, timeout=60): - """Initialize the cache - client: The memcache client - timeout: number of seconds to keep a cached entry - """ - self.client = client - self.timeout = timeout - - def store(self, key, value): - """Add new record to cache - key: entry key - value: data of entry - """ - self.client.set(key, value, time=self.timeout) - - def get(self, key, timeout=None): - """Get cached entry if exists and not expired - key: which entry to get - timeout: override timeout with this value [optional]. DOES NOT WORK HERE - """ - return self.client.get(key) - - def count(self): - """Get count of entries currently stored in cache. RETURN 0""" - raise NotImplementedError - - def cleanup(self): - """Delete any expired entries in cache. NO-OP""" - raise NotImplementedError - - def flush(self): - """Delete all cached entries. NO-OP""" - raise NotImplementedError - -class RedisCache(Cache): - '''Cache running in a redis server''' - - def __init__(self, client, timeout=60, keys_container = 'tweepy:keys', pre_identifier = 'tweepy:'): - Cache.__init__(self, timeout) - self.client = client - self.keys_container = keys_container - self.pre_identifier = pre_identifier - - def _is_expired(self, entry, timeout): - # Returns true if the entry has expired - return timeout > 0 and (time.time() - entry[0]) >= timeout - - def store(self, key, value): - '''Store the key, value pair in our redis server''' - # Prepend tweepy to our key, this makes it easier to identify tweepy keys in our redis server - key = self.pre_identifier + key - # Get a pipe (to execute several redis commands in one step) - pipe = self.client.pipeline() - # Set our values in a redis hash (similar to python dict) - pipe.set(key, pickle.dumps((time.time(), value))) - # Set the expiration - pipe.expire(key, self.timeout) - # Add the key to a set containing all the keys - pipe.sadd(self.keys_container, key) - # Execute the instructions in the redis server - pipe.execute() - - def get(self, key, timeout=None): - '''Given a key, returns an element from the redis table''' - key = self.pre_identifier + key - # Check to see if we have this key - unpickled_entry = self.client.get(key) - if not unpickled_entry: - # No hit, return nothing - return None - - entry = pickle.loads(unpickled_entry) - # Use provided timeout in arguments if provided - # otherwise use the one provided during init. - if timeout is None: - timeout = self.timeout - - # Make sure entry is not expired - if self._is_expired(entry, timeout): - # entry expired, delete and return nothing - self.delete_entry(key) - return None - # entry found and not expired, return it - return entry[1] - - def count(self): - '''Note: This is not very efficient, since it retreives all the keys from the redis - server to know how many keys we have''' - return len(self.client.smembers(self.keys_container)) - - def delete_entry(self, key): - '''Delete an object from the redis table''' - pipe = self.client.pipeline() - pipe.srem(self.keys_container, key) - pipe.delete(key) - pipe.execute() - - def cleanup(self): - '''Cleanup all the expired keys''' - keys = self.client.smembers(self.keys_container) - for key in keys: - entry = self.client.get(key) - if entry: - entry = pickle.loads(entry) - if self._is_expired(entry, self.timeout): - self.delete_entry(key) - - def flush(self): - '''Delete all entries from the cache''' - keys = self.client.smembers(self.keys_container) - for key in keys: - self.delete_entry(key) - - -class MongodbCache(Cache): - """A simple pickle-based MongoDB cache sytem.""" - - def __init__(self, db, timeout=3600, collection='tweepy_cache'): - """Should receive a "database" cursor from pymongo.""" - Cache.__init__(self, timeout) - self.timeout = timeout - self.col = db[collection] - self.col.create_index('created', expireAfterSeconds=timeout) - - def store(self, key, value): - from bson.binary import Binary - - now = datetime.datetime.utcnow() - blob = Binary(pickle.dumps(value)) - - self.col.insert({'created': now, '_id': key, 'value': blob}) - - def get(self, key, timeout=None): - if timeout: - raise NotImplementedError - obj = self.col.find_one({'_id': key}) - if obj: - return pickle.loads(obj['value']) - - def count(self): - return self.col.find({}).count() - - def delete_entry(self, key): - return self.col.remove({'_id': key}) - - def cleanup(self): - """MongoDB will automatically clear expired keys.""" - pass - - def flush(self): - self.col.drop() - self.col.create_index('created', expireAfterSeconds=self.timeout) diff --git a/lib/tweepy/cursor.py b/lib/tweepy/cursor.py deleted file mode 100644 index 9061bfd..0000000 --- a/lib/tweepy/cursor.py +++ /dev/null @@ -1,170 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -from tweepy.error import TweepError - -class Cursor(object): - """Pagination helper class""" - - def __init__(self, method, *args, **kargs): - if hasattr(method, 'pagination_mode'): - if method.pagination_mode == 'cursor': - self.iterator = CursorIterator(method, args, kargs) - elif method.pagination_mode == 'id': - self.iterator = IdIterator(method, args, kargs) - elif method.pagination_mode == 'page': - self.iterator = PageIterator(method, args, kargs) - else: - raise TweepError('Invalid pagination mode.') - else: - raise TweepError('This method does not perform pagination') - - def pages(self, limit=0): - """Return iterator for pages""" - if limit > 0: - self.iterator.limit = limit - return self.iterator - - def items(self, limit=0): - """Return iterator for items in each page""" - i = ItemIterator(self.iterator) - i.limit = limit - return i - -class BaseIterator(object): - - def __init__(self, method, args, kargs): - self.method = method - self.args = args - self.kargs = kargs - self.limit = 0 - - def next(self): - raise NotImplementedError - - def prev(self): - raise NotImplementedError - - def __iter__(self): - return self - -class CursorIterator(BaseIterator): - - def __init__(self, method, args, kargs): - BaseIterator.__init__(self, method, args, kargs) - self.next_cursor = -1 - self.prev_cursor = 0 - self.count = 0 - - def next(self): - if self.next_cursor == 0 or (self.limit and self.count == self.limit): - raise StopIteration - data, cursors = self.method( - cursor=self.next_cursor, *self.args, **self.kargs - ) - self.prev_cursor, self.next_cursor = cursors - if len(data) == 0: - raise StopIteration - self.count += 1 - return data - - def prev(self): - if self.prev_cursor == 0: - raise TweepError('Can not page back more, at first page') - data, self.next_cursor, self.prev_cursor = self.method( - cursor=self.prev_cursor, *self.args, **self.kargs - ) - self.count -= 1 - return data - -class IdIterator(BaseIterator): - - def __init__(self, method, args, kargs): - BaseIterator.__init__(self, method, args, kargs) - self.max_id = kargs.get('max_id') - self.since_id = kargs.get('since_id') - self.count = 0 - - def next(self): - """Fetch a set of items with IDs less than current set.""" - if self.limit and self.limit == self.count: - raise StopIteration - - # max_id is inclusive so decrement by one - # to avoid requesting duplicate items. - max_id = self.since_id - 1 if self.max_id else None - data = self.method(max_id = max_id, *self.args, **self.kargs) - if len(data) == 0: - raise StopIteration - self.max_id = data.max_id - self.since_id = data.since_id - self.count += 1 - return data - - def prev(self): - """Fetch a set of items with IDs greater than current set.""" - if self.limit and self.limit == self.count: - raise StopIteration - - since_id = self.max_id - data = self.method(since_id = since_id, *self.args, **self.kargs) - if len(data) == 0: - raise StopIteration - self.max_id = data.max_id - self.since_id = data.since_id - self.count += 1 - return data - -class PageIterator(BaseIterator): - - def __init__(self, method, args, kargs): - BaseIterator.__init__(self, method, args, kargs) - self.current_page = 0 - - def next(self): - self.current_page += 1 - items = self.method(page=self.current_page, *self.args, **self.kargs) - if len(items) == 0 or (self.limit > 0 and self.current_page > self.limit): - raise StopIteration - return items - - def prev(self): - if (self.current_page == 1): - raise TweepError('Can not page back more, at first page') - self.current_page -= 1 - return self.method(page=self.current_page, *self.args, **self.kargs) - -class ItemIterator(BaseIterator): - - def __init__(self, page_iterator): - self.page_iterator = page_iterator - self.limit = 0 - self.current_page = None - self.page_index = -1 - self.count = 0 - - def next(self): - if self.limit > 0 and self.count == self.limit: - raise StopIteration - if self.current_page is None or self.page_index == len(self.current_page) - 1: - # Reached end of current page, get the next page... - self.current_page = self.page_iterator.next() - self.page_index = -1 - self.page_index += 1 - self.count += 1 - return self.current_page[self.page_index] - - def prev(self): - if self.current_page is None: - raise TweepError('Can not go back more, at first page') - if self.page_index == 0: - # At the beginning of the current page, move to next... - self.current_page = self.page_iterator.prev() - self.page_index = len(self.current_page) - if self.page_index == 0: - raise TweepError('No more items') - self.page_index -= 1 - self.count -= 1 - return self.current_page[self.page_index] - diff --git a/lib/tweepy/error.py b/lib/tweepy/error.py deleted file mode 100644 index 753e2fe..0000000 --- a/lib/tweepy/error.py +++ /dev/null @@ -1,15 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -class TweepError(Exception): - """Tweepy exception""" - - def __init__(self, reason, response=None): - self.reason = unicode(reason) - self.response = response - Exception.__init__(self, reason) - - def __str__(self): - return self.reason - diff --git a/lib/tweepy/models.py b/lib/tweepy/models.py deleted file mode 100644 index 3442790..0000000 --- a/lib/tweepy/models.py +++ /dev/null @@ -1,431 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -from tweepy.error import TweepError -from tweepy.utils import parse_datetime, parse_html_value, parse_a_href, \ - parse_search_datetime, unescape_html - - -class ResultSet(list): - """A list like object that holds results from a Twitter API query.""" - def __init__(self, max_id=None, since_id=None): - super(ResultSet, self).__init__() - self._max_id = max_id - self._since_id = since_id - - @property - def max_id(self): - if self._max_id: - return self._max_id - ids = self.ids() - return max(ids) if ids else None - - @property - def since_id(self): - if self._since_id: - return self._since_id - ids = self.ids() - return min(ids) if ids else None - - def ids(self): - return [item.id for item in self if hasattr(item, 'id')] - -class Model(object): - - def __init__(self, api=None): - self._api = api - - def __getstate__(self): - # pickle - pickle = dict(self.__dict__) - try: - del pickle['_api'] # do not pickle the API reference - except KeyError: - pass - return pickle - - @classmethod - def parse(cls, api, json): - """Parse a JSON object into a model instance.""" - raise NotImplementedError - - @classmethod - def parse_list(cls, api, json_list): - """Parse a list of JSON objects into a result set of model instances.""" - results = ResultSet() - for obj in json_list: - if obj: - results.append(cls.parse(api, obj)) - return results - - -class Status(Model): - - @classmethod - def parse(cls, api, json): - status = cls(api) - for k, v in json.items(): - if k == 'user': - user_model = getattr(api.parser.model_factory, 'user') - user = user_model.parse(api, v) - setattr(status, 'author', user) - setattr(status, 'user', user) # DEPRECIATED - elif k == 'created_at': - setattr(status, k, parse_datetime(v)) - elif k == 'source': - if '<' in v: - setattr(status, k, parse_html_value(v)) - setattr(status, 'source_url', parse_a_href(v)) - else: - setattr(status, k, v) - setattr(status, 'source_url', None) - elif k == 'retweeted_status': - setattr(status, k, Status.parse(api, v)) - elif k == 'place': - if v is not None: - setattr(status, k, Place.parse(api, v)) - else: - setattr(status, k, None) - else: - setattr(status, k, v) - return status - - def destroy(self): - return self._api.destroy_status(self.id) - - def retweet(self): - return self._api.retweet(self.id) - - def retweets(self): - return self._api.retweets(self.id) - - def favorite(self): - return self._api.create_favorite(self.id) - - -class User(Model): - - @classmethod - def parse(cls, api, json): - user = cls(api) - for k, v in json.items(): - if k == 'created_at': - setattr(user, k, parse_datetime(v)) - elif k == 'status': - setattr(user, k, Status.parse(api, v)) - elif k == 'following': - # twitter sets this to null if it is false - if v is True: - setattr(user, k, True) - else: - setattr(user, k, False) - else: - setattr(user, k, v) - return user - - @classmethod - def parse_list(cls, api, json_list): - if isinstance(json_list, list): - item_list = json_list - else: - item_list = json_list['users'] - - results = ResultSet() - for obj in item_list: - results.append(cls.parse(api, obj)) - return results - - def timeline(self, **kargs): - return self._api.user_timeline(user_id=self.id, **kargs) - - def friends(self, **kargs): - return self._api.friends(user_id=self.id, **kargs) - - def followers(self, **kargs): - return self._api.followers(user_id=self.id, **kargs) - - def follow(self): - self._api.create_friendship(user_id=self.id) - self.following = True - - def unfollow(self): - self._api.destroy_friendship(user_id=self.id) - self.following = False - - def lists_memberships(self, *args, **kargs): - return self._api.lists_memberships(user=self.screen_name, *args, **kargs) - - def lists_subscriptions(self, *args, **kargs): - return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs) - - def lists(self, *args, **kargs): - return self._api.lists(user=self.screen_name, *args, **kargs) - - def followers_ids(self, *args, **kargs): - return self._api.followers_ids(user_id=self.id, *args, **kargs) - - -class DirectMessage(Model): - - @classmethod - def parse(cls, api, json): - dm = cls(api) - for k, v in json.items(): - if k == 'sender' or k == 'recipient': - setattr(dm, k, User.parse(api, v)) - elif k == 'created_at': - setattr(dm, k, parse_datetime(v)) - else: - setattr(dm, k, v) - return dm - - def destroy(self): - return self._api.destroy_direct_message(self.id) - - -class Friendship(Model): - - @classmethod - def parse(cls, api, json): - relationship = json['relationship'] - - # parse source - source = cls(api) - for k, v in relationship['source'].items(): - setattr(source, k, v) - - # parse target - target = cls(api) - for k, v in relationship['target'].items(): - setattr(target, k, v) - - return source, target - - -class Category(Model): - - @classmethod - def parse(cls, api, json): - category = cls(api) - for k, v in json.items(): - setattr(category, k, v) - return category - - -class SavedSearch(Model): - - @classmethod - def parse(cls, api, json): - ss = cls(api) - for k, v in json.items(): - if k == 'created_at': - setattr(ss, k, parse_datetime(v)) - else: - setattr(ss, k, v) - return ss - - def destroy(self): - return self._api.destroy_saved_search(self.id) - - -class SearchResults(ResultSet): - - @classmethod - def parse(cls, api, json): - metadata = json['search_metadata'] - results = SearchResults(metadata.get('max_id'), metadata.get('since_id')) - results.refresh_url = metadata.get('refresh_url') - results.completed_in = metadata.get('completed_in') - results.query = metadata.get('query') - - for status in json['statuses']: - results.append(Status.parse(api, status)) - return results - - -class List(Model): - - @classmethod - def parse(cls, api, json): - lst = List(api) - for k,v in json.items(): - if k == 'user': - setattr(lst, k, User.parse(api, v)) - elif k == 'created_at': - setattr(lst, k, parse_datetime(v)) - else: - setattr(lst, k, v) - return lst - - @classmethod - def parse_list(cls, api, json_list, result_set=None): - results = ResultSet() - if isinstance(json_list, dict): - json_list = json_list['lists'] - for obj in json_list: - results.append(cls.parse(api, obj)) - return results - - def update(self, **kargs): - return self._api.update_list(self.slug, **kargs) - - def destroy(self): - return self._api.destroy_list(self.slug) - - def timeline(self, **kargs): - return self._api.list_timeline(self.user.screen_name, self.slug, **kargs) - - def add_member(self, id): - return self._api.add_list_member(self.slug, id) - - def remove_member(self, id): - return self._api.remove_list_member(self.slug, id) - - def members(self, **kargs): - return self._api.list_members(self.user.screen_name, self.slug, **kargs) - - def is_member(self, id): - return self._api.is_list_member(self.user.screen_name, self.slug, id) - - def subscribe(self): - return self._api.subscribe_list(self.user.screen_name, self.slug) - - def unsubscribe(self): - return self._api.unsubscribe_list(self.user.screen_name, self.slug) - - def subscribers(self, **kargs): - return self._api.list_subscribers(self.user.screen_name, self.slug, **kargs) - - def is_subscribed(self, id): - return self._api.is_subscribed_list(self.user.screen_name, self.slug, id) - -class Relation(Model): - @classmethod - def parse(cls, api, json): - result = cls(api) - for k,v in json.items(): - if k == 'value' and json['kind'] in ['Tweet', 'LookedupStatus']: - setattr(result, k, Status.parse(api, v)) - elif k == 'results': - setattr(result, k, Relation.parse_list(api, v)) - else: - setattr(result, k, v) - return result - -class Relationship(Model): - @classmethod - def parse(cls, api, json): - result = cls(api) - for k,v in json.items(): - if k == 'connections': - setattr(result, 'is_following', 'following' in v) - setattr(result, 'is_followed_by', 'followed_by' in v) - else: - setattr(result, k, v) - return result - -class JSONModel(Model): - - @classmethod - def parse(cls, api, json): - return json - - -class IDModel(Model): - - @classmethod - def parse(cls, api, json): - if isinstance(json, list): - return json - else: - return json['ids'] - - -class BoundingBox(Model): - - @classmethod - def parse(cls, api, json): - result = cls(api) - if json is not None: - for k, v in json.items(): - setattr(result, k, v) - return result - - def origin(self): - """ - Return longitude, latitude of southwest (bottom, left) corner of - bounding box, as a tuple. - - This assumes that bounding box is always a rectangle, which - appears to be the case at present. - """ - return tuple(self.coordinates[0][0]) - - def corner(self): - """ - Return longitude, latitude of northeast (top, right) corner of - bounding box, as a tuple. - - This assumes that bounding box is always a rectangle, which - appears to be the case at present. - """ - return tuple(self.coordinates[0][2]) - - -class Place(Model): - - @classmethod - def parse(cls, api, json): - place = cls(api) - for k, v in json.items(): - if k == 'bounding_box': - # bounding_box value may be null (None.) - # Example: "United States" (id=96683cc9126741d1) - if v is not None: - t = BoundingBox.parse(api, v) - else: - t = v - setattr(place, k, t) - elif k == 'contained_within': - # contained_within is a list of Places. - setattr(place, k, Place.parse_list(api, v)) - else: - setattr(place, k, v) - return place - - @classmethod - def parse_list(cls, api, json_list): - if isinstance(json_list, list): - item_list = json_list - else: - item_list = json_list['result']['places'] - - results = ResultSet() - for obj in item_list: - results.append(cls.parse(api, obj)) - return results - -class ModelFactory(object): - """ - Used by parsers for creating instances - of models. You may subclass this factory - to add your own extended models. - """ - - status = Status - user = User - direct_message = DirectMessage - friendship = Friendship - saved_search = SavedSearch - search_results = SearchResults - category = Category - list = List - relation = Relation - relationship = Relationship - - json = JSONModel - ids = IDModel - place = Place - bounding_box = BoundingBox - diff --git a/lib/tweepy/oauth.py b/lib/tweepy/oauth.py deleted file mode 100644 index 286de18..0000000 --- a/lib/tweepy/oauth.py +++ /dev/null @@ -1,655 +0,0 @@ -""" -The MIT License - -Copyright (c) 2007 Leah Culver - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -import cgi -import urllib -import time -import random -import urlparse -import hmac -import binascii - - -VERSION = '1.0' # Hi Blaine! -HTTP_METHOD = 'GET' -SIGNATURE_METHOD = 'PLAINTEXT' - - -class OAuthError(RuntimeError): - """Generic exception class.""" - def __init__(self, message='OAuth error occured.'): - self.message = message - -def build_authenticate_header(realm=''): - """Optional WWW-Authenticate header (401 error)""" - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} - -def escape(s): - """Escape a URL including any /.""" - return urllib.quote(s, safe='~') - -def _utf8_str(s): - """Convert unicode to utf-8.""" - if isinstance(s, unicode): - return s.encode("utf-8") - else: - return str(s) - -def generate_timestamp(): - """Get seconds since epoch (UTC).""" - return int(time.time()) - -def generate_nonce(length=8): - """Generate pseudorandom number.""" - return ''.join([str(random.randint(0, 9)) for i in range(length)]) - -def generate_verifier(length=8): - """Generate pseudorandom number.""" - return ''.join([str(random.randint(0, 9)) for i in range(length)]) - - -class OAuthConsumer(object): - """Consumer of OAuth authentication. - - OAuthConsumer is a data type that represents the identity of the Consumer - via its shared secret with the Service Provider. - - """ - key = None - secret = None - - def __init__(self, key, secret): - self.key = key - self.secret = secret - - -class OAuthToken(object): - """OAuthToken is a data type that represents an End User via either an access - or request token. - - key -- the token - secret -- the token secret - - """ - key = None - secret = None - callback = None - callback_confirmed = None - verifier = None - - def __init__(self, key, secret): - self.key = key - self.secret = secret - - def set_callback(self, callback): - self.callback = callback - self.callback_confirmed = 'true' - - def set_verifier(self, verifier=None): - if verifier is not None: - self.verifier = verifier - else: - self.verifier = generate_verifier() - - def get_callback_url(self): - if self.callback and self.verifier: - # Append the oauth_verifier. - parts = urlparse.urlparse(self.callback) - scheme, netloc, path, params, query, fragment = parts[:6] - if query: - query = '%s&oauth_verifier=%s' % (query, self.verifier) - else: - query = 'oauth_verifier=%s' % self.verifier - return urlparse.urlunparse((scheme, netloc, path, params, - query, fragment)) - return self.callback - - def to_string(self): - data = { - 'oauth_token': self.key, - 'oauth_token_secret': self.secret, - } - if self.callback_confirmed is not None: - data['oauth_callback_confirmed'] = self.callback_confirmed - return urllib.urlencode(data) - - def from_string(s): - """ Returns a token from something like: - oauth_token_secret=xxx&oauth_token=xxx - """ - params = cgi.parse_qs(s, keep_blank_values=False) - key = params['oauth_token'][0] - secret = params['oauth_token_secret'][0] - token = OAuthToken(key, secret) - try: - token.callback_confirmed = params['oauth_callback_confirmed'][0] - except KeyError: - pass # 1.0, no callback confirmed. - return token - from_string = staticmethod(from_string) - - def __str__(self): - return self.to_string() - - -class OAuthRequest(object): - """OAuthRequest represents the request and can be serialized. - - OAuth parameters: - - oauth_consumer_key - - oauth_token - - oauth_signature_method - - oauth_signature - - oauth_timestamp - - oauth_nonce - - oauth_version - - oauth_verifier - ... any additional parameters, as defined by the Service Provider. - """ - parameters = None # OAuth parameters. - http_method = HTTP_METHOD - http_url = None - version = VERSION - - def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): - self.http_method = http_method - self.http_url = http_url - self.parameters = parameters or {} - - def set_parameter(self, parameter, value): - self.parameters[parameter] = value - - def get_parameter(self, parameter): - try: - return self.parameters[parameter] - except: - raise OAuthError('Parameter not found: %s' % parameter) - - def _get_timestamp_nonce(self): - return self.get_parameter('oauth_timestamp'), self.get_parameter( - 'oauth_nonce') - - def get_nonoauth_parameters(self): - """Get any non-OAuth parameters.""" - parameters = {} - for k, v in self.parameters.iteritems(): - # Ignore oauth parameters. - if k.find('oauth_') < 0: - parameters[k] = v - return parameters - - def to_header(self, realm=''): - """Serialize as a header for an HTTPAuth request.""" - auth_header = 'OAuth realm="%s"' % realm - # Add the oauth parameters. - if self.parameters: - for k, v in self.parameters.iteritems(): - if k[:6] == 'oauth_': - auth_header += ', %s="%s"' % (k, escape(str(v))) - return {'Authorization': auth_header} - - def to_postdata(self): - """Serialize as post data for a POST request.""" - return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \ - for k, v in self.parameters.iteritems()]) - - def to_url(self): - """Serialize as a URL for a GET request.""" - return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) - - def get_normalized_parameters(self): - """Return a string that contains the parameters that must be signed.""" - params = self.parameters - try: - # Exclude the signature if it exists. - del params['oauth_signature'] - except: - pass - # Escape key values before sorting. - key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \ - for k,v in params.items()] - # Sort lexicographically, first after key, then after value. - key_values.sort() - # Combine key value pairs into a string. - return '&'.join(['%s=%s' % (k, v) for k, v in key_values]) - - def get_normalized_http_method(self): - """Uppercases the http method.""" - return self.http_method.upper() - - def get_normalized_http_url(self): - """Parses the URL and rebuilds it to be scheme://host/path.""" - parts = urlparse.urlparse(self.http_url) - scheme, netloc, path = parts[:3] - # Exclude default port numbers. - if scheme == 'http' and netloc[-3:] == ':80': - netloc = netloc[:-3] - elif scheme == 'https' and netloc[-4:] == ':443': - netloc = netloc[:-4] - return '%s://%s%s' % (scheme, netloc, path) - - def sign_request(self, signature_method, consumer, token): - """Set the signature parameter to the result of build_signature.""" - # Set the signature method. - self.set_parameter('oauth_signature_method', - signature_method.get_name()) - # Set the signature. - self.set_parameter('oauth_signature', - self.build_signature(signature_method, consumer, token)) - - def build_signature(self, signature_method, consumer, token): - """Calls the build signature method within the signature method.""" - return signature_method.build_signature(self, consumer, token) - - def from_request(http_method, http_url, headers=None, parameters=None, - query_string=None): - """Combines multiple parameter sources.""" - if parameters is None: - parameters = {} - - # Headers - if headers and 'Authorization' in headers: - auth_header = headers['Authorization'] - # Check that the authorization header is OAuth. - if auth_header[:6] == 'OAuth ': - auth_header = auth_header[6:] - try: - # Get the parameters from the header. - header_params = OAuthRequest._split_header(auth_header) - parameters.update(header_params) - except: - raise OAuthError('Unable to parse OAuth parameters from ' - 'Authorization header.') - - # GET or POST query string. - if query_string: - query_params = OAuthRequest._split_url_string(query_string) - parameters.update(query_params) - - # URL parameters. - param_str = urlparse.urlparse(http_url)[4] # query - url_params = OAuthRequest._split_url_string(param_str) - parameters.update(url_params) - - if parameters: - return OAuthRequest(http_method, http_url, parameters) - - return None - from_request = staticmethod(from_request) - - def from_consumer_and_token(oauth_consumer, token=None, - callback=None, verifier=None, http_method=HTTP_METHOD, - http_url=None, parameters=None): - if not parameters: - parameters = {} - - defaults = { - 'oauth_consumer_key': oauth_consumer.key, - 'oauth_timestamp': generate_timestamp(), - 'oauth_nonce': generate_nonce(), - 'oauth_version': OAuthRequest.version, - } - - defaults.update(parameters) - parameters = defaults - - if token: - parameters['oauth_token'] = token.key - if token.callback: - parameters['oauth_callback'] = token.callback - # 1.0a support for verifier. - if verifier: - parameters['oauth_verifier'] = verifier - elif callback: - # 1.0a support for callback in the request token request. - parameters['oauth_callback'] = callback - - return OAuthRequest(http_method, http_url, parameters) - from_consumer_and_token = staticmethod(from_consumer_and_token) - - def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, - http_url=None, parameters=None): - if not parameters: - parameters = {} - - parameters['oauth_token'] = token.key - - if callback: - parameters['oauth_callback'] = callback - - return OAuthRequest(http_method, http_url, parameters) - from_token_and_callback = staticmethod(from_token_and_callback) - - def _split_header(header): - """Turn Authorization: header into parameters.""" - params = {} - parts = header.split(',') - for param in parts: - # Ignore realm parameter. - if param.find('realm') > -1: - continue - # Remove whitespace. - param = param.strip() - # Split key-value. - param_parts = param.split('=', 1) - # Remove quotes and unescape the value. - params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) - return params - _split_header = staticmethod(_split_header) - - def _split_url_string(param_str): - """Turn URL string into parameters.""" - parameters = cgi.parse_qs(param_str, keep_blank_values=False) - for k, v in parameters.iteritems(): - parameters[k] = urllib.unquote(v[0]) - return parameters - _split_url_string = staticmethod(_split_url_string) - -class OAuthServer(object): - """A worker to check the validity of a request against a data store.""" - timestamp_threshold = 300 # In seconds, five minutes. - version = VERSION - signature_methods = None - data_store = None - - def __init__(self, data_store=None, signature_methods=None): - self.data_store = data_store - self.signature_methods = signature_methods or {} - - def set_data_store(self, data_store): - self.data_store = data_store - - def get_data_store(self): - return self.data_store - - def add_signature_method(self, signature_method): - self.signature_methods[signature_method.get_name()] = signature_method - return self.signature_methods - - def fetch_request_token(self, oauth_request): - """Processes a request_token request and returns the - request token on success. - """ - try: - # Get the request token for authorization. - token = self._get_token(oauth_request, 'request') - except OAuthError: - # No token required for the initial token request. - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - try: - callback = self.get_callback(oauth_request) - except OAuthError: - callback = None # 1.0, no callback specified. - self._check_signature(oauth_request, consumer, None) - # Fetch a new token. - token = self.data_store.fetch_request_token(consumer, callback) - return token - - def fetch_access_token(self, oauth_request): - """Processes an access_token request and returns the - access token on success. - """ - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - try: - verifier = self._get_verifier(oauth_request) - except OAuthError: - verifier = None - # Get the request token. - token = self._get_token(oauth_request, 'request') - self._check_signature(oauth_request, consumer, token) - new_token = self.data_store.fetch_access_token(consumer, token, verifier) - return new_token - - def verify_request(self, oauth_request): - """Verifies an api call and checks all the parameters.""" - # -> consumer and token - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - # Get the access token. - token = self._get_token(oauth_request, 'access') - self._check_signature(oauth_request, consumer, token) - parameters = oauth_request.get_nonoauth_parameters() - return consumer, token, parameters - - def authorize_token(self, token, user): - """Authorize a request token.""" - return self.data_store.authorize_request_token(token, user) - - def get_callback(self, oauth_request): - """Get the callback URL.""" - return oauth_request.get_parameter('oauth_callback') - - def build_authenticate_header(self, realm=''): - """Optional support for the authenticate header.""" - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} - - def _get_version(self, oauth_request): - """Verify the correct version request for this server.""" - try: - version = oauth_request.get_parameter('oauth_version') - except: - version = VERSION - if version and version != self.version: - raise OAuthError('OAuth version %s not supported.' % str(version)) - return version - - def _get_signature_method(self, oauth_request): - """Figure out the signature with some defaults.""" - try: - signature_method = oauth_request.get_parameter( - 'oauth_signature_method') - except: - signature_method = SIGNATURE_METHOD - try: - # Get the signature method object. - signature_method = self.signature_methods[signature_method] - except: - signature_method_names = ', '.join(self.signature_methods.keys()) - raise OAuthError('Signature method %s not supported try one of the ' - 'following: %s' % (signature_method, signature_method_names)) - - return signature_method - - def _get_consumer(self, oauth_request): - consumer_key = oauth_request.get_parameter('oauth_consumer_key') - consumer = self.data_store.lookup_consumer(consumer_key) - if not consumer: - raise OAuthError('Invalid consumer.') - return consumer - - def _get_token(self, oauth_request, token_type='access'): - """Try to find the token for the provided request token key.""" - token_field = oauth_request.get_parameter('oauth_token') - token = self.data_store.lookup_token(token_type, token_field) - if not token: - raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) - return token - - def _get_verifier(self, oauth_request): - return oauth_request.get_parameter('oauth_verifier') - - def _check_signature(self, oauth_request, consumer, token): - timestamp, nonce = oauth_request._get_timestamp_nonce() - self._check_timestamp(timestamp) - self._check_nonce(consumer, token, nonce) - signature_method = self._get_signature_method(oauth_request) - try: - signature = oauth_request.get_parameter('oauth_signature') - except: - raise OAuthError('Missing signature.') - # Validate the signature. - valid_sig = signature_method.check_signature(oauth_request, consumer, - token, signature) - if not valid_sig: - key, base = signature_method.build_signature_base_string( - oauth_request, consumer, token) - raise OAuthError('Invalid signature. Expected signature base ' - 'string: %s' % base) - built = signature_method.build_signature(oauth_request, consumer, token) - - def _check_timestamp(self, timestamp): - """Verify that timestamp is recentish.""" - timestamp = int(timestamp) - now = int(time.time()) - lapsed = abs(now - timestamp) - if lapsed > self.timestamp_threshold: - raise OAuthError('Expired timestamp: given %d and now %s has a ' - 'greater difference than threshold %d' % - (timestamp, now, self.timestamp_threshold)) - - def _check_nonce(self, consumer, token, nonce): - """Verify that the nonce is uniqueish.""" - nonce = self.data_store.lookup_nonce(consumer, token, nonce) - if nonce: - raise OAuthError('Nonce already used: %s' % str(nonce)) - - -class OAuthClient(object): - """OAuthClient is a worker to attempt to execute a request.""" - consumer = None - token = None - - def __init__(self, oauth_consumer, oauth_token): - self.consumer = oauth_consumer - self.token = oauth_token - - def get_consumer(self): - return self.consumer - - def get_token(self): - return self.token - - def fetch_request_token(self, oauth_request): - """-> OAuthToken.""" - raise NotImplementedError - - def fetch_access_token(self, oauth_request): - """-> OAuthToken.""" - raise NotImplementedError - - def access_resource(self, oauth_request): - """-> Some protected resource.""" - raise NotImplementedError - - -class OAuthDataStore(object): - """A database abstraction used to lookup consumers and tokens.""" - - def lookup_consumer(self, key): - """-> OAuthConsumer.""" - raise NotImplementedError - - def lookup_token(self, oauth_consumer, token_type, token_token): - """-> OAuthToken.""" - raise NotImplementedError - - def lookup_nonce(self, oauth_consumer, oauth_token, nonce): - """-> OAuthToken.""" - raise NotImplementedError - - def fetch_request_token(self, oauth_consumer, oauth_callback): - """-> OAuthToken.""" - raise NotImplementedError - - def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier): - """-> OAuthToken.""" - raise NotImplementedError - - def authorize_request_token(self, oauth_token, user): - """-> OAuthToken.""" - raise NotImplementedError - - -class OAuthSignatureMethod(object): - """A strategy class that implements a signature method.""" - def get_name(self): - """-> str.""" - raise NotImplementedError - - def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): - """-> str key, str raw.""" - raise NotImplementedError - - def build_signature(self, oauth_request, oauth_consumer, oauth_token): - """-> str.""" - raise NotImplementedError - - def check_signature(self, oauth_request, consumer, token, signature): - built = self.build_signature(oauth_request, consumer, token) - return built == signature - - -class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): - - def get_name(self): - return 'HMAC-SHA1' - - def build_signature_base_string(self, oauth_request, consumer, token): - sig = ( - escape(oauth_request.get_normalized_http_method()), - escape(oauth_request.get_normalized_http_url()), - escape(oauth_request.get_normalized_parameters()), - ) - - key = '%s&' % escape(consumer.secret) - if token: - key += escape(token.secret) - raw = '&'.join(sig) - return key, raw - - def build_signature(self, oauth_request, consumer, token): - """Builds the base signature string.""" - key, raw = self.build_signature_base_string(oauth_request, consumer, - token) - - # HMAC object. - try: - import hashlib # 2.5 - hashed = hmac.new(key, raw, hashlib.sha1) - except: - import sha # Deprecated - hashed = hmac.new(key, raw, sha) - - # Calculate the digest base 64. - return binascii.b2a_base64(hashed.digest())[:-1] - - -class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): - - def get_name(self): - return 'PLAINTEXT' - - def build_signature_base_string(self, oauth_request, consumer, token): - """Concatenates the consumer key and secret.""" - sig = '%s&' % escape(consumer.secret) - if token: - sig = sig + escape(token.secret) - return sig, sig - - def build_signature(self, oauth_request, consumer, token): - key, raw = self.build_signature_base_string(oauth_request, consumer, - token) - return key \ No newline at end of file diff --git a/lib/tweepy/parsers.py b/lib/tweepy/parsers.py deleted file mode 100644 index 55a5ba8..0000000 --- a/lib/tweepy/parsers.py +++ /dev/null @@ -1,97 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -from tweepy.models import ModelFactory -from tweepy.utils import import_simplejson -from tweepy.error import TweepError - - -class Parser(object): - - def parse(self, method, payload): - """ - Parse the response payload and return the result. - Returns a tuple that contains the result data and the cursors - (or None if not present). - """ - raise NotImplementedError - - def parse_error(self, payload): - """ - Parse the error message from payload. - If unable to parse the message, throw an exception - and default error message will be used. - """ - raise NotImplementedError - - -class RawParser(Parser): - - def __init__(self): - pass - - def parse(self, method, payload): - return payload - - def parse_error(self, payload): - return payload - - -class JSONParser(Parser): - - payload_format = 'json' - - def __init__(self): - self.json_lib = import_simplejson() - - def parse(self, method, payload): - try: - json = self.json_lib.loads(payload) - except Exception, e: - raise TweepError('Failed to parse JSON payload: %s' % e) - - needsCursors = method.parameters.has_key('cursor') - if needsCursors and isinstance(json, dict) and 'previous_cursor' in json and 'next_cursor' in json: - cursors = json['previous_cursor'], json['next_cursor'] - return json, cursors - else: - return json - - def parse_error(self, payload): - error = self.json_lib.loads(payload) - if error.has_key('error'): - return error['error'] - else: - return error['errors'] - - -class ModelParser(JSONParser): - - def __init__(self, model_factory=None): - JSONParser.__init__(self) - self.model_factory = model_factory or ModelFactory - - def parse(self, method, payload): - try: - if method.payload_type is None: return - model = getattr(self.model_factory, method.payload_type) - except AttributeError: - raise TweepError('No model for this payload type: %s' % method.payload_type) - - json = JSONParser.parse(self, method, payload) - if isinstance(json, tuple): - json, cursors = json - else: - cursors = None - - if method.payload_list: - result = model.parse_list(method.api, json) - else: - result = model.parse(method.api, json) - - if cursors: - return result, cursors - else: - return result - diff --git a/lib/tweepy/streaming.py b/lib/tweepy/streaming.py deleted file mode 100644 index f6d37f4..0000000 --- a/lib/tweepy/streaming.py +++ /dev/null @@ -1,248 +0,0 @@ -# Tweepy -# Copyright 2009-2010 Joshua Roesslein -# See LICENSE for details. - -import httplib -from socket import timeout -from threading import Thread -from time import sleep - -from tweepy.models import Status -from tweepy.api import API -from tweepy.error import TweepError - -from tweepy.utils import import_simplejson, urlencode_noplus -json = import_simplejson() - -STREAM_VERSION = '1.1' - - -class StreamListener(object): - - def __init__(self, api=None): - self.api = api or API() - - def on_connect(self): - """Called once connected to streaming server. - - This will be invoked once a successful response - is received from the server. Allows the listener - to perform some work prior to entering the read loop. - """ - pass - - def on_data(self, data): - """Called when raw data is received from connection. - - Override this method if you wish to manually handle - the stream data. Return False to stop stream and close connection. - """ - - if 'in_reply_to_status_id' in data: - status = Status.parse(self.api, json.loads(data)) - if self.on_status(status) is False: - return False - elif 'delete' in data: - delete = json.loads(data)['delete']['status'] - if self.on_delete(delete['id'], delete['user_id']) is False: - return False - elif 'limit' in data: - if self.on_limit(json.loads(data)['limit']['track']) is False: - return False - - def on_status(self, status): - """Called when a new status arrives""" - return - - def on_delete(self, status_id, user_id): - """Called when a delete notice arrives for a status""" - return - - def on_limit(self, track): - """Called when a limitation notice arrvies""" - return - - def on_error(self, status_code): - """Called when a non-200 status code is returned""" - return False - - def on_timeout(self): - """Called when stream connection times out""" - return - - -class Stream(object): - - host = 'stream.twitter.com' - - def __init__(self, auth, listener, **options): - self.auth = auth - self.listener = listener - self.running = False - self.timeout = options.get("timeout", 300.0) - self.retry_count = options.get("retry_count") - self.retry_time = options.get("retry_time", 10.0) - self.snooze_time = options.get("snooze_time", 5.0) - self.buffer_size = options.get("buffer_size", 1500) - if options.get("secure", True): - self.scheme = "https" - else: - self.scheme = "http" - - self.api = API() - self.headers = options.get("headers") or {} - self.parameters = None - self.body = None - - def _run(self): - # Authenticate - url = "%s://%s%s" % (self.scheme, self.host, self.url) - - # Connect and process the stream - error_counter = 0 - conn = None - exception = None - while self.running: - if self.retry_count is not None and error_counter > self.retry_count: - # quit if error count greater than retry count - break - try: - if self.scheme == "http": - conn = httplib.HTTPConnection(self.host) - else: - conn = httplib.HTTPSConnection(self.host) - self.auth.apply_auth(url, 'POST', self.headers, self.parameters) - conn.connect() - conn.sock.settimeout(self.timeout) - conn.request('POST', self.url, self.body, headers=self.headers) - resp = conn.getresponse() - if resp.status != 200: - if self.listener.on_error(resp.status) is False: - break - error_counter += 1 - sleep(self.retry_time) - else: - error_counter = 0 - self.listener.on_connect() - self._read_loop(resp) - except timeout: - if self.listener.on_timeout() == False: - break - if self.running is False: - break - conn.close() - sleep(self.snooze_time) - except Exception, exception: - # any other exception is fatal, so kill loop - break - - # cleanup - self.running = False - if conn: - conn.close() - - if exception: - raise - - def _data(self, data): - if self.listener.on_data(data) is False: - self.running = False - - def _read_loop(self, resp): - - while self.running and not resp.isclosed(): - - # Note: keep-alive newlines might be inserted before each length value. - # read until we get a digit... - c = '\n' - while c == '\n' and self.running and not resp.isclosed(): - c = resp.read(1) - delimited_string = c - - # read rest of delimiter length.. - d = '' - while d != '\n' and self.running and not resp.isclosed(): - d = resp.read(1) - delimited_string += d - - # read the next twitter status object - if delimited_string.strip().isdigit(): - next_status_obj = resp.read( int(delimited_string) ) - self._data(next_status_obj) - - if resp.isclosed(): - self.on_closed(resp) - - def _start(self, async): - self.running = True - if async: - Thread(target=self._run).start() - else: - self._run() - - def on_closed(self, resp): - """ Called when the response has been closed by Twitter """ - pass - - def userstream(self, count=None, async=False, secure=True): - self.parameters = {'delimited': 'length'} - if self.running: - raise TweepError('Stream object already connected!') - self.url = '/2/user.json?delimited=length' - self.host='userstream.twitter.com' - self._start(async) - - def firehose(self, count=None, async=False): - self.parameters = {'delimited': 'length'} - if self.running: - raise TweepError('Stream object already connected!') - self.url = '/%s/statuses/firehose.json?delimited=length' % STREAM_VERSION - if count: - self.url += '&count=%s' % count - self._start(async) - - def retweet(self, async=False): - self.parameters = {'delimited': 'length'} - if self.running: - raise TweepError('Stream object already connected!') - self.url = '/%s/statuses/retweet.json?delimited=length' % STREAM_VERSION - self._start(async) - - def sample(self, count=None, async=False): - self.parameters = {'delimited': 'length'} - if self.running: - raise TweepError('Stream object already connected!') - self.url = '/%s/statuses/sample.json?delimited=length' % STREAM_VERSION - if count: - self.url += '&count=%s' % count - self._start(async) - - def filter(self, follow=None, track=None, async=False, locations=None, - count = None, stall_warnings=False, languages=None): - self.parameters = {} - self.headers['Content-type'] = "application/x-www-form-urlencoded" - if self.running: - raise TweepError('Stream object already connected!') - self.url = '/%s/statuses/filter.json?delimited=length' % STREAM_VERSION - if follow: - self.parameters['follow'] = ','.join(map(str, follow)) - if track: - self.parameters['track'] = ','.join(map(str, track)) - if locations and len(locations) > 0: - assert len(locations) % 4 == 0 - self.parameters['locations'] = ','.join(['%.2f' % l for l in locations]) - if count: - self.parameters['count'] = count - if stall_warnings: - self.parameters['stall_warnings'] = stall_warnings - if languages: - self.parameters['language'] = ','.join(map(str, languages)) - self.body = urlencode_noplus(self.parameters) - self.parameters['delimited'] = 'length' - self._start(async) - - def disconnect(self): - if self.running is False: - return - self.running = False - diff --git a/lib/tweepy/utils.py b/lib/tweepy/utils.py deleted file mode 100644 index 52c6c79..0000000 --- a/lib/tweepy/utils.py +++ /dev/null @@ -1,103 +0,0 @@ -# Tweepy -# Copyright 2010 Joshua Roesslein -# See LICENSE for details. - -from datetime import datetime -import time -import htmlentitydefs -import re -import locale -from urllib import quote - - -def parse_datetime(string): - # Set locale for date parsing - locale.setlocale(locale.LC_TIME, 'C') - - # We must parse datetime this way to work in python 2.4 - date = datetime(*(time.strptime(string, '%a %b %d %H:%M:%S +0000 %Y')[0:6])) - - # Reset locale back to the default setting - locale.setlocale(locale.LC_TIME, '') - return date - - -def parse_html_value(html): - - return html[html.find('>')+1:html.rfind('<')] - - -def parse_a_href(atag): - - start = atag.find('"') + 1 - end = atag.find('"', start) - return atag[start:end] - - -def parse_search_datetime(string): - # Set locale for date parsing - locale.setlocale(locale.LC_TIME, 'C') - - # We must parse datetime this way to work in python 2.4 - date = datetime(*(time.strptime(string, '%a, %d %b %Y %H:%M:%S +0000')[0:6])) - - # Reset locale back to the default setting - locale.setlocale(locale.LC_TIME, '') - return date - - -def unescape_html(text): - """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" - def fixup(m): - text = m.group(0) - if text[:2] == "&#": - # character reference - try: - if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)) - else: - return unichr(int(text[2:-1])) - except ValueError: - pass - else: - # named entity - try: - text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) - except KeyError: - pass - return text # leave as is - return re.sub("&#?\w+;", fixup, text) - - -def convert_to_utf8_str(arg): - # written by Michael Norton (http://docondev.blogspot.com/) - if isinstance(arg, unicode): - arg = arg.encode('utf-8') - elif not isinstance(arg, str): - arg = str(arg) - return arg - - - -def import_simplejson(): - try: - import simplejson as json - except ImportError: - try: - import json # Python 2.6+ - except ImportError: - try: - from django.utils import simplejson as json # Google App Engine - except ImportError: - raise ImportError, "Can't load a json library" - - return json - -def list_to_csv(item_list): - if item_list: - return ','.join([str(i) for i in item_list]) - -def urlencode_noplus(query): - return '&'.join(['%s=%s' % (quote(str(k)), quote(str(v))) \ - for k, v in query.iteritems()]) - diff --git a/plugins/8ball.py b/plugins/8ball.py new file mode 100755 index 0000000..4c7f56d --- /dev/null +++ b/plugins/8ball.py @@ -0,0 +1,23 @@ +from util import hook +from util.text import multiword_replace +import random + +color_codes = { + "<r>": "\x02\x0305", + "<g>": "\x02\x0303", + "<y>": "\x02" +} + +with open("plugins/data/8ball_responses.txt") as f: + responses = [line.strip() for line in + f.readlines()if not line.startswith("//")] + + +@hook.command('8ball') +def eightball(input, me=None): + "8ball <question> -- The all knowing magic eight ball, " \ + "in electronic form. Ask and it shall be answered!" + + # here we use voodoo magic to tell the future + magic = multiword_replace(random.choice(responses), color_codes) + me("shakes the magic 8 ball... %s" % magic) diff --git a/plugins/admin.py b/plugins/admin.py old mode 100644 new mode 100755 index dde067e..1e23e6d --- a/plugins/admin.py +++ b/plugins/admin.py @@ -1,230 +1,171 @@ +from util import hook import os -import sys import re import json import time import subprocess -from util import hook - -@hook.command(autohelp=False, permissions=["permissions_users"]) -def permissions(inp, bot=None, notice=None): - """permissions [group] -- lists the users and their permission level who have permissions.""" - permissions = bot.config.get("permissions", []) - groups = [] - if inp: - for k in permissions: - if inp == k: - groups.append(k) - else: - for k in permissions: - groups.append(k) - if not groups: - notice("{} is not a group with permissions".format(inp)) - return None - - for v in groups: - members = "" - for value in permissions[v]["users"]: - members = members + value + ", " - if members: - notice("the members in the {} group are..".format(v)) - notice(members[:-2]) +@hook.command(adminonly=True) +def addadmin(inp, notice=None, bot=None, config=None): + "addadmin <nick|host> -- Make <nick|host> an admin. " \ + "(you can add multiple admins at once)" + targets = inp.split() + for target in targets: + if target in bot.config["admins"]: + notice("%s is already an admin." % target) else: - notice("there are no members in the {} group".format(v)) + notice("%s is now an admin." % target) + bot.config["admins"].append(target) + bot.config["admins"].sort() + json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) + return -@hook.command(permissions=["permissions_users"]) -def deluser(inp, bot=None, notice=None): - """deluser [user] [group] -- removes elevated permissions from [user]. - If [group] is specified, they will only be removed from [group].""" - permissions = bot.config.get("permissions", []) - inp = inp.split(" ") - groups = [] - try: - specgroup = inp[1] - except IndexError: - specgroup = None - for k in permissions: - groups.append(k) +@hook.command(adminonly=True) +def deladmin(inp, notice=None, bot=None, config=None): + "deladmin <nick|host> -- Make <nick|host> a non-admin." \ + "(you can delete multiple admins at once)" + targets = inp.split() + for target in targets: + if target in bot.config["admins"]: + notice("%s is no longer an admin." % target) + bot.config["admins"].remove(target) + bot.config["admins"].sort() + json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) + else: + notice("%s is not an admin." % target) + return + + +@hook.command(autohelp=False) +def admins(inp, notice=None, bot=None): + "admins -- Lists bot's admins." + if bot.config["admins"]: + notice("Admins are: %s." % ", ".join(bot.config["admins"])) else: - for k in permissions: - if specgroup == k: - groups.append(k) - if not groups: - notice("{} is not a group with permissions".format(inp[1])) - return None - - removed = 0 - for v in groups: - users = permissions[v]["users"] - for value in users: - if inp[0] == value: - users.remove(inp[0]) - removed = 1 - notice("{} has been removed from the group {}".format(inp[0], v)) - json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) - if specgroup: - if removed == 0: - notice("{} is not in the group {}".format(inp[0], specgroup)) - else: - if removed == 0: - notice("{} is not in any groups".format(inp[0])) + notice("There are no users with admin powers.") + return -@hook.command(permissions=["permissions_users"]) -def adduser(inp, bot=None, notice=None): - """adduser [user] [group] -- adds elevated permissions to [user]. - [group] must be specified.""" - permissions = bot.config.get("permissions", []) - inp = inp.split(" ") - try: - user = inp[0] - targetgroup = inp[1] - except IndexError: - notice("the group must be specified") - return None - if not re.search('.+!.+@.+', user): - notice("the user must be in the form of \"nick!user@host\"") - return None - try: - users = permissions[targetgroup]["users"] - except KeyError: - notice("no such group as {}".format(targetgroup)) - return None - if user in users: - notice("{} is already in {}".format(user, targetgroup)) - return None - - users.append(user) - notice("{} has been added to the group {}".format(user, targetgroup)) - users.sort() - json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) - - -@hook.command("quit", autohelp=False, permissions=["botcontrol"]) -@hook.command(autohelp=False, permissions=["botcontrol"]) +@hook.command("quit", autohelp=False, adminonly=True) +@hook.command(autohelp=False, adminonly=True) def stop(inp, nick=None, conn=None): - """stop [reason] -- Kills the bot with [reason] as its quit message.""" + "stop [reason] -- Kills the bot with [reason] as its quit message." if inp: - conn.cmd("QUIT", ["Killed by {} ({})".format(nick, inp)]) + conn.cmd("QUIT", ["Killed by %s (%s)" % (nick, inp)]) else: - conn.cmd("QUIT", ["Killed by {}.".format(nick)]) + conn.cmd("QUIT", ["Killed by %s." % nick]) time.sleep(5) os.execl("./cloudbot", "cloudbot", "stop") -@hook.command(autohelp=False, permissions=["botcontrol"]) -def restart(inp, nick=None, conn=None, bot=None): - """restart [reason] -- Restarts the bot with [reason] as its quit message.""" - for botcon in bot.conns: - if inp: - bot.conns[botcon].cmd("QUIT", ["Restarted by {} ({})".format(nick, inp)]) - else: - bot.conns[botcon].cmd("QUIT", ["Restarted by {}.".format(nick)]) +@hook.command(autohelp=False, adminonly=True) +def restart(inp, nick=None, conn=None): + "restart [reason] -- Restarts the bot with [reason] as its quit message." + if inp: + conn.cmd("QUIT", ["Restarted by %s (%s)" % (nick, inp)]) + else: + conn.cmd("QUIT", ["Restarted by %s." % nick]) time.sleep(5) - #os.execl("./cloudbot", "cloudbot", "restart") - args = sys.argv[:] - args.insert(0, sys.executable) - os.execv(sys.executable, args) + os.execl("./cloudbot", "cloudbot", "restart") -@hook.command(autohelp=False, permissions=["botcontrol"]) +@hook.command(autohelp=False, adminonly=True) def clearlogs(inp, input=None): - """clearlogs -- Clears the bots log(s).""" + "clearlogs -- Clears the bots log(s)." subprocess.call(["./cloudbot", "clear"]) -@hook.command(permissions=["botcontrol"]) +@hook.command(adminonly=True) def join(inp, conn=None, notice=None): - """join <channel> -- Joins <channel>.""" - for target in inp.split(" "): - if not target.startswith("#"): - target = "#{}".format(target) - notice("Attempting to join {}...".format(target)) - conn.join(target) + "join <channel> -- Joins <channel>." + notice("Attempting to join %s..." % inp) + conn.join(inp) -@hook.command(autohelp=False, permissions=["botcontrol"]) +@hook.command(autohelp=False, adminonly=True) def part(inp, conn=None, chan=None, notice=None): - """part <channel> -- Leaves <channel>. - If [channel] is blank the bot will leave the - channel the command was used in.""" - if inp: - targets = inp - else: - targets = chan - for target in targets.split(" "): - if not target.startswith("#"): - target = "#{}".format(target) - notice("Attempting to leave {}...".format(target)) - conn.part(target) - - -@hook.command(autohelp=False, permissions=["botcontrol"]) -def cycle(inp, conn=None, chan=None, notice=None): - """cycle <channel> -- Cycles <channel>. - If [channel] is blank the bot will cycle the - channel the command was used in.""" + "part <channel> -- Leaves <channel>." \ + "If [channel] is blank the bot will leave the " \ + "channel the command was used in." if inp: target = inp else: target = chan - notice("Attempting to cycle {}...".format(target)) + notice("Attempting to leave %s..." % target) + conn.part(target) + + +@hook.command(autohelp=False, adminonly=True) +def cycle(inp, conn=None, chan=None, notice=None): + "cycle <channel> -- Cycles <channel>." \ + "If [channel] is blank the bot will cycle the " \ + "channel the command was used in." + if inp: + target = inp + else: + target = chan + notice("Attempting to cycle %s..." % target) conn.part(target) conn.join(target) -@hook.command(permissions=["botcontrol"]) -def nick(inp, notice=None, conn=None): - """nick <nick> -- Changes the bots nickname to <nick>.""" +@hook.command(adminonly=True) +def nick(inp, input=None, notice=None, conn=None): + "nick <nick> -- Changes the bots nickname to <nick>." if not re.match("^[A-Za-z0-9_|.-\]\[]*$", inp.lower()): notice("Invalid username!") return - notice("Attempting to change nick to \"{}\"...".format(inp)) + notice("Attempting to change nick to \"%s\"..." % inp) conn.set_nick(inp) -@hook.command(permissions=["botcontrol"]) +@hook.command(adminonly=True) def raw(inp, conn=None, notice=None): - """raw <command> -- Sends a RAW IRC command.""" + "raw <command> -- Sends a RAW IRC command." notice("Raw command sent.") conn.send(inp) -@hook.command(permissions=["botcontrol"]) -def say(inp, conn=None, chan=None): - """say [channel] <message> -- Makes the bot say <message> in [channel]. - If [channel] is blank the bot will say the <message> in the channel - the command was used in.""" - inp = inp.split(" ") - if inp[0][0] == "#": - message = u" ".join(inp[1:]) - out = u"PRIVMSG {} :{}".format(inp[0], message) - else: - message = u" ".join(inp[0:]) - out = u"PRIVMSG {} :{}".format(chan, message) - conn.send(out) - - -@hook.command("act", permissions=["botcontrol"]) -@hook.command(permissions=["botcontrol"]) -def me(inp, conn=None, chan=None): - """me [channel] <action> -- Makes the bot act out <action> in [channel]. - If [channel] is blank the bot will act the <action> in the channel the - command was used in.""" +@hook.command(adminonly=True) +def say(inp, conn=None, chan=None, notice=None): + "say [channel] <message> -- Makes the bot say <message> in [channel]. " \ + "If [channel] is blank the bot will say the <message> in the channel " \ + "the command was used in." inp = inp.split(" ") if inp[0][0] == "#": message = "" for x in inp[1:]: message = message + x + " " message = message[:-1] - out = u"PRIVMSG {} :\x01ACTION {}\x01".format(inp[0], message) + out = "PRIVMSG %s :%s" % (inp[0], message) else: message = "" for x in inp[0:]: message = message + x + " " message = message[:-1] - out = u"PRIVMSG {} :\x01ACTION {}\x01".format(chan, message) + out = "PRIVMSG %s :%s" % (chan, message) + conn.send(out) + + +@hook.command("act", adminonly=True) +@hook.command(adminonly=True) +def me(inp, conn=None, chan=None, notice=None): + "me [channel] <action> -- Makes the bot act out <action> in [channel]. " \ + "If [channel] is blank the bot will act the <action> in the channel the " \ + "command was used in." + inp = inp.split(" ") + if inp[0][0] == "#": + message = "" + for x in inp[1:]: + message = message + x + " " + message = message[:-1] + out = "PRIVMSG %s :\x01ACTION %s\x01" % (inp[0], message) + else: + message = "" + for x in inp[0:]: + message = message + x + " " + message = message[:-1] + out = "PRIVMSG %s :\x01ACTION %s\x01" % (chan, message) conn.send(out) diff --git a/disabled_stuff/yahooanswers.py b/plugins/answers.py similarity index 50% rename from disabled_stuff/yahooanswers.py rename to plugins/answers.py index e28ed63..da846bc 100644 --- a/disabled_stuff/yahooanswers.py +++ b/plugins/answers.py @@ -3,14 +3,14 @@ from util import hook, web, text @hook.command def answer(inp): - """answer <query> -- find the answer to a question on Yahoo! Answers""" + ".answer <query> -- find the answer to a question on Yahoo! Answers" query = "SELECT Subject, ChosenAnswer, Link FROM answers.search WHERE query=@query LIMIT 1" result = web.query(query, {"query": inp.strip()}).one() - short_url = web.try_isgd(result["Link"]) + short_url = web.isgd(result["Link"]) # we split the answer and .join() it to remove newlines/extra spaces - answer_text = text.truncate_str(' '.join(result["ChosenAnswer"].split()), 80) + answer = text.truncate_str(" ".join(result["ChosenAnswer"].split()), 80) - return u'\x02{}\x02 "{}" - {}'.format(result["Subject"], answer_text, short_url) + return u"\x02{}\x02 \"{}\" - {}".format(result["Subject"], answer, short_url) \ No newline at end of file diff --git a/plugins/bad_version.py b/plugins/bad_version.py deleted file mode 100644 index 9465f2d..0000000 --- a/plugins/bad_version.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -from util import hook -from thread import start_new_thread -from time import sleep - -def wait_and_send(conn, wait, msg): - sleep(wait) - conn.send(msg) - -@hook.command("check") -def check_nick(inp, conn=None): - conn.send("PRIVMSG %s :\x01VERSION\x01" % inp) - - -@hook.event("JOIN") -def handle_join(info, input=None, conn=None): - start_new_thread(wait_and_send, (conn, 5, "PRIVMSG %s :\x01VERSION\x01" % input.nick)) - -@hook.event("NOTICE") -def handle_ctcp_rply(info, input=None, conn=None, nick=None): - print "notice..." - print "-%s-" % input.lastparam - if input.lastparam == ("\1VERSION %s\1" % "mIRC v7.22 Khaled Mardam-Bey"): - for chan in conn.channels: - if chan != "#logbot": - conn.send("KICK %s %s :bad version" % (chan, nick)) - conn.send("MODE %s +b %s!*@*$#logbot" % (chan, nick)) - diff --git a/plugins/bandwidth.py b/plugins/bandwidth.py deleted file mode 100644 index ea23333..0000000 --- a/plugins/bandwidth.py +++ /dev/null @@ -1,21 +0,0 @@ -from util import hook, http, web -from subprocess import check_output, CalledProcessError -from datetime import datetime - -@hook.command("bw", autohelp=False) -def bw(inp): - """bw - list last bandwidth measurement to the outside.""" - - try: - o = check_output("/bin/chch-bandwidth") - except CalledProcessError as err: - return "chch-bandwidth: returned %s" % (str(err)) - - os = o.split(",") - upl = int(os[-1])/1024.0/1024.0 - dl = int(os[-2])/1024.0/1024.0 - ts = os[0] - tsd = datetime.strptime(ts, "%Y%m%d%H%M%S") - - return "%s: upl = %f Mbit/s; dl = %f Mbit/s;" % (tsd, upl, dl) - diff --git a/disabled_stuff/brainfuck.py b/plugins/bf.py old mode 100644 new mode 100755 similarity index 84% rename from disabled_stuff/brainfuck.py rename to plugins/bf.py index a7dc12e..0290279 --- a/disabled_stuff/brainfuck.py +++ b/plugins/bf.py @@ -1,5 +1,5 @@ -"""brainfuck interpreter adapted from (public domain) code at -http://brainfuck.sourceforge.net/brain.py""" +'''brainfuck interpreter adapted from (public domain) code at +http://brainfuck.sourceforge.net/brain.py''' import re import random @@ -14,7 +14,7 @@ MAX_STEPS = 1000000 @hook.command('brainfuck') @hook.command def bf(inp): - """bf <prog> -- Executes <prog> as Brainfuck code.""" + "bf <prog> -- Executes <prog> as Brainfuck code." program = re.sub('[^][<>+-.,]', '', inp) @@ -45,10 +45,10 @@ def bf(inp): # the main program loop: while ip < len(program): c = program[ip] - if c == '+': - memory[mp] += 1 % 256 + if c == '+': + memory[mp] = memory[mp] + 1 % 256 elif c == '-': - memory[mp] -= 1 % 256 + memory[mp] = memory[mp] - 1 % 256 elif c == '>': mp += 1 if mp > rightmost: @@ -57,7 +57,7 @@ def bf(inp): # no restriction on memory growth! memory.extend([0] * BUFFER_SIZE) elif c == '<': - mp -= 1 % len(memory) + mp = mp - 1 % len(memory) elif c == '.': output += chr(memory[mp]) if len(output) > 500: @@ -76,7 +76,7 @@ def bf(inp): if steps > MAX_STEPS: if output == '': output = '(no output)' - output += '[exceeded {} iterations]'.format(MAX_STEPS) + output += '[exceeded %d iterations]' % MAX_STEPS break stripped_output = re.sub(r'[\x00-\x1F]', '', output) diff --git a/plugins/bitcoin.py b/plugins/bitcoin.py new file mode 100755 index 0000000..52835da --- /dev/null +++ b/plugins/bitcoin.py @@ -0,0 +1,10 @@ +from util import http, hook + + +@hook.command(autohelp=False) +def bitcoin(inp, say=None): + "bitcoin -- Gets current exchange rate for bitcoins from mtgox." + data = http.get_json("https://mtgox.com/code/data/ticker.php") + ticker = data['ticker'] + say("Current: \x0307$%(buy).2f\x0f - High: \x0307$%(high).2f\x0f" + " - Low: \x0307$%(low).2f\x0f - Volume: %(vol)s" % ticker) diff --git a/plugins/chch_worker.py b/plugins/chch_worker.py deleted file mode 100644 index e57a9cc..0000000 --- a/plugins/chch_worker.py +++ /dev/null @@ -1,310 +0,0 @@ -# -*- coding: utf-8 -*- -from util import hook -import re -import time -import requests -import urllib -from subprocess import check_output -import json -import socket -import struct - -#def run_ecmd(cmd): -## baseuri = "http://netio.chch.lan.ffc/ecmd?" -## baseuri = "http://10.8.128.35/ecmd?" -# baseuri = "http://127.0.0.1:4280/ecmd?" -# cmds = "%20".join(cmd) -# req = requests.get("%s%s" % (baseuri, cmds)) -# return req.text.strip() - -#def run_udp(cmd): -# ip="127.0.0.1" -# port=49152 -# s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -# # 100 ms timeout -# s.settimeout(0.1) -# s.connect((ip, port)) -# s.send(cmd) -# try: -# rec = s.recv(1024) -# except: -# rec = "" -# s.close() -# return rec - -## lamp_lounge handling -#@hook.command("lamp_lounge", autohelp=True) -#def cmd_lamp_lounge(inp, reply=None): -# """lamp_lounge color - set the lamp color""" -# args = inp.split(" ") -# if len(args) < 1: -# reply("lamp_lounge color - set the lamp color") -# return -# -# if len(args[0]) != 6: -# reply("lamp_lounge color - set the lamp color") -# return -# -# c = "a\x00\x03" + struct.pack('BBB', int(args[0][2:4], 16), int(args[0][0:2], 16), int(args[0][4:6], 16)) -# -# rep = run_tcp(c) -# -# if len(rep) < 3: -# reply("Error: no reply") -# return -# -# if rep[0] == 'a': -# reply("OK") -# elif rep[0] == 'e': -# reply("error: " + rep[3:]) -# else: -# reply("fatal error") - -@hook.command("corridor_light_toggle", autohelp=False) -def cmd_corridor_light_toggle(inp, reply=None): - """toggle corridor light modes""" - reply(check_output("echo corridor_light_toggle | ssh -q -p 2322 command@127.0.0.1", shell=True).strip("\n").decode("utf-8")) - -@hook.command("corridor_light", autohelp=False) -def cmd_corridor_light(inp, reply=None): - """set corridor light color""" - args = inp.split(" ") - if len(args) < 1: - reply("corridor_light color - set the light color") - return - - if len(args[0]) != 6: - reply("corridor_light color - set the light color") - return - - reply(check_output("echo corridor_light " + args[0] + " | ssh -q -p 2322 command@127.0.0.1", shell=True).strip("\n").decode("utf-8")) - -@hook.command("lounge_light_toggle", autohelp=False) -def cmd_lounge_light_toggle(inp, reply=None): - """toggle lounge light modes""" - reply(check_output("echo lounge_light_toggle | ssh -q -p 2322 command@127.0.0.1", shell=True).strip("\n").decode("utf-8")) - -@hook.command("lounge_light", autohelp=False) -def cmd_lounge_light(inp, reply=None): - """set lounge light color""" - args = inp.split(" ") - if len(args) < 1: - reply("lounge_light color - set the light color") - return - - if len(args[0]) != 6: - reply("lounge_light color - set the light color") - return - - reply(check_output("echo lounge_light " + args[0] + " | ssh -q -p 2322 command@127.0.0.1", shell=True).strip("\n").decode("utf-8")) - -@hook.command("elab_light_toggle", autohelp=False) -def cmd_elab_light_toggle(inp, reply=None): - """toggle e-lab light modes""" - reply(check_output("echo e-lab_light_toggle | ssh -q -p 2322 command@127.0.0.1", shell=True).strip("\n").decode("utf-8")) - -@hook.command("elab_light", autohelp=False) -def cmd_elab_light(inp, reply=None): - """set e-lab light color""" - args = inp.split(" ") - if len(args) < 1: - reply("e-lab_light color - set the light color") - return - - if len(args[0]) != 6: - reply("e-lab_light color - set the light color") - return - - reply(check_output("echo e-lab_light " + args[0] + " | ssh -q -p 2322 command@127.0.0.1", shell=True).strip("\n").decode("utf-8")) - -## Lamp handling -#@hook.command("lamp", autohelp=True) -#def cmd_lamp(inp, reply=None): -# """lamp color [mode] - set the lamp color""" -# args = inp.split(" ") -# if len(args) < 1: -# reply("""lamp color [mode] - set the lamp color""") -# return -# -# if len(args[0]) != 6: -# reply("""lamp color [mode] - set the lamp color""") -# return -# -# cmode = "s" -# if len(args) > 1: -# if args[1] == "s" or args[1] == "y" or args[1] == "f": -# cmode = args[1] -# -# c = [] -# c.append([5, int(args[0][0:2], 16)]) -# c.append([4, int(args[0][2:4], 16)]) -# c.append([3, int(args[0][4:6], 16)]) -# -# for ce in c: -# res = run_ecmd(["channel", str(ce[0]), str(ce[1]), cmode]) -# if res != "OK": -# return -# reply("OK") - -#@hook.command("lamp_fadestep", autohelp=True) -#def cmd_lamp_fadestep(inp, reply=None): -# """lamp_fadestep step - set the lamp fadestep""" -# args = inp.split(" ") -# -# if len(args) < 1: -# reply("""lamp_fadestep step - set the lamp fadestep""") -# return -# -# reply(run_ecmd(["fadestep", args[0]])) - -#@hook.command("lamp_fadestep_get", autohelp=False) -#def cmd_lamp_fadestep_get(inp, reply=None): -# """lamp_fadestep_get - get the lamp fadestep""" -# reply(run_ecmd(["fadestep"])) -# -#@hook.command("lamp_channels", autohelp=False) -#def cmd_lamp_channels(inp, reply=None): -# """lamp_chanels - get the lamp channel count""" -# reply(run_ecmd(["channels"])) - -# Wiki handling -def wiki_changes(cmd=False): - tmpfile = "/tmp/wikichanges.timestamp.txt" - basewikiuri = "https://www.chaoschemnitz.de/index.php?title=%s" - wikiapiuri = "https://www.chaoschemnitz.de/api.php?"\ - "action=query&list=recentchanges&format=json&"\ - "rcprop=user|userid|comment|parsedcomment|timestamp|"\ - "title|sha1|sizes|redirect|loginfo|tags|flags"\ - "&rclist=edit|external|new|log" - - try: - fdch = open(tmpfile, "rw") - timestamp = fdch.read() - fdch.close() - except IOError: - timestamp = None - - try: - r = requests.get(wikiapiuri, verify=False) - except: - return [] - - rarr = [] - changes = r.json()["query"]["recentchanges"] - ntimestamp = changes[0]["timestamp"] - for change in changes: - if change["timestamp"] == timestamp: - break - uri = basewikiuri % (urllib.quote(change["title"].encode("utf-8"), safe="")) - rarr.append("wiki: %s changed '%s' ( %s ) comment: %s" %\ - (change["user"], change["title"], uri,\ - change["comment"].strip("\r\n\t"))) - - if cmd == False: - fdch = open(tmpfile, "w+") - fdch.write("%s" % (ntimestamp)) - fdch.close() - - return rarr - -def print_wiki_changes(info, conn=None, chan=None): - """print_wiki_changes - print wiki changes, when the worker calls""" - ch = wiki_changes(cmd=False) - if len(ch) == 0: - return - for c in ch[::-1]: - conn.msg("#chaoschemnitz", c) - time.sleep(0.5) - -@hook.command("wikichanges", autohelp=False) -def cmd_wikichanges(inp, reply=None): - """wikichanges - Return new recent wiki changes""" - ch = wiki_changes(cmd=True) - if len(ch) == 0: - reply("No changes since the last call were made to the wiki.") - else: - for c in ch[::-1][-4:]: - reply(c) - time.sleep(0.5) - -# Status handling -def getstatus(): -# try: - fd = requests.get('http://www.chaoschemnitz.de/chch.json') - chch_info = fd.json() - if 'message' in chch_info['state']: - message = chch_info['state']['message'] - if " | " in message: - message = message.split(" | ", 1)[0] - else: - message = "" - - if chch_info['state']['open']: - state = "geöffnet".decode("utf-8") - else: - state = "geschlossen" - - return "%s (%s)" % (state, message) -# return check_output("sudo /bin/chch-status", shell=True).strip("\n").decode("utf-8") -# except: -# return "unbekannt" - -@hook.command("status", autohelp=False) -def cmd_status(inp, reply=None): - """status - Return the door status""" - reply("Chaostreff Status: %s" % (getstatus())) - -@hook.event("TOPIC") -def topic_update(info, conn=None, chan=None): - print("topic update") - """topic_update -- Update the topic on TOPIC command""" - if chan != "#ChaosChemnitz": - return - - status = getstatus() - print("status: %s" % (status.encode('utf8'))) - - topic = info[-1].split(" | ") - print("topic: %s" % ([ elem.encode('utf8') for elem in topic ])) - - sstr = "Status: %s" % (status) - print("sstr: %s" % (sstr.encode('utf8'))) - didset = False - i = 0 - while i < len(topic): - if sstr in topic[i]: - print("Found current status in topic.") - didset = True - break - if 'Status: ' in topic[i]: - print("Found Status field in topic.") - didset = True - topic[i] = sstr - i += 1 - if didset == False: - print("No topic fiel was found, appending.") - topic.append(sstr) - - newtopic = " | ".join(topic) - if newtopic != info[-1]: - conn.send("TOPIC %s :%s" % (chan, newtopic)) - -@hook.event("332") -def e332_update(info, conn=None, chan=None): - """e332_update -- run after current topic was requested, runs worker tasks too""" - chan = info[1] - topic_update(info, conn=conn, chan=chan) - print_wiki_changes(info, conn=conn, chan=chan) - -@hook.singlethread -@hook.event("353") -def e353_update(info, conn=None, chan=None): - """e353_update -- runs after a channel (#chaoschemnitz) was joined""" - chan = info[2] - if chan.lower() == "#chaoschemnitz": - conn.send("PRIVMSG Chanserv :op #chaoschemnitz") - - while True: - time.sleep(60) - conn.send("TOPIC %s" % (chan)) - diff --git a/disabled_stuff/choose.py b/plugins/choose.py old mode 100644 new mode 100755 similarity index 71% rename from disabled_stuff/choose.py rename to plugins/choose.py index f478328..37b1077 --- a/disabled_stuff/choose.py +++ b/plugins/choose.py @@ -6,8 +6,8 @@ from util import hook @hook.command def choose(inp): - """choose <choice1>, [choice2], [choice3], [choice4], ... -- - Randomly picks one of the given choices.""" + "choose <choice1>, [choice2], [choice3], [choice4], ... -- " \ + "Randomly picks one of the given choices." c = re.findall(r'([^,]+)', inp) if len(c) == 1: diff --git a/disabled_stuff/coin.py b/plugins/coin.py old mode 100644 new mode 100755 similarity index 54% rename from disabled_stuff/coin.py rename to plugins/coin.py index 7cc2a2a..d96c587 --- a/disabled_stuff/coin.py +++ b/plugins/coin.py @@ -1,11 +1,10 @@ -import random - from util import hook +import random @hook.command(autohelp=False) -def coin(inp, action=None): - """coin [amount] -- Flips [amount] of coins.""" +def coin(inp, me=None): + "coin [amount] -- Flips [amount] of coins." if inp: try: @@ -16,10 +15,11 @@ def coin(inp, action=None): amount = 1 if amount == 1: - action("flips a coin and gets {}.".format(random.choice(["heads", "tails"]))) + me("flips a coin and gets %s." % random.choice(["heads", "tails"])) elif amount == 0: - action("makes a coin flipping motion with its hands.") + me("makes a coin flipping motion with its hands.") else: heads = int(random.normalvariate(.5 * amount, (.75 * amount) ** .5)) tails = amount - heads - action("flips {} coins and gets {} heads and {} tails.".format(amount, heads, tails)) + me("flips %i coins and gets " \ + "%i heads and %i tails." % (amount, heads, tails)) diff --git a/plugins/core_sieve.py b/plugins/core_sieve.py deleted file mode 100644 index 9d41c54..0000000 --- a/plugins/core_sieve.py +++ /dev/null @@ -1,60 +0,0 @@ -import re -from fnmatch import fnmatch - -from util import hook - - -@hook.sieve -def sieve_suite(bot, input, func, kind, args): - if input.command == 'PRIVMSG' and \ - input.nick.endswith('bot') and args.get('ignorebots', True): - return None - - if kind == "command": - if input.trigger in bot.config.get('disabled_commands', []): - return None - - fn = re.match(r'^plugins.(.+).py$', func._filename) - disabled = bot.config.get('disabled_plugins', []) - if fn and fn.group(1).lower() in disabled: - return None - - acl = bot.config.get('acls', {}).get(func.__name__) - if acl: - if 'deny-except' in acl: - allowed_channels = map(unicode.lower, acl['deny-except']) - if input.chan.lower() not in allowed_channels: - return None - if 'allow-except' in acl: - denied_channels = map(unicode.lower, acl['allow-except']) - if input.chan.lower() in denied_channels: - return None - - # shim so plugins using the old "adminonly" permissions format still work - if args.get('adminonly', False): - args["permissions"] = ["adminonly"] - - if args.get('permissions', False): - groups = bot.config.get("permissions", []) - - allowed_permissions = args.get('permissions', []) - - mask = input.mask.lower() - - # loop over every group - for key, group in groups.iteritems(): - # loop over every permission the command allows - for permission in allowed_permissions: - # see if the group has that permission - if permission in group["perms"]: - # if so, check it - group_users = [_mask.lower() for _mask in group["users"]] - for pattern in group_users: - if fnmatch(mask, pattern): - print "Allowed group {}.".format(group) - return input - - input.notice("Sorry, you are not allowed to use this command.") - return None - - return input diff --git a/plugins/core_ctcp.py b/plugins/ctcp.py old mode 100644 new mode 100755 similarity index 58% rename from plugins/core_ctcp.py rename to plugins/ctcp.py index 3e7a200..ba7c7c2 --- a/plugins/core_ctcp.py +++ b/plugins/ctcp.py @@ -1,5 +1,6 @@ +# Plugin by neersighted import time - +import getpass from util import hook @@ -16,4 +17,9 @@ def ctcp_ping(inp, notice=None): @hook.regex(r'^\x01TIME\x01$') def ctcp_time(inp, notice=None): - notice('\x01TIME: The time is: {}'.format(time.strftime("%r", time.localtime()))) + notice('\x01TIME: The time is: %s' % time.strftime("%r", time.localtime())) + + +@hook.regex(r'^\x01FINGER\x01$') +def ctcp_finger(inp, notice=None): + notice('\x01FINGER: Username is: $s' % getpass.getuser()) diff --git a/plugins/cypher.py b/plugins/cypher.py new file mode 100755 index 0000000..ddff3dc --- /dev/null +++ b/plugins/cypher.py @@ -0,0 +1,74 @@ +''' +Plugin which (de)cyphers a string +Doesn't cypher non-alphanumeric strings yet. +by instanceoftom +''' + +from util import hook +chars = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ " +len_chars = len(chars) + + +@hook.command +def cypher(inp): + "cypher <pass> <string> -- Cyphers <string> with <password>." + + passwd = inp.split(" ")[0] + len_passwd = len(passwd) + inp = " ".join(inp.split(" ")[1:]) + + out = "" + passwd_index = 0 + for character in inp: + try: + chr_index = chars.index(character) + passwd_chr_index = chars.index(passwd[passwd_index]) + + out_chr_index = (chr_index + passwd_chr_index) % len_chars + out_chr = chars[out_chr_index] + + out += out_chr + + passwd_index = (passwd_index + 1) % len_passwd + except ValueError: + out += character + continue + return out + + +@hook.command +def decypher(inp): + "decypher <pass> <string> -- Decyphers <string> with <password>." + + passwd = inp.split(" ")[0] + len_passwd = len(passwd) + inp = " ".join(inp.split(" ")[1:]) + + passwd_index = 0 + for character in inp: + try: + chr_index = chars.index(character) + passwd_index = (passwd_index + 1) % len_passwd + except ValueError: + continue + + passwd_index = passwd_index - 1 + reversed_message = inp[::-1] + + out = "" + for character in reversed_message: + try: + chr_index = chars.index(character) + passwd_chr_index = chars.index(passwd[passwd_index]) + + out_chr_index = (chr_index - passwd_chr_index) % len_chars + out_chr = chars[out_chr_index] + + out += out_chr + + passwd_index = (passwd_index - 1) % len_passwd + except ValueError: + out += character + continue + + return out[::-1] diff --git a/disabled_stuff/data/8ball_responses.txt b/plugins/data/8ball_responses.txt similarity index 100% rename from disabled_stuff/data/8ball_responses.txt rename to plugins/data/8ball_responses.txt diff --git a/disabled_stuff/data/flirts.txt b/plugins/data/flirts.txt old mode 100644 new mode 100755 similarity index 87% rename from disabled_stuff/data/flirts.txt rename to plugins/data/flirts.txt index 6490da8..f5eed69 --- a/disabled_stuff/data/flirts.txt +++ b/plugins/data/flirts.txt @@ -44,11 +44,4 @@ Do you live on a chicken farm? Because you sure know how to raise cocks. Are you wearing space pants? Because your ass is out of this world. Nice legs. What time do they open? Are you lost? Because it’s so strange to see an angel so far from heaven. -Your daddy must have been a baker, because you've got a nice set of buns. -You're so beautiful that last night you made me forget my pickup line. -I've never seen such dark eyes with so much light in them. -I think we should just be friends with sexual tension. -Whenever I see you I feel like a dog dying to get out of the car. -If I'd have held you any closer I'd be in back of you. -I wish I were on Facebook so I could poke you. -I want you like JFK wanted a car with a roof. +Your daddy must have been a baker, because you've got a nice set of buns. \ No newline at end of file diff --git a/disabled_stuff/data/fortunes.txt b/plugins/data/fortunes.txt old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/data/fortunes.txt rename to plugins/data/fortunes.txt diff --git a/disabled_stuff/data/GeoLiteCity.dat b/plugins/data/geoip.dat similarity index 63% rename from disabled_stuff/data/GeoLiteCity.dat rename to plugins/data/geoip.dat index e94f60e..718e0e1 100644 Binary files a/disabled_stuff/data/GeoLiteCity.dat and b/plugins/data/geoip.dat differ diff --git a/disabled_stuff/data/geoip_regions.json b/plugins/data/geoip_regions.json similarity index 100% rename from disabled_stuff/data/geoip_regions.json rename to plugins/data/geoip_regions.json diff --git a/disabled_stuff/data/insults.txt b/plugins/data/insults.txt old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/data/insults.txt rename to plugins/data/insults.txt diff --git a/plugins/data/itemids.txt b/plugins/data/itemids.txt new file mode 100755 index 0000000..37e1f50 --- /dev/null +++ b/plugins/data/itemids.txt @@ -0,0 +1,386 @@ +// obtained from <https://github.com/blha303/skybot/commit/d4ba73d6e3f21cc60a01342f3de9d0d4abd14b3d> +// edited by Lukeroge, _frozen, and MufinMcFlufin +// Block id +1 Stone +2 Grass Block +3 Dirt +4 Cobblestone +5 Wooden Planks +5:1 Pine Wood Planks +5:2 Birch Wood Planks +5:3 Jungle Wood Planks +6 Sapling +7 Bedrock +8 Water +9 Stationary Water +10 Lava +11 Stationary Lava +12 Sand +13 Gravel +14 Gold Ore +15 Iron Ore +16 Coal Ore +17 Wood +17:1 Pine Wood +17:2 Birch Wood +17:3 Jungle Wood +18 Leaves +19 Sponge +20 Glass +21 Lapis Lazuli Ore +22 Lapis Lazuli Block +23 Dispenser +24 Sandstone +25 Note Block +26 Bed +27 Powered Rail +28 Detector Rail +29 Sticky Piston +30 Cobweb +31 Grass +32 Dead Bush +33 Piston +//34 Piston Extended +35 Wool +35:1 Orange Wool +35:2 Magenta Wool +35:3 Light Blue Wool +35:4 Yellow Wool +35:5 Lime Wool +35:6 Pink Wool +35:7 Gray Wool +35:8 Light Gray Wool +35:9 Cyan Wool +35:10 Purple Wool +35:11 Blue Wool +35:12 Brown Wool +35:13 Green Wool +35:14 Red Wool +35:15 Black Wool +35:0 White Wool +//36 Block Moved by Piston +37 Flower +38 Rose +39 Brown Mushroom +40 Red Mushroom +41 Block of Gold +42 Block of Iron +43 Double Slabs +44 Slabs +45 Bricks +46 TNT +47 Bookshelf +48 Moss Stone +49 Obsidian +50 Torch +51 Fire +52 Monster Spawner +53 Wooden Stairs +54 Chest +//55 Redstone Wire +56 Diamond Ore +57 Block of Diamond +58 Crafting Table +59 Crops +60 Farmland +61 Furnace +62 Lit Furnace +63 Sign +64 Wooden Door +65 Ladder +66 Rail +67 Stone Stairs +68 Sign +69 Lever +70 Pressure Plate +71 Iron Door +72 Pressure Plate +73 Redstone Ore +74 Glowing Redstone Ore +75 Redstone Torch +76 Lit Redstone Torch +77 Button +78 Snow +79 Ice +80 Snow +81 Cactus +82 Clay +83 Sugar cane +84 Jukebox +85 Fence +86 Pumpkin +87 Netherrack +88 Soul Sand +89 Glowstone +90 Portal +91 Jack 'o' Lantern +92 Cake Block +93 Redstone Repeater +94 Redstone Repeater (On) +95 Locked chest +96 Trapdoor +97 Hidden Silverfish +98 Stone Bricks +99 Mushroom +100 Mushroom +101 Iron Bars +102 Glass Pane +103 Melon +104 Pumpkin Stem +105 Melon Stem +106 Vines +107 Fence Gate +108 Brick Stairs +109 Stone Brick Stairs +110 Mycelium +111 Lily Pad +112 Nether Brick +113 Nether Brick Fence +114 Nether Brick Stairs +115 Nether Wart +116 Enchantment Table +117 Brewing stand +118 Cauldron +119 End Portal +120 End Portal Frame +121 End Stone +122 Dragon Egg +123 Redstone Lamp +124 Lit Redstone Lamp +127 Cocoa +128 Sandstone Stairs +129 Emerald Ore +130 Ender Chest +131 Tripwire Hook +132 Tripwire +133 Block of Emerald +// Items Ids +256 Iron Shovel +257 Iron Pickaxe +258 Iron Axe +259 Flint and Steel +260 Apple +261 Bow +262 Arrow +263 Coal +264 Diamond +265 Iron Ingot +266 Gold Ingot +267 Iron Sword +268 Wooden Sword +269 Wooden Shovel +270 Wooden Pickaxe +271 Wooden Axe +272 Stone Sword +273 Stone Shovel +274 Stone Pickaxe +275 Stone Axe +276 Diamond Sword +277 Diamond Shovel +278 Diamond Pickaxe +279 Diamond Axe +280 Stick +281 Bowl +282 Mushroom Stew +283 Golden Sword +284 Golden Shovel +285 Golden Pickaxe +286 Golden Axe +287 String +288 Feather +289 Gunpowder +290 Wooden Hoe +291 Stone Hoe +292 Iron Hoe +293 Diamond Hoe +294 Golden Hoe +295 Seeds +296 Wheat +297 Bread +298 Leather Cap +299 Leather Tunic +300 Leather Pants +301 Leather Boots +302 Chain Helmet +303 Chain Chestplate +304 Chain Leggings +305 Chain Boots +306 Iron Helmet +307 Iron Chestplate +308 Iron Leggings +309 Iron Boots +310 Diamond Helmet +311 Diamond Chestplate +312 Diamond Leggings +313 Diamond Boots +314 Golden Helmet +315 Golden Chestplate +316 Golden Leggings +317 Golden boots +318 Flint +319 Raw Porkchop +320 Cooked Porkchop +321 Painting +322 Golden Apple +323 Sign +324 Wooden Door +325 Bucket +326 Water Bucket +327 Lava bucket +328 Minecart +329 Saddle +330 Iron Door +331 Redstone +332 Snowball +333 Boat +334 Leather +335 Milk +336 Brick +337 Clay +338 Sugar Canes +339 Paper +340 Book +341 Slimeball +342 Minecart with Chest +343 Minecart with Furnace +344 Egg +345 Compass +346 Fishing Rod +347 Clock +348 Glowstone Dust +349 Raw Fish +350 Cooked Fish +351 Dye +351:0 Ink Sac +351:1 Rose Red +351:2 Cactus Green +351:3 Cocoa Beans +351:4 Lapis Lazuli +351:5 Purple Dye +351:6 Cyan Dye +351:7 Light Gray Dye +351:8 Gray Dye +351:9 Pink Dye +351:10 Lime Dye +351:11 Dandelion Yellow +351:12 Light Blue Dye +351:13 Magenta Dye +351:14 Orange Dye +351:15 Bone Meal +352 Bone +353 Sugar +354 Cake +355 Bed +356 Redstone Repeater +357 Cookie +358 Map +359 Shears +360 Melon +361 Pumpkin Seeds +362 Melon Seeds +363 Raw Beef +364 Steak +365 Raw Chicken +366 Cooked Chicken +367 Rotten Flesh +368 Ender Pearl +369 Blaze Rod +370 Ghast Tear +371 Gold Nugget +372 Nether Wart +373 Potion +373:16 Awkward Potion +373:32 Thick Potion +373:64 Mundane Potion +373:8193 Regeneration Potion (0:45) +373:8194 Swiftness Potion (3:00) +373:8195 Fire Resistance Potion (3:00) +373:8196 Poison Potion (0:45) +373:8197 Healing Potion +373:8200 Weakness Potion (1:30) +373:8201 Strength Potion (3:00) +373:8202 Slowness Potion (1:30) +373:8204 Harming Potion +373:8225 Regeneration Potion II (0:22) +373:8226 Swiftness Potion II (1:30) +373:8228 Poison Potion II (0:22) +373:8229 Healing Potion II +373:8233 Strength Potion II (1:30) +373:8236 Harming Potion II +373:8257 Regeneration Potion (2:00) +373:8258 Swiftness Potion (8:00) +373:8259 Fire Resistance Potion (8:00) +373:8260 Poison Potion (2:00) +373:8264 Weakness Potion (4:00) +373:8265 Strength Potion (8:00) +373:8266 Slowness Potion (4:00) +373:16378 Fire Resistance Splash (2:15) +373:16385 Regeneration Splash (0:33) +373:16386 Swiftness Splash (2:15) +373:16388 Poison Splash (0:33) +373:16389 Healing Splash +373:16392 Weakness Splash (1:07) +373:16393 Strength Splash (2:15) +373:16394 Slowness Splash (1:07) +373:16396 Harming Splash +373:16418 Swiftness Splash II (1:07) +373:16420 Poison Splash II (0:16) +373:16421 Healing Splash II +373:16425 Strength Splash II (1:07) +373:16428 Harming Splash II +373:16449 Regeneration Splash (1:30) +373:16450 Swiftness Splash (6:00) +373:16451 Fire Resistance Splash (6:00) +373:16452 Poison Splash (1:30) +373:16456 Weakness Splash (3:00) +373:16457 Strength Splash (6:00) +373:16458 Slowness Splash (3:00) +373:16471 Regeneration Splash II (0:16) +374 Glass Bottle +375 Spider Eye +376 Fermented Spider Eye +377 Blaze Powder +378 Magma Cream +379 Brewing Stand +380 Cauldron +381 Eye of Ender +382 Glistering Melon +// 383 Spawn Egg +383:50 Creeper Egg +383:51 Skeleton Egg +383:52 Spider Egg +383:54 Zombie Egg +383:55 Slime Egg +383:56 Ghast Egg +383:57 Zombie Pigman Egg +383:58 Enderman Egg +383:59 Cave Spider Egg +383:60 Silverfish Egg +383:61 Blaze Egg +383:62 Magma Cube Egg +383:90 Pig Egg +383:91 Sheep Egg +383:92 Cow Egg +383:93 Chicken Egg +383:94 Squid Egg +383:95 Wolf Egg +383:96 Mooshroom Egg +383:98 Ocelot Egg +383:120 Villager Egg +384 Bottle Of Enchanting +385 Fire Charge +386 Book and Quill +387 Written Book +388 Emerald +// Records +2256 Music Disc 13 +2257 Music Disc Cat +2258 Music Disc Blocks +2259 Music Disc Chirp +2260 Music Disc Far +2261 Music Disc Mall +2262 Music Disc Mellohi +2263 Music Disc Stal +2264 Music Disc Strad +2265 Music Disc Ward +2266 Music Disc 11 diff --git a/disabled_stuff/data/kills.txt b/plugins/data/kills.txt old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/data/kills.txt rename to plugins/data/kills.txt diff --git a/disabled_stuff/data/larts.txt b/plugins/data/larts.txt old mode 100644 new mode 100755 similarity index 89% rename from disabled_stuff/data/larts.txt rename to plugins/data/larts.txt index 029e3a0..5cdad23 --- a/disabled_stuff/data/larts.txt +++ b/plugins/data/larts.txt @@ -1,6 +1,6 @@ smacks {user} in the face with a burlap sack full of broken glass. swaps {user}'s shampoo with glue. -installs Windows Vista on {user}'s computer. +installs Windows on {user}'s computer. forces {user} to use perl for 3 weeks. registers {user}'s name with 50 known spammers. resizes {user}'s console to 40x24. @@ -37,8 +37,10 @@ takes away {user}'s internet connection. pushes {user} past the Shoe Event Horizon. counts '1, 2, 5... er... 3!' and hurls the Holy Handgrenade Of Antioch at {user}. puts {user} in a nest of camel spiders. +makes {user} read slashdot at -1. puts 'alias vim=emacs' in {user}'s /etc/profile. uninstalls every web browser from {user}'s system. +locks {user} in the Chateau d'If. signs {user} up for getting hit on the head lessons. makes {user} try to set up a Lexmark printer. fills {user}'s eyedrop bottle with lime juice. @@ -51,10 +53,13 @@ puts sugar between {user}'s bedsheets. pours sand into {user}'s breakfast. mixes epoxy into {user}'s toothpaste. puts Icy-Hot in {user}'s lube container. +straps {user} to a chair, and plays a endless low bitrate MP3 loop of "the world's most annoying sound" from "Dumb and Dumber". +tells Dr. Dre that {user} was talking smack. forces {user} to use a Commodore 64 for all their word processing. puts {user} in a room with several heavily armed manic depressives. makes {user} watch reruns of "Blue's Clues". puts lye in {user}'s coffee. +introduces {user} to the clue-by-four. tattoos the Windows symbol on {user}'s ass. lets Borg have his way with {user}. signs {user} up for line dancing classes at the local senior center. @@ -89,11 +94,12 @@ signs {user} up for the Iowa State Ferret Legging Championship. attempts to hotswap {user}'s RAM. dragon punches {user}. puts railroad spikes into {user}'s side. -replaces {user}'s lubricant with liquid weld. +replaces {user}'s Astroglide with JB Weld. replaces {user}'s stress pills with rat poison pellets. -replaces {user}'s itch cream with hair removal cream. +replaces {user}'s crotch itch cream with Nair. does the Australian Death Grip on {user}. dances upon the grave of {user}'s ancestors. -farts loudly in {user}'s general direction. +farts in {user}'s general direction. flogs {user} with stinging nettle. +intoduces {user} to the Knights who say Ni. hands {user} a poison ivy joint. diff --git a/disabled_stuff/data/name_files/dragons.json b/plugins/data/name_files/dragons.json similarity index 100% rename from disabled_stuff/data/name_files/dragons.json rename to plugins/data/name_files/dragons.json diff --git a/disabled_stuff/data/name_files/dwarves.json b/plugins/data/name_files/dwarves.json similarity index 100% rename from disabled_stuff/data/name_files/dwarves.json rename to plugins/data/name_files/dwarves.json diff --git a/disabled_stuff/data/name_files/elves_female.json b/plugins/data/name_files/elves_female.json similarity index 100% rename from disabled_stuff/data/name_files/elves_female.json rename to plugins/data/name_files/elves_female.json diff --git a/disabled_stuff/data/name_files/elves_male.json b/plugins/data/name_files/elves_male.json similarity index 100% rename from disabled_stuff/data/name_files/elves_male.json rename to plugins/data/name_files/elves_male.json diff --git a/disabled_stuff/data/name_files/fantasy.json b/plugins/data/name_files/fantasy.json similarity index 100% rename from disabled_stuff/data/name_files/fantasy.json rename to plugins/data/name_files/fantasy.json diff --git a/disabled_stuff/data/name_files/female.json b/plugins/data/name_files/female.json similarity index 100% rename from disabled_stuff/data/name_files/female.json rename to plugins/data/name_files/female.json diff --git a/disabled_stuff/data/name_files/general.json b/plugins/data/name_files/general.json similarity index 100% rename from disabled_stuff/data/name_files/general.json rename to plugins/data/name_files/general.json diff --git a/disabled_stuff/data/name_files/hobbits.json b/plugins/data/name_files/hobbits.json similarity index 100% rename from disabled_stuff/data/name_files/hobbits.json rename to plugins/data/name_files/hobbits.json diff --git a/disabled_stuff/data/name_files/inns.json b/plugins/data/name_files/inns.json similarity index 100% rename from disabled_stuff/data/name_files/inns.json rename to plugins/data/name_files/inns.json diff --git a/disabled_stuff/data/name_files/items.json b/plugins/data/name_files/items.json similarity index 100% rename from disabled_stuff/data/name_files/items.json rename to plugins/data/name_files/items.json diff --git a/disabled_stuff/data/name_files/male.json b/plugins/data/name_files/male.json similarity index 100% rename from disabled_stuff/data/name_files/male.json rename to plugins/data/name_files/male.json diff --git a/disabled_stuff/data/name_files/narn.json b/plugins/data/name_files/narn.json similarity index 100% rename from disabled_stuff/data/name_files/narn.json rename to plugins/data/name_files/narn.json diff --git a/disabled_stuff/data/name_files/warrior_cats.json b/plugins/data/name_files/warrior_cats.json similarity index 100% rename from disabled_stuff/data/name_files/warrior_cats.json rename to plugins/data/name_files/warrior_cats.json diff --git a/disabled_stuff/data/recipes.txt b/plugins/data/recipes.txt old mode 100644 new mode 100755 similarity index 68% rename from disabled_stuff/data/recipes.txt rename to plugins/data/recipes.txt index 2b0e1db..14ce8cf --- a/disabled_stuff/data/recipes.txt +++ b/plugins/data/recipes.txt @@ -1,7 +1,7 @@ //Minecraft Recipes List //Created by _303 //Obtained from https://github.com/ClouDev/CloudBot/blob/develop/plugins/data/recipes.txt -//Edited by CHCMATT for Minecraft version: 1.7.4 +//Edited by _frozen // //Summary of Use: Each column is seperated by a comma (,) and rows by a vertical bar (|). Order of Recipes & Categories taken from //www.minecraftwiki.net/wiki/Crafting for easier updating in the future (The Future!) @@ -21,10 +21,7 @@ 1x Block of Gold: Gold Ingot, Gold Ingot, Gold Ingot | Gold Ingot, Gold Ingot, Gold Ingot | Gold Ingot, Gold Ingot, Gold Ingot 1x Block of Iron: Iron Ingot, Iron Ingot, Iron Ingot | Iron Ingot, Iron Ingot, Iron Ingot | Iron Ingot, Iron Ingot, Iron Ingot 1x Block of Diamond: Diamond, Diamond, Diamond | Diamond, Diamond, Diamond | Diamond, Diamond, Diamond -1x Block of Coal: Coal, Coal, Coal | Coal, Coal, Coal | Coal, Coal, Coal -1x Block of Redstone: Redstone Dust, Redstone Dust, Redstone Dust | Redstone Dust, Redstone Dust, Redstone Dust | Redstone Dust, Redstone Dust, Redstone Dust 1x Lapis Lazuli Block: Lapis Lazuli, Lapis Lazuli, Lapis Lazuli | Lapis Lazuli, Lapis Lazuli, Lapis Lazuli | Lapis Lazuli, Lapis Lazuli, Lapis Lazuli -1x Emerald Block: Emerald, Emerald, Emerald | Emerald, Emerald, Emerald | Emerald, Emerald, Emerald 1x Glowstone: Glowstone Dust, Glowstone Dust | Glowstone Dust, Glowstone Dust 1x Wool: String, String | String, String 1x TNT: Gunpowder, Sand, Gunpowder | Sand, Gunpowder, Sand | Gunpowder, Sand, Gunpowder @@ -120,7 +117,6 @@ 6x Powered Rail: Gold Ingot, None, Gold Ingot | Gold Ingot, Stick, Gold Ingot | Gold Ingot, Redstone, Gold Ingot 6x Detector Rail: Iron Ingot, None, Iron Ingot | Iron Ingot, Pressure Plate, Iron Ingot | Iron Ingot, Redstone, Iron Ingot 1x Boat: Wooden Planks, None, Wooden Planks | Wooden Planks, Wooden Planks, Wooden Planks -1x Carrot On A Stick: Fishing Rod | None, Carrot // //Mechanism Recipes // @@ -129,8 +125,7 @@ 2x Trapdoor: Wooden Planks, Wooden Planks, Wooden Planks | Wooden Planks, Wooden Planks, Wooden Planks 1x Stone Pressure Plate: Stone, Stone 1x Wooden Pressure Plate: Wooden Planks, Wooden Planks -1x Stone Button: Stone -1x Wooden Button: Wooden Planks +1x Button: Stone | Stone 1x Redstone Torch: Redstone | Stick 1x Lever: Stick | Cobblestone 1x Note Block: Wooden Planks, Wooden Planks, Wooden Planks | Wooden Planks, Redstone, Wooden Planks | Wooden Planks, Wooden Planks, Wooden Planks @@ -138,13 +133,8 @@ 1x Dispenser: Cobblestone, Cobblestone, Cobblestone | Cobblestone, Bow, Cobblestone | Cobblestone, Redstone, Cobblestone 1x Redstone Repeater: Redstone Torch, Redstone, Redstone Torch | Stone, Stone, Stone 1x Piston: Wooden Planks, Wooden Planks, Wooden Planks | Cobblestone, Iron Ingot, Cobblestone | Cobblestone, Redstone, Cobblestone -1x Sticky Piston: Slime Ball | Piston -1x Redstone Lamp: None, Redstone Dust, None | Redstone Dust, Glowstone Block, Redstone Dust | None, Redstone Dust, None -1x Trapped Chest: Chest, Tripwire Hook -1x Dropper: Cobblestone, Cobblestone, Cobblestone | Cobblestone, None, Cobblestone | Cobblestone, Redstone Dust, Cobblestone -1x Weighted Pressure Plate (Heavy): Iron Ingot, Iron Ingot -1x Weighted Pressure Plate (Light): Gold Ingot, Gold Ingot -2x Tripwire Hook: Iron Ingot | Stick | Wooden Planks +1x Sticky Piston: none, slime ball, none | none, piston, none +1x Redstone Lamp: none, redstone dust, none | redstone dust, glowstone block, redstone | none, redstone dust, none // //Food Recipes // @@ -179,11 +169,6 @@ 9x Gold Nugget: Gold Ingot 1x Gold Ingot: Gold Nugget, Gold Nugget, Gold Nugget | Gold Nugget, Gold Nugget, Gold Nugget | Gold Nugget, Gold Nugget, Gold Nugget 1x Eye of Ender: Ender Pearl | Blaze Powder -1x Item Frame: Stick, Stick, Stick | Stick, Leather, Stick | Stick, Stick, Stick -1x Anvil: Block of Iron, Block of Iron, Block of Iron | None, Iron Ingot, None | Iron Ingot, Iron Ingot, Iron Ingot -1x Ender Chest: Obsidian, Obsidian, Obsidian | Osbidian, Eye of Ender, Obsidian | Obsidian, Obsidian, Obsidian -1x Flower Pot: Brick, None, Brick | None, Brick, None -2x Lead: None, String, String | None, Slime Ball, String | String, None, None // //Dye Recipes // @@ -229,41 +214,4 @@ 1x Fermented Spider Eye: Spider Eye | Brown Mushroom, Sugar 1x Glistering Melon: Melon Slice, Gold Nugget 9x Gold Nugget: Gold Ingot -1x Enchantment Table: None, Book, None | Diamond, Obsidian, Diamond | Obsidian, Obsidian, Obsidian -// -//Stained Glass Recipes -// -8x White Stained Glass: Glass, Glass, Glass | Glass, Bone Meal, Glass | Glass, Glass, Glass -8x Orange Stained Glass: Glass, Glass, Glass | Glass, Orange Dye, Glass | Glass, Glass, Glass -8x Magenta Stained Glass: Glass, Glass, Glass | Glass, Magenta Dye, Glass | Glass, Glass, Glass -8x Light Blue Stained Glass: Glass, Glass, Glass | Glass, Light Blue Dye, Glass | Glass, Glass, Glass -8x Yellow Stained Glass: Glass, Glass, Glass | Glass, Dandelion Yellow, Glass | Glass, Glass, Glass -8x Lime Stained Glass: Glass, Glass, Glass | Glass, Lime Dye, Glass | Glass, Glass, Glass -8x Pink Stained Glass: Glass, Glass, Glass | Glass, Pink Dye, Glass | Glass, Glass, Glass -8x Gray Stained Glass: Glass, Glass, Glass | Glass, Gray Dye, Glass | Glass, Glass, Glass -8x Light Gray Stained Glass: Glass, Glass, Glass | Glass, Light Gray Dye, Glass | Glass, Glass, Glass -8x Cyan Stained Glass: Glass, Glass, Glass | Glass, Cyan Dye, Glass | Glass, Glass, Glass -8x Purple Stained Glass: Glass, Glass, Glass | Glass, Purple Dye, Glass | Glass, Glass, Glass -8x Blue Stained Glass: Glass, Glass, Glass | Glass, Lapis Lazuli, Glass | Glass, Glass, Glass -8x Brown Stained Glass: Glass, Glass, Glass | Glass, Cocoa Beans, Glass | Glass, Glass, Glass -8x Green Stained Glass: Glass, Glass, Glass | Glass, Cactus Green, Glass | Glass, Glass, Glass -8x Red Stained Glass: Glass, Glass, Glass | Glass, Rose Red, Glass | Glass, Glass, Glass -8x Black Stained Glass: Glass, Glass, Glass | Glass, Inc Sac, Glass | Glass, Glass, Glass -// -//Stained Glass Panes -// -16x White Stained Glass Panes: White Stained Glass, White Stained Glass, White Stained Glass | White Stained Glass, White Stained Glass, White Stained Glass -16x Orange Stained Glass Panes: Orange Stained Glass, Orange Stained Glass, Orange Stained Glass | Orange Stained Glass, Orange Stained Glass, Orange Stained Glass -16x Magenta Stained Glass Panes: Magenta Stained Glass, Magenta Stained Glass, Magenta Stained Glass | Magenta Stained Glass, Magenta Stained Glass, Magenta Stained Glass -16x Light Blue Stained Glass Panes: Light Blue Stained Glass, Light Blue Stained Glass, Light Blue Stained Glass | Light Blue Stained Glass, Light Blue Stained Glass, Light Blue Stained Glass -16x Yellow Stained Glass Panes: Yellow Stained Glass, Yellow Stained Glass, Yellow Stained Glass | Yellow Stained Glass, Yellow Stained Glass, Yellow Stained Glass -16x Lime Stained Glass Panes: Lime Stained Glass, Lime Stained Glass, Lime Stained Glass | Lime Stained Glass, Lime Stained Glass, Lime Stained Glass -16x Pink Stained Glass Panes: Pink Stained Glass, Pink Stained Glass, Pink Stained Glass | Pink Stained Glass, Pink Stained Glass, Pink Stained Glass -16x Gray Stained Glass Panes: Gray Stained Glass, Gray Stained Glass, Gray Stained Glass | Gray Stained Glass, Gray Stained Glass, Gray Stained Glass -16x Light Gray Stained Glass Panes: Light Gray Stained Glass, Light Gray Stained Glass, Light Gray Stained Glass | Light Gray Stained Glass, Light Gray Stained Glass, Light Gray Stained Glass -16x Cyan Stained Glass Panes: Cyan Stained Glass, Cyan Stained Glass, Cyan Stained Glass | Cyan Stained Glass, Cyan Stained Glass, Cyan Stained Glass -16x Purple Stained Glass Panes: Purple Stained Glass, Purple Stained Glass, Purple Stained Glass | Purple Stained Glass, Purple Stained Glass, Purple Stained Glass -16x Blue Stained Glass Panes: Blue Stained Glass, Blue Stained Glass, Blue Stained Glass | Blue Stained Glass, Blue Stained Glass, Blue Stained Glass -16x Brown Stained Glass Panes: Brown Stained Glass, Brown Stained Glass, Brown Stained Glass | Brown Stained Glass, Brown Stained Glass, Brown Stained Glass -16x Green Stained Glass Panes: Green Stained Glass, Green Stained Glass, Green Stained Glass | Green Stained Glass, Green Stained Glass, Green Stained Glass -16x Black Stained Glass Panes: Black Stained Glass, Black Stained Glass, Black Stained Glass | Black Stained Glass, Black Stained Glass, Black Stained Glass +1x Enchantment Table: None, Book, None | Diamond, Obsidian, Diamond | Obsidian, Obsidian, Obsidian \ No newline at end of file diff --git a/plugins/data/slap_items.txt b/plugins/data/slap_items.txt new file mode 100755 index 0000000..2f53980 --- /dev/null +++ b/plugins/data/slap_items.txt @@ -0,0 +1,19 @@ +cast iron skillet +large trout +baseball bat +wooden cane +CRT monitor +diamond sword +physics textbook +television +mau5head +five ton truck +roll of duct tape +book +cobblestone block +lava bucket +rubber chicken +gold block +fire extinguisher +heavy rock +chunk of dirt diff --git a/plugins/data/slaps.txt b/plugins/data/slaps.txt new file mode 100755 index 0000000..8952952 --- /dev/null +++ b/plugins/data/slaps.txt @@ -0,0 +1,14 @@ +slaps {user} with a {item}. +slaps {user} around a bit with a {item}. +throws a {item} at {user}. +chucks a few {item}s at {user}. +grabs a {item} and throws it in {user}'s face. +launches a {item} in {user}'s general direction. +sits on {user}'s face while slamming a {item} into their crotch. +starts slapping {user} silly with a {item}. +holds {user} down and repeatedly whacks them with a {item}. +prods {user} with a flaming {item}. +picks up a {item} and whacks {user} with it. +ties {user} to a chair and throws a {item} at them. +hits {user} on the head with a {item}. +ties {user} to a pole and whips them with a {item}. diff --git a/disabled_stuff/data/slogans.txt b/plugins/data/slogans.txt old mode 100644 new mode 100755 similarity index 100% rename from disabled_stuff/data/slogans.txt rename to plugins/data/slogans.txt diff --git a/disabled_stuff/dice.py b/plugins/dice.py old mode 100644 new mode 100755 similarity index 69% rename from disabled_stuff/dice.py rename to plugins/dice.py index a89f3d5..d479f84 --- a/disabled_stuff/dice.py +++ b/plugins/dice.py @@ -14,8 +14,8 @@ sign_re = re.compile(r'[+-]?(?:\d*d)?(?:\d+|F)', re.I) split_re = re.compile(r'([\d+-]*)d?(F|\d*)', re.I) -def n_rolls(count, n): - """roll an n-sided die count times""" +def nrolls(count, n): + "roll an n-sided die count times" if n == "F": return [random.randint(-1, 1) for x in xrange(min(count, 100))] if n < 2: # it's a coin @@ -28,16 +28,16 @@ def n_rolls(count, n): return [random.randint(1, n) for x in xrange(count)] else: # fake it return [int(random.normalvariate(.5 * (1 + n) * count, - (((n + 1) * (2 * n + 1) / 6. - - (.5 * (1 + n)) ** 2) * count) ** .5))] + (((n + 1) * (2 * n + 1) / 6. - + (.5 * (1 + n)) ** 2) * count) ** .5))] @hook.command('roll') #@hook.regex(valid_diceroll, re.I) @hook.command def dice(inp): - """dice <dice roll> -- Simulates dice rolls. Example of <dice roll>: - 'dice 2d20-d5+4 roll 2'. D20s, subtract 1D5, add 4""" + "dice <diceroll> -- Simulates dicerolls. Example of <diceroll>:" \ + " 'dice 2d20-d5+4 roll 2'. D20s, subtract 1D5, add 4" try: # if inp is a re.match object... (inp, desc) = inp.groups() @@ -49,7 +49,7 @@ def dice(inp): spec = whitespace_re.sub('', inp) if not valid_diceroll_re.match(spec): - return "Invalid dice roll" + return "Invalid diceroll" groups = sign_re.findall(spec) total = 0 @@ -59,7 +59,7 @@ def dice(inp): count, side = split_re.match(roll).groups() count = int(count) if count not in " +-" else 1 if side.upper() == "F": # fudge dice are basically 1d3-2 - for fudge in n_rolls(count, "F"): + for fudge in nrolls(count, "F"): if fudge == 1: rolls.append("\x033+\x0F") elif fudge == -1: @@ -73,18 +73,17 @@ def dice(inp): side = int(side) try: if count > 0: - d = n_rolls(count, side) - rolls += map(str, d) - total += sum(d) + dice = nrolls(count, side) + rolls += map(str, dice) + total += sum(dice) else: - d = n_rolls(-count, side) - rolls += [str(-x) for x in d] - total -= sum(d) + dice = nrolls(-count, side) + rolls += [str(-x) for x in dice] + total -= sum(dice) except OverflowError: - # I have never seen this happen. If you make this happen, you win a cookie return "Thanks for overflowing a float, jerk >:[" if desc: - return "{}: {} ({})".format(desc.strip(), total, ", ".join(rolls)) + return "%s: %d (%s)" % (desc.strip(), total, ", ".join(rolls)) else: - return "{} ({})".format(total, ", ".join(rolls)) + return "%d (%s)" % (total, ", ".join(rolls)) diff --git a/disabled_stuff/dictionary.py b/plugins/dictionary.py old mode 100644 new mode 100755 similarity index 82% rename from disabled_stuff/dictionary.py rename to plugins/dictionary.py index 5b4123b..2bd5ae6 --- a/disabled_stuff/dictionary.py +++ b/plugins/dictionary.py @@ -1,6 +1,5 @@ # Plugin by GhettoWizard and Scaevolus import re - from util import hook from util import http @@ -8,7 +7,7 @@ from util import http @hook.command('dictionary') @hook.command def define(inp): - """define <word> -- Fetches definition of <word>.""" + "define <word> -- Fetches definition of <word>." url = 'http://ninjawords.com/' @@ -19,14 +18,14 @@ def define(inp): '//div[@class="example"]') if not definition: - return u'No results for {} :('.format(inp) + return 'No results for ' + inp + ' :(' def format_output(show_examples): - result = u'{}: '.format(h.xpath('//dt[@class="title-word"]/a/text()')[0]) + result = '%s: ' % h.xpath('//dt[@class="title-word"]/a/text()')[0] correction = h.xpath('//span[@class="correct-word"]/text()') if correction: - result = 'Definition for "{}": '.format(correction[0]) + result = 'Definition for "%s": ' % correction[0] sections = [] for section in definition: @@ -41,7 +40,7 @@ def define(inp): for article in sections: result += article[0] if len(article) > 2: - result += u' '.join(u'{}. {}'.format(n + 1, section) + result += ' '.join('%d. %s' % (n + 1, section) for n, section in enumerate(article[1:])) else: result += article[1] + ' ' @@ -68,7 +67,7 @@ def define(inp): @hook.command('e') @hook.command def etymology(inp): - """etymology <word> -- Retrieves the etymology of <word>.""" + "etymology <word> -- Retrieves the etymology of <word>." url = 'http://www.etymonline.com/index.php' @@ -77,7 +76,7 @@ def etymology(inp): etym = h.xpath('//dl') if not etym: - return u'No etymology found for {} :('.format(inp) + return 'No etymology found for ' + inp + ' :(' etym = etym[0].text_content() diff --git a/disabled_stuff/down.py b/plugins/down.py old mode 100644 new mode 100755 similarity index 67% rename from disabled_stuff/down.py rename to plugins/down.py index f03c078..aab75b9 --- a/disabled_stuff/down.py +++ b/plugins/down.py @@ -5,7 +5,7 @@ from util import hook, http @hook.command def down(inp): - """down <url> -- Checks if the site at <url> is up or down.""" + "down <url> -- Checks if the site at <url> is up or down." if 'http://' not in inp: inp = 'http://' + inp @@ -15,6 +15,6 @@ def down(inp): # http://mail.python.org/pipermail/python-list/2006-December/589854.html try: http.get(inp, get_method='HEAD') - return '{} seems to be up'.format(inp) + return inp + ' seems to be up' except http.URLError: - return '{} seems to be down'.format(inp) + return inp + ' seems to be down' diff --git a/disabled_stuff/drama.py b/plugins/drama.py old mode 100644 new mode 100755 similarity index 64% rename from disabled_stuff/drama.py rename to plugins/drama.py index d348cba..47df5e2 --- a/disabled_stuff/drama.py +++ b/plugins/drama.py @@ -1,7 +1,4 @@ -import re - -from util import hook, http, text - +from util import hook, http api_url = "http://encyclopediadramatica.se/api.php?action=opensearch" ed_url = "http://encyclopediadramatica.se/" @@ -9,11 +6,10 @@ ed_url = "http://encyclopediadramatica.se/" @hook.command def drama(inp): - """drama <phrase> -- Gets the first paragraph of - the Encyclopedia Dramatica article on <phrase>.""" + "drama <phrase> -- Gets the first paragraph of" \ + " the Encyclopedia Dramatica article on <phrase>." j = http.get_json(api_url, search=inp) - if not j[1]: return "No results found." article_name = j[1][0].replace(' ', '_').encode('utf8') @@ -24,8 +20,8 @@ def drama(inp): for p in page.xpath('//div[@id="bodyContent"]/p'): if p.text_content(): summary = " ".join(p.text_content().splitlines()) - summary = re.sub("\[\d+\]", "", summary) - summary = text.truncate_str(summary, 220) - return "{} :: {}".format(summary, url) + if len(summary) > 300: + summary = summary[:summary.rfind(' ', 0, 300)] + "..." + return "%s :: \x02%s\x02" % (summary, url) return "Unknown Error." diff --git a/disabled_stuff/fact.py b/plugins/fact.py old mode 100644 new mode 100755 similarity index 67% rename from disabled_stuff/fact.py rename to plugins/fact.py index 1d48ae7..6ca9a64 --- a/disabled_stuff/fact.py +++ b/plugins/fact.py @@ -2,8 +2,8 @@ from util import hook, http, web @hook.command(autohelp=False) -def fact(inp): - """fact -- Gets a random fact from OMGFACTS.""" +def fact(inp, say=False, nick=False): + "fact -- Gets a random fact from OMGFACTS." attempts = 0 @@ -20,10 +20,10 @@ def fact(inp): response = soup.find('a', {'class': 'surprise'}) link = response['href'] - fact_data = ''.join(response.find(text=True)) + fact = ''.join(response.find(text=True)) - if fact_data: - fact_data = fact_data.strip() + if fact: + fact = fact.strip() break else: if attempts > 2: @@ -32,6 +32,9 @@ def fact(inp): attempts += 1 continue - url = web.try_isgd(link) + try: + url = web.isgd(link) + except (web.ShortenError, http.HTTPError): + url = link - return "{} - {}".format(fact_data, url) + return "%s - %s" % (fact, url) diff --git a/disabled_stuff/factoids.py b/plugins/factoids.py old mode 100644 new mode 100755 similarity index 54% rename from disabled_stuff/factoids.py rename to plugins/factoids.py index 403e6f5..e3d2fbc --- a/disabled_stuff/factoids.py +++ b/plugins/factoids.py @@ -1,34 +1,29 @@ # Written by Scaevolus 2010 +from util import hook, http, text, execute import string +import sqlite3 import re -from util import hook, http, text, pyexec - - re_lineends = re.compile(r'[\r\n]*') -db_ready = False - # some simple "shortcodes" for formatting purposes shortcodes = { - '[b]': '\x02', - '[/b]': '\x02', - '[u]': '\x1F', - '[/u]': '\x1F', - '[i]': '\x16', - '[/i]': '\x16'} +'[b]': '\x02', +'[/b]': '\x02', +'[u]': '\x1F', +'[/u]': '\x1F', +'[i]': '\x16', +'[/i]': '\x16'} def db_init(db): - global db_ready - if not db_ready: - db.execute("create table if not exists mem(word, data, nick," - " primary key(word))") - db.commit() - db_ready = True + db.execute("create table if not exists mem(word, data, nick," + " primary key(word))") + db.commit() def get_memory(db, word): + row = db.execute("select data from mem where word=lower(?)", [word]).fetchone() if row: @@ -37,11 +32,11 @@ def get_memory(db, word): return None -@hook.command("r", permissions=["addfactoid"]) -@hook.command(permissions=["addfactoid"]) -def remember(inp, nick='', db=None, notice=None): - """remember <word> [+]<data> -- Remembers <data> with <word>. Add + - to <data> to append.""" +@hook.command("r", adminonly=True) +@hook.command(adminonly=True) +def remember(inp, nick='', db=None, say=None, input=None, notice=None): + "remember <word> [+]<data> -- Remembers <data> with <word>. Add +" + " to <data> to append." db_init(db) append = False @@ -69,18 +64,20 @@ def remember(inp, nick='', db=None, notice=None): if old_data: if append: - notice("Appending \x02{}\x02 to \x02{}\x02".format(new_data, old_data)) + notice("Appending \x02%s\x02 to \x02%s\x02" % (new_data, old_data)) else: - notice('Remembering \x02{}\x02 for \x02{}\x02. Type ?{} to see it.'.format(data, word, word)) - notice('Previous data was \x02{}\x02'.format(old_data)) + notice('Remembering \x02%s\x02 for \x02%s\x02. Type ?%s to see it.' + % (data, word, word)) + notice('Previous data was \x02%s\x02' % old_data) else: - notice('Remembering \x02{}\x02 for \x02{}\x02. Type ?{} to see it.'.format(data, word, word)) + notice('Remembering \x02%s\x02 for \x02%s\x02. Type ?%s to see it.' + % (data, word, word)) -@hook.command("f", permissions=["delfactoid"]) -@hook.command(permissions=["delfactoid"]) -def forget(inp, db=None, notice=None): - """forget <word> -- Forgets a remembered <word>.""" +@hook.command("f", adminonly=True) +@hook.command(adminonly=True) +def forget(inp, db=None, input=None, notice=None): + "forget <word> -- Forgets a remembered <word>." db_init(db) data = get_memory(db, inp) @@ -98,7 +95,7 @@ def forget(inp, db=None, notice=None): @hook.command def info(inp, notice=None, db=None): - """info <factoid> -- Shows the source of a factoid.""" + "info <factoid> -- Shows the source of a factoid." db_init(db) @@ -112,8 +109,8 @@ def info(inp, notice=None, db=None): @hook.regex(r'^\? ?(.+)') -def factoid(inp, message=None, db=None, bot=None, action=None, conn=None, input=None): - """?<word> -- Shows what data is associated with <word>.""" +def factoid(inp, say=None, db=None, bot=None, me=None, conn=None, input=None): + "?<word> -- Shows what data is associated with <word>." try: prefix_on = bot.config["plugins"]["factoids"].get("prefix", False) except KeyError: @@ -136,10 +133,15 @@ def factoid(inp, message=None, db=None, bot=None, action=None, conn=None, input= # factoid preprocessors if data.startswith("<py>"): code = data[4:].strip() - variables = 'input="""{}"""; nick="{}"; chan="{}"; bot_nick="{}";'.format(arguments.replace('"', '\\"'), - input.nick, input.chan, - input.conn.nick) - result = pyexec.eval_py(variables + code) + variables = 'input="""%s"""; nick="%s"; chan="%s"; bot_nick="%s";' % (arguments.replace('"', '\\"'), + input.nick, input.chan, input.conn.nick) + result = execute.eval_py(variables + code) + elif data.startswith("<url>"): + url = data[5:].strip() + try: + result = http.get(url) + except http.HttpError: + result = "Could not fetch URL." else: result = data @@ -148,15 +150,9 @@ def factoid(inp, message=None, db=None, bot=None, action=None, conn=None, input= if result.startswith("<act>"): result = result[5:].strip() - action(result) - elif result.startswith("<url>"): - url = result[5:].strip() - try: - message(http.get(url)) - except http.HttpError: - message("Could not fetch URL.") + me(result) else: if prefix_on: - message("\x02[{}]:\x02 {}".format(factoid_id, result)) + say("\x02[%s]:\x02 %s" % (factoid_id, result)) else: - message(result) + say(result) diff --git a/plugins/feelings.py b/plugins/feelings.py new file mode 100755 index 0000000..f8906b1 --- /dev/null +++ b/plugins/feelings.py @@ -0,0 +1,50 @@ +from util import hook +import re +import random + +nick_re = re.compile(r"^[A-Za-z0-9_|\.\-\]\[]*$") + + +with open("plugins/data/insults.txt") as f: + insults = [line.strip() for line in f.readlines() + if not line.startswith("//")] + +with open("plugins/data/flirts.txt") as f: + flirts = [line.strip() for line in f.readlines() + if not line.startswith("//")] + + +@hook.command +def insult(inp, nick=None, me=None, conn=None): + "insult <user> -- Makes the bot insult <user>." + target = inp.strip() + + if not re.match(nick_re, target): + notice("Invalid username!") + return + + if target == conn.nick.lower() or target == "itself": + target = nick + else: + target = inp + + out = 'insults %s... "%s"' % (target, random.choice(insults)) + me(out) + + +@hook.command +def flirt(inp, nick=None, me=None, conn=None): + "flirt <user> -- Make the bot flirt with <user>." + target = inp.strip() + + if not re.match(nick_re, target): + notice("Invalid username!") + return + + if target == conn.nick.lower() or target == "itself": + target = 'itself' + else: + target = inp + + out = 'flirts with %s... "%s"' % (target, random.choice(flirts)) + me(out) diff --git a/plugins/fishbans.py b/plugins/fishbans.py new file mode 100644 index 0000000..12ce47b --- /dev/null +++ b/plugins/fishbans.py @@ -0,0 +1,55 @@ +from util import hook, http +from urllib import quote_plus + +api_url = "http://www.fishbans.com/api/stats/%s/force/" + + +@hook.command("bans") +@hook.command +def fishbans(inp): + "fishbans <user> -- Gets information on <user>s minecraft bans from fishbans" + user = inp + + try: + request = http.get_json(api_url % quote_plus(user)) + except (http.HTTPError, http.URLError) as e: + return "Could not fetch ban data from the Fishbans API: {}".format(e) + + if request["success"] == False: + return "Could not fetch ban data for %s." % user + + user_url = "http://fishbans.com/u/%s" % user + ban_count = request["stats"]["totalbans"] + + return "The user \x02%s\x02 has \x02%s\x02 ban(s). See detailed info " \ + "at %s" % (user, ban_count, user_url) + + +@hook.command +def bancount(inp): + "bancount <user> -- Gets a count of <user>s minecraft bans from fishbans" + user = inp + + try: + request = http.get_json(api_url % quote_plus(user)) + except (http.HTTPError, http.URLError) as e: + return "Could not fetch ban data from the Fishbans API: {}".format(e) + + if request["success"] == False: + return "Could not fetch ban data for %s." % user + + user_url = "http://fishbans.com/u/%s" % user + services = request["stats"]["service"] + + out = [] + for service, ban_count in services.items(): + if ban_count != 0: + out.append("%s: \x02%s\x02" % (service, ban_count)) + else: + pass + + if not out: + return "The user \x02%s\x02 has no bans." % user + else: + text = "Bans for \x02%s\x02: " % user + return text + ", ".join(out) diff --git a/disabled_stuff/fmylife.py b/plugins/fmylife.py old mode 100644 new mode 100755 similarity index 75% rename from disabled_stuff/fmylife.py rename to plugins/fmylife.py index 1d8c0fa..5a15e8d --- a/disabled_stuff/fmylife.py +++ b/plugins/fmylife.py @@ -8,9 +8,9 @@ def refresh_cache(): soup = http.get_soup('http://www.fmylife.com/random/') for e in soup.find_all('div', {'class': 'post article'}): - fml_id = int(e['id']) + id = int(e['id']) text = ''.join(e.find('p').find_all(text=True)) - fml_cache.append((fml_id, text)) + fml_cache.append((id, text)) # do an initial refresh of the cache refresh_cache() @@ -18,12 +18,12 @@ refresh_cache() @hook.command(autohelp=False) def fml(inp, reply=None): - """fml -- Gets a random quote from fmyfife.com.""" + "fml -- Gets a random quote from fmyfife.com." # grab the last item in the fml cache and remove it - fml_id, text = fml_cache.pop() + id, text = fml_cache.pop() # reply with the fml we grabbed - reply('(#{}) {}'.format(fml_id, text)) + reply('(#%d) %s' % (id, text)) # refresh fml cache if its getting empty if len(fml_cache) < 3: refresh_cache() diff --git a/disabled_stuff/fortune.py b/plugins/fortune.py old mode 100644 new mode 100755 similarity index 84% rename from disabled_stuff/fortune.py rename to plugins/fortune.py index 5f1c478..3d458ab --- a/disabled_stuff/fortune.py +++ b/plugins/fortune.py @@ -1,7 +1,5 @@ -import random - from util import hook - +import random with open("plugins/data/fortunes.txt") as f: fortunes = [line.strip() for line in f.readlines() @@ -10,5 +8,5 @@ with open("plugins/data/fortunes.txt") as f: @hook.command(autohelp=False) def fortune(inp): - """fortune -- Fortune cookies on demand.""" + "fortune -- Fortune cookies on demand." return random.choice(fortunes) diff --git a/plugins/gcalc.py b/plugins/gcalc.py new file mode 100755 index 0000000..369cd65 --- /dev/null +++ b/plugins/gcalc.py @@ -0,0 +1,15 @@ +from util import hook, http + + +@hook.command("math") +@hook.command +def calc(inp): + "calc <term> -- Calculate <term> with Google Calc." + + soup = http.get_soup('http://www.google.com/search', q=inp) + + result = soup.find('h2', {'class': 'r'}) + if not result: + return "Could not calculate '%s'" % inp + + return result.contents[0] diff --git a/disabled_stuff/geoip.py b/plugins/geoip.py old mode 100644 new mode 100755 similarity index 51% rename from disabled_stuff/geoip.py rename to plugins/geoip.py index b7ca61d..58d881c --- a/disabled_stuff/geoip.py +++ b/plugins/geoip.py @@ -1,36 +1,20 @@ +from util import hook import os.path -import json -import gzip -from StringIO import StringIO - import pygeoip +import json -from util import hook, http +# initalise geolocation database +geo = pygeoip.GeoIP(os.path.abspath("./plugins/data/geoip.dat")) # load region database with open("./plugins/data/geoip_regions.json", "rb") as f: regions = json.loads(f.read()) -if os.path.isfile(os.path.abspath("./plugins/data/GeoLiteCity.dat")): - # initialise geolocation database - geo = pygeoip.GeoIP(os.path.abspath("./plugins/data/GeoLiteCity.dat")) -else: - download = http.get("http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz") - string_io = StringIO(download) - geoip_file = gzip.GzipFile(fileobj=string_io, mode='rb') - - output = open(os.path.abspath("./plugins/data/GeoLiteCity.dat"), 'wb') - output.write(geoip_file.read()) - output.close() - - geo = pygeoip.GeoIP(os.path.abspath("./plugins/data/GeoLiteCity.dat")) - @hook.command def geoip(inp): - """geoip <host/ip> -- Gets the location of <host/ip>""" - + "geoip <host/ip> -- Gets the location of <host/ip>" try: record = geo.record_by_name(inp) except: @@ -51,4 +35,4 @@ def geoip(inp): data["cc"] = record["country_code"] or "N/A" data["country"] = record["country_name"] or "Unknown" data["city"] = record["city"] or "Unknown" - return u"\x02Country:\x02 {country} ({cc}), \x02City:\x02 {city}{region}".format(**data) + return "\x02Country:\x02 {country} ({cc}), \x02City:\x02 {city}{region}".format(**data) diff --git a/plugins/gitio.py b/plugins/gitio.py new file mode 100755 index 0000000..9844282 --- /dev/null +++ b/plugins/gitio.py @@ -0,0 +1,52 @@ +# Plugin by neersighted and Lukeroge +from util import hook +import urllib2 + + +@hook.command +def gitio(inp): + "gitio <url> [code] -- Shorten Github URLs with git.io. [code] is" \ + " a optional custom short code." + split = inp.split(" ") + url = split[0] + + try: + code = split[1] + except: + code = None + + # if the first 8 chars of "url" are not "https://" then append + # "https://" to the url, also convert "http://" to "https://" + if url[:8] != "https://": + if url[:7] != "http://": + url = "https://" + url + else: + url = "https://" + url[7:] + url = 'url=' + str(url) + if code: + url = url + '&code=' + str(code) + req = urllib2.Request(url='http://git.io', data=url) + + # try getting url, catch http error + try: + f = urllib2.urlopen(req) + except urllib2.HTTPError: + return "Failed to get URL!" + urlinfo = str(f.info()) + + # loop over the rows in urlinfo and pick out location and + # status (this is pretty odd code, but urllib2.Request is weird) + for row in urlinfo.split("\n"): + if row.find("Status") != -1: + status = row + if row.find("Location") != -1: + location = row + + print status + if not "201" in status: + return "Failed to get URL!" + + # this wont work for some reason, so lets ignore it ^ + + # return location, minus the first 10 chars + return location[10:] diff --git a/disabled_stuff/google.py b/plugins/google.py old mode 100644 new mode 100755 similarity index 70% rename from disabled_stuff/google.py rename to plugins/google.py index fe9e288..8338b14 --- a/disabled_stuff/google.py +++ b/plugins/google.py @@ -1,5 +1,4 @@ import random - from util import hook, http, text @@ -14,25 +13,28 @@ def api_get(kind, query): @hook.command('gis') @hook.command def googleimage(inp): - """gis <query> -- Returns first Google Image result for <query>.""" + "gis <query> -- Returns first Google Image result for <query>." parsed = api_get('images', inp) if not 200 <= parsed['responseStatus'] < 300: - raise IOError('error searching for images: {}: {}'.format(parsed['responseStatus'], '')) + raise IOError('error searching for images: %d: %s' % ( \ + parsed['responseStatus'], '')) if not parsed['responseData']['results']: return 'no images found' - return random.choice(parsed['responseData']['results'][:10])['unescapedUrl'] + return random.choice(parsed['responseData']['results'][:10]) \ + ['unescapedUrl'] @hook.command('search') @hook.command('g') @hook.command def google(inp): - """google <query> -- Returns first google search result for <query>.""" + "google <query> -- Returns first google search result for <query>." parsed = api_get('web', inp) if not 200 <= parsed['responseStatus'] < 300: - raise IOError('error searching for pages: {}: {}'.format(parsed['responseStatus'], '')) + raise IOError('error searching for pages: %d: %s' % ( + parsed['responseStatus'], '')) if not parsed['responseData']['results']: return 'No results found.' @@ -48,4 +50,6 @@ def google(inp): content = http.html.fromstring(content).text_content() content = text.truncate_str(content, 150) - return u'{} -- \x02{}\x02: "{}"'.format(result['unescapedUrl'], title, content) + out = '%s -- \x02%s\x02: "%s"' % (result['unescapedUrl'], title, content) + + return out diff --git a/plugins/hash.py b/plugins/hash.py new file mode 100755 index 0000000..f9bb266 --- /dev/null +++ b/plugins/hash.py @@ -0,0 +1,9 @@ +import hashlib +from util import hook + + +@hook.command +def hash(inp): + "hash <text> -- Returns hashes of <text>." + return ', '.join(x + ": " + getattr(hashlib, x)(inp).hexdigest() + for x in ['md5', 'sha1', 'sha256']) diff --git a/plugins/help.py b/plugins/help.py old mode 100644 new mode 100755 index d2b3d2e..cbba8c7 --- a/plugins/help.py +++ b/plugins/help.py @@ -1,11 +1,10 @@ import re - from util import hook -@hook.command("help", autohelp=False) -def help_command(inp, notice=None, conn=None, bot=None): - """help -- Gives a list of commands/help for a command.""" +@hook.command(autohelp=False) +def help(inp, say=None, notice=None, input=None, conn=None, bot=None): + "help -- Gives a list of commands/help for a command." funcs = {} disabled = bot.config.get('disabled_plugins', []) @@ -13,39 +12,37 @@ def help_command(inp, notice=None, conn=None, bot=None): for command, (func, args) in bot.commands.iteritems(): fn = re.match(r'^plugins.(.+).py$', func._filename) if fn.group(1).lower() not in disabled: - if command not in disabled_comm: - if func.__doc__ is not None: - if func in funcs: - if len(funcs[func]) < len(command): + if not args.get('adminonly', False) or\ + input.nick in bot.config["admins"] or\ + input.mask in bot.config["admins"]: + if command not in disabled_comm: + if func.__doc__ is not None: + if func in funcs: + if len(funcs[func]) < len(command): + funcs[func] = command + else: funcs[func] = command - else: - funcs[func] = command commands = dict((value, key) for key, value in funcs.iteritems()) if not inp: - out = [""] + out = ["", ""] well = [] for x in commands: well.append(x) well.sort() - count = 0 for x in well: - if len(out[count]) + len(str(x)) > 405: - count += 1 - out.append(str(x)) + if len(out[0]) + len(str(x)) > 405: + out[1] += " " + str(x) else: - out[count] += " " + str(x) + out[0] += " " + str(x) notice("Commands I recognise: " + out[0][1:]) - if len(out) > 1: - for x in out[1:]: - notice(x) - notice("For detailed help, do '%shelp <example>' where <example> " + if out[1]: + notice(out[1][1:]) + notice("For detailed help, do '%shelp <example>' where <example> "\ "is the name of the command you want help for." % conn.conf["command_prefix"]) else: if inp in commands: notice(conn.conf["command_prefix"] + commands[inp].__doc__) - else: - notice("Command {}{} not found".format(conn.conf["command_prefix"], inp)) diff --git a/plugins/ignore.py b/plugins/ignore.py old mode 100644 new mode 100755 index 3bafbfb..6432ac6 --- a/plugins/ignore.py +++ b/plugins/ignore.py @@ -1,72 +1,61 @@ import json -from fnmatch import fnmatch - from util import hook @hook.sieve -def ignore_sieve(bot, input, func, type, args): - """ blocks input from ignored channels/hosts """ +def ignoresieve(bot, input, func, type, args): + """ blocks input from ignored channels/nicks/hosts """ ignorelist = bot.config["plugins"]["ignore"]["ignored"] - mask = input.mask.lower() - # don't block input to event hooks if type == "event": return input - - if ignorelist: - for pattern in ignorelist: - if pattern.startswith("#") and pattern in ignorelist: - if input.command == "PRIVMSG" and input.lastparam[1:] == "unignore": - return input - else: - return None - elif fnmatch(mask, pattern): - if input.command == "PRIVMSG" and input.lastparam[1:] == "unignore": - return input - else: - return None - + if input.chan.lower() in ignorelist or\ + input.nick.lower() in ignorelist or\ + input.mask.lower() in ignorelist: + if input.command == "PRIVMSG" and input.lastparam[1:] == "unignore": + return input + else: + return None return input @hook.command(autohelp=False) def ignored(inp, notice=None, bot=None): - """ignored -- Lists ignored channels/users.""" + "ignored -- Lists ignored channels/nicks/hosts." ignorelist = bot.config["plugins"]["ignore"]["ignored"] if ignorelist: - notice("Ignored channels/users are: {}".format(", ".join(ignorelist))) + notice("Ignored channels/nicks/hosts are: %s" % ", ".join(ignorelist)) else: - notice("No masks are currently ignored.") + notice("No channels/nicks/hosts are currently ignored.") return -@hook.command(permissions=["ignore"]) +@hook.command(adminonly=True) def ignore(inp, notice=None, bot=None, config=None): - """ignore <channel|nick|host> -- Makes the bot ignore <channel|user>.""" + "ignore <channel|nick|host> -- Makes the bot ignore <channel|nick|host>." target = inp.lower() ignorelist = bot.config["plugins"]["ignore"]["ignored"] if target in ignorelist: - notice("{} is already ignored.".format(target)) + notice("%s is already ignored." % target) else: - notice("{} has been ignored.".format(target)) + notice("%s has been ignored." % target) ignorelist.append(target) ignorelist.sort() json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) return -@hook.command(permissions=["ignore"]) +@hook.command(adminonly=True) def unignore(inp, notice=None, bot=None, config=None): - """unignore <channel|user> -- Makes the bot listen to - <channel|user>.""" + "unignore <channel|nick|host> -- Makes the bot listen to"\ + " <channel|nick|host>." target = inp.lower() ignorelist = bot.config["plugins"]["ignore"]["ignored"] if target in ignorelist: - notice("{} has been unignored.".format(target)) + notice("%s has been unignored." % target) ignorelist.remove(target) ignorelist.sort() json.dump(bot.config, open('config', 'w'), sort_keys=True, indent=2) else: - notice("{} is not ignored.".format(target)) + notice("%s is not ignored." % target) return diff --git a/plugins/imdb.py b/plugins/imdb.py new file mode 100755 index 0000000..5d015fa --- /dev/null +++ b/plugins/imdb.py @@ -0,0 +1,34 @@ +# IMDb lookup plugin by Ghetto Wizard (2011). + +from util import hook, http +import re + +id_re = re.compile("tt\d+") + + +@hook.command +def imdb(inp): + "imdb <movie> -- Gets information about <movie> from IMDb." + + strip = inp.strip() + + if id_re.match(strip): + content = http.get_json("http://www.omdbapi.com/", i=strip) + else: + content = http.get_json("http://www.omdbapi.com/", t=strip) + + if content.get('Error', None) == 'Movie not found!': + return 'Movie not found!' + elif content['Response'] == 'True': + content['URL'] = 'http://www.imdb.com/title/%(imdbID)s' % content + + out = '\x02%(Title)s\x02 (%(Year)s) (%(Genre)s): %(Plot)s' + if content['Runtime'] != 'N/A': + out += ' \x02%(Runtime)s\x02.' + if content['imdbRating'] != 'N/A' and content['imdbVotes'] != 'N/A': + out += ' \x02%(imdbRating)s/10\x02 with \x02%(imdbVotes)s\x02' \ + ' votes.' + out += ' %(URL)s' + return out % content + else: + return 'Unknown error.' diff --git a/disabled_stuff/lastfm.py b/plugins/lastfm.py old mode 100644 new mode 100755 similarity index 79% rename from disabled_stuff/lastfm.py rename to plugins/lastfm.py index b928b1e..c075a65 --- a/disabled_stuff/lastfm.py +++ b/plugins/lastfm.py @@ -1,16 +1,14 @@ -from datetime import datetime - from util import hook, http, timesince - +from datetime import datetime api_url = "http://ws.audioscrobbler.com/2.0/?format=json" @hook.command('l', autohelp=False) @hook.command(autohelp=False) -def lastfm(inp, nick='', db=None, bot=None, notice=None): - """lastfm [user] [dontsave] -- Displays the now playing (or last played) - track of LastFM user [user].""" +def lastfm(inp, nick='', say=None, db=None, bot=None, notice=None): + "lastfm [user] [dontsave] -- Displays the now playing (or last played)" \ + " track of LastFM user [user]." api_key = bot.config.get("api_keys", {}).get("lastfm") if not api_key: return "error: no api key set" @@ -36,10 +34,10 @@ def lastfm(inp, nick='', db=None, bot=None, notice=None): api_key=api_key, user=user, limit=1) if 'error' in response: - return u"Error: {}.".format(response["message"]) + return "Error: %s." % response["message"] if not "track" in response["recenttracks"] or len(response["recenttracks"]["track"]) == 0: - return u'No recent tracks for user "{}" found.'.format(user) + return 'No recent tracks for user "%s" found.' % user tracks = response["recenttracks"]["track"] @@ -57,7 +55,7 @@ def lastfm(inp, nick='', db=None, bot=None, notice=None): # lets see how long ago they listened to it time_listened = datetime.fromtimestamp(int(track["date"]["uts"])) time_since = timesince.timesince(time_listened) - ending = ' ({} ago)'.format(time_since) + ending = ' (%s ago)' % time_since else: return "error: could not parse track listing" @@ -66,18 +64,18 @@ def lastfm(inp, nick='', db=None, bot=None, notice=None): album = track["album"]["#text"] artist = track["artist"]["#text"] - out = u'{} {} "{}"'.format(user, status, title) + out = '%s %s "%s"' % (user, status, title) if artist: - out += u" by \x02{}\x0f".format(artist) + out += " by \x02%s\x0f" % artist if album: - out += u" from the album \x02{}\x0f".format(album) + out += " from the album \x02%s\x0f" % album # append ending based on what type it was out += ending if inp and not dontsave: db.execute("insert or replace into lastfm(nick, acc) values (?,?)", - (nick.lower(), user)) + (nick.lower(), user)) db.commit() return out diff --git a/disabled_stuff/lmgtfy.py b/plugins/lmgtfy.py similarity index 53% rename from disabled_stuff/lmgtfy.py rename to plugins/lmgtfy.py index 768075f..fa4e763 100644 --- a/disabled_stuff/lmgtfy.py +++ b/plugins/lmgtfy.py @@ -3,10 +3,10 @@ from util import hook, web, http @hook.command('gfy') @hook.command -def lmgtfy(inp): - """lmgtfy [phrase] - Posts a google link for the specified phrase""" +def lmgtfy(inp, bot=None): + "lmgtfy [phrase] - Posts a google link for the specified phrase" - link = u"http://lmgtfy.com/?q={}".format(http.quote_plus(inp)) + link = "http://lmgtfy.com/?q=%s" % http.quote_plus(inp) try: return web.isgd(link) diff --git a/disabled_stuff/log.py b/plugins/log.py old mode 100644 new mode 100755 similarity index 82% rename from disabled_stuff/log.py rename to plugins/log.py index d72dc1a..1d37563 --- a/disabled_stuff/log.py +++ b/plugins/log.py @@ -27,11 +27,11 @@ formats = { } ctcp_formats = { - 'ACTION': '* %(nick)s %(ctcpmsg)s', - 'VERSION': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', - 'PING': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', - 'TIME': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', - 'FINGER': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s' +'ACTION': '* %(nick)s %(ctcpmsg)s', +'VERSION': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', +'PING': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', +'TIME': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', +'FINGER': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s' } irc_color_re = re.compile(r'(\x03(\d+,\d+|\d)|[\x0f\x02\x16\x1f])') @@ -39,7 +39,7 @@ irc_color_re = re.compile(r'(\x03(\d+,\d+|\d)|[\x0f\x02\x16\x1f])') def get_log_filename(dir, server, chan): return os.path.join(dir, 'log', gmtime('%Y'), server, chan, - (gmtime('%%s.%m-%d.log') % chan).lower()) + (gmtime('%%s.%m-%d.log') % chan).lower()) def gmtime(format): @@ -64,8 +64,8 @@ def beautify(input): ctcp += [''] args['ctcpcmd'], args['ctcpmsg'] = ctcp format = ctcp_formats.get(args['ctcpcmd'], - '%(nick)s [%(user)s@%(host)s] requested unknown CTCP ' - '%(ctcpcmd)s from %(chan)s: %(ctcpmsg)s') + '%(nick)s [%(user)s@%(host)s] requested unknown CTCP ' + '%(ctcpcmd)s from %(chan)s: %(ctcpmsg)s') return format % args diff --git a/disabled_stuff/minecraft_items.py b/plugins/mcitems.py old mode 100644 new mode 100755 similarity index 64% rename from disabled_stuff/minecraft_items.py rename to plugins/mcitems.py index f1e94f9..516a26b --- a/disabled_stuff/minecraft_items.py +++ b/plugins/mcitems.py @@ -1,10 +1,9 @@ """ plugin by _303 (?) """ -import re - from util import hook - +import re +import itertools pattern = re.compile(r'^(?P<count>\d+)x (?P<name>.+?): (?P<ingredients>.*)$') @@ -33,9 +32,9 @@ with open("plugins/data/recipes.txt") as f: if not match: continue recipelist.append(Recipe(line=line, - output=match.group("name").lower(), - ingredients=match.group("ingredients"), - count=match.group("count"))) + output=match.group("name").lower(), + ingredients=match.group("ingredients"), + count=match.group("count"))) ids = [] @@ -44,35 +43,36 @@ with open("plugins/data/itemids.txt") as f: if line.startswith("//"): continue parts = line.strip().split() - itemid = parts[0] + id = parts[0] name = " ".join(parts[1:]) - ids.append((itemid, name)) + ids.append((id, name)) @hook.command("mcid") @hook.command -def mcitem(inp, reply=None): - """mcitem <item/id> -- gets the id from an item or vice versa""" - inp = inp.lower().strip() +def mcitem(input, reply=None): + "mcitem <item/id> -- gets the id from an item or vice versa" + input = input.lower().strip() - if inp == "": + if input == "": reply("error: no input.") return results = [] - for item_id, item_name in ids: - if inp == item_id: - results = ["\x02[{}]\x02 {}".format(item_id, item_name)] + for id, name in ids: + if input == id: + results = ["\x02[%s]\x02 %s" % (id, name)] break - elif inp in item_name.lower(): - results.append("\x02[{}]\x02 {}".format(item_id, item_name)) + elif input in name.lower(): + results.append("\x02[%s]\x02 %s" % (id, name)) if not results: return "No matches found." if len(results) > 12: - reply("There are too many options, please narrow your search. ({})".format(str(len(results)))) + reply("There are too many options, please narrow your search. " \ + "(%s)" % len(results)) return out = ", ".join(results) @@ -82,18 +82,19 @@ def mcitem(inp, reply=None): @hook.command("mccraft") @hook.command -def mcrecipe(inp, reply=None): - """mcrecipe <item> -- gets the crafting recipe for an item""" - inp = inp.lower().strip() +def mcrecipe(input, reply=None): + "mcrecipe <item> -- gets the crafting recipe for an item" + input = input.lower().strip() results = [recipe.line for recipe in recipelist - if inp in recipe.output] + if input in recipe.output] if not results: return "No matches found." if len(results) > 3: - reply("There are too many options, please narrow your search. ({})".format(len(results))) + reply("There are too many options, please narrow your search. " \ + "(%s)" % len(results)) return for result in results: diff --git a/plugins/mctools.py b/plugins/mctools.py new file mode 100755 index 0000000..d4e8388 --- /dev/null +++ b/plugins/mctools.py @@ -0,0 +1,110 @@ +from util import hook, http +import socket +import json +import struct + + +def mcping_connect(host, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + sock.send('\xfe\x01') + response = sock.recv(1) + print response + + if response[0] != '\xff': + return "Server gave invalid response: " + repr(response) + length = struct.unpack('!h', sock.recv(2))[0] + + values = sock.recv(length * 2).decode('utf-16be') + + data = values.split(u'\x00') # try to decode data using new format + if len(data) == 1: + # failed to decode data, server is using old format + data = values.split(u'\xa7') + message = u"{} - {}/{} players".format(data[0], data[1], data[2]) + else: + # decoded data, server is using new format + message = u"{} - {} - {}/{} players".format(data[3], data[2], data[4], data[5]) + + sock.close() + return message + + except: + return "Error pinging " + host + ":" + str(port) +\ + ", is it up? Double-check your address!" + + +@hook.command(autohelp=False) +def mclogin(inp, bot=None): + "mclogin -- Checks the status of Minecraft's login servers." + username = bot.config.get("api_keys", {}).get("mc_user", None) + password = bot.config.get("api_keys", {}).get("mc_pass", None) + if password is None: + return "error: no login set" + + login = http.get("https://login.minecraft.net/", user=username, + password=password, version=13) + + if username.lower() in login.lower(): + return "Minecraft login servers appear to be online!" + else: + return "Minecraft login servers appear to be offline!" + + +@hook.command(autohelp=False) +def mcstatus(inp, say=None): + "mcstatus -- Checks the status of various Mojang (the creators of Minecraft) servers." + + try: + request = http.get("http://status.mojang.com/check") + except (http.URLError, http.HTTPError) as e: + return "Unable to get Minecraft server status: {}".format(e) + + # change the json from a list of dictionaies to a dictionary + data = json.loads(request.replace("}", "").replace("{", "").replace("]", "}").replace("[", "{")) + + out = [] + # use a loop so we don't have to update it if they add more servers + for server, status in data.items(): + if status == "green": + out.append("{} is \x033\x02online\x02\x03".format(server)) + else: + out.append("{} is \x034\x02offline\x02\x03".format(server)) + + return ", ".join(out) + "." + + +@hook.command("haspaid") +@hook.command +def mcpaid(inp): + "mcpaid <username> -- Checks if <username> has a premium Minecraft account." + + user = inp.strip() + + try: + status = http.get("http://www.minecraft.net/haspaid.jsp", user=user) + except (http.URLError, http.HTTPError) as e: + return "Unable to get user registration status: {}".format(e) + + if "true" in status: + return 'The account "{}" is a premium Minecraft account!'.format(inp) + else: + return 'The account "{}" is not a premium Minecraft account!'.format(inp) + + +@hook.command +def mcping(inp): + "mcping <server>[:port] - Ping a Minecraft server to check status." + inp = inp.strip().split(" ")[0] + + if ":" in inp: + host, port = inp.split(":", 1) + try: + port = int(port) + except: + return "error: invalid port!" + else: + host = inp + port = 25565 + return mcping_connect(host, port) diff --git a/plugins/mcwiki.py b/plugins/mcwiki.py new file mode 100755 index 0000000..fe9aaea --- /dev/null +++ b/plugins/mcwiki.py @@ -0,0 +1,29 @@ +from util import hook, http, text +import re + +api_url = "http://minecraftwiki.net/api.php?action=opensearch" +mc_url = "http://minecraftwiki.net/wiki/" + + +@hook.command +def mcwiki(inp): + "mcwiki <phrase> -- Gets the first paragraph of" \ + " the Minecraft Wiki article on <phrase>." + + j = http.get_json(api_url, search=inp) + + if not j[1]: + return "No results found." + article_name = j[1][0].replace(' ', '_').encode('utf8') + + url = mc_url + http.quote(article_name, '') + page = http.get_html(url) + + for p in page.xpath('//div[@class="mw-content-ltr"]/p'): + if p.text_content(): + summary = " ".join(p.text_content().splitlines()) + summary = re.sub("\[\d+\]", "", summary) + summary = text.truncate_str(summary, 250) + return "%s :: \x02%s\x02" % (summary, url) + + return "Unknown Error." diff --git a/disabled_stuff/metacritic.py b/plugins/metacritic.py old mode 100644 new mode 100755 similarity index 59% rename from disabled_stuff/metacritic.py rename to plugins/metacritic.py index 92d0933..e86809d --- a/disabled_stuff/metacritic.py +++ b/plugins/metacritic.py @@ -9,13 +9,14 @@ from util import hook, http @hook.command('mc') @hook.command def metacritic(inp): - """mc [all|movie|tv|album|x360|ps3|pc|gba|ds|3ds|wii|vita|wiiu|xone|ps4] <title> - Gets rating for <title> from metacritic on the specified medium.""" + "mc [all|movie|tv|album|x360|ps3|wii|pc|ds|3ds|vita] <title> -- Gets rating for <title> from metacritic on the specified medium." + + # if the results suck, it's metacritic's fault args = inp.strip() - game_platforms = ('x360', 'ps3', 'pc', 'gba', 'ds', '3ds', 'wii', - 'vita', 'wiiu', 'xone', 'ps4') + game_platforms = ('x360', 'ps3', 'pc', 'ds', 'wii', '3ds', 'gba', + 'psp', 'vita') all_platforms = game_platforms + ('all', 'movie', 'tv', 'album') @@ -34,14 +35,45 @@ def metacritic(inp): title_safe = http.quote_plus(title) - url = 'http://www.metacritic.com/search/{}/{}/results'.format(cat, title_safe) + url = 'http://www.metacritic.com/search/%s/%s/results' % (cat, title_safe) try: doc = http.get_html(url) except HTTPError: return 'error fetching results' + ''' result format: + -- game result, with score + -- subsequent results are the same structure, without first_result class + <li class="result first_result"> + <div class="result_type"> + <strong>Game</strong> + <span class="platform">WII</span> + </div> + <div class="result_wrap"> + <div class="basic_stats has_score"> + <div class="main_stats"> + <h3 class="product_title basic_stat">...</h3> + <div class="std_score"> + <div class="score_wrap"> + <span class="label">Metascore: </span> + <span class="data metascore score_favorable">87</span> + </div> + </div> + </div> + <div class="more_stats extended_stats">...</div> + </div> + </div> + </li> + + -- other platforms are the same basic layout + -- if it doesn't have a score, there is no div.basic_score + -- the <div class="result_type"> changes content for non-games: + <div class="result_type"><strong>Movie</strong></div> + ''' + # get the proper result element we want to pull data from + result = None if not doc.find_class('query_results'): @@ -86,7 +118,7 @@ def metacritic(inp): link = 'http://metacritic.com' + product_title.find('a').attrib['href'] try: - release = result.find_class('release_date')[0]. \ + release = result.find_class('release_date')[0].\ find_class('data')[0].text_content() # strip extra spaces out of the release date @@ -95,10 +127,11 @@ def metacritic(inp): release = None try: - score = result.find_class('metascore_w')[0].text_content() + score = result.find_class('metascore')[0].text_content() except IndexError: score = None - return '[{}] {} - \x02{}/100\x02, {} - {}'.format(plat.upper(), name, score or 'no score', - 'release: \x02%s\x02' % release if release else 'unreleased', - link) + return '[%s] %s - \x02%s/100\x02, %s - %s' % (plat.upper(), name, + score or 'no score', + 'release: \x02%s\x02' % release if release else 'unreleased', + link) diff --git a/plugins/core_misc.py b/plugins/misc.py old mode 100644 new mode 100755 similarity index 85% rename from plugins/core_misc.py rename to plugins/misc.py index c1fafca..4fb082f --- a/plugins/core_misc.py +++ b/plugins/misc.py @@ -4,7 +4,6 @@ import re from util import hook - socket.setdefaulttimeout(10) nick_re = re.compile(":(.+?)!") @@ -23,15 +22,11 @@ def invite(paraml, conn=None): def onjoin(paraml, conn=None, bot=None): nickserv_password = conn.conf.get('nickserv_password', '') nickserv_name = conn.conf.get('nickserv_name', 'nickserv') - nickserv_account_name = conn.conf.get('nickserv_user', '') - nickserv_command = conn.conf.get('nickserv_command', 'IDENTIFY') + nickserv_command = conn.conf.get('nickserv_command', 'IDENTIFY %s') if nickserv_password: if nickserv_password in bot.config['censored_strings']: bot.config['censored_strings'].remove(nickserv_password) - if nickserv_account_name: - conn.msg(nickserv_name, "{} {} {}".format(nickserv_command, nickserv_account_name, nickserv_password)) - else: - conn.msg(nickserv_name, "{} {}".format(nickserv_command, nickserv_password)) + conn.msg(nickserv_name, nickserv_command % nickserv_password) bot.config['censored_strings'].append(nickserv_password) time.sleep(1) diff --git a/plugins/munge.py b/plugins/munge.py new file mode 100755 index 0000000..3742115 --- /dev/null +++ b/plugins/munge.py @@ -0,0 +1,7 @@ +from util import hook, text + + +@hook.command +def munge(inp): + "munge <text> -- Munges up <text>." + return text.munge(inp) diff --git a/plugins/namegen.py b/plugins/namegen.py new file mode 100755 index 0000000..098bea7 --- /dev/null +++ b/plugins/namegen.py @@ -0,0 +1,93 @@ +# Plugin by Lukeroge +from util import hook +from util.text import get_text_list +import json, random, re, os + +TEMPLATE_RE = re.compile(r"\{(.+?)\}") +GEN_DIR = "./plugins/data/name_files/" + + +def get_generator(_json): + data = json.loads(_json) + return NameGenerator(data["name"], data["templates"], + data["default_templates"], data["parts"]) + + +class NameGenerator(object): + def __init__(self, name, templates, default_templates, parts): + self.name = name + self.templates = templates + self.default_templates = default_templates + self.parts = parts + + def generate_name(self, template=None): + """ + Generates one name using the specified templates. + If no templates are specified, use a random template from the default_templates list. + """ + name = self.templates[template or random.choice(self.default_templates)] + + # get a list of all name parts we need + name_parts = TEMPLATE_RE.findall(name) + + for name_part in name_parts: + part = random.choice(self.parts[name_part]) + name = name.replace("{%s}" % name_part, part) + + return name + + def generate_names(self, amount, template=None): + names = [] + for i in xrange(amount): + names.append(self.generate_name()) + return names + + def get_template(self, template): + return self.templates[template] + + +@hook.command(autohelp=False) +def namegen(inp, notice=None): + "namegen [generator] -- Generates some names using the chosen generator. " \ + "'namegen list' will display a list of all generators." + + # clean up the input + inp = inp.strip().lower() + + # get a list of available name generators + files = os.listdir(GEN_DIR) + all_modules = [] + for i in files: + if os.path.splitext(i)[1] == ".json": + all_modules.append(os.path.splitext(i)[0]) + all_modules.sort() + + # command to return a list of all available generators + if inp == "list": + message = "Available generators: " + message += get_text_list(all_modules, 'and') + notice(message) + return + + if inp: + selected_module = inp.split()[0] + else: + # make some generic fantasy names + selected_module = "fantasy" + + # check if the selected module is valid + if not selected_module in all_modules: + return "Invalid name generator :(" + + # load the name generator + with open(os.path.join(GEN_DIR, "{}.json".format(selected_module))) as f: + try: + generator = get_generator(f.read()) + except ValueError as error: + return "Unable to read name file: {}".format(error) + + # time to generate some names + name_list = generator.generate_names(10) + + # and finally return the final message :D + return "Some names to ponder: {}.".format(get_text_list(name_list, 'and')) diff --git a/plugins/op.py b/plugins/op.py old mode 100644 new mode 100755 index 695b74a..63e8668 --- a/plugins/op.py +++ b/plugins/op.py @@ -1,181 +1,110 @@ +# Plugin made by Lukeroge and neersighted from util import hook -def mode_cmd(mode, text, inp, chan, conn, notice): - """ generic mode setting function """ - split = inp.split(" ") - if split[0].startswith("#"): - channel = split[0] - target = split[1] - notice("Attempting to {} {} in {}...".format(text, target, channel)) - conn.send("MODE {} {} {}".format(channel, mode, target)) +@hook.command(adminonly=True) +def topic(inp, conn=None, chan=None, notice=None): + "topic [channel] <topic> -- Change the topic of a channel." + inp = inp.split(" ") + if inp[0][0] == "#": + out = "PRIVMSG %s :%s" % (inp[0], message) else: - channel = chan - target = split[0] - notice("Attempting to {} {} in {}...".format(text, target, channel)) - conn.send("MODE {} {} {}".format(channel, mode, target)) - - -def mode_cmd_no_target(mode, text, inp, chan, conn, notice): - """ generic mode setting function without a target""" - split = inp.split(" ") - if split[0].startswith("#"): - channel = split[0] - notice("Attempting to {} {}...".format(text, channel)) - conn.send("MODE {} {}".format(channel, mode)) - else: - channel = chan - notice("Attempting to {} {}...".format(text, channel)) - conn.send("MODE {} {}".format(channel, mode)) - - -@hook.command(permissions=["op_ban", "op"]) -def ban(inp, conn=None, chan=None, notice=None): - """ban [channel] <user> -- Makes the bot ban <user> in [channel]. - If [channel] is blank the bot will ban <user> in - the channel the command was used in.""" - mode_cmd("+b", "ban", inp, chan, conn, notice) - - -@hook.command(permissions=["op_ban", "op"]) -def unban(inp, conn=None, chan=None, notice=None): - """unban [channel] <user> -- Makes the bot unban <user> in [channel]. - If [channel] is blank the bot will unban <user> in - the channel the command was used in.""" - mode_cmd("-b", "unban", inp, chan, conn, notice) - - -@hook.command(permissions=["op_quiet", "op"]) -def quiet(inp, conn=None, chan=None, notice=None): - """quiet [channel] <user> -- Makes the bot quiet <user> in [channel]. - If [channel] is blank the bot will quiet <user> in - the channel the command was used in.""" - mode_cmd("+q", "quiet", inp, chan, conn, notice) - - -@hook.command(permissions=["op_quiet", "op"]) -def unquiet(inp, conn=None, chan=None, notice=None): - """unquiet [channel] <user> -- Makes the bot unquiet <user> in [channel]. - If [channel] is blank the bot will unquiet <user> in - the channel the command was used in.""" - mode_cmd("-q", "unquiet", inp, chan, conn, notice) - - -@hook.command(permissions=["op_voice", "op"]) -def voice(inp, conn=None, chan=None, notice=None): - """voice [channel] <user> -- Makes the bot voice <user> in [channel]. - If [channel] is blank the bot will voice <user> in - the channel the command was used in.""" - mode_cmd("+v", "voice", inp, chan, conn, notice) - - -@hook.command(permissions=["op_voice", "op"]) -def devoice(inp, conn=None, chan=None, notice=None): - """devoice [channel] <user> -- Makes the bot devoice <user> in [channel]. - If [channel] is blank the bot will devoice <user> in - the channel the command was used in.""" - mode_cmd("-v", "devoice", inp, chan, conn, notice) - - -@hook.command(permissions=["op_op", "op"]) -def op(inp, conn=None, chan=None, notice=None): - """op [channel] <user> -- Makes the bot op <user> in [channel]. - If [channel] is blank the bot will op <user> in - the channel the command was used in.""" - mode_cmd("+o", "op", inp, chan, conn, notice) - - -@hook.command(permissions=["op_op", "op"]) -def deop(inp, conn=None, chan=None, notice=None): - """deop [channel] <user> -- Makes the bot deop <user> in [channel]. - If [channel] is blank the bot will deop <user> in - the channel the command was used in.""" - mode_cmd("-o", "deop", inp, chan, conn, notice) - - -@hook.command(permissions=["op_topic", "op"]) -def topic(inp, conn=None, chan=None): - """topic [channel] <topic> -- Change the topic of a channel.""" - split = inp.split(" ") - if split[0].startswith("#"): - message = " ".join(split[1:]) - chan = split[0] - out = "TOPIC {} :{}".format(chan, message) - else: - message = " ".join(split) - out = "TOPIC {} :{}".format(chan, message) + out = "TOPIC %s :%s" % (chan, message) conn.send(out) -@hook.command(permissions=["op_kick", "op"]) +@hook.command(adminonly=True) def kick(inp, chan=None, conn=None, notice=None): - """kick [channel] <user> [reason] -- Makes the bot kick <user> in [channel] - If [channel] is blank the bot will kick the <user> in - the channel the command was used in.""" - split = inp.split(" ") - - if split[0].startswith("#"): - channel = split[0] - target = split[1] - if len(split) > 2: - reason = " ".join(split[2:]) - out = "KICK {} {}: {}".format(channel, target, reason) - else: - out = "KICK {} {}".format(channel, target) + "kick [channel] <user> [reason] -- Makes the bot kick <user> in [channel] "\ + "If [channel] is blank the bot will kick the <user> in "\ + "the channel the command was used in." + inp = inp.split(" ") + if inp[0][0] == "#": + chan = inp[0] + user = inp[1] + out = "KICK %s %s" % (chan, user) + if len(inp) > 2: + reason = "" + for x in inp[2:]: + reason = reason + x + " " + reason = reason[:-1] + out = out + " :" + reason else: - channel = chan - target = split[0] - if len(split) > 1: - reason = " ".join(split[1:]) - out = "KICK {} {} :{}".format(channel, target, reason) - else: - out = "KICK {} {}".format(channel, target) + user = inp[0] + out = "KICK %s %s" % (chan, user) + if len(inp) > 1: + reason = "" + for x in inp[1:]: + reason = reason + x + " " + reason = reason[:-1] + out = out + " :" + reason - notice("Attempting to kick {} from {}...".format(target, channel)) + notice("Attempting to kick %s from %s..." % (user, chan)) conn.send(out) -@hook.command(permissions=["op_rem", "op"]) -def remove(inp, chan=None, conn=None): - """remove [channel] [user] -- Force a user to part from a channel.""" - split = inp.split(" ") - if split[0].startswith("#"): - message = " ".join(split[1:]) - chan = split[0] - out = "REMOVE {} :{}".format(chan, message) +@hook.command(adminonly=True) +def ban(inp, conn=None, chan=None, notice=None): + "ban [channel] <user> -- Makes the bot ban <user> in [channel]. "\ + "If [channel] is blank the bot will ban <user> in "\ + "the channel the command was used in." + inp = inp.split(" ") + if inp[0][0] == "#": + chan = inp[0] + user = inp[1] + out = "MODE %s +b %s" % (chan, user) else: - message = " ".join(split) - out = "REMOVE {} :{}".format(chan, message) + user = inp[0] + out = "MODE %s +b %s" % (chan, user) + notice("Attempting to ban %s from %s..." % (user, chan)) conn.send(out) -@hook.command(permissions=["op_mute", "op"], autohelp=False) -def mute(inp, conn=None, chan=None, notice=None): - """mute [channel] -- Makes the bot mute a channel.. - If [channel] is blank the bot will mute - the channel the command was used in.""" - mode_cmd_no_target("+m", "mute", inp, chan, conn, notice) +@hook.command(adminonly=True) +def unban(inp, conn=None, chan=None, notice=None): + "unban [channel] <user> -- Makes the bot unban <user> in [channel]. "\ + "If [channel] is blank the bot will unban <user> in "\ + "the channel the command was used in." + inp = inp.split(" ") + if inp[0][0] == "#": + chan = inp[0] + user = inp[1] + out = "MODE %s -b %s" % (chan, user) + else: + user = inp[0] + out = "MODE %s -b %s" % (chan, user) + notice("Attempting to unban %s from %s..." % (user, chan)) + conn.send(out) -@hook.command(permissions=["op_mute", "op"], autohelp=False) -def unmute(inp, conn=None, chan=None, notice=None): - """mute [channel] -- Makes the bot mute a channel.. - If [channel] is blank the bot will mute - the channel the command was used in.""" - mode_cmd_no_target("-m", "unmute", inp, chan, conn, notice) +@hook.command(adminonly=True) +def kickban(inp, chan=None, conn=None, notice=None): + "kickban [channel] <user> [reason] -- Makes the bot kickban <user> in [channel] "\ + "If [channel] is blank the bot will kickban the <user> in "\ + "the channel the command was used in." + inp = inp.split(" ") + if inp[0][0] == "#": + chan = inp[0] + user = inp[1] + out1 = "MODE %s +b %s" % (chan, user) + out2 = "KICK %s %s" % (chan, user) + if len(inp) > 2: + reason = "" + for x in inp[2:]: + reason = reason + x + " " + reason = reason[:-1] + out = out + " :" + reason + else: + user = inp[0] + out1 = "MODE %s +b %s" % (chan, user) + out2 = "KICK %s %s" % (chan, user) + if len(inp) > 1: + reason = "" + for x in inp[1:]: + reason = reason + x + " " + reason = reason[:-1] + out = out + " :" + reason - -@hook.command(permissions=["op_lock", "op"], autohelp=False) -def lock(inp, conn=None, chan=None, notice=None): - """lock [channel] -- Makes the bot lock a channel. - If [channel] is blank the bot will mute - the channel the command was used in.""" - mode_cmd_no_target("+i", "lock", inp, chan, conn, notice) - - -@hook.command(permissions=["op_lock", "op"], autohelp=False) -def unlock(inp, conn=None, chan=None, notice=None): - """unlock [channel] -- Makes the bot unlock a channel.. - If [channel] is blank the bot will mute - the channel the command was used in.""" - mode_cmd_no_target("-i", "unlock", inp, chan, conn, notice) + notice("Attempting to kickban %s from %s..." % (user, chan)) + conn.send(out1) + conn.send(out2) diff --git a/plugins/parsers.py b/plugins/parsers.py new file mode 100755 index 0000000..ae9f20b --- /dev/null +++ b/plugins/parsers.py @@ -0,0 +1,19 @@ +from util import hook, http +import re + +reddit_re = (r'.*((www\.)?reddit\.com/r[^ ]+)', re.I) + +@hook.regex(*reddit_re) +def reddit_url(match): + + thread = http.get_html(match.group(0)) + + title = thread.xpath('//title/text()')[0] + upvotes = thread.xpath("//span[@class='upvotes']/span[@class='number']/text()")[0] + downvotes = thread.xpath("//span[@class='downvotes']/span[@class='number']/text()")[0] + author = thread.xpath("//div[@id='siteTable']//a[contains(@class,'author')]/text()")[0] + timeago = thread.xpath("//div[@id='siteTable']//p[@class='tagline']/time/text()")[0] + comments = thread.xpath("//div[@id='siteTable']//a[@class='comments']/text()")[0] + + return '\x02%s\x02 - posted by \x02%s\x02 %s ago - %s upvotes, %s downvotes - %s' % ( + title, author, timeago, upvotes, downvotes, comments) diff --git a/disabled_stuff/password.py b/plugins/password.py old mode 100644 new mode 100755 similarity index 66% rename from disabled_stuff/password.py rename to plugins/password.py index 34a379b..e19a6e0 --- a/disabled_stuff/password.py +++ b/plugins/password.py @@ -1,15 +1,12 @@ -# TODO: Add some kind of pronounceable password generation -# TODO: Improve randomness +# based on password generation code by TheNoodle +from util import hook import string import random -from util import hook - @hook.command def password(inp, notice=None): - """password <length> [types] -- Generates a password of <length> (default 10). - [types] can include 'alpha', 'no caps', 'numeric', 'symbols' or any combination of the inp, eg. 'numbers symbols'""" + "password <length> [types] -- Generates a password of <length> (default 10). [types] can include 'alpha', 'no caps', 'numeric', 'symbols' or any combination of the inp, eg. 'numbers symbols'" okay = [] # find the length needed for the password @@ -33,18 +30,17 @@ def password(inp, notice=None): # add symbols if "symbol" in inp: - sym = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '_', '+', '[', ']', '{', '}', '\\', '|', ';', - ':', "'", '.', '>', ',', '<', '/', '?', '`', '~', '"'] + sym = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '_', '+', '[', ']', '{', '}', '\\', '|', ';', ':', "'", '.', '>', ',', '<', '/', '?', '`', '~', '"'] okay += okay + sym # defaults to lowercase alpha password if the okay list is empty if not okay: okay = okay + list(string.ascii_lowercase) - pw = "" + password = "" # generates password for x in range(length): - pw = pw + random.choice(okay) + password = password + random.choice(okay) - notice(pw) + notice(password) diff --git a/plugins/ping.py b/plugins/ping.py old mode 100644 new mode 100755 index 66286df..9692217 --- a/plugins/ping.py +++ b/plugins/ping.py @@ -1,26 +1,23 @@ # ping plugin by neersighted +from util import hook import subprocess import re import os -from util import hook - - ping_regex = re.compile(r"(\d+.\d+)/(\d+.\d+)/(\d+.\d+)/(\d+.\d+)") @hook.command def ping(inp, reply=None): - """ping <host> [count] -- Pings <host> [count] times.""" + "ping <host> [count] -- Pings <host> [count] times." if os.name == "nt": return "Sorry, this command is not supported on Windows systems." - # TODO: Rewrite this entire command to work on Windows, somehow args = inp.split(' ') host = args[0] - # check for a second argument and set the ping count + # check for a seccond argument and set the ping count if len(args) > 1: count = int(args[1]) if count > 20: @@ -30,11 +27,9 @@ def ping(inp, reply=None): count = str(count) - # I suck at regex, but this is causing issues, and I'm just going to remove it - # I assume it's no longer needed with the way we run the process - # host = re.sub(r'([^\s\w\.])+', '', host) + host = re.sub(r'([^\s\w\.])+', '', host) - reply("Attempting to ping {} {} times...".format(host, count)) + reply("Attempting to ping %s %s times..." % (host, count)) pingcmd = subprocess.check_output(["ping", "-c", count, host]) if "request timed out" in pingcmd or "unknown host" in pingcmd: @@ -42,4 +37,4 @@ def ping(inp, reply=None): else: m = re.search(ping_regex, pingcmd) return "min: %sms, max: %sms, average: %sms, range: %sms, count: %s" \ - % (m.group(1), m.group(3), m.group(2), m.group(4), count) + % (m.group(1), m.group(3), m.group(2), m.group(4), count) diff --git a/plugins/potato.py b/plugins/potato.py new file mode 100755 index 0000000..f6afaf4 --- /dev/null +++ b/plugins/potato.py @@ -0,0 +1,22 @@ +from util import hook +import re +import random + +potatoes = ['AC Belmont', 'AC Blue Pride', 'AC Brador', 'AC Chaleur', 'AC Domino', 'AC Dubuc', 'AC Glacier Chip', 'AC Maple Gold', 'AC Novachip', 'AC Peregrine Red', 'AC Ptarmigan', 'AC Red Island', 'AC Saguenor', 'AC Stampede Russet', 'AC Sunbury', 'Abeille', 'Abnaki', 'Acadia', 'Acadia Russet', 'Accent', 'Adirondack Blue', 'Adirondack Red', 'Adora', 'Agria', 'All Blue', 'All Red', 'Alpha', 'Alta Russet', 'Alturas Russet', 'Amandine', 'Amisk', 'Andover', 'Anoka', 'Anson', 'Aquilon', 'Arran Consul', 'Asterix', 'Atlantic', 'Austrian Crescent', 'Avalanche', 'Banana', 'Bannock Russet', 'Batoche', 'BeRus', 'Belle De Fonteney', 'Belleisle', 'Bintje', 'Blossom', 'Blue Christie', 'Blue Mac', 'Brigus', 'Brise du Nord', 'Butte', 'Butterfinger', 'Caesar', 'CalWhite', 'CalRed', 'Caribe', 'Carlingford', 'Carlton', 'Carola', 'Cascade', 'Castile', 'Centennial Russet', 'Century Russet', 'Charlotte', 'Cherie', 'Cherokee', 'Cherry Red', 'Chieftain', 'Chipeta', 'Coastal Russet', 'Colorado Rose', 'Concurrent', 'Conestoga', 'Cowhorn', 'Crestone Russet', 'Crispin', 'Cupids', 'Daisy Gold', 'Dakota Pearl', 'Defender', 'Delikat', 'Denali', 'Desiree', 'Divina', 'Dundrod', 'Durango Red', 'Early Rose', 'Elba', 'Envol', 'Epicure', 'Eramosa', 'Estima', 'Eva', 'Fabula', 'Fambo', 'Fremont Russet', 'French Fingerling', 'Frontier Russet', 'Fundy', 'Garnet Chile', 'Gem Russet', 'GemStar Russet', 'Gemchip', 'German Butterball', 'Gigant', 'Goldrush', 'Granola', 'Green Mountain', 'Haida', 'Hertha', 'Hilite Russet', 'Huckleberry', 'Hunter', 'Huron', 'IdaRose', 'Innovator', 'Irish Cobbler', 'Island Sunshine', 'Ivory Crisp', 'Jacqueline Lee', 'Jemseg', 'Kanona', 'Katahdin', 'Kennebec', "Kerr's Pink", 'Keswick', 'Keuka Gold', 'Keystone Russet', 'King Edward VII', 'Kipfel', 'Klamath Russet', 'Krantz', 'LaRatte', 'Lady Rosetta', 'Latona', 'Lemhi Russet', 'Liberator', 'Lili', 'MaineChip', 'Marfona', 'Maris Bard', 'Maris Piper', 'Matilda', 'Mazama', 'McIntyre', 'Michigan Purple', 'Millenium Russet', 'Mirton Pearl', 'Modoc', 'Mondial', 'Monona', 'Morene', 'Morning Gold', 'Mouraska', 'Navan', 'Nicola', 'Nipigon', 'Niska', 'Nooksack', 'NorValley', 'Norchip', 'Nordonna', 'Norgold Russet', 'Norking Russet', 'Norland', 'Norwis', 'Obelix', 'Ozette', 'Peanut', 'Penta', 'Peribonka', 'Peruvian Purple', 'Pike', 'Pink Pearl', 'Prospect', 'Pungo', 'Purple Majesty', 'Purple Viking', 'Ranger Russet', 'Reba', 'Red Cloud', 'Red Gold', 'Red La Soda', 'Red Pontiac', 'Red Ruby', 'Red Thumb', 'Redsen', 'Rocket', 'Rose Finn Apple', 'Rose Gold', 'Roselys', 'Rote Erstling', 'Ruby Crescent', 'Russet Burbank', 'Russet Legend', 'Russet Norkotah', 'Russet Nugget', 'Russian Banana', 'Saginaw Gold', 'Sangre', 'Sant�', 'Satina', 'Saxon', 'Sebago', 'Shepody', 'Sierra', 'Silverton Russet', 'Simcoe', 'Snowden', 'Spunta', "St. John's", 'Summit Russet', 'Sunrise', 'Superior', 'Symfonia', 'Tolaas', 'Trent', 'True Blue', 'Ulla', 'Umatilla Russet', 'Valisa', 'Van Gogh', 'Viking', 'Wallowa Russet', 'Warba', 'Western Russet', 'White Rose', 'Willamette', 'Winema', 'Yellow Finn', 'Yukon Gold'] + + +@hook.command +def potato(inp, me=None, input=None): + "potato <user> - Makes <user> a tasty little potato." + inp = inp.strip() + + if not re.match("^[A-Za-z0-9_|.-\]\[]*$", inp.lower()): + return "I cant make a tasty potato for that user!" + + potato_type = random.choice(potatoes) + size = random.choice(['small', 'little', 'mid-sized', 'medium-sized', 'large', 'gigantic']) + flavor = random.choice(['tasty', 'delectable', 'delicious', 'yummy', 'toothsome', 'scrumptious', 'luscious']) + method = random.choice(['bakes', 'fries', 'boils', 'roasts']) + side_dish = random.choice(['side salad', 'dollop of sour cream', 'piece of chicken', 'bowl of shredded bacon']) + + me("%s a %s %s %s potato for %s and serves it with a small %s!" % (method, flavor, size, potato_type, inp, side_dish)) diff --git a/plugins/pyexec.py b/plugins/pyexec.py new file mode 100755 index 0000000..41c8016 --- /dev/null +++ b/plugins/pyexec.py @@ -0,0 +1,10 @@ +import re + +from util import hook, http +from util.execute import eval_py + +@hook.command +def python(inp): + "python <prog> -- Executes <prog> as Python code." + + return eval_py(inp) diff --git a/disabled_stuff/quote.py b/plugins/quote.py old mode 100644 new mode 100755 similarity index 90% rename from disabled_stuff/quote.py rename to plugins/quote.py index 6beefc5..7cb7cb1 --- a/disabled_stuff/quote.py +++ b/plugins/quote.py @@ -8,15 +8,15 @@ from util import hook def format_quote(q, num, n_quotes): """Returns a formatted string of a quote""" ctime, nick, msg = q - return "[{}/{}] <{}> {}".format(num, n_quotes, - nick, msg) + return "[%d/%d] <%s> %s" % (num, n_quotes, + nick, msg) def create_table_if_not_exists(db): """Creates an empty quote table if one does not already exist""" db.execute("create table if not exists quote" - "(chan, nick, add_nick, msg, time real, deleted default 0, " - "primary key (chan, nick, msg))") + "(chan, nick, add_nick, msg, time real, deleted default 0, " + "primary key (chan, nick, msg))") db.commit() @@ -45,11 +45,12 @@ def get_quote_num(num, count, name): if num: # Make sure num is a number if it isn't false num = int(num) if count == 0: # Error on no quotes - raise Exception("No quotes found for {}.".format(name)) + raise Exception("No quotes found for %s." % name) if num and num < 0: # Count back if possible num = count + num + 1 if num + count > -1 else count + 1 if num and num > count: # If there are not enough quotes, raise an error - raise Exception("I only have {} quote{} for {}.".format(count, ('s', '')[count == 1], name)) + raise Exception("I only have %d quote%s for %s."\ + % (count, ('s', '')[count == 1], name)) if num and num == 0: # If the number is zero, set it to one num = 1 if not num: # If a number is not given, select a random one @@ -123,8 +124,8 @@ def get_quote_by_chan(db, chan, num=False): @hook.command('q') @hook.command def quote(inp, nick='', chan='', db=None, notice=None): - """quote [#chan] [nick] [#n]/.quote add <nick> <msg> - Gets random or [#n]th quote by <nick> or from <#chan>/adds quote.""" + "quote [#chan] [nick] [#n]/.quote add <nick> <msg> -- Gets " \ + "random or [#n]th quote by <nick> or from <#chan>/adds quote." create_table_if_not_exists(db) add = re.match(r"add[^\w@]+(\S+?)>?\s+(.*)", inp, re.I) diff --git a/disabled_stuff/rottentomatoes.py b/plugins/rottentomatoes.py similarity index 76% rename from disabled_stuff/rottentomatoes.py rename to plugins/rottentomatoes.py index 2d7af38..fe86059 100644 --- a/disabled_stuff/rottentomatoes.py +++ b/plugins/rottentomatoes.py @@ -7,7 +7,7 @@ movie_reviews_url = api_root + 'movies/%s/reviews.json' @hook.command('rt') def rottentomatoes(inp, bot=None): - """rt <title> -- gets ratings for <title> from Rotten Tomatoes""" + 'rt <title> -- gets ratings for <title> from Rotten Tomatoes' api_key = bot.config.get("api_keys", {}).get("rottentomatoes", None) if not api_key: @@ -21,7 +21,7 @@ def rottentomatoes(inp, bot=None): movie = results['movies'][0] title = movie['title'] - movie_id = movie['id'] + id = movie['id'] critics_score = movie['ratings']['critics_score'] audience_score = movie['ratings']['audience_score'] url = movie['links']['alternate'] @@ -29,11 +29,11 @@ def rottentomatoes(inp, bot=None): if critics_score == -1: return - reviews = http.get_json(movie_reviews_url % movie_id, apikey=api_key, review_type='all') + reviews = http.get_json(movie_reviews_url % id, apikey=api_key, review_type='all') review_count = reviews['total'] fresh = critics_score * review_count / 100 rotten = review_count - fresh return u"{} - Critics Rating: \x02{}%\x02 ({} liked, {} disliked) " \ - "Audience Rating: \x02{}%\x02 - {}".format(title, critics_score, fresh, rotten, audience_score, url) + "Audience Rating: \x02{}%\x02 - {}".format(title, critics_score, fresh, rotten, audience_score, url) \ No newline at end of file diff --git a/disabled_stuff/rss.py b/plugins/rss.py similarity index 77% rename from disabled_stuff/rss.py rename to plugins/rss.py index f7ed1c4..520fdf6 100644 --- a/disabled_stuff/rss.py +++ b/plugins/rss.py @@ -3,8 +3,8 @@ from util import hook, http, web, text @hook.command("feed") @hook.command -def rss(inp, message=None): - """rss <feed> -- Gets the first three items from the RSS feed <feed>.""" +def rss(inp, say=None): + "rss <feed> -- Gets the first three items from the RSS feed <feed>." limit = 3 # preset news feeds @@ -31,10 +31,10 @@ def rss(inp, message=None): link = web.isgd(row["link"]) except (web.ShortenError, http.HTTPError, http.URLError): link = row["link"] - message(u"{} - {}".format(title, link)) + say(u"{} - {}".format(title, link)) @hook.command(autohelp=False) -def rb(inp, message=None): - """rb -- Shows the latest Craftbukkit recommended build""" - rss("bukkit", message) +def rb(inp, say=None): + "rb -- Shows the latest Craftbukkit recommended build" + rss("bukkit", say) diff --git a/plugins/seen.py b/plugins/seen.py new file mode 100755 index 0000000..57194ac --- /dev/null +++ b/plugins/seen.py @@ -0,0 +1,58 @@ +" seen.py: written by sklnd in about two beers July 2009" + +import time +import re + +from util import hook, timesince + +db_ready = False + + +def db_init(db): + "check to see that our db has the the seen table and return a connection." + db.execute("create table if not exists seen_user(name, time, quote, chan, host, " + "primary key(name, chan))") + db.commit() + db_ready = True + + +@hook.singlethread +@hook.event('PRIVMSG', ignorebots=False) +def seen_sieve(paraml, input=None, db=None, bot=None): + if not db_ready: + db_init(db) + # keep private messages private + if input.chan[:1] == "#": + db.execute("insert or replace into seen_user(name, time, quote, chan, host)" + "values(?,?,?,?,?)", (input.nick.lower(), time.time(), input.msg, + input.chan, input.mask)) + db.commit() + + +@hook.command +def seen(inp, nick='', chan='', db=None, input=None): + "seen <nick> -- Tell when a nickname was last in active in one of this bot's channels." + + if input.conn.nick.lower() == inp.lower(): + return "You need to get your eyes checked." + + if inp.lower() == nick.lower(): + return "Have you looked in a mirror lately?" + + if not re.match("^[A-Za-z0-9_|.-\]\[]*$", inp.lower()): + return "I can't look up that name, its impossible to use!" + + if not db_ready: + db_init(db) + + last_seen = db.execute("select name, time, quote from seen_user where name" + " like ? and chan = ?", (inp, chan)).fetchone() + + if last_seen: + reltime = timesince.timesince(last_seen[1]) + if last_seen[0] != inp.lower(): # for glob matching + inp = last_seen[0] + return '%s was last seen %s ago saying: %s' % \ + (inp, reltime, last_seen[2]) + else: + return "I've never seen %s talking in this channel." % inp diff --git a/disabled_stuff/shorten.py b/plugins/shorten.py old mode 100644 new mode 100755 similarity index 71% rename from disabled_stuff/shorten.py rename to plugins/shorten.py index 39d993b..bb668c2 --- a/disabled_stuff/shorten.py +++ b/plugins/shorten.py @@ -3,7 +3,7 @@ from util import hook, http, web @hook.command def shorten(inp): - """shorten <url> - Makes an is.gd shortlink to the url provided.""" + "shorten <url> - Makes an is.gd shortlink to the url provided." try: return web.isgd(inp) diff --git a/plugins/sieve.py b/plugins/sieve.py new file mode 100755 index 0000000..569414e --- /dev/null +++ b/plugins/sieve.py @@ -0,0 +1,39 @@ +import re + +from util import hook + + +@hook.sieve +def sieve_suite(bot, input, func, kind, args): + if input.command == 'PRIVMSG' and\ + input.nick.endswith('bot') and args.get('ignorebots', True): + return None + + if kind == "command": + if input.trigger in bot.config.get('disabled_commands', []): + return None + + fn = re.match(r'^plugins.(.+).py$', func._filename) + disabled = bot.config.get('disabled_plugins', []) + if fn and fn.group(1).lower() in disabled: + return None + + acl = bot.config.get('acls', {}).get(func.__name__) + if acl: + if 'deny-except' in acl: + allowed_channels = map(unicode.lower, acl['deny-except']) + if input.chan.lower() not in allowed_channels: + return None + if 'allow-except' in acl: + denied_channels = map(unicode.lower, acl['allow-except']) + if input.chan.lower() in denied_channels: + return None + + if args.get('adminonly', False): + admins = bot.config.get('admins', []) + + if input.nick not in admins and input.mask not in admins: + input.notice("Sorry, you are not allowed to use this command.") + return None + + return input diff --git a/disabled_stuff/slogan.py b/plugins/slogan.py old mode 100644 new mode 100755 similarity index 87% rename from disabled_stuff/slogan.py rename to plugins/slogan.py index 279c41d..dadcfba --- a/disabled_stuff/slogan.py +++ b/plugins/slogan.py @@ -1,6 +1,5 @@ -import random - from util import hook, text +import random with open("plugins/data/slogans.txt") as f: @@ -10,7 +9,7 @@ with open("plugins/data/slogans.txt") as f: @hook.command def slogan(inp): - """slogan <word> -- Makes a slogan for <word>.""" + "slogan <word> -- Makes a slogan for <word>." out = random.choice(slogans) if inp.lower() and out.startswith("<text>"): inp = text.capitalize_first(inp) diff --git a/disabled_stuff/snopes.py b/plugins/snopes.py old mode 100644 new mode 100755 similarity index 80% rename from disabled_stuff/snopes.py rename to plugins/snopes.py index 9850a68..f0fe7bd --- a/disabled_stuff/snopes.py +++ b/plugins/snopes.py @@ -8,7 +8,7 @@ search_url = "http://search.atomz.com/search/?sp_a=00062d45-sp00000000" @hook.command def snopes(inp): - """snopes <topic> -- Searches snopes for an urban legend about <topic>.""" + "snopes <topic> -- Searches snopes for an urban legend about <topic>." search_page = http.get_html(search_url, sp_q=inp, sp_c="1") result_urls = search_page.xpath("//a[@target='_self']/@href") @@ -26,9 +26,9 @@ def snopes(inp): status = status.group(0).strip() else: # new-style statuses status = "Status: %s." % re.search(r"FALSE|TRUE|MIXTURE|UNDETERMINED", - snopes_text).group(0).title() + snopes_text).group(0).title() claim = re.sub(r"[\s\xa0]+", " ", claim) # compress whitespace status = re.sub(r"[\s\xa0]+", " ", status) - return "{} {} {}".format(claim, status, result_urls[0]) + return "%s %s %s" % (claim, status, result_urls[0]) diff --git a/plugins/snowy-evening.py b/plugins/snowy-evening.py new file mode 100644 index 0000000..333c23e --- /dev/null +++ b/plugins/snowy-evening.py @@ -0,0 +1,25 @@ +from util import hook, http, text +import json, urllib2 +import re + +snowy_re = (r'(?:snowy-evening.com/)' + '([-_a-zA-Z0-9/]+)', re.I) + + +@hook.regex(*snowy_re) +def snowy(match, nick="", reply=""): + owner, name, id, blankSpace = match.group(1).split("/") + soup = http.get_soup("https://snowy-evening.com/%s/%s/%s" % (owner, name, id)) + + header = soup.find('section', {'class': 'container'}).header.h2(text=True)[1].split(" ", 1)[1] + reply("Project {} by {}: Issue #{}: {}".format(name, owner, id, text.truncate_str(header, 150))) + stats = soup.find('ul', {'id':'stats'}).find_all('strong') + if len(stats) == 6: + priority, number, type, status, age, assignee = [i.contents[0].lower() for i in stats] + else: + priority, number, type, status, age = [i.contents[0].lower() for i in stats] + + if status == "assigned": + reply("This issue has a priority of \x02{}\x02. It's age is \x02{}\x02 and it is a \x02{}\x02. This issue is \x02{}\x02 to \x02{}\x02.".format(priority, age, type, status, assignee)) + else: + reply("This issue has a priority of \x02{}\x02. It's age is \x02{}\x02 and it is a \x02{}\x02. It's status is \x02{}\x02.".format(priority, age, type, status)) \ No newline at end of file diff --git a/plugins/spellcheck.py b/plugins/spellcheck.py new file mode 100755 index 0000000..5f6b244 --- /dev/null +++ b/plugins/spellcheck.py @@ -0,0 +1,27 @@ +import enchant +from util import hook + +locale = "en_US" + + +@hook.command +def spell(inp): + "spell <word> -- Check spelling of <word>." + + if ' ' in inp: + return "This command only supports one word at a time." + + if not enchant.dict_exists(locale): + return "Could not find dictionary: {}".format(locale) + + dictionary = enchant.Dict(locale) + is_correct = dictionary.check(inp) + suggestions = dictionary.suggest(inp) + s_string = ', '.join(suggestions[:10]) + + if is_correct: + return '"{}" appears to be \x02valid\x02! ' \ + '(suggestions: {})'.format(inp, s_string) + else: + return '"{}" appears to be \x02invalid\x02! ' \ + '(suggestions: {})'.format(inp, s_string) diff --git a/plugins/spotify.py b/plugins/spotify.py new file mode 100644 index 0000000..935e118 --- /dev/null +++ b/plugins/spotify.py @@ -0,0 +1,33 @@ +import re +import spotimeta + +from util import hook, http + +gateway = 'http://open.spotify.com/{}/{}' # http spotify gw address + +spotify_re = (r'(spotify:(track|album|artist|user):([a-zA-Z0-9]+))', re.I) +http_re = (r'(open\.spotify\.com\/(track|album|artist|user)\/' + '([a-zA-Z0-9]+))', re.I) + +@hook.command +def spotify(inp): + "spotify <song> -- Search Spotify for <song>" + data = spotimeta.search_track(inp.strip()) + type, id = data["result"][0]["href"].split(":")[1:] + url = gateway.format(type, id) + return u"{} by {} - {}".format(data["result"][0]["name"], data["result"][0]["artist"]["name"], url) + + +@hook.regex(*http_re) +@hook.regex(*spotify_re) +def spotify_url(match): + type = match.group(2) + spotify_id = match.group(3) + url = gateway.format(type, spotify_id) + data = spotimeta.lookup(url) + if type == "track": + return u"Spotify Track: {} by {} from the album {}".format(data["result"]["name"], data["result"]["artist"]["name"], data["result"]["album"]["name"]) + elif type == "artist": + return u"Spotify Artist: {}".format(data["result"]["name"]) + elif type == "album": + return u"Spotify Album: {} - {}".format(data["result"]["artist"]["name"], data["result"]["name"]) \ No newline at end of file diff --git a/plugins/statuslog.py b/plugins/statuslog.py deleted file mode 100644 index e70c7f8..0000000 --- a/plugins/statuslog.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import codecs -import time -import re - -from util import hook - -timestamp_format = '%H:%M:%S' - -formats = { - 'PRIVMSG': '<%(nick)s> %(msg)s', - 'PART': '-!- %(nick)s [%(user)s@%(host)s] has left %(chan)s', - 'JOIN': '-!- %(nick)s [%(user)s@%(host)s] has joined %(param0)s', - 'MODE': '-!- mode/%(chan)s [%(param_tail)s] by %(nick)s', - 'KICK': '-!- %(param1)s was kicked from %(chan)s by %(nick)s [%(msg)s]', - 'TOPIC': '-!- %(nick)s changed the topic of %(chan)s to: %(msg)s', - 'QUIT': '-!- %(nick)s has quit [%(msg)s]', - 'PING': '', - 'NOTICE': '-%(nick)s- %(msg)s' -} - -ctcp_formats = { - 'ACTION': '* %(nick)s %(ctcpmsg)s', - 'VERSION': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', - 'PING': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', - 'TIME': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s', - 'FINGER': '%(nick)s has requested CTCP %(ctcpcmd)s from %(chan)s: %(ctcpmsg)s' -} - -irc_color_re = re.compile(r'(\x03(\d+,\d+|\d)|[\x0f\x02\x16\x1f])') - - -def gmtime(format): - return time.strftime(format, time.gmtime()) - - -def beautify(input): - format = formats.get(input.command, '%(raw)s') - args = dict(input) - - leng = len(args['paraml']) - for n, p in enumerate(args['paraml']): - args['param' + str(n)] = p - args['param_' + str(abs(n - leng))] = p - - args['param_tail'] = ' '.join(args['paraml'][1:]) - args['msg'] = irc_color_re.sub('', args['msg']) - - if input.command == 'PRIVMSG' and input.msg.count('\x01') >= 2: - ctcp = input.msg.split('\x01', 2)[1].split(' ', 1) - if len(ctcp) == 1: - ctcp += [''] - args['ctcpcmd'], args['ctcpmsg'] = ctcp - format = ctcp_formats.get(args['ctcpcmd'], - '%(nick)s [%(user)s@%(host)s] requested unknown CTCP ' - '%(ctcpcmd)s from %(chan)s: %(ctcpmsg)s') - - return format % args - -@hook.singlethread -@hook.event('*') -def log(paraml, input=None, bot=None): - timestamp = gmtime(timestamp_format) - - if input.command == 'QUIT': # these are temporary fixes until proper - input.chan = 'quit' # presence tracking is implemented - if input.command == 'NICK': - input.chan = 'nick' - - beau = beautify(input) - - if beau == '': # don't log this - return - - print timestamp, input.chan, beau.encode('utf8', 'ignore') - diff --git a/plugins/steam.py b/plugins/steam.py new file mode 100644 index 0000000..458831d --- /dev/null +++ b/plugins/steam.py @@ -0,0 +1,65 @@ +from util import hook, http, web, text +import re + +# this is still beta code. some things will be improved later. + +count_re = re.compile(r"Found (.*?) Games with a value of ") +value_re = re.compile(r'\$(\d+\.\d{2})') + + +def db_init(db): + "check to see that our db has the the top steam users table." + db.execute("create table if not exists steam_rankings(id, value, count, " + "primary key(id))") + db.commit() + + +@hook.command +def steamcalc(inp, db=None): + "steamcalc <user> -- Check the value of <user>s steam account." + db_init(db) + + if " " in inp: + return "Invalid Steam ID" + + uid = inp.strip().lower() + url = "http://steamcalculator.com/id/{}".format(http.quote_plus(uid)) + + # get the web page + try: + page = http.get_html(url) + except Exception as e: + return "Could not get Steam game listing: {}".format(e) + + # extract the info we need + try: + count_text = page.xpath("//div[@id='rightdetail']/text()")[0] + count = int(count_re.findall(count_text)[0]) + + value_text = page.xpath("//div[@id='rightdetail']/h1/text()")[0] + value = float(value_re.findall(value_text)[0]) + except IndexError: + return "Could not get Steam game listing." + + # save the info in the DB for steam rankings + db.execute("insert or replace into steam_rankings(id, value, count)" + "values(?,?,?)", (uid, value, count)) + db.commit() + + # shorten the URL + try: + short_url = web.isgd(url) + except web.ShortenError: + short_url = url + + return u"\x02Games:\x02 {}, \x02Total Value:\x02 ${:.2f} USD - {}".format(count, value, short_url) + + +@hook.command(autohelp=False) +def steamtop(inp, db=None): + "steamtop -- Shows the top five users from steamcalc." + rows = [] + for row in db.execute("SELECT id, value, count FROM steam_rankings ORDER BY value DESC LIMIT 5"): + rows.append(u"{} - \x02${:.2f}\x02 ({} games)".format(text.munge(row[0], 1), row[1], row[2])) + + return u"Top Steam Users: {}".format(", ".join(rows)) diff --git a/plugins/stock.py b/plugins/stock.py new file mode 100755 index 0000000..a77f0cd --- /dev/null +++ b/plugins/stock.py @@ -0,0 +1,40 @@ +import random + +from util import hook, http + +url = 'http://www.google.com/ig/api' + +@hook.command +def stock(inp): + "stock <symbol> -- Gets information about stock symbol <symbol>." + + parsed = http.get_xml(url, stock=inp) + + if len(parsed) != 1: + return "error getting stock info" + + # Stuff the results in a dict for easy string formatting + results = dict((el.tag, el.attrib['data']) + for el in parsed.xpath('//finance/*')) + + # if we dont get a company name back, the symbol doesn't match a company + if results['company'] == '': + return "error: unknown ticker symbol (%s)" % inp + + if results['last'] == '0.00': + return "%(company)s - last known stock value was 0.00 %(currency)s" \ + " as of %(trade_timestamp)s" % (results) + + if results['change'][0] == '-': + results['color'] = "5" + else: + results['color'] = "3" + + ret = "%(company)s - %(last)s %(currency)s " \ + "\x03%(color)s%(change)s (%(perc_change)s%%)\x03 " \ + "as of %(trade_timestamp)s" % results + + if results['delay'] != '0': + ret += " (delayed %s minutes)" % results['delay'] + + return ret diff --git a/disabled_stuff/system.py b/plugins/system.py old mode 100644 new mode 100755 similarity index 67% rename from disabled_stuff/system.py rename to plugins/system.py index 08891fd..744ca72 --- a/disabled_stuff/system.py +++ b/plugins/system.py @@ -2,9 +2,8 @@ import os import re import time import platform -from datetime import timedelta - from util import hook +from datetime import timedelta def convert_kilobytes(kilobytes): @@ -18,21 +17,21 @@ def convert_kilobytes(kilobytes): @hook.command(autohelp=False) def system(inp): - """system -- Retrieves information about the host system.""" + "system -- Retrieves information about the host system." hostname = platform.node() os = platform.platform() python_imp = platform.python_implementation() python_ver = platform.python_version() architecture = '-'.join(platform.architecture()) cpu = platform.machine() - return "Hostname: \x02{}\x02, Operating System: \x02{}\x02, Python " \ - "Version: \x02{} {}\x02, Architecture: \x02{}\x02, CPU: \x02{}" \ - "\x02".format(hostname, os, python_imp, python_ver, architecture, cpu) + return "Hostname: \x02%s\x02, Operating System: \x02%s\x02, Python " \ + "Version: \x02%s %s\x02, Architecture: \x02%s\x02, CPU: \x02%s" \ + "\x02" % (hostname, os, python_imp, python_ver, architecture, cpu) @hook.command(autohelp=False) def memory(inp): - """memory -- Displays the bot's current memory usage.""" + "memory -- Displays the bot's current memory usage." if os.name == "posix": # get process info status_file = open('/proc/self/status').read() @@ -42,9 +41,9 @@ def memory(inp): data = [float(i.replace(' kB', '')) for i in data] strings = [convert_kilobytes(i) for i in data] # prepare the output - out = "Threads: \x02{}\x02, Real Memory: \x02{}\x02, Allocated Memory: \x02{}\x02, Peak " \ - "Allocated Memory: \x02{}\x02, Stack Size: \x02{}\x02, Heap " \ - "Size: \x02{}\x02".format(s['Threads'], strings[0], strings[1], strings[2], + out = "Threads: \x02%s\x02, Real Memory: \x02%s\x02, Allocated Memory: \x02%s\x02, Peak " \ + "Allocated Memory: \x02%s\x02, Stack Size: \x02%s\x02, Heap " \ + "Size: \x02%s\x02" % (s['Threads'], strings[0], strings[1], strings[2], strings[3], strings[4]) # return output return out @@ -56,7 +55,7 @@ def memory(inp): for amount in re.findall(r'([,0-9]+) K', out): memory += float(amount.replace(',', '')) memory = convert_kilobytes(memory) - return "Memory Usage: \x02{}\x02".format(memory) + return "Memory Usage: \x02%s\x02" % memory else: return "Sorry, this command is not supported on your OS." @@ -64,13 +63,13 @@ def memory(inp): @hook.command(autohelp=False) def uptime(inp, bot=None): - """uptime -- Shows the bot's uptime.""" + "uptime -- Shows the bot's uptime." uptime_raw = round(time.time() - bot.start_time) uptime = timedelta(seconds=uptime_raw) - return "Uptime: \x02{}\x02".format(uptime) + return "Uptime: \x02%s\x02" % uptime @hook.command(autohelp=False) def pid(inp): - """pid -- Prints the bot's PID.""" - return "PID: \x02{}\x02".format(os.getpid()) + "pid -- Prints the bot's PID." + return "PID: \x02%s\x02" % os.getpid() diff --git a/plugins/tell.py b/plugins/tell.py new file mode 100755 index 0000000..42eb98d --- /dev/null +++ b/plugins/tell.py @@ -0,0 +1,118 @@ +" tell.py: written by sklnd in July 2009" +" 2010.01.25 - modified by Scaevolus" + +import time +import re + +from util import hook, timesince + + +def db_init(db): + "check to see that our db has the tell table and return a dbection." + db.execute("create table if not exists tell" + "(user_to, user_from, message, chan, time," + "primary key(user_to, message))") + db.commit() + + return db + + +def get_tells(db, user_to): + return db.execute("select user_from, message, time, chan from tell where" + " user_to=lower(?) order by time", + (user_to.lower(),)).fetchall() + + +@hook.singlethread +@hook.event('PRIVMSG') +def tellinput(paraml, input=None, notice=None, db=None, bot=None, nick=None, conn=None): + if 'showtells' in input.msg.lower(): + return + + db_init(db) + + tells = get_tells(db, nick) + + if tells: + user_from, message, time, chan = tells[0] + reltime = timesince.timesince(time) + + reply = "%s sent you a message %s ago from %s: %s" % (user_from, reltime, chan, + message) + if len(tells) > 1: + reply += " (+%d more, %sshowtells to view)" % (len(tells) - 1, conn.conf["command_prefix"]) + + db.execute("delete from tell where user_to=lower(?) and message=?", + (nick, message)) + db.commit() + notice(reply) + + +@hook.command(autohelp=False) +def showtells(inp, nick='', chan='', notice=None, db=None): + "showtells -- View all pending tell messages (sent in a notice)." + + db_init(db) + + tells = get_tells(db, nick) + + if not tells: + notice("You have no pending tells.") + return + + for tell in tells: + user_from, message, time, chan = tell + past = timesince.timesince(time) + notice("%s sent you a message %s ago from %s: %s" % (user_from, past, chan, message)) + + db.execute("delete from tell where user_to=lower(?)", + (nick,)) + db.commit() + + +@hook.command +def tell(inp, nick='', chan='', db=None, input=None, notice=None): + "tell <nick> <message> -- Relay <message> to <nick> when <nick> is around." + query = inp.split(' ', 1) + + if len(query) != 2: + notice(tell.__doc__) + return + + user_to = query[0].lower() + message = query[1].strip() + user_from = nick + + if chan.lower() == user_from.lower(): + chan = 'a pm' + + if user_to == user_from.lower(): + notice("Have you looked in a mirror lately?") + return + + if user_to.lower() == input.conn.nick.lower(): + # user is looking for us, being a smartass + notice("Thanks for the message, %s!" % user_from) + return + + if not re.match("^[A-Za-z0-9_|.-\]\[]*$", user_to.lower()): + notice("I cant send a message to that user!") + return + + db_init(db) + + if db.execute("select count() from tell where user_to=?", + (user_to,)).fetchone()[0] >= 10: + notice("That person has too many messages queued.") + return + + try: + db.execute("insert into tell(user_to, user_from, message, chan," + "time) values(?,?,?,?,?)", (user_to, user_from, message, + chan, time.time())) + db.commit() + except db.IntegrityError: + notice("Message has already been queued.") + return + + notice("Your message has been sent!") diff --git a/plugins/time.py b/plugins/time.py new file mode 100755 index 0000000..88937d6 --- /dev/null +++ b/plugins/time.py @@ -0,0 +1,58 @@ +from util import hook, http +import time +from util.text import capitalize_first + +api_url = 'http://api.wolframalpha.com/v2/query?format=plaintext' + + +@hook.command("time") +def time_command(inp, bot=None): + "time <area> -- Gets the time in <area>" + + query = "current time in %s" % inp + + api_key = bot.config.get("api_keys", {}).get("wolframalpha", None) + if not api_key: + return "error: no wolfram alpha api key set" + + request = http.get_xml(api_url, input=query, appid=api_key) + time = " ".join(request.xpath("//pod[@title='Result']/subpod/plain" \ + "text/text()")) + time = time.replace(" | ", ", ") + + if time: + # nice place name for UNIX time + if inp.lower() == "unix": + place = "Unix Epoch" + else: + place = capitalize_first(" ".join(request.xpath("//pod[@" \ + "title='Input interpretation']/subpod/plaintext/text()"))[16:]) + return "%s - \x02%s\x02" % (time, place) + else: + return "Could not get the time for '%s'." % inp + + +@hook.command(autohelp=False) +def beats(inp): + "beats -- Gets the current time in .beats (Swatch Internet Time). " + + if inp.lower() == "wut": + return "Instead of hours and minutes, the mean solar day is divided " \ + "up into 1000 parts called \".beats\". Each .beat lasts 1 minute and" \ + " 26.4 seconds. Times are notated as a 3-digit number out of 1000 af" \ + "ter midnight. So, @248 would indicate a time 248 .beats after midni" \ + "ght representing 248/1000 of a day, just over 5 hours and 57 minute" \ + "s. There are no timezones." + + t = time.gmtime() + h, m, s = t.tm_hour, t.tm_min, t.tm_sec + + utc = 3600 * h + 60 * m + s + bmt = utc + 3600 # Biel Mean Time (BMT) + + beat = bmt / 86.4 + + if beat > 1000: + beat -= 1000 + + return "Swatch Internet Time: @%06.2f" % beat diff --git a/disabled_stuff/title.py b/plugins/title.py old mode 100644 new mode 100755 similarity index 69% rename from disabled_stuff/title.py rename to plugins/title.py index 4264188..c8e0c67 --- a/disabled_stuff/title.py +++ b/plugins/title.py @@ -1,11 +1,10 @@ -from bs4 import BeautifulSoup - from util import hook, http, urlnorm +from bs4 import BeautifulSoup @hook.command def title(inp): - """title <url> -- gets the title of a web page""" + "title <url> -- gets the title of a web page" url = urlnorm.normalize(inp.encode('utf-8'), assume_scheme="http") try: @@ -15,9 +14,9 @@ def title(inp): except (http.HTTPError, http.URLError): return "Could not fetch page." - page_title = soup.find('title').contents[0] + title = soup.find('title').contents[0] - if not page_title: + if not title: return "Could not find title." - return u"{} [{}]".format(page_title, real_url) + return u"{} [{}]".format(title, real_url) diff --git a/disabled_stuff/notes.py b/plugins/todo.py old mode 100644 new mode 100755 similarity index 78% rename from disabled_stuff/notes.py rename to plugins/todo.py index 070a9fb..83537ec --- a/disabled_stuff/notes.py +++ b/plugins/todo.py @@ -1,18 +1,16 @@ +from util import hook import re -from util import hook +db_inited = False -db_ready = False - - -def clean_sql(sql): +def cleanSQL(sql): return re.sub(r'\s+', " ", sql).strip() def db_init(db): - global db_ready - if db_ready: + global db_inited + if db_inited: return exists = db.execute(""" @@ -22,7 +20,7 @@ def db_init(db): """).fetchone()[0] == 1 if not exists: - db.execute(clean_sql(""" + db.execute(cleanSQL(""" create virtual table todos using fts4( user, text, @@ -32,7 +30,7 @@ def db_init(db): db.commit() - db_ready = True + db_inited = True def db_getall(db, nick, limit=-1): @@ -46,14 +44,14 @@ def db_getall(db, nick, limit=-1): """, (nick, limit)) -def db_get(db, nick, note_id): +def db_get(db, nick, id): return db.execute(""" select added, text from todos where lower(user) = lower(?) order by added desc limit 1 offset ? - """, (nick, note_id)).fetchone() + """, (nick, id)).fetchone() def db_del(db, nick, limit='all'): @@ -66,8 +64,8 @@ def db_del(db, nick, limit='all'): limit ? offset ?) """, (nick, - -1 if limit == 'all' else 1, - 0 if limit == 'all' else limit)) + -1 if limit == 'all' else 1, + 0 if limit == 'all' else limit)) db.commit() return row @@ -90,10 +88,9 @@ def db_search(db, nick, query): """, (query, nick)) -@hook.command("notes") @hook.command -def note(inp, nick='', chan='', db=None, notice=None, bot=None): - """note(s) <add|del|list|search> args -- Manipulates your list of notes.""" +def todo(inp, nick='', chan='', db=None, notice=None, bot=None): + "todo (add|del|list|search) args -- Manipulates your list of todos." db_init(db) @@ -103,7 +100,7 @@ def note(inp, nick='', chan='', db=None, notice=None, bot=None): args = parts[1:] # code to allow users to access each others factoids and a copy of help - # ".note (add|del|list|search) [@user] args -- Manipulates your list of todos." + # ".todo (add|del|list|search) [@user] args -- Manipulates your list of todos." #if len(args) and args[0].startswith("@"): # nick = args[0][1:] # args = args[1:] @@ -116,7 +113,7 @@ def note(inp, nick='', chan='', db=None, notice=None, bot=None): db_add(db, nick, text) - notice("Note added!") + notice("Task added!") return elif cmd == 'get': if len(args): @@ -133,7 +130,7 @@ def note(inp, nick='', chan='', db=None, notice=None, bot=None): if not row: notice("No such entry.") return - notice("[{}]: {}: {}".format(index, row[0], row[1])) + notice("[%d]: %s: %s" % (index, row[0], row[1])) elif cmd == 'del' or cmd == 'delete' or cmd == 'remove': if not len(args): return "error" @@ -149,7 +146,7 @@ def note(inp, nick='', chan='', db=None, notice=None, bot=None): rows = db_del(db, nick, index) - notice("Deleted {} entries".format(rows.rowcount)) + notice("Deleted %d entries" % rows.rowcount) elif cmd == 'list': limit = -1 @@ -166,11 +163,11 @@ def note(inp, nick='', chan='', db=None, notice=None, bot=None): found = False for (index, row) in enumerate(rows): - notice("[{}]: {}: {}".format(index, row[0], row[1])) + notice("[%d]: %s: %s" % (index, row[0], row[1])) found = True if not found: - notice("{} has no entries.".format(nick)) + notice("%s has no entries." % nick) elif cmd == 'search': if not len(args): notice("No search query given!") @@ -181,11 +178,11 @@ def note(inp, nick='', chan='', db=None, notice=None, bot=None): found = False for (index, row) in enumerate(rows): - notice("[{}]: {}: {}".format(index, row[0], row[1])) + notice("[%d]: %s: %s" % (index, row[0], row[1])) found = True if not found: - notice("{} has no matching entries for: {}".format(nick, query)) + notice("%s has no matching entries for: %s" % (nick, query)) else: - notice("Unknown command: {}".format(cmd)) + notice("Unknown command: %s" % cmd) diff --git a/disabled_stuff/tvdb.py b/plugins/tvdb.py old mode 100644 new mode 100755 similarity index 59% rename from disabled_stuff/tvdb.py rename to plugins/tvdb.py index b5fa12f..e0fda5b --- a/disabled_stuff/tvdb.py +++ b/plugins/tvdb.py @@ -1,5 +1,14 @@ -import datetime +""" +TV information, written by Lurchington 2010 +modified by rmmh 2010 +""" +import datetime +from urllib2 import URLError +from zipfile import ZipFile +from cStringIO import StringIO + +from lxml import etree from util import hook, http @@ -7,12 +16,21 @@ base_url = "http://thetvdb.com/api/" api_key = "469B73127CA0C411" -def get_episodes_for_series(series_name, api_key): +def get_zipped_xml(*args, **kwargs): + try: + path = kwargs.pop("path") + except KeyError: + raise KeyError("must specify a path for the zipped file to be read") + zip_buffer = StringIO(http.get(*args, **kwargs)) + return etree.parse(ZipFile(zip_buffer, "r").open(path)) + + +def get_episodes_for_series(seriesname, api_key): res = {"error": None, "ended": False, "episodes": None, "name": None} # http://thetvdb.com/wiki/index.php/API:GetSeries try: - query = http.get_xml(base_url + 'GetSeries.php', seriesname=series_name) - except http.URLError: + query = http.get_xml(base_url + 'GetSeries.php', seriesname=seriesname) + except URLError: res["error"] = "error contacting thetvdb.com" return res @@ -25,8 +43,9 @@ def get_episodes_for_series(series_name, api_key): series_id = series_id[0] try: - series = http.get_xml(base_url + '%s/series/%s/all/en.xml' % (api_key, series_id)) - except http.URLError: + series = get_zipped_xml(base_url + '%s/series/%s/all/en.zip' % + (api_key, series_id), path="en.xml") + except URLError: res["error"] = "Error contacting thetvdb.com." return res @@ -44,7 +63,7 @@ def get_episode_info(episode, api_key): first_aired = episode.findtext("FirstAired") try: - air_date = datetime.date(*map(int, first_aired.split('-'))) + airdate = datetime.date(*map(int, first_aired.split('-'))) except (ValueError, TypeError): return None @@ -57,16 +76,16 @@ def get_episode_info(episode, api_key): if episode_name == "TBA": episode_name = None - episode_desc = '{}'.format(episode_num) + episode_desc = '%s' % episode_num if episode_name: - episode_desc += ' - {}'.format(episode_name) - return first_aired, air_date, episode_desc + episode_desc += ' - %s' % episode_name + return (first_aired, airdate, episode_desc) @hook.command @hook.command('tv') def tv_next(inp, bot=None): - """tv <series> -- Get the next episode of <series>.""" + "tv <series> -- Get the next episode of <series>." api_key = bot.config.get("api_keys", {}).get("tvdb", None) if api_key is None: @@ -81,7 +100,7 @@ def tv_next(inp, bot=None): episodes = episodes["episodes"] if ended: - return "{} has ended.".format(series_name) + return "%s has ended." % series_name next_eps = [] today = datetime.date.today() @@ -92,31 +111,31 @@ def tv_next(inp, bot=None): if ep_info is None: continue - (first_aired, air_date, episode_desc) = ep_info + (first_aired, airdate, episode_desc) = ep_info - if air_date > today: - next_eps = ['{} ({})'.format(first_aired, episode_desc)] - elif air_date == today: - next_eps = ['Today ({})'.format(episode_desc)] + next_eps + if airdate > today: + next_eps = ['%s (%s)' % (first_aired, episode_desc)] + elif airdate == today: + next_eps = ['Today (%s)' % episode_desc] + next_eps else: - # we're iterating in reverse order with newest episodes last - # so, as soon as we're past today, break out of loop + #we're iterating in reverse order with newest episodes last + #so, as soon as we're past today, break out of loop break if not next_eps: - return "There are no new episodes scheduled for {}.".format(series_name) + return "There are no new episodes scheduled for %s." % series_name if len(next_eps) == 1: - return "The next episode of {} airs {}".format(series_name, next_eps[0]) + return "The next episode of %s airs %s" % (series_name, next_eps[0]) else: next_eps = ', '.join(next_eps) - return "The next episodes of {}: {}".format(series_name, next_eps) + return "The next episodes of %s: %s" % (series_name, next_eps) @hook.command @hook.command('tv_prev') def tv_last(inp, bot=None): - """tv_last <series> -- Gets the most recently aired episode of <series>.""" + "tv_last <series> -- Gets the most recently aired episode of <series>." api_key = bot.config.get("api_keys", {}).get("tvdb", None) if api_key is None: @@ -139,16 +158,16 @@ def tv_last(inp, bot=None): if ep_info is None: continue - (first_aired, air_date, episode_desc) = ep_info + (first_aired, airdate, episode_desc) = ep_info - if air_date < today: + if airdate < today: #iterating in reverse order, so the first episode encountered #before today was the most recently aired - prev_ep = '{} ({})'.format(first_aired, episode_desc) + prev_ep = '%s (%s)' % (first_aired, episode_desc) break if not prev_ep: - return "There are no previously aired episodes for {}.".format(series_name) + return "There are no previously aired episodes for %s." % series_name if ended: - return '{} ended. The last episode aired {}.'.format(series_name, prev_ep) - return "The last episode of {} aired {}.".format(series_name, prev_ep) + return '%s ended. The last episode aired %s.' % (series_name, prev_ep) + return "The last episode of %s aired %s." % (series_name, prev_ep) diff --git a/plugins/twitter.py b/plugins/twitter.py new file mode 100755 index 0000000..d798e12 --- /dev/null +++ b/plugins/twitter.py @@ -0,0 +1,166 @@ +# written by Scaevolus, modified by Lukeroge + +from util import hook, http + +import random +import re +from time import strftime, strptime +from datetime import datetime + +from util.timesince import timesince + + +def unescape_xml(string): + """Unescapes XML""" + return string.replace('>', '>').replace('<', '<').replace(''', + "'").replace('"e;', '"').replace('&', '&') + +history = [] +history_max_size = 250 + + +def parseDateTime(s): + """Parses the date from a string""" + if s is None: + return None + m = re.match(r'(.*?)(?:\.(\d+))?(([-+]\d{1,2}):(\d{2}))?$', + str(s)) + datestr, fractional, tzname, tzhour, tzmin = m.groups() + + if tzname is None: + tz = None + else: + tzhour, tzmin = int(tzhour), int(tzmin) + if tzhour == tzmin == 0: + tzname = 'UTC' + tz = FixedOffset(timedelta(hours=tzhour, + minutes=tzmin), tzname) + + x = datetime.strptime(datestr, "%Y-%m-%d %H:%M:%S") + if fractional is None: + fractional = '0' + fracpower = 6 - len(fractional) + fractional = float(fractional) * (10 ** fracpower) + + return x.replace(microsecond=int(fractional), tzinfo=tz) + + +@hook.command +def twitter(inp): + "twitter <user>/<user> <n>/<id>/#<hashtag>/@<user> -- Gets last/<n>th " \ + "tweet from <user>/gets tweet <id>/Gets random tweet with #<hashtag>/" \ + "gets replied tweet from @<user>." + + def add_reply(reply_name, reply_id): + if len(history) == history_max_size: + history.pop() + history.insert(0, (reply_name, reply_id)) + + def find_reply(reply_name): + for name, id in history: + if name == reply_name: + return id if id != -1 else name + + if inp[0] == '@': + reply_inp = find_reply(inp[1:]) + if reply_inp == None: + return 'No replies to %s found.' % inp + inp = reply_inp + + url = 'http://api.twitter.com' + getting_nth = False + getting_id = False + searching_hashtag = False + + time = 'status/created_at' + text = 'status/text' + retweeted_text = 'status/retweeted_status/text' + retweeted_screen_name = 'status/retweeted_status/user/screen_name' + reply_name = 'status/in_reply_to_screen_name' + reply_id = 'status/in_reply_to_status_id' + reply_user = 'status/in_reply_to_user_id' + + if re.match(r'^\d+$', inp): + getting_id = True + url += '/statuses/show/%s.xml' % inp + screen_name = 'user/screen_name' + time = 'created_at' + text = 'text' + reply_name = 'in_reply_to_screen_name' + reply_id = 'in_reply_to_status_id' + reply_user = 'in_reply_to_user_id' + elif re.match(r'^\w{1,15}$', inp) or re.match(r'^\w{1,15}\s+\d+$', inp): + getting_nth = True + if inp.find(' ') == -1: + name = inp + num = 1 + else: + name, num = inp.split() + if int(num) > 3200: + return 'error: only supports up to the 3200th tweet' + url += ('/1/statuses/user_timeline.xml?include_rts=true&' + 'screen_name=%s&count=1&page=%s' % (name, num)) + screen_name = 'status/user/screen_name' + elif re.match(r'^#\w+$', inp): + url = 'http://search.twitter.com/search.atom?q=%23' + inp[1:] + searching_hashtag = True + else: + return 'Error: Invalid request.' + + try: + tweet = http.get_xml(url) + except http.HTTPError as e: + errors = {400: 'Bad request (ratelimited?)', + 401: 'Tweet is private', + 403: 'Tweet is private', + 404: 'Invalid user/id', + 500: 'Twitter is broken', + 502: 'Twitter is down ("getting upgraded")', + 503: 'Twitter is overloaded'} + if e.code == 404: + return 'error: invalid ' + ['username', 'tweet id'][getting_id] + if e.code in errors: + return 'Error: %s.' % errors[e.code] + return 'Unknown Error: %s' % e.code + except http.URLError as e: + return 'Error: Request timed out.' + + if searching_hashtag: + ns = '{http://www.w3.org/2005/Atom}' + tweets = tweet.findall(ns + 'entry/' + ns + 'id') + if not tweets: + return 'Hashtag not found!' + id = random.choice(tweets).text + id = id[id.rfind(':') + 1:] + return twitter(id) + + if getting_nth: + if tweet.find('status') is None: + return "User doesn't have that many tweets!" + + time = tweet.find(time) + if time is None: + return "User has no tweets!" + + reply_name = tweet.find(reply_name).text + reply_id = tweet.find(reply_id).text + reply_user = tweet.find(reply_user).text + if reply_name is not None and (reply_id is not None or + reply_user is not None): + add_reply(reply_name, reply_id or -1) + + time_raw = strftime('%Y-%m-%d %H:%M:%S', + strptime(time.text, + '%a %b %d %H:%M:%S +0000 %Y')) + + time_nice = timesince(parseDateTime(time_raw), datetime.utcnow()) + + if tweet.find(retweeted_text) is not None: + text = 'RT @%s:' % tweet.find(retweeted_screen_name).text + text += unescape_xml(tweet.find(retweeted_text).text.replace('\n', '')) + else: + text = unescape_xml(tweet.find(text).text.replace('\n', '')) + + screen_name = tweet.find(screen_name).text + + return "\x02@%s\x02: %s (%s ago)" % (screen_name, text, time_nice) diff --git a/plugins/urban.py b/plugins/urban.py new file mode 100755 index 0000000..376b124 --- /dev/null +++ b/plugins/urban.py @@ -0,0 +1,39 @@ +from util import hook, http, text + +base_url = 'http://www.urbandictionary.com/iphone/search/define' + + +@hook.command('u') +@hook.command +def urban(inp): + "urban <phrase> [id] -- Looks up <phrase> on urbandictionary.com." + + # clean and split the input + input = inp.lower().strip() + parts = input.split() + + # if the last word is a number, set the ID to that number + if parts[-1].isdigit(): + id = int(parts[-1]) + # remove the ID from the input string + del parts[-1] + input = " ".join(parts) + else: + id = 1 + + # fetch the definitions + page = http.get_json(base_url, term=input, referer="http://m.urbandictionary.com") + defs = page['list'] + + if page['result_type'] == 'no_results': + return 'Not found.' + + # try getting the requested definition + try: + output = u"[%i/%i] %s: %s" % \ + (id, len(defs), defs[id - 1]['word'], + defs[id - 1]['definition'].replace('\r\n', ' ')) + except IndexError: + return 'Not found.' + + return text.truncate_str(output, 250) diff --git a/util/__init__.py b/plugins/util/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from util/__init__.py rename to plugins/util/__init__.py diff --git a/plugins/util/exec.py b/plugins/util/exec.py new file mode 100644 index 0000000..55a4ef2 --- /dev/null +++ b/plugins/util/exec.py @@ -0,0 +1,28 @@ +import http +import json, urllib2, sys + + +def haste(data): + URL = "http://paste.dmptr.com" + request = urllib2.Request(URL + "/documents", data) + response = urllib2.urlopen(request) + return("%s/%s" % (URL, json.loads(response.read())['key'])) + + +def execute_eval(code, paste_multiline=True): + while True: + output = http.get("http://eval.appspot.com/eval", statement=code).rstrip('\n') + if output: + break + else: + pass + + if "Traceback (most recent call last):" in output: + status = "Python error: " + else: + status = "Code executed sucessfully: " + + if "\n" in output and paste_multiline: + return status + haste(output) + else: + return output \ No newline at end of file diff --git a/util/pyexec.py b/plugins/util/execute.py similarity index 97% rename from util/pyexec.py rename to plugins/util/execute.py index 4a12589..9aecd7e 100644 --- a/util/pyexec.py +++ b/plugins/util/execute.py @@ -1,5 +1,4 @@ -import http -import web +import http, web def eval_py(code, paste_multiline=True): diff --git a/util/hook.py b/plugins/util/hook.py old mode 100644 new mode 100755 similarity index 92% rename from util/hook.py rename to plugins/util/hook.py index 5e191d3..ff11e82 --- a/util/hook.py +++ b/plugins/util/hook.py @@ -22,14 +22,14 @@ def _hook_add(func, add, name=''): n_args -= 1 if n_args != 1: err = '%ss must take 1 non-keyword argument (%s)' % (name, - func.__name__) + func.__name__) raise ValueError(err) args = [] if argspec.defaults: end = bool(argspec.keywords) + bool(argspec.varargs) args.extend(argspec.args[-len(argspec.defaults): - end if end else None]) + end if end else None]) if argspec.keywords: args.append(0) # means kwargs present func._args = args @@ -41,7 +41,7 @@ def _hook_add(func, add, name=''): def sieve(func): if func.func_code.co_argcount != 5: raise ValueError( - 'sieves must take 5 arguments: (bot, input, func, type, args)') + 'sieves must take 5 arguments: (bot, input, func, type, args)') _hook_add(func, ['sieve', (func,)]) return func diff --git a/util/http.py b/plugins/util/http.py old mode 100644 new mode 100755 similarity index 83% rename from util/http.py rename to plugins/util/http.py index 7f8bd66..8a426fd --- a/util/http.py +++ b/plugins/util/http.py @@ -19,7 +19,7 @@ ua_cloudbot = 'Cloudbot/DEV http://github.com/CloudDev/CloudBot' ua_firefox = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:17.0) Gecko/17.0' \ ' Firefox/17.0' ua_old_firefox = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; ' \ - 'rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6' + 'rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6' ua_internetexplorer = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)' ua_chrome = 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.4 (KHTML, ' \ 'like Gecko) Chrome/22.0.1229.79 Safari/537.4' @@ -52,7 +52,8 @@ def get_json(*args, **kwargs): def open(url, query_params=None, user_agent=None, post_data=None, - referer=None, get_method=None, cookies=False, timeout=None, headers=None, **kwargs): + referer=None, get_method=None, cookies=False, **kwargs): + if query_params is None: query_params = {} @@ -68,10 +69,6 @@ def open(url, query_params=None, user_agent=None, post_data=None, if get_method is not None: request.get_method = lambda: get_method - if headers is not None: - for header_key, header_value in headers.iteritems(): - request.add_header(header_key, header_value) - request.add_header('User-Agent', user_agent) if referer is not None: @@ -82,10 +79,7 @@ def open(url, query_params=None, user_agent=None, post_data=None, else: opener = urllib2.build_opener() - if timeout: - return opener.open(request, timeout=timeout) - else: - return opener.open(request) + return opener.open(request) def prepare_url(url, queries): @@ -95,7 +89,7 @@ def prepare_url(url, queries): query = dict(urlparse.parse_qsl(query)) query.update(queries) query = urllib.urlencode(dict((to_utf8(key), to_utf8(value)) - for key, value in query.iteritems())) + for key, value in query.iteritems())) url = urlparse.urlunsplit((scheme, netloc, path, query, fragment)) diff --git a/util/text.py b/plugins/util/text.py old mode 100644 new mode 100755 similarity index 80% rename from util/text.py rename to plugins/util/text.py index e3604dc..8e5c8a0 --- a/util/text.py +++ b/plugins/util/text.py @@ -6,38 +6,9 @@ import re -from HTMLParser import HTMLParser -import htmlentitydefs - - -class HTMLTextExtractor(HTMLParser): - def __init__(self): - HTMLParser.__init__(self) - self.result = [] - - def handle_data(self, d): - self.result.append(d) - - def handle_charref(self, number): - codepoint = int(number[1:], 16) if number[0] in (u'x', u'X') else int(number) - self.result.append(unichr(codepoint)) - - def handle_entityref(self, name): - codepoint = htmlentitydefs.name2codepoint[name] - self.result.append(unichr(codepoint)) - - def get_text(self): - return u''.join(self.result) - - -def strip_html(html): - s = HTMLTextExtractor() - s.feed(html) - return s.get_text() - def munge(text, munge_count=0): - """munges up text.""" + "munges up text." reps = 0 for n in xrange(len(text)): rep = character_replacements.get(text[n]) @@ -124,29 +95,9 @@ def multiword_replace(text, wordDic): return rc.sub(translate, text) -def truncate_words(content, length=10, suffix='...'): - """Truncates a string after a certain number of words.""" - nmsg = content.split(" ") - out = None - x = 0 - for i in nmsg: - if x <= length: - if out: - out = out + " " + nmsg[x] - else: - out = nmsg[x] - x += 1 - if x <= length: - return out - else: - return out + suffix - - # from <http://stackoverflow.com/questions/250357/smart-truncate-in-python> def truncate_str(content, length=100, suffix='...'): - """Truncates a string after a certain number of chars. - @rtype : str - """ + "Truncates a string after a certain number of chars." if len(content) <= length: return content else: @@ -186,7 +137,7 @@ def truncate_str(content, length=100, suffix='...'): # Expression to match some_token and some_token="with spaces" (and similarly # for single-quoted strings). -split_re = re.compile(r"""((?:[^\s'"]*(?:(?:"(?:[^"\\]|\\.)*" | '(?:[""" +split_re = re.compile(r"""((?:[^\s'"]*(?:(?:"(?:[^"\\]|\\.)*" | '(?:[""" \ r"""^'\\]|\\.)*')[^\s'"]*)+) | \S+)""", re.VERBOSE) @@ -228,5 +179,5 @@ def get_text_list(list_, last_word='or'): return list_[0] return '%s %s %s' % ( # Translators: This string is used as a separator between list elements - ', '.join([i for i in list_][:-1]), + (', ').join([i for i in list_][:-1]), last_word, list_[-1]) diff --git a/util/timesince.py b/plugins/util/timesince.py old mode 100644 new mode 100755 similarity index 91% rename from util/timesince.py rename to plugins/util/timesince.py index 56ec8b0..a3cea46 --- a/util/timesince.py +++ b/plugins/util/timesince.py @@ -43,21 +43,18 @@ def timesince(d, now=None): Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ chunks = ( - (60 * 60 * 24 * 365, ('year', 'years')), - (60 * 60 * 24 * 30, ('month', 'months')), - (60 * 60 * 24 * 7, ('week', 'weeks')), - (60 * 60 * 24, ('day', 'days')), - (60 * 60, ('hour', 'hours')), - (60, ('minute', 'minutes')) + (60 * 60 * 24 * 365, ('year', 'years')), + (60 * 60 * 24 * 30, ('month', 'months')), + (60 * 60 * 24 * 7, ('week', 'weeks')), + (60 * 60 * 24, ('day', 'days')), + (60 * 60, ('hour', 'hours')), + (60, ('minute', 'minutes')) ) # Convert int or float (unix epoch) to datetime.datetime for comparison if isinstance(d, int) or isinstance(d, float): d = datetime.datetime.fromtimestamp(d) - if isinstance(now, int) or isinstance(now, float): - now = datetime.datetime.fromtimestamp(now) - # Convert datetime.date to datetime.datetime for comparison. if not isinstance(d, datetime.datetime): d = datetime.datetime(d.year, d.month, d.day) diff --git a/util/urlnorm.py b/plugins/util/urlnorm.py old mode 100644 new mode 100755 similarity index 82% rename from util/urlnorm.py rename to plugins/util/urlnorm.py index b3c351b..23b4a45 --- a/util/urlnorm.py +++ b/plugins/util/urlnorm.py @@ -38,15 +38,13 @@ class Normalizer(object): self.regex = regex self.normalize = normalize_func - -normalizers = (Normalizer(re.compile( - r'(?:https?://)?(?:[a-zA-Z0-9\-]+\.)?(?:amazon|amzn){1}\.(?P<tld>[a-zA-Z\.]{2,})\/(gp/(?:product|offer-listing|customer-media/product-gallery)/|exec/obidos/tg/detail/-/|o/ASIN/|dp/|(?:[A-Za-z0-9\-]+)/dp/)?(?P<ASIN>[0-9A-Za-z]{10})'), - lambda m: r'http://amazon.%s/dp/%s' % (m.group('tld'), m.group('ASIN'))), - Normalizer(re.compile(r'.*waffleimages\.com.*/([0-9a-fA-F]{40})'), - lambda m: r'http://img.waffleimages.com/%s' % m.group(1)), - Normalizer(re.compile(r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)'), - lambda m: r'http://youtube.com/watch?v=%s' % m.group(1)), -) +normalizers = ( Normalizer( re.compile(r'(?:https?://)?(?:[a-zA-Z0-9\-]+\.)?(?:amazon|amzn){1}\.(?P<tld>[a-zA-Z\.]{2,})\/(gp/(?:product|offer-listing|customer-media/product-gallery)/|exec/obidos/tg/detail/-/|o/ASIN/|dp/|(?:[A-Za-z0-9\-]+)/dp/)?(?P<ASIN>[0-9A-Za-z]{10})'), + lambda m: r'http://amazon.%s/dp/%s' % (m.group('tld'), m.group('ASIN'))), + Normalizer( re.compile(r'.*waffleimages\.com.*/([0-9a-fA-F]{40})'), + lambda m: r'http://img.waffleimages.com/%s' % m.group(1) ), + Normalizer( re.compile(r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)'), + lambda m: r'http://youtube.com/watch?v=%s' % m.group(1) ), + ) def normalize(url, assume_scheme=False): @@ -80,13 +78,12 @@ def normalize(url, assume_scheme=False): def clean(string): string = unicode(unquote(string), 'utf-8', 'replace') return unicodedata.normalize('NFC', string).encode('utf-8') - path = quote(clean(path), "~:/?#[]@!$&'()*+,;=") fragment = quote(clean(fragment), "~") # note care must be taken to only encode & and = characters as values query = "&".join(["=".join([quote(clean(t), "~:/?#[]@!$'()*+,;=") - for t in q.split("=", 1)]) for q in query.split("&")]) + for t in q.split("=", 1)]) for q in query.split("&")]) # Prevent dot-segments appearing in non-relative URI paths. if scheme in ["", "http", "https", "ftp", "file"]: @@ -131,7 +128,7 @@ def normalize(url, assume_scheme=False): if url.endswith("#") and query == "" and fragment == "": path += "#" normal_url = urlparse.urlunsplit((scheme, auth, path, query, - fragment)).replace("http:///", "http://") + fragment)).replace("http:///", "http://") for norm in normalizers: m = norm.regex.match(normal_url) if m: diff --git a/util/web.py b/plugins/util/web.py old mode 100644 new mode 100755 similarity index 73% rename from util/web.py rename to plugins/util/web.py index 1180bca..834af69 --- a/util/web.py +++ b/plugins/util/web.py @@ -1,13 +1,11 @@ """ web.py - handy functions for web services """ -import http -import urlnorm -import json -import urllib +import http, urlnorm +import json, urllib import yql short_url = "http://is.gd/create.php" -paste_url = "http://hastebin.com" +paste_url = "http://paste.dmptr.com" yql_env = "http://datatables.org/alltables.env" YQL = yql.Public() @@ -23,7 +21,7 @@ class ShortenError(Exception): def isgd(url): - """ shortens a URL with the is.gd API """ + """ shortens a URL with the is.gd PAI """ url = urlnorm.normalize(url.encode('utf-8'), assume_scheme='http') params = urllib.urlencode({'format': 'json', 'url': url}) request = http.get_json("http://is.gd/create.php?%s" % params) @@ -34,19 +32,11 @@ def isgd(url): return request["shorturl"] -def try_isgd(url): - try: - out = isgd(url) - except (ShortenError, http.HTTPError): - out = url - return out - - -def haste(text, ext='txt'): +def haste(text): """ pastes text to a hastebin server """ page = http.get(paste_url + "/documents", post_data=text) data = json.loads(page) - return ("%s/%s.%s" % (paste_url, data['key'], ext)) + return("%s/%s.txt" % (paste_url, data['key'])) def query(query, params={}): diff --git a/disabled_stuff/validate.py b/plugins/validate.py old mode 100644 new mode 100755 similarity index 55% rename from disabled_stuff/validate.py rename to plugins/validate.py index 88022b7..93c6023 --- a/disabled_stuff/validate.py +++ b/plugins/validate.py @@ -1,8 +1,8 @@ -""" +''' Runs a given url through the w3c validator by Vladi -""" +''' from util import hook, http @@ -10,7 +10,7 @@ from util import hook, http @hook.command('w3c') @hook.command def validate(inp): - """validate <url> -- Runs url through the w3c markup validator.""" + "validate <url> -- Runs url through the w3c markup validator." if not inp.startswith('http://'): inp = 'http://' + inp @@ -20,7 +20,7 @@ def validate(inp): status = info['x-w3c-validator-status'].lower() if status in ("valid", "invalid"): - error_count = info['x-w3c-validator-errors'] - warning_count = info['x-w3c-validator-warnings'] - return "{} was found to be {} with {} errors and {} warnings." \ - " see: {}".format(inp, status, error_count, warning_count, url) + errorcount = info['x-w3c-validator-errors'] + warningcount = info['x-w3c-validator-warnings'] + return "%s was found to be %s with %s errors and %s warnings." \ + " see: %s" % (inp, status, errorcount, warningcount, url) diff --git a/plugins/vimeo.py b/plugins/vimeo.py new file mode 100755 index 0000000..6edb990 --- /dev/null +++ b/plugins/vimeo.py @@ -0,0 +1,15 @@ +from util import hook, http + + +@hook.regex(r'vimeo.com/([0-9]+)') +def vimeo_url(match): + "vimeo <url> -- returns information on the Vimeo video at <url>" + info = http.get_json('http://vimeo.com/api/v2/video/%s.json' + % match.group(1)) + + if info: + return ("\x02%(title)s\x02 - length \x02%(duration)ss\x02 - " + "\x02%(stats_number_of_likes)s\x02 likes - " + "\x02%(stats_number_of_plays)s\x02 plays - " + "\x02%(user_name)s\x02 on \x02%(upload_date)s\x02" + % info[0]) diff --git a/plugins/violence.py b/plugins/violence.py new file mode 100755 index 0000000..6100bec --- /dev/null +++ b/plugins/violence.py @@ -0,0 +1,78 @@ +from util import hook +import random + +with open("plugins/data/larts.txt") as f: + larts = [line.strip() for line in f.readlines() + if not line.startswith("//")] + +with open("plugins/data/slaps.txt") as f: + slaps = [line.strip() for line in f.readlines() + if not line.startswith("//")] + +with open("plugins/data/slap_items.txt") as f: + items = [line.strip() for line in f.readlines() + if not line.startswith("//")] + +with open("plugins/data/kills.txt") as f: + kills = [line.strip() for line in f.readlines() + if not line.startswith("//")] + + +@hook.command +def slap(inp, me=None, nick=None, conn=None, notice=None): + "slap <user> -- Makes the bot slap <user>." + target = inp.strip() + + if " " in target: + notice("Invalid username!") + return + + # if the user is trying to make the bot slap itself, slap them + if target.lower() == conn.nick.lower() or target.lower() == "itself": + target = nick + + values = {"item": random.choice(items), "user": target} + phrase = random.choice(slaps) + + # act out the message + me(phrase.format(**values)) + + +@hook.command +def lart(inp, me=None, nick=None, conn=None, notice=None): + "lart <user> -- LARTs <user>." + target = inp.strip() + + if " " in target: + notice("Invalid username!") + return + + # if the user is trying to make the bot slap itself, slap them + if target.lower() == conn.nick.lower() or target.lower() == "itself": + target = nick + + values = {"user": target} + phrase = random.choice(larts) + + # act out the message + me(phrase.format(**values)) + + +@hook.command +def kill(inp, me=None, nick=None, conn=None, notice=None): + "kill <user> -- Makes the bot kill <user>." + target = inp.strip() + + if " " in target: + notice("Invalid username!") + return + + # if the user is trying to make the bot slap itself, slap them + if target.lower() == conn.nick.lower() or target.lower() == "itself": + target = nick + + values = {"user": target} + phrase = random.choice(kills) + + # act out the message + me(phrase.format(**values)) diff --git a/plugins/weather.py b/plugins/weather.py new file mode 100755 index 0000000..b235e3b --- /dev/null +++ b/plugins/weather.py @@ -0,0 +1,133 @@ +from util import hook, web + + +def get_weather(location): + """uses the yahoo weather API to get weather information for a location""" + + query = "SELECT * FROM weather.bylocation WHERE location=@location LIMIT 1" + result = web.query(query, {"location": location}) + + data = result.rows[0]["rss"]["channel"] + + # wind conversions + data['wind']['chill_c'] = int(round((int(data['wind']['chill']) - 32) / 1.8, 0)) + data['wind']['speed_kph'] = int(round(float(data['wind']['speed']) * 1.609344)) + + # textual wind direction + direction = data['wind']['direction'] + if direction >= 0 and direction < 45: + data['wind']['text'] = 'N' + elif direction >= 45 and direction < 90: + data['wind']['text'] = 'NE' + elif direction >= 90 and direction < 135: + data['wind']['text'] = 'E' + elif direction >= 135 and direction < 180: + data['wind']['text'] = 'SE' + elif direction >= 180 and direction < 225: + data['wind']['text'] = 'S' + elif direction >= 225 and direction < 270: + data['wind']['text'] = 'SW' + elif direction >= 270 and direction < 315: + data['wind']['text'] = 'W' + elif direction >= 315 and direction < 360: + data['wind']['text'] = 'NW' + else: + data['wind']['text'] = 'N' + + # visibility and pressure conversions + data['atmosphere']['visibility_km'] = \ + int(round(float(data['atmosphere']['visibility']) * 1.609344)) + data['atmosphere']['visibility_km'] = \ + str(round((float(data['atmosphere']['visibility']) * 33.8637526), 2)) + + # textual value for air pressure + rising = data['atmosphere']['rising'] + if rising == 0: + data['atmosphere']['tendancy'] = 'steady' + elif rising == 1: + data['atmosphere']['tendancy'] = 'rising' + elif rising == 2: + data['atmosphere']['tendancy'] = 'falling' + + # current conditions + data['item']['condition']['temp_c'] = \ + int(round(((float(data['item']['condition']['temp']) - 32) / 9) * 5)) + + # forecasts + for i in data['item']['forecast']: + i['high_c'] = \ + int(round(((float(i['high']) - 32) / 9) * 5)) + i['low_c'] = \ + int(round(((float(i['low']) - 32) / 9) * 5)) + + return data + + +@hook.command(autohelp=False) +def weather(inp, nick="", reply=None, db=None, notice=None): + "weather <location> [dontsave] -- Gets weather data"\ + " for <location> from Yahoo." + + # initalise weather DB + db.execute("create table if not exists weather(nick primary key, loc)") + + # if there is no input, try getting the users last location from the DB + if not inp: + location = db.execute("select loc from weather where nick=lower(?)", + [nick]).fetchone() + if not location: + # no location saved in the database, send the user help text + notice(weather.__doc__) + return + location = location[0] + + # no need to save a location, we already have it + dontsave = True + else: + # see if the input ends with "dontsave" + dontsave = inp.endswith(" dontsave") + + # remove "dontsave" from the input string after checking for it + if dontsave: + location = inp[:-9].strip().lower() + else: + location = inp + + # now, to get the actual weather + try: + data = get_weather(location) + except KeyError: + return "Could not get weather for that location." + + # put all the stuff we want to use in a dictionary for easy formatting of the output + weather_data = { + "place": data['location']['city'], + "conditions": data['item']['condition']['text'], + "temp_f": data['item']['condition']['temp'], + "temp_c": data['item']['condition']['temp_c'], + "humidity": data['atmosphere']['humidity'], + "wind_kph": data['wind']['speed_kph'], + "wind_mph": data['wind']['speed'], + "wind_text": data['wind']['text'], + "forecast": data['item']['forecast'][0]['text'], + "high_f": data['item']['forecast'][0]['high'], + "high_c": data['item']['forecast'][0]['high_c'], + "low_f": data['item']['forecast'][0]['low'], + "low_c": data['item']['forecast'][0]['low_c'], + "_forecast": data['item']['forecast'][1]['text'], + "_high_f": data['item']['forecast'][1]['high'], + "_high_c": data['item']['forecast'][1]['high_c'], + "_low_f": data['item']['forecast'][1]['low'], + "_low_c": data['item']['forecast'][1]['low_c'] + } + + reply("\x02{place}\x02 - \x02Current:\x02 {conditions}, {temp_f}F/{temp_c}C, Humidity: {humidity}%, " \ + "Wind: {wind_kph}KPH/{wind_mph}MPH {wind_text}, \x02Today:\x02 {forecast}, " \ + "High: {high_f}F/{high_c}C, Low: {low_f}F/{low_c}C." \ + "\x02Tomorrow:\x02 {_forecast}, High: {_high_f}F" \ + "/{_high_c}C, Low: {_low_f}F/{_low_c}C.".format(**weather_data)) + + if location and not dontsave: + db.execute("insert or replace into weather(nick, loc) values (?,?)", + (nick.lower(), location)) + db.commit() \ No newline at end of file diff --git a/disabled_stuff/wikipedia.py b/plugins/wikipedia.py old mode 100644 new mode 100755 similarity index 68% rename from disabled_stuff/wikipedia.py rename to plugins/wikipedia.py index 90461f4..f82dd6a --- a/disabled_stuff/wikipedia.py +++ b/plugins/wikipedia.py @@ -1,5 +1,5 @@ -"""Searches wikipedia and returns first sentence of article -Scaevolus 2009""" +'''Searches wikipedia and returns first sentence of article +Scaevolus 2009''' import re @@ -15,14 +15,14 @@ paren_re = re.compile('\s*\(.*\)$') @hook.command('w') @hook.command def wiki(inp): - """wiki <phrase> -- Gets first sentence of Wikipedia article on <phrase>.""" + "wiki <phrase> -- Gets first sentence of Wikipedia article on <phrase>." x = http.get_xml(search_url, search=inp) ns = '{http://opensearch.org/searchsuggest2}' items = x.findall(ns + 'Section/' + ns + 'Item') - if not items: + if items == []: if x.find('error') is not None: return 'error: %(code)s: %(info)s' % x.find('error').attrib else: @@ -30,7 +30,7 @@ def wiki(inp): def extract(item): return [item.find(ns + x).text for x in - ('Text', 'Description', 'Url')] + ('Text', 'Description', 'Url')] title, desc, url = extract(items[0]) @@ -42,8 +42,8 @@ def wiki(inp): if title.lower() not in desc.lower(): desc = title + desc - desc = u' '.join(desc.split()) # remove excess spaces + desc = re.sub('\s+', ' ', desc).strip() # remove excess spaces - desc = text.truncate_str(desc, 200) + desc = text.truncate_str(desc, 250) - return u'{} :: {}'.format(desc, http.quote(url, ':/')) + return '%s -- %s' % (desc, http.quote(url, ':/')) diff --git a/disabled_stuff/wolframalpha.py b/plugins/wolframalpha.py old mode 100644 new mode 100755 similarity index 80% rename from disabled_stuff/wolframalpha.py rename to plugins/wolframalpha.py index b20ffed..0040817 --- a/disabled_stuff/wolframalpha.py +++ b/plugins/wolframalpha.py @@ -3,12 +3,11 @@ import re from util import hook, http, text, web -@hook.command('math') -@hook.command('calc') @hook.command('wa') @hook.command def wolframalpha(inp, bot=None): - """wa <query> -- Computes <query> using Wolfram Alpha.""" + "wa <query> -- Computes <query> using Wolfram Alpha." + api_key = bot.config.get("api_keys", {}).get("wolframalpha", None) if not api_key: @@ -21,7 +20,10 @@ def wolframalpha(inp, bot=None): # get the URL for a user to view this query in a browser query_url = "http://www.wolframalpha.com/input/?i=" + \ http.quote_plus(inp.encode('utf-8')) - short_url = web.try_isgd(query_url) + try: + short_url = web.isgd(query_url) + except (web.ShortenError, http.HTTPError): + short_url = query_url pod_texts = [] for pod in result.xpath("//pod[@primary='true']"): @@ -36,9 +38,9 @@ def wolframalpha(inp, bot=None): if subpod: results.append(subpod) if results: - pod_texts.append(title + u': ' + u', '.join(results)) + pod_texts.append(title + ': ' + ', '.join(results)) - ret = u' - '.join(pod_texts) + ret = ' - '.join(pod_texts) if not pod_texts: return 'No results.' @@ -55,4 +57,4 @@ def wolframalpha(inp, bot=None): if not ret: return 'No results.' - return u"{} - {}".format(ret, short_url) + return "%s - %s" % (ret, short_url) diff --git a/plugins/youtube.py b/plugins/youtube.py new file mode 100755 index 0000000..665ee8e --- /dev/null +++ b/plugins/youtube.py @@ -0,0 +1,85 @@ +import re +import time + +from util import hook, http + + +youtube_re = (r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)' + '([-_a-zA-Z0-9]+)', re.I) + +base_url = 'http://gdata.youtube.com/feeds/api/' +api_url = base_url + 'videos/{}?v=2&alt=jsonc' +search_api_url = base_url + 'videos?v=2&alt=jsonc&max-results=1' +video_url = "http://youtu.be/%s" + + +def get_video_description(video_id): + request = http.get_json(api_url.format(video_id)) + + if request.get('error'): + return + + data = request['data'] + + out = '\x02%s\x02' % data['title'] + + if not data.get('duration'): + return out + + out += ' - length \x02' + length = data['duration'] + if length / 3600: # > 1 hour + out += '%dh ' % (length / 3600) + if length / 60: + out += '%dm ' % (length / 60 % 60) + out += "%ds\x02" % (length % 60) + + if 'rating' in data: + out += ' - rated \x02%.2f/5.0\x02 (%d)' % (data['rating'], + data['ratingCount']) + + if 'viewCount' in data: + out += ' - \x02%s\x02 views' % format(data['viewCount'], ",d") + + upload_time = time.strptime(data['uploaded'], "%Y-%m-%dT%H:%M:%S.000Z") + out += ' - \x02%s\x02 on \x02%s\x02' % (data['uploader'], + time.strftime("%Y.%m.%d", upload_time)) + + if 'contentRating' in data: + out += ' - \x034NSFW\x02' + + return out + + +def GetInHMS(seconds): + hours = seconds / 3600 + seconds -= 3600 * hours + minutes = seconds / 60 + seconds -= 60 * minutes + if hours == 0: + return "%02d:%02d" % (minutes, seconds) + return "%02d:%02d:%02d" % (hours, minutes, seconds) + + +@hook.regex(*youtube_re) +def youtube_url(match): + return get_video_description(match.group(1)) + + +@hook.command('yt') +@hook.command('y') +@hook.command +def youtube(inp): + "youtube <query> -- Returns the first YouTube search result for <query>." + + request = http.get_json(search_api_url, q=inp) + + if 'error' in request: + return 'error performing search' + + if request['data']['totalItems'] == 0: + return 'no results found' + + video_id = request['data']['items'][0]['id'] + + return get_video_description(video_id) + " - " + video_url % video_id diff --git a/requirements.txt b/requirements.txt index ada6c3c..91c1f29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,3 @@ -GitPython==0.3.2.RC1 -oauth2 -pygeoip -tweepy BeautifulSoup==3.2.1 lxml==3.1beta1 pyenchant==1.6.5 -pydns>=2.3.6 -BeautifulSoup4 -pycrypto diff --git a/restartcloudbot.sh b/restartcloudbot.sh deleted file mode 100755 index 25f2324..0000000 --- a/restartcloudbot.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -cd $(dirname $0) - -( -while true -do - sudo -u cloudbot ./cloudbot.py 2>&1 | logger -s -t $(basename $0) - sleep 10 -done -) & -disown $! diff --git a/util/bucket.py b/util/bucket.py deleted file mode 100644 index 3a4a699..0000000 --- a/util/bucket.py +++ /dev/null @@ -1,40 +0,0 @@ -from time import time - - -class TokenBucket(object): - """An implementation of the token bucket algorithm. - - >>> bucket = TokenBucket(80, 0.5) - >>> print bucket.consume(10) - True - >>> print bucket.consume(90) - False - """ - def __init__(self, tokens, fill_rate): - """tokens is the total tokens in the bucket. fill_rate is the - rate in tokens/second that the bucket will be refilled.""" - self.capacity = float(tokens) - self._tokens = float(tokens) - self.fill_rate = float(fill_rate) - self.timestamp = time() - - def consume(self, tokens): - """Consume tokens from the bucket. Returns True if there were - sufficient tokens otherwise False.""" - if tokens <= self.tokens: - self._tokens -= tokens - else: - return False - return True - - def refill(self): - self._tokens = self.capacity - - def get_tokens(self): - now = time() - if self._tokens < self.capacity: - delta = self.fill_rate * (now - self.timestamp) - self._tokens = min(self.capacity, self._tokens + delta) - self.timestamp = now - return self._tokens - tokens = property(get_tokens) \ No newline at end of file diff --git a/util/textgen.py b/util/textgen.py deleted file mode 100644 index 9954f17..0000000 --- a/util/textgen.py +++ /dev/null @@ -1,51 +0,0 @@ -import re -import random - -TEMPLATE_RE = re.compile(r"\{(.+?)\}") - - -class TextGenerator(object): - def __init__(self, templates, parts, default_templates=None, variables=None): - self.templates = templates - self.default_templates = default_templates - self.parts = parts - self.variables = variables - - def generate_string(self, template=None): - """ - Generates one string using the specified templates. - If no templates are specified, use a random template from the default_templates list. - """ - # this is bad - if self.default_templates: - text = self.templates[template or random.choice(self.default_templates)] - else: - text = random.choice(self.templates) - - # replace static variables in the template with provided values - if self.variables: - for key, value in self.variables.items(): - text = text.replace("{%s}" % key, value) - - # get a list of all text parts we need - required_parts = TEMPLATE_RE.findall(text) - - for required_part in required_parts: - ppart = self.parts[required_part] - # check if the part is a single string or a list - if not isinstance(ppart, basestring): - part = random.choice(self.parts[required_part]) - else: - part = self.parts[required_part] - text = text.replace("{%s}" % required_part, part) - - return text - - def generate_strings(self, amount, template=None): - strings = [] - for i in xrange(amount): - strings.append(self.generate_string()) - return strings - - def get_template(self, template): - return self.templates[template] diff --git a/util/timeformat.py b/util/timeformat.py deleted file mode 100644 index 7ec6abf..0000000 --- a/util/timeformat.py +++ /dev/null @@ -1,66 +0,0 @@ -from util import text - -def format_time(seconds, count=3, accuracy=6, simple=False): - """ - Takes a length of time in seconds and returns a string describing that length of time. - This function has a number of optional arguments that can be combined: - - SIMPLE: displays the time in a simple format - >>> format_time(SECONDS) - 1 hour, 2 minutes and 34 seconds - >>> format_time(SECONDS, simple=True) - 1h 2m 34s - - COUNT: how many periods should be shown (default 3) - >>> format_time(SECONDS) - 147 years, 9 months and 8 weeks - >>> format_time(SECONDS, count=6) - 147 years, 9 months, 7 weeks, 18 hours, 12 minutes and 34 seconds - """ - - if simple: - periods = [ - ('c', 60 * 60 * 24 * 365 * 100), - ('de', 60 * 60 * 24 * 365 * 10), - ('y', 60 * 60 * 24 * 365), - ('m', 60 * 60 * 24 * 30), - ('d', 60 * 60 * 24), - ('h', 60 * 60), - ('m', 60), - ('s', 1) - ] - else: - periods = [ - (('century', 'centuries'), 60 * 60 * 24 * 365 * 100), - (('decade', 'decades'), 60 * 60 * 24 * 365 * 10), - (('year', 'years'), 60 * 60 * 24 * 365), - (('month', 'months'), 60 * 60 * 24 * 30), - (('day', 'days'), 60 * 60 * 24), - (('hour', 'hours'), 60 * 60), - (('minute', 'minutes'), 60), - (('second', 'seconds'), 1) - ] - - periods = periods[-accuracy:] - - strings = [] - i = 0 - for period_name, period_seconds in periods: - if i < count: - if seconds > period_seconds: - period_value, seconds = divmod(seconds, period_seconds) - i += 1 - if simple: - strings.append("{}{}".format(period_value, period_name)) - else: - if period_value == 1: - strings.append("{} {}".format(period_value, period_name[0])) - else: - strings.append("{} {}".format(period_value, period_name[1])) - else: - break - - if simple: - return " ".join(strings) - else: - return text.get_text_list(strings, "and") \ No newline at end of file