Compare commits
6 Commits
ChaosChemn
...
feature/sn
Author | SHA1 | Date |
---|---|---|
Luke Rogers | 16210639bc | |
Luke Rogers | 8d8260a2f7 | |
Luke Rogers | 971e8e4066 | |
nasonfish | 64974dc355 | |
nasonfish | 560f2e7516 | |
nasonfish | b648706bfd |
|
@ -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
|
|
@ -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
|
|
@ -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)
|
34
CONTRIBUTORS
34
CONTRIBUTORS
|
@ -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 :)
|
|
@ -0,0 +1 @@
|
|||
Please see the wiki @ http://git.io/cloudbotircwiki
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
## Notes
|
||||
|
||||
³ eventually
|
||||
|
|
|
@ -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 <http://git.io/cloudbotirc>'
|
||||
print 'CloudBot %s (%s) <http://git.io/cloudbotirc>' % (__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()
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
2
disabled_stuff/mygengo_translate.py → disabled_plugins/mygengo_translate.py
Normal file → Executable file
2
disabled_stuff/mygengo_translate.py → disabled_plugins/mygengo_translate.py
Normal file → Executable file
|
@ -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
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import json
|
||||
import random
|
||||
import re
|
||||
|
||||
from util import hook, http
|
||||
|
||||
|
||||
@hook.command
|
||||
def suggest(inp, inp_unstripped=''):
|
||||
".suggest [#n] <phrase> -- 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('<b>', '').replace('</b>', ''))
|
|
@ -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 <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
|
||||
action(phrase.format(**values))
|
||||
|
||||
|
||||
@hook.command
|
||||
def insult(inp, nick=None, action=None, conn=None, notice=None):
|
||||
"""insult <user> -- Makes the bot insult <user>."""
|
||||
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 <user> -- Make the bot flirt with <user>."""
|
||||
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)
|
|
@ -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
|
||||
'''
|
|
@ -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)
|
||||
|
|
@ -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 <exchange> -- 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']))
|
|
@ -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 <pass> <string> -- Cyphers <string> with <password>."""
|
||||
|
||||
passwd = inp.split(" ")[0]
|
||||
inp = " ".join(inp.split(" ")[1:])
|
||||
return encode(passwd, inp)
|
||||
|
||||
|
||||
@hook.command
|
||||
def decypher(inp):
|
||||
"""decypher <pass> <string> -- Decyphers <string> with <password>."""
|
||||
passwd = inp.split(" ")[0]
|
||||
inp = " ".join(inp.split(" ")[1:])
|
||||
return decode(passwd, inp)
|
|
@ -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)
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
from util import hook, http
|
||||
|
||||
|
||||
@hook.command
|
||||
def domainr(inp):
|
||||
"""domainr <domain> - 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
|
|
@ -1,23 +0,0 @@
|
|||
import random
|
||||
|
||||
from util import hook, text
|
||||
|
||||
|
||||
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(inp, action=None):
|
||||
"""8ball <question> -- 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))
|
|
@ -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
|
||||
# <http://stackoverflow.com/questions/12524994/encrypt-decrypt-using-pycrypto-aes-256>
|
||||
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 <pass> <string> -- Encrypts <string> with <pass>. (<string> 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 <pass> <string> -- Decrypts <string> with <pass>. (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)
|
|
@ -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 <user> -- Gets information on <user>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 <user> -- Gets a count of <user>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)
|
|
@ -1,13 +0,0 @@
|
|||
from util import hook, http, web
|
||||
from subprocess import check_output, CalledProcessError
|
||||
|
||||
@hook.command
|
||||
def freddycode(inp):
|
||||
"""freddycode <code> - 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))
|
||||
|
|
@ -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 <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:]
|
|
@ -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]] <sentence> -- translates
|
||||
<sentence> 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")
|
||||
]
|
|
@ -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!"
|
|
@ -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 <nick> <channel> -- 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)
|
|
@ -1,56 +0,0 @@
|
|||
# Plugin by Infinity - <https://github.com/infinitylabs/UguuBot>
|
||||
|
||||
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 <sign> -- 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 <sign> -- 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
|
|
@ -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> - 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))
|
|
@ -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 <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/{}'.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.'
|
|
@ -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 <subreddit> 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))
|
|
@ -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."
|
|
@ -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)
|
|
@ -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 <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 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())
|
|
@ -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> - 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(" ", "+")
|
|
@ -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 <slug/name> - 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)
|
|
@ -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 <server>[: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)
|
|
@ -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")
|
|
@ -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 <username> -- Gets information about the Minecraft user <account>."""
|
||||
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."
|
|
@ -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 <phrase> -- Gets the first paragraph of
|
||||
the Minecraft Wiki article on <phrase>."""
|
||||
|
||||
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."
|
|
@ -1,34 +0,0 @@
|
|||
# Plugin by Infinity - <https://github.com/infinitylabs/UguuBot>
|
||||
|
||||
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()
|
|
@ -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'))
|
|
@ -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 <item name> -- Searches newegg.com for <item name>"""
|
||||
|
||||
# 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."
|
||||
|
||||
|
|
@ -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
|
|
@ -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 <github user> -- Gets an Open Source Report Card for <github user>"""
|
||||
|
||||
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)
|
|
@ -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)"
|
|
@ -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<EFBFBD>', '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 <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'])
|
||||
|
||||
action("{} a {} {} {} potato for {} and serves it with a small {}!".format(method, flavor, size, potato_type, inp,
|
||||
side_dish))
|
|
@ -1,38 +0,0 @@
|
|||
import datetime
|
||||
|
||||
from util import hook, http, timesince
|
||||
|
||||
|
||||
@hook.command("scene")
|
||||
@hook.command
|
||||
def pre(inp):
|
||||
"""pre <query> -- 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)
|
|
@ -1,9 +0,0 @@
|
|||
from util import hook
|
||||
from util.pyexec import eval_py
|
||||
|
||||
|
||||
@hook.command
|
||||
def python(inp):
|
||||
"""python <prog> -- Executes <prog> as Python code."""
|
||||
|
||||
return eval_py(inp)
|
|
@ -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)
|
|
@ -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 <search term> - 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 term> - 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 term> - 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 term> - 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)
|
|
@ -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"] == "<http://schema.org/Recipe>":
|
||||
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))
|
|
@ -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 <subreddit> [n] -- Gets a random post from <subreddit>, 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)
|
|
@ -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)
|
|
@ -1,38 +0,0 @@
|
|||
from util import hook, http
|
||||
|
||||
|
||||
@hook.command('god')
|
||||
@hook.command
|
||||
def bible(inp):
|
||||
""".bible <passage> -- gets <passage> 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 <chapter.verse> -- gets <chapter.verse> 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()
|
|
@ -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 <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
|
||||
|
||||
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())
|
|
@ -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)
|
|
@ -1,47 +0,0 @@
|
|||
from enchant.checker import SpellChecker
|
||||
import enchant
|
||||
|
||||
from util import hook
|
||||
|
||||
|
||||
locale = "en_US"
|
||||
|
||||
|
||||
@hook.command
|
||||
def spell(inp):
|
||||
"""spell <word/sentence> -- 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)
|
|
@ -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("<br/>")[0]
|
||||
return message
|
||||
else:
|
||||
return web.try_isgd(inp)
|
||||
|
||||
|
||||
@hook.command('sptrack')
|
||||
@hook.command
|
||||
def spotify(inp):
|
||||
"""spotify <song> -- Search Spotify for <song>"""
|
||||
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 <album> -- Search Spotify for <album>"""
|
||||
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 <artist> -- Search Spotify for <artist>"""
|
||||
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)))
|
|
@ -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)
|
||||
|
|
@ -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 <b></b> tag in details_block
|
||||
for b in details.findAll('b'):
|
||||
# get the contents of the <b></b> 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 <b></b> 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 <b> and <a> tags.
|
||||
# so we find the next <a> 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 <a></a> tag
|
||||
if isinstance(next_element, Tag) and next_element.name == 'a':
|
||||
text = next_element.string.strip()
|
||||
if text:
|
||||
# we found valid text (in the <a></a> 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'])
|
|
@ -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 <username> [currency] - Gets value of steam account and
|
||||
total hours played. Uses steamcommunity.com/id/<nickname>. """
|
||||
|
||||
# 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("<br/>", ": ") # 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)
|
|
@ -1,30 +0,0 @@
|
|||
from util import hook, web
|
||||
|
||||
|
||||
@hook.command
|
||||
def stock(inp):
|
||||
"""stock <symbol> -- 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)
|
|
@ -1,19 +0,0 @@
|
|||
from util import hook, http, text
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
@hook.command
|
||||
def suggest(inp):
|
||||
"""suggest <phrase> -- 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)
|
|
@ -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 <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 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!")
|
|
@ -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 <area> -- Gets the time in <area>"""
|
||||
|
||||
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
|
|
@ -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))
|
|
@ -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 <user> [n] -- Gets last/[n]th tweet from <user>"""
|
||||
|
||||
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 <user> -- Get info on the Twitter user <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)
|
|
@ -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")
|
|
@ -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 <phrase> [id] -- Looks up <phrase> 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
|
|
@ -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 <string> -- Capitalizes <string>."""
|
||||
return inp.capitalize()
|
||||
|
||||
|
||||
@hook.command
|
||||
def upper(inp):
|
||||
"""upper <string> -- Convert string to uppercase."""
|
||||
return inp.upper()
|
||||
|
||||
|
||||
@hook.command
|
||||
def lower(inp):
|
||||
"""lower <string> -- Convert string to lowercase."""
|
||||
return inp.lower()
|
||||
|
||||
|
||||
@hook.command
|
||||
def titlecase(inp):
|
||||
"""title <string> -- Convert string to title case."""
|
||||
return inp.title()
|
||||
|
||||
|
||||
@hook.command
|
||||
def swapcase(inp):
|
||||
"""swapcase <string> -- Swaps the capitalization of <string>."""
|
||||
return inp.swapcase()
|
||||
|
||||
|
||||
# encoding
|
||||
|
||||
|
||||
@hook.command
|
||||
def rot13(inp):
|
||||
"""rot13 <string> -- Encode <string> with rot13."""
|
||||
return inp.encode('rot13')
|
||||
|
||||
|
||||
@hook.command
|
||||
def base64(inp):
|
||||
"""base64 <string> -- Encode <string> with base64."""
|
||||
return inp.encode('base64')
|
||||
|
||||
|
||||
@hook.command
|
||||
def unbase64(inp):
|
||||
"""unbase64 <string> -- Decode <string> 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 <string> -- Unescapes <string>."""
|
||||
try:
|
||||
return inp.decode('unicode-escape')
|
||||
except Exception as e:
|
||||
return "Error: {}".format(e)
|
||||
|
||||
|
||||
@hook.command
|
||||
def escape(inp):
|
||||
"""escape <string> -- Escapes <string>."""
|
||||
try:
|
||||
return inp.encode('unicode-escape')
|
||||
except Exception as e:
|
||||
return "Error: {}".format(e)
|
||||
|
||||
|
||||
# length
|
||||
|
||||
|
||||
@hook.command
|
||||
def length(inp):
|
||||
"""length <string> -- gets the length of <string>"""
|
||||
return "The length of that string is {} characters.".format(len(inp))
|
||||
|
||||
|
||||
# reverse
|
||||
|
||||
|
||||
@hook.command
|
||||
def reverse(inp):
|
||||
"""reverse <string> -- reverses <string>."""
|
||||
return inp[::-1]
|
||||
|
||||
|
||||
# hashing
|
||||
|
||||
|
||||
@hook.command("hash")
|
||||
def hash_command(inp):
|
||||
"""hash <string> -- Returns hashes of <string>."""
|
||||
return ', '.join(x + ": " + getattr(hashlib, x)(inp).hexdigest()
|
||||
for x in ['md5', 'sha1', 'sha256'])
|
||||
|
||||
|
||||
# novelty
|
||||
|
||||
|
||||
@hook.command
|
||||
def munge(inp):
|
||||
"""munge <text> -- Munges up <text>."""
|
||||
return text.munge(inp)
|
||||
|
||||
|
||||
# colors - based on code by Reece Selwood - <https://github.com/hitzler/homero>
|
||||
|
||||
|
||||
@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
|
|
@ -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 <quote> - 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 <quote> - 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 <quote> - Look up Portal 2 Perpetual Testing Inititive quote.
|
||||
Example: .portal2 Cave here."""
|
||||
return get_sound_info("portal2dlc2", inp)
|
||||
|
||||
|
||||
@hook.command
|
||||
def portal2music(inp):
|
||||
"""portal2music <title> - 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)
|
|
@ -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])
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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())
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue