Compare commits
6 commits
ChaosChemn
...
feature/sn
Author | SHA1 | Date | |
---|---|---|---|
![]() |
16210639bc | ||
![]() |
8d8260a2f7 | ||
![]() |
971e8e4066 | ||
![]() |
64974dc355 | ||
![]() |
560f2e7516 | ||
![]() |
b648706bfd |
241 changed files with 5032 additions and 13024 deletions
|
@ -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
|
|
4
.gitignore
vendored
Normal file → Executable file
4
.gitignore
vendored
Normal file → Executable file
|
@ -1,6 +1,5 @@
|
||||||
persist
|
persist
|
||||||
config
|
config
|
||||||
config.ssl
|
|
||||||
gitflow
|
gitflow
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
|
@ -8,8 +7,7 @@ gitflow
|
||||||
*.pyc
|
*.pyc
|
||||||
*.orig
|
*.orig
|
||||||
.project
|
.project
|
||||||
|
.pydevproject
|
||||||
.geany
|
.geany
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
.idea/
|
|
||||||
plugins/data/GeoLiteCity.dat
|
|
||||||
|
|
|
@ -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 :)
|
|
1
DOCUMENTATION
Executable file
1
DOCUMENTATION
Executable file
|
@ -0,0 +1 @@
|
||||||
|
Please see the wiki @ http://git.io/cloudbotircwiki
|
0
LICENSE
Normal file → Executable file
0
LICENSE
Normal file → Executable file
87
README.md
Normal file → Executable file
87
README.md
Normal file → Executable file
|
@ -1,8 +1,21 @@
|
||||||
# CloudBot
|
# CloudBot/DEV
|
||||||
|
|
||||||
## About
|
## 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
|
## Getting and using CloudBot
|
||||||
|
|
||||||
|
@ -14,33 +27,51 @@ Unzip the resulting file, and continue to read this document.
|
||||||
|
|
||||||
### Install
|
### 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.
|
Before you can run the bot, you need to install a few Python dependencies. These can be installed with `pip` (The Python package manager):
|
||||||
|
|
||||||
|
|
||||||
These can be installed with `pip` (The Python package manager):
|
|
||||||
|
|
||||||
[sudo] pip install -r requirements.txt
|
[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`
|
#### How to install `pip`
|
||||||
|
|
||||||
curl -O http://python-distribute.org/distribute_setup.py # or download with your browser on windows
|
curl -O http://python-distribute.org/distribute_setup.py # or download with your browser on windows
|
||||||
python distribute_setup.py
|
python distribute_setup.py
|
||||||
easy_install pip
|
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
|
### 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`
|
`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
|
## 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).
|
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
|
### Support
|
||||||
|
|
||||||
The developers reside in [#CloudBot](irc://irc.esper.net/cloudbot) on [EsperNet](http://esper.net) and would be glad to help you.
|
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
|
### 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.
|
It **requires the Python module** `lXML`, and `Enchant` is needed for the spellcheck plugin.
|
||||||
The module `Enchant` is needed for the spellcheck plugin.
|
|
||||||
The module `PyDNS` is needed for SRV record lookup in the mcping 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
|
## 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
|
## License
|
||||||
|
|
||||||
CloudBot is **licensed** under the **GPL v3** license. The terms are as follows.
|
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
|
CloudBot is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
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
|
You should have received a copy of the GNU General Public License
|
||||||
along with CloudBot. If not, see <http://www.gnu.org/licenses/>.
|
along with CloudBot. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
³ eventually
|
||||||
|
|
|
@ -1,28 +1,45 @@
|
||||||
#!/usr/bin/env python
|
#!/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 os
|
||||||
import Queue
|
import Queue
|
||||||
import sys
|
import sys
|
||||||
import time
|
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
|
os.chdir(sys.path[0] or '.') # do stuff relative to the install directory
|
||||||
|
|
||||||
|
|
||||||
class Bot(object):
|
class Bot(object):
|
||||||
pass
|
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 = Bot()
|
||||||
bot.vars = {}
|
|
||||||
|
|
||||||
# record start time for the uptime command
|
|
||||||
bot.start_time = time.time()
|
bot.start_time = time.time()
|
||||||
|
|
||||||
print 'Begin Plugin Loading.'
|
print 'Loading plugins...'
|
||||||
|
|
||||||
# bootstrap the reloader
|
# bootstrap the reloader
|
||||||
eval(compile(open(os.path.join('core', 'reload.py'), 'U').read(),
|
eval(compile(open(os.path.join('core', 'reload.py'), 'U').read(),
|
||||||
|
@ -39,9 +56,6 @@ bot.conns = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for name, conf in bot.config['connections'].iteritems():
|
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']
|
print 'Connecting to server: %s' % conf['server']
|
||||||
if conf.get('ssl'):
|
if conf.get('ssl'):
|
||||||
bot.conns[name] = SSLIRC(name, conf['server'], conf['nick'], conf=conf,
|
bot.conns[name] = SSLIRC(name, conf['server'], conf['nick'], conf=conf,
|
0
disabled_stuff/cloudbot.sh → cloudbot
Normal file → Executable file
0
disabled_stuff/cloudbot.sh → cloudbot
Normal file → Executable file
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
57
core/config.py
Normal file → Executable file
57
core/config.py
Normal file → Executable file
|
@ -7,8 +7,60 @@ def save(conf):
|
||||||
json.dump(conf, open('config', 'w'), sort_keys=True, indent=2)
|
json.dump(conf, open('config', 'w'), sort_keys=True, indent=2)
|
||||||
|
|
||||||
if not os.path.exists('config'):
|
if not os.path.exists('config'):
|
||||||
print "Please rename 'config.default' to 'config' to set up your bot!"
|
open('config', 'w').write(inspect.cleandoc(
|
||||||
print "For help, see http://git.io/cloudbotirc"
|
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!"
|
print "Thank you for using CloudBot!"
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
@ -25,3 +77,4 @@ def config():
|
||||||
|
|
||||||
|
|
||||||
bot._config_mtime = 0
|
bot._config_mtime = 0
|
||||||
|
|
||||||
|
|
2
core/db.py
Normal file → Executable file
2
core/db.py
Normal file → Executable file
|
@ -6,7 +6,7 @@ threaddbs = {}
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection(conn, name=''):
|
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:
|
if not name:
|
||||||
name = '{}.db'.format(conn.name)
|
name = '{}.db'.format(conn.name)
|
||||||
|
|
62
core/irc.py
Normal file → Executable file
62
core/irc.py
Normal file → Executable file
|
@ -17,18 +17,16 @@ def decode(txt):
|
||||||
|
|
||||||
|
|
||||||
def censor(text):
|
def censor(text):
|
||||||
text = text.replace('\n', '').replace('\r', '')
|
|
||||||
replacement = '[censored]'
|
replacement = '[censored]'
|
||||||
if 'censored_strings' in bot.config:
|
if 'censored_strings' in bot.config:
|
||||||
if bot.config['censored_strings']:
|
|
||||||
words = map(re.escape, bot.config['censored_strings'])
|
words = map(re.escape, bot.config['censored_strings'])
|
||||||
regex = re.compile('({})'.format("|".join(words)))
|
regex = re.compile('(%s)' % "|".join(words))
|
||||||
text = regex.sub(replacement, text)
|
text = regex.sub(replacement, text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
class crlf_tcp(object):
|
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):
|
def __init__(self, host, port, timeout=300):
|
||||||
self.ibuffer = ""
|
self.ibuffer = ""
|
||||||
|
@ -44,16 +42,7 @@ class crlf_tcp(object):
|
||||||
return socket.socket(socket.AF_INET, socket.TCP_NODELAY)
|
return socket.socket(socket.AF_INET, socket.TCP_NODELAY)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
noerror = 0
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
self.socket.connect((self.host, self.port))
|
self.socket.connect((self.host, self.port))
|
||||||
break
|
|
||||||
except socket.gaierror as e:
|
|
||||||
time.sleep(5)
|
|
||||||
except socket.timeout as e:
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
thread.start_new_thread(self.recv_loop, ())
|
thread.start_new_thread(self.recv_loop, ())
|
||||||
thread.start_new_thread(self.send_loop, ())
|
thread.start_new_thread(self.send_loop, ())
|
||||||
|
|
||||||
|
@ -64,20 +53,12 @@ class crlf_tcp(object):
|
||||||
return socket.timeout
|
return socket.timeout
|
||||||
|
|
||||||
def handle_receive_exception(self, error, last_timestamp):
|
def handle_receive_exception(self, error, last_timestamp):
|
||||||
print("Receive exception: %s" % (error))
|
|
||||||
if time.time() - last_timestamp > self.timeout:
|
if time.time() - last_timestamp > self.timeout:
|
||||||
print("Receive timeout. Restart connection.")
|
|
||||||
self.iqueue.put(StopIteration)
|
self.iqueue.put(StopIteration)
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
return True
|
return True
|
||||||
return False
|
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):
|
def recv_loop(self):
|
||||||
last_timestamp = time.time()
|
last_timestamp = time.time()
|
||||||
while True:
|
while True:
|
||||||
|
@ -96,8 +77,6 @@ class crlf_tcp(object):
|
||||||
if self.handle_receive_exception(e, last_timestamp):
|
if self.handle_receive_exception(e, last_timestamp):
|
||||||
return
|
return
|
||||||
continue
|
continue
|
||||||
except AttributeError:
|
|
||||||
return
|
|
||||||
|
|
||||||
while '\r\n' in self.ibuffer:
|
while '\r\n' in self.ibuffer:
|
||||||
line, self.ibuffer = self.ibuffer.split('\r\n', 1)
|
line, self.ibuffer = self.ibuffer.split('\r\n', 1)
|
||||||
|
@ -105,23 +84,16 @@ class crlf_tcp(object):
|
||||||
|
|
||||||
def send_loop(self):
|
def send_loop(self):
|
||||||
while True:
|
while True:
|
||||||
try:
|
|
||||||
line = self.oqueue.get().splitlines()[0][:500]
|
line = self.oqueue.get().splitlines()[0][:500]
|
||||||
if line == StopIteration:
|
|
||||||
return
|
|
||||||
print ">>> %r" % line
|
print ">>> %r" % line
|
||||||
self.obuffer += line.encode('utf-8', 'replace') + '\r\n'
|
self.obuffer += line.encode('utf-8', 'replace') + '\r\n'
|
||||||
while self.obuffer:
|
while self.obuffer:
|
||||||
sent = self.socket.send(self.obuffer)
|
sent = self.socket.send(self.obuffer)
|
||||||
self.obuffer = self.obuffer[sent:]
|
self.obuffer = self.obuffer[sent:]
|
||||||
|
|
||||||
except socket.error as e:
|
|
||||||
self.handle_send_exception(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
class crlf_ssl_tcp(crlf_tcp):
|
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):
|
def __init__(self, host, port, ignore_cert_errors, timeout=300):
|
||||||
self.ignore_cert_errors = ignore_cert_errors
|
self.ignore_cert_errors = ignore_cert_errors
|
||||||
crlf_tcp.__init__(self, host, port, timeout)
|
crlf_tcp.__init__(self, host, port, timeout)
|
||||||
|
@ -139,14 +111,10 @@ class crlf_ssl_tcp(crlf_tcp):
|
||||||
|
|
||||||
def handle_receive_exception(self, error, last_timestamp):
|
def handle_receive_exception(self, error, last_timestamp):
|
||||||
# this is terrible
|
# this is terrible
|
||||||
#if not "timed out" in error.args[0]:
|
if not "timed out" in error.args[0]:
|
||||||
# raise
|
raise
|
||||||
return crlf_tcp.handle_receive_exception(self, error, last_timestamp)
|
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_prefix_rem = re.compile(r'(.*?) (.*?) (.*)').match
|
||||||
irc_noprefix_rem = re.compile(r'()(.*?) (.*)').match
|
irc_noprefix_rem = re.compile(r'()(.*?) (.*)').match
|
||||||
irc_netmask_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):
|
class IRC(object):
|
||||||
"""handles the IRC protocol"""
|
"handles the IRC protocol"
|
||||||
|
|
||||||
def __init__(self, name, server, nick, port=6667, channels=[], conf={}):
|
def __init__(self, name, server, nick, port=6667, channels=[], conf={}):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.channels = channels
|
self.channels = channels
|
||||||
|
@ -163,8 +130,6 @@ class IRC(object):
|
||||||
self.server = server
|
self.server = server
|
||||||
self.port = port
|
self.port = port
|
||||||
self.nick = nick
|
self.nick = nick
|
||||||
self.history = {}
|
|
||||||
self.vars = {}
|
|
||||||
|
|
||||||
self.out = Queue.Queue() # responses from the server are placed here
|
self.out = Queue.Queue() # responses from the server are placed here
|
||||||
# format: [rawline, prefix, command, params,
|
# format: [rawline, prefix, command, params,
|
||||||
|
@ -200,7 +165,7 @@ class IRC(object):
|
||||||
else:
|
else:
|
||||||
prefix, command, params = irc_noprefix_rem(msg).groups()
|
prefix, command, params = irc_noprefix_rem(msg).groups()
|
||||||
nick, user, host = irc_netmask_rem(prefix).groups()
|
nick, user, host = irc_netmask_rem(prefix).groups()
|
||||||
mask = nick + "!" + user + "@" + host
|
mask = user + "@" + host
|
||||||
paramlist = irc_param_ref(params)
|
paramlist = irc_param_ref(params)
|
||||||
lastparam = ""
|
lastparam = ""
|
||||||
if paramlist:
|
if paramlist:
|
||||||
|
@ -223,7 +188,7 @@ class IRC(object):
|
||||||
|
|
||||||
def join(self, channel):
|
def join(self, channel):
|
||||||
""" makes the bot join a channel """
|
""" makes the bot join a channel """
|
||||||
self.send("JOIN {}".format(channel))
|
self.send("JOIN %s" % channel)
|
||||||
if channel not in self.channels:
|
if channel not in self.channels:
|
||||||
self.channels.append(channel)
|
self.channels.append(channel)
|
||||||
|
|
||||||
|
@ -234,18 +199,13 @@ class IRC(object):
|
||||||
self.channels.remove(channel)
|
self.channels.remove(channel)
|
||||||
|
|
||||||
def msg(self, target, text):
|
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])
|
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):
|
def cmd(self, command, params=None):
|
||||||
if params:
|
if params:
|
||||||
params[-1] = u':' + params[-1]
|
params[-1] = ':' + params[-1]
|
||||||
self.send(u"{} {}".format(command, ' '.join(params)))
|
self.send(command + ' ' + ' '.join(map(censor, params)))
|
||||||
else:
|
else:
|
||||||
self.send(command)
|
self.send(command)
|
||||||
|
|
||||||
|
|
50
core/main.py
Normal file → Executable file
50
core/main.py
Normal file → Executable file
|
@ -13,34 +13,29 @@ class Input(dict):
|
||||||
if chan == conn.nick.lower(): # is a PM
|
if chan == conn.nick.lower(): # is a PM
|
||||||
chan = nick
|
chan = nick
|
||||||
|
|
||||||
def message(message, target=chan):
|
def say(msg):
|
||||||
"""sends a message to a specific or current channel/user"""
|
conn.msg(chan, msg)
|
||||||
conn.msg(target, message)
|
|
||||||
|
|
||||||
def reply(message, target=chan):
|
def pm(msg):
|
||||||
"""sends a message to the current channel/user with a prefix"""
|
conn.msg(nick, msg)
|
||||||
if target == nick:
|
|
||||||
conn.msg(target, message)
|
def reply(msg):
|
||||||
|
if chan == nick: # PMs don't need prefixes
|
||||||
|
conn.msg(chan, msg)
|
||||||
else:
|
else:
|
||||||
conn.msg(target, u"({}) {}".format(nick, message))
|
conn.msg(chan, '(' + nick + ') ' + msg)
|
||||||
|
|
||||||
def action(message, target=chan):
|
def me(msg):
|
||||||
"""sends an action to the current channel/user or a specific channel/user"""
|
conn.msg(chan, "\x01%s %s\x01" % ("ACTION", msg))
|
||||||
conn.ctcp(target, "ACTION", message)
|
|
||||||
|
|
||||||
def ctcp(message, ctcp_type, target=chan):
|
def notice(msg):
|
||||||
"""sends an ctcp to the current channel/user or a specific channel/user"""
|
conn.cmd('NOTICE', [nick, msg])
|
||||||
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])
|
|
||||||
|
|
||||||
dict.__init__(self, conn=conn, raw=raw, prefix=prefix, command=command,
|
dict.__init__(self, conn=conn, raw=raw, prefix=prefix, command=command,
|
||||||
params=params, nick=nick, user=user, host=host, mask=mask,
|
params=params, nick=nick, user=user, host=host, mask=mask,
|
||||||
paraml=paraml, msg=msg, server=conn.server, chan=chan,
|
paraml=paraml, msg=msg, server=conn.server, chan=chan,
|
||||||
notice=notice, message=message, reply=reply, bot=bot,
|
notice=notice, say=say, reply=reply, pm=pm, bot=bot,
|
||||||
action=action, ctcp=ctcp, lastparam=paraml[-1])
|
me=me, lastparam=paraml[-1])
|
||||||
|
|
||||||
# make dict keys accessible as attributes
|
# make dict keys accessible as attributes
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
|
@ -82,8 +77,7 @@ def do_sieve(sieve, bot, input, func, type, args):
|
||||||
|
|
||||||
|
|
||||||
class Handler(object):
|
class Handler(object):
|
||||||
"""Runs plugins in their own threads (ensures order)"""
|
'''Runs plugins in their own threads (ensures order)'''
|
||||||
|
|
||||||
def __init__(self, func):
|
def __init__(self, func):
|
||||||
self.func = func
|
self.func = func
|
||||||
self.input_queue = Queue.Queue()
|
self.input_queue = Queue.Queue()
|
||||||
|
@ -109,7 +103,6 @@ class Handler(object):
|
||||||
run(self.func, input)
|
run(self.func, input)
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
@ -122,10 +115,11 @@ class Handler(object):
|
||||||
def dispatch(input, kind, func, args, autohelp=False):
|
def dispatch(input, kind, func, args, autohelp=False):
|
||||||
for sieve, in bot.plugs['sieve']:
|
for sieve, in bot.plugs['sieve']:
|
||||||
input = do_sieve(sieve, bot, input, func, kind, args)
|
input = do_sieve(sieve, bot, input, func, kind, args)
|
||||||
if input is None:
|
if input == None:
|
||||||
return
|
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__)
|
input.notice(input.conn.conf["command_prefix"] + func.__doc__)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -159,9 +153,9 @@ def main(conn, out):
|
||||||
if inp.command == 'PRIVMSG':
|
if inp.command == 'PRIVMSG':
|
||||||
# COMMANDS
|
# COMMANDS
|
||||||
if inp.chan == inp.nick: # private message, no command prefix
|
if inp.chan == inp.nick: # private message, no command prefix
|
||||||
prefix = '^(?:[{}]?|'.format(command_prefix)
|
prefix = '^(?:[%s]?|' % command_prefix
|
||||||
else:
|
else:
|
||||||
prefix = '^(?:[{}]|'.format(command_prefix)
|
prefix = '^(?:[%s]|' % command_prefix
|
||||||
|
|
||||||
command_re = prefix + inp.conn.nick
|
command_re = prefix + inp.conn.nick
|
||||||
command_re += r'[,;:]+\s+)(\w+)(?:$|\s+)(.*)'
|
command_re += r'[,;:]+\s+)(\w+)(?:$|\s+)(.*)'
|
||||||
|
@ -174,7 +168,7 @@ def main(conn, out):
|
||||||
|
|
||||||
if isinstance(command, list): # multiple potential matches
|
if isinstance(command, list): # multiple potential matches
|
||||||
input = Input(conn, *out)
|
input = Input(conn, *out)
|
||||||
input.notice("Did you mean {} or {}?".format
|
input.notice("Did you mean %s or %s?" %
|
||||||
(', '.join(command[:-1]), command[-1]))
|
(', '.join(command[:-1]), command[-1]))
|
||||||
elif command in bot.commands:
|
elif command in bot.commands:
|
||||||
input = Input(conn, *out)
|
input = Input(conn, *out)
|
||||||
|
|
13
core/reload.py
Normal file → Executable file
13
core/reload.py
Normal file → Executable file
|
@ -17,8 +17,8 @@ def make_signature(f):
|
||||||
return f.func_code.co_filename, f.func_name, f.func_code.co_firstlineno
|
return f.func_code.co_filename, f.func_name, f.func_code.co_firstlineno
|
||||||
|
|
||||||
|
|
||||||
def format_plug(plug, kind='', lpad=0):
|
def format_plug(plug, kind='', lpad=0, width=40):
|
||||||
out = ' ' * lpad + '{}:{}:{}'.format(*make_signature(plug[0]))
|
out = ' ' * lpad + '%s:%s:%s' % make_signature(plug[0])
|
||||||
if kind == 'command':
|
if kind == 'command':
|
||||||
out += ' ' * (50 - len(out)) + plug[1]['name']
|
out += ' ' * (50 - len(out)) + plug[1]['name']
|
||||||
|
|
||||||
|
@ -118,11 +118,12 @@ def reload(init=False):
|
||||||
for plug in bot.plugs['command']:
|
for plug in bot.plugs['command']:
|
||||||
name = plug[1]['name'].lower()
|
name = plug[1]['name'].lower()
|
||||||
if not re.match(r'^\w+$', name):
|
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
|
continue
|
||||||
if name in bot.commands:
|
if name in bot.commands:
|
||||||
print "### ERROR: command '{}' already registered ({}, {})".format(name,
|
print "### ERROR: command '%s' already registered (%s, %s)" % \
|
||||||
format_plug(bot.commands[name]),
|
(name, format_plug(bot.commands[name]),
|
||||||
format_plug(plug))
|
format_plug(plug))
|
||||||
continue
|
continue
|
||||||
bot.commands[name] = plug
|
bot.commands[name] = plug
|
||||||
|
@ -154,7 +155,7 @@ def reload(init=False):
|
||||||
for kind, plugs in sorted(bot.plugs.iteritems()):
|
for kind, plugs in sorted(bot.plugs.iteritems()):
|
||||||
if kind == 'command':
|
if kind == 'command':
|
||||||
continue
|
continue
|
||||||
print ' {}:'.format(kind)
|
print ' %s:' % kind
|
||||||
for plug in plugs:
|
for plug in plugs:
|
||||||
print format_plug(plug, kind=kind, lpad=6)
|
print format_plug(plug, kind=kind, lpad=6)
|
||||||
print
|
print
|
||||||
|
|
33
disabled_plugins/antiflood.py
Executable file
33
disabled_plugins/antiflood.py
Executable file
|
@ -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
|
0
disabled_stuff/mtg.py → disabled_plugins/mtg.py
Normal file → Executable file
0
disabled_stuff/mtg.py → disabled_plugins/mtg.py
Normal file → Executable file
0
disabled_stuff/mygengo_translate.py → disabled_plugins/mygengo_translate.py
Normal file → Executable file
0
disabled_stuff/mygengo_translate.py → disabled_plugins/mygengo_translate.py
Normal file → Executable file
0
disabled_stuff/repaste.py → disabled_plugins/repaste.py
Normal file → Executable file
0
disabled_stuff/repaste.py → disabled_plugins/repaste.py
Normal file → Executable file
33
disabled_plugins/suggest.py
Executable file
33
disabled_plugins/suggest.py
Executable file
|
@ -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>', ''))
|
0
disabled_stuff/urlhistory.py → disabled_plugins/urlhistory.py
Normal file → Executable file
0
disabled_stuff/urlhistory.py → disabled_plugins/urlhistory.py
Normal file → Executable file
0
disabled_stuff/wordoftheday.py → disabled_plugins/wordoftheday.py
Normal file → Executable file
0
disabled_stuff/wordoftheday.py → disabled_plugins/wordoftheday.py
Normal file → Executable file
0
disabled_stuff/wrapper.old → disabled_plugins/wrapper.old
Normal file → Executable file
0
disabled_stuff/wrapper.old → disabled_plugins/wrapper.old
Normal file → Executable file
|
@ -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)"
|
__author__ = "Leonard Richardson (leonardr@segfault.org)"
|
||||||
__version__ = "4.2.1"
|
__version__ = "4.1.3"
|
||||||
__copyright__ = "Copyright (c) 2004-2013 Leonard Richardson"
|
__copyright__ = "Copyright (c) 2004-2012 Leonard Richardson"
|
||||||
__license__ = "MIT"
|
__license__ = "MIT"
|
||||||
|
|
||||||
__all__ = ['BeautifulSoup']
|
__all__ = ['BeautifulSoup']
|
||||||
|
@ -201,9 +201,9 @@ class BeautifulSoup(Tag):
|
||||||
"""Create a new tag associated with this soup."""
|
"""Create a new tag associated with this soup."""
|
||||||
return Tag(None, self.builder, name, namespace, nsprefix, attrs)
|
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."""
|
"""Create a new NavigableString associated with this soup."""
|
||||||
navigable = subclass(s)
|
navigable = NavigableString(s)
|
||||||
navigable.setup()
|
navigable.setup()
|
||||||
return navigable
|
return navigable
|
||||||
|
|
||||||
|
@ -245,15 +245,13 @@ class BeautifulSoup(Tag):
|
||||||
o = containerClass(currentData)
|
o = containerClass(currentData)
|
||||||
self.object_was_parsed(o)
|
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."""
|
"""Add an object to the parse tree."""
|
||||||
parent = parent or self.currentTag
|
o.setup(self.currentTag, self.previous_element)
|
||||||
most_recent_element = most_recent_element or self._most_recent_element
|
if self.previous_element:
|
||||||
o.setup(parent, most_recent_element)
|
self.previous_element.next_element = o
|
||||||
if most_recent_element is not None:
|
self.previous_element = o
|
||||||
most_recent_element.next_element = o
|
self.currentTag.contents.append(o)
|
||||||
self._most_recent_element = o
|
|
||||||
parent.contents.append(o)
|
|
||||||
|
|
||||||
def _popToTag(self, name, nsprefix=None, inclusivePop=True):
|
def _popToTag(self, name, nsprefix=None, inclusivePop=True):
|
||||||
"""Pops the tag stack up to and including the most recent
|
"""Pops the tag stack up to and including the most recent
|
||||||
|
@ -297,12 +295,12 @@ class BeautifulSoup(Tag):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tag = Tag(self, self.builder, name, namespace, nsprefix, attrs,
|
tag = Tag(self, self.builder, name, namespace, nsprefix, attrs,
|
||||||
self.currentTag, self._most_recent_element)
|
self.currentTag, self.previous_element)
|
||||||
if tag is None:
|
if tag is None:
|
||||||
return tag
|
return tag
|
||||||
if self._most_recent_element:
|
if self.previous_element:
|
||||||
self._most_recent_element.next_element = tag
|
self.previous_element.next_element = tag
|
||||||
self._most_recent_element = tag
|
self.previous_element = tag
|
||||||
self.pushTag(tag)
|
self.pushTag(tag)
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
@ -335,10 +333,6 @@ class BeautifulSoup(Tag):
|
||||||
return prefix + super(BeautifulSoup, self).decode(
|
return prefix + super(BeautifulSoup, self).decode(
|
||||||
indent_level, eventual_encoding, formatter)
|
indent_level, eventual_encoding, formatter)
|
||||||
|
|
||||||
# Alias to make it easier to type import: 'from bs4 import _soup'
|
|
||||||
_s = BeautifulSoup
|
|
||||||
_soup = BeautifulSoup
|
|
||||||
|
|
||||||
class BeautifulStoneSoup(BeautifulSoup):
|
class BeautifulStoneSoup(BeautifulSoup):
|
||||||
"""Deprecated interface to an XML parser."""
|
"""Deprecated interface to an XML parser."""
|
||||||
|
|
||||||
|
|
|
@ -152,7 +152,7 @@ class TreeBuilder(object):
|
||||||
tag_specific = self.cdata_list_attributes.get(
|
tag_specific = self.cdata_list_attributes.get(
|
||||||
tag_name.lower(), [])
|
tag_name.lower(), [])
|
||||||
for cdata_list_attr in itertools.chain(universal, tag_specific):
|
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
|
# Basically, we have a "class" attribute whose
|
||||||
# value is a whitespace-separated list of CSS
|
# value is a whitespace-separated list of CSS
|
||||||
# classes. Split it into a list.
|
# classes. Split it into a list.
|
||||||
|
|
|
@ -131,9 +131,9 @@ class Element(html5lib.treebuilders._base.Node):
|
||||||
old_element = self.element.contents[-1]
|
old_element = self.element.contents[-1]
|
||||||
new_element = self.soup.new_string(old_element + node.element)
|
new_element = self.soup.new_string(old_element + node.element)
|
||||||
old_element.replace_with(new_element)
|
old_element.replace_with(new_element)
|
||||||
self.soup._most_recent_element = new_element
|
|
||||||
else:
|
else:
|
||||||
self.soup.object_was_parsed(node.element, parent=self.element)
|
self.element.append(node.element)
|
||||||
|
node.parent = self
|
||||||
|
|
||||||
def getAttributes(self):
|
def getAttributes(self):
|
||||||
return AttrList(self.element)
|
return AttrList(self.element)
|
||||||
|
|
|
@ -58,8 +58,6 @@ class BeautifulSoupHTMLParser(HTMLParser):
|
||||||
# it's fixed.
|
# it's fixed.
|
||||||
if name.startswith('x'):
|
if name.startswith('x'):
|
||||||
real_name = int(name.lstrip('x'), 16)
|
real_name = int(name.lstrip('x'), 16)
|
||||||
elif name.startswith('X'):
|
|
||||||
real_name = int(name.lstrip('X'), 16)
|
|
||||||
else:
|
else:
|
||||||
real_name = int(name)
|
real_name = int(name)
|
||||||
|
|
||||||
|
@ -87,9 +85,6 @@ class BeautifulSoupHTMLParser(HTMLParser):
|
||||||
self.soup.endData()
|
self.soup.endData()
|
||||||
if data.startswith("DOCTYPE "):
|
if data.startswith("DOCTYPE "):
|
||||||
data = data[len("DOCTYPE "):]
|
data = data[len("DOCTYPE "):]
|
||||||
elif data == 'DOCTYPE':
|
|
||||||
# i.e. "<!DOCTYPE>"
|
|
||||||
data = ''
|
|
||||||
self.soup.handle_data(data)
|
self.soup.handle_data(data)
|
||||||
self.soup.endData(Doctype)
|
self.soup.endData(Doctype)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ __all__ = [
|
||||||
'LXMLTreeBuilder',
|
'LXMLTreeBuilder',
|
||||||
]
|
]
|
||||||
|
|
||||||
from io import BytesIO
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
import collections
|
import collections
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
@ -29,10 +28,6 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||||
|
|
||||||
CHUNK_SIZE = 512
|
CHUNK_SIZE = 512
|
||||||
|
|
||||||
# This namespace mapping is specified in the XML Namespace
|
|
||||||
# standard.
|
|
||||||
DEFAULT_NSMAPS = {'http://www.w3.org/XML/1998/namespace' : "xml"}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_parser(self):
|
def default_parser(self):
|
||||||
# This can either return a parser object or a class, which
|
# 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)
|
parser = parser(target=self, strip_cdata=False)
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
self.soup = None
|
self.soup = None
|
||||||
self.nsmaps = [self.DEFAULT_NSMAPS]
|
self.nsmaps = None
|
||||||
|
|
||||||
def _getNsTag(self, tag):
|
def _getNsTag(self, tag):
|
||||||
# Split the namespace URL out of a fully-qualified lxml tag
|
# Split the namespace URL out of a fully-qualified lxml tag
|
||||||
|
@ -76,9 +71,7 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||||
dammit.contains_replacement_characters)
|
dammit.contains_replacement_characters)
|
||||||
|
|
||||||
def feed(self, markup):
|
def feed(self, markup):
|
||||||
if isinstance(markup, bytes):
|
if isinstance(markup, basestring):
|
||||||
markup = BytesIO(markup)
|
|
||||||
elif isinstance(markup, unicode):
|
|
||||||
markup = StringIO(markup)
|
markup = StringIO(markup)
|
||||||
# Call feed() at least once, even if the markup is empty,
|
# Call feed() at least once, even if the markup is empty,
|
||||||
# or the parser won't be initialized.
|
# or the parser won't be initialized.
|
||||||
|
@ -92,20 +85,23 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||||
self.parser.close()
|
self.parser.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.nsmaps = [self.DEFAULT_NSMAPS]
|
self.nsmaps = None
|
||||||
|
|
||||||
def start(self, name, attrs, nsmap={}):
|
def start(self, name, attrs, nsmap={}):
|
||||||
# Make sure attrs is a mutable dict--lxml may send an immutable dictproxy.
|
# Make sure attrs is a mutable dict--lxml may send an immutable dictproxy.
|
||||||
attrs = dict(attrs)
|
attrs = dict(attrs)
|
||||||
|
|
||||||
nsprefix = None
|
nsprefix = None
|
||||||
# Invert each namespace map as it comes in.
|
# Invert each namespace map as it comes in.
|
||||||
if len(self.nsmaps) > 1:
|
if len(nsmap) == 0 and self.nsmaps != None:
|
||||||
# There are no new namespaces for this tag, but
|
# There are no new namespaces for this tag, but namespaces
|
||||||
# non-default namespaces are in play, so we need a
|
# are in play, so we need a separate tag stack to know
|
||||||
# separate tag stack to know when they end.
|
# when they end.
|
||||||
self.nsmaps.append(None)
|
self.nsmaps.append(None)
|
||||||
elif len(nsmap) > 0:
|
elif len(nsmap) > 0:
|
||||||
# A new namespace mapping has come into play.
|
# 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())
|
inverted_nsmap = dict((value, key) for key, value in nsmap.items())
|
||||||
self.nsmaps.append(inverted_nsmap)
|
self.nsmaps.append(inverted_nsmap)
|
||||||
# Also treat the namespace mapping as a set of attributes on the
|
# Also treat the namespace mapping as a set of attributes on the
|
||||||
|
@ -116,6 +112,7 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||||
"xmlns", prefix, "http://www.w3.org/2000/xmlns/")
|
"xmlns", prefix, "http://www.w3.org/2000/xmlns/")
|
||||||
attrs[attribute] = namespace
|
attrs[attribute] = namespace
|
||||||
|
|
||||||
|
if self.nsmaps is not None and len(self.nsmaps) > 0:
|
||||||
# Namespaces are in play. Find any attributes that came in
|
# Namespaces are in play. Find any attributes that came in
|
||||||
# from lxml with namespaces attached to their names, and
|
# from lxml with namespaces attached to their names, and
|
||||||
# turn then into NamespacedAttribute objects.
|
# turn then into NamespacedAttribute objects.
|
||||||
|
@ -141,7 +138,6 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||||
for inverted_nsmap in reversed(self.nsmaps):
|
for inverted_nsmap in reversed(self.nsmaps):
|
||||||
if inverted_nsmap is not None and namespace in inverted_nsmap:
|
if inverted_nsmap is not None and namespace in inverted_nsmap:
|
||||||
return inverted_nsmap[namespace]
|
return inverted_nsmap[namespace]
|
||||||
return None
|
|
||||||
|
|
||||||
def end(self, name):
|
def end(self, name):
|
||||||
self.soup.endData()
|
self.soup.endData()
|
||||||
|
@ -154,10 +150,14 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||||
nsprefix = inverted_nsmap[namespace]
|
nsprefix = inverted_nsmap[namespace]
|
||||||
break
|
break
|
||||||
self.soup.handle_endtag(name, nsprefix)
|
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
|
# This tag, or one of its parents, introduced a namespace
|
||||||
# mapping, so pop it off the stack.
|
# mapping, so pop it off the stack.
|
||||||
self.nsmaps.pop()
|
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):
|
def pi(self, target, data):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -81,8 +81,6 @@ class EntitySubstitution(object):
|
||||||
"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
|
"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
|
||||||
")")
|
")")
|
||||||
|
|
||||||
AMPERSAND_OR_BRACKET = re.compile("([<>&])")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _substitute_html_entity(cls, matchobj):
|
def _substitute_html_entity(cls, matchobj):
|
||||||
entity = cls.CHARACTER_TO_HTML_ENTITY.get(matchobj.group(0))
|
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):
|
def substitute_xml(cls, value, make_quoted_attribute=False):
|
||||||
"""Substitute XML entities for special XML characters.
|
"""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
|
:param value: A string to be substituted. The less-than sign will
|
||||||
become <, the greater-than sign will become >, and any
|
become <, the greater-than sign will become >, and any
|
||||||
ampersands that are not part of an entity defition will
|
ampersands that are not part of an entity defition will
|
||||||
|
@ -175,7 +151,6 @@ class EntitySubstitution(object):
|
||||||
value = cls.quoted_attribute_value(value)
|
value = cls.quoted_attribute_value(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def substitute_html(cls, s):
|
def substitute_html(cls, s):
|
||||||
"""Replace certain Unicode characters with named HTML entities.
|
"""Replace certain Unicode characters with named HTML entities.
|
||||||
|
@ -298,6 +273,7 @@ class UnicodeDammit:
|
||||||
return None
|
return None
|
||||||
self.tried_encodings.append((proposed, errors))
|
self.tried_encodings.append((proposed, errors))
|
||||||
markup = self.markup
|
markup = self.markup
|
||||||
|
|
||||||
# Convert smart quotes to HTML if coming from an encoding
|
# Convert smart quotes to HTML if coming from an encoding
|
||||||
# that might have them.
|
# that might have them.
|
||||||
if (self.smart_quotes_to is not None
|
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):
|
def __new__(cls, prefix, name, namespace=None):
|
||||||
if name is None:
|
if name is None:
|
||||||
obj = unicode.__new__(cls, prefix)
|
obj = unicode.__new__(cls, prefix)
|
||||||
elif prefix is None:
|
|
||||||
# Not really namespaced.
|
|
||||||
obj = unicode.__new__(cls, name)
|
|
||||||
else:
|
else:
|
||||||
obj = unicode.__new__(cls, prefix + ":" + name)
|
obj = unicode.__new__(cls, prefix + ":" + name)
|
||||||
obj.prefix = prefix
|
obj.prefix = prefix
|
||||||
|
@ -81,40 +78,6 @@ class ContentMetaAttributeValue(AttributeValueWithCharsetSubstitution):
|
||||||
return match.group(1) + encoding
|
return match.group(1) + encoding
|
||||||
return self.CHARSET_RE.sub(rewrite, self.original_value)
|
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):
|
class PageElement(object):
|
||||||
"""Contains the navigational information for some part of the page
|
"""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
|
# converted to entities. This is not recommended, but it's
|
||||||
# faster than "minimal".
|
# faster than "minimal".
|
||||||
# A function - This function will be called on every string that
|
# A function - This function will be called on every string that
|
||||||
# needs to undergo entity substitution.
|
# needs to undergo entity substition
|
||||||
#
|
FORMATTERS = {
|
||||||
|
|
||||||
# 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 = {
|
|
||||||
"html" : EntitySubstitution.substitute_html,
|
"html" : EntitySubstitution.substitute_html,
|
||||||
"minimal" : EntitySubstitution.substitute_xml,
|
"minimal" : EntitySubstitution.substitute_xml,
|
||||||
None : None
|
None : None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def format_string(self, s, formatter='minimal'):
|
def format_string(self, s, formatter='minimal'):
|
||||||
"""Format the given string using the given formatter."""
|
"""Format the given string using the given formatter."""
|
||||||
if not callable(formatter):
|
if not callable(formatter):
|
||||||
formatter = self._formatter_for_name(formatter)
|
formatter = self.FORMATTERS.get(
|
||||||
|
formatter, EntitySubstitution.substitute_xml)
|
||||||
if formatter is None:
|
if formatter is None:
|
||||||
output = s
|
output = s
|
||||||
else:
|
else:
|
||||||
output = formatter(s)
|
output = formatter(s)
|
||||||
return output
|
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):
|
def setup(self, parent=None, previous_element=None):
|
||||||
"""Sets up the initial relations between this element and
|
"""Sets up the initial relations between this element and
|
||||||
other elements."""
|
other elements."""
|
||||||
|
@ -438,7 +366,7 @@ class PageElement(object):
|
||||||
# NOTE: We can't use _find_one because findParents takes a different
|
# NOTE: We can't use _find_one because findParents takes a different
|
||||||
# set of arguments.
|
# set of arguments.
|
||||||
r = None
|
r = None
|
||||||
l = self.find_parents(name, attrs, 1, **kwargs)
|
l = self.find_parents(name, attrs, 1)
|
||||||
if l:
|
if l:
|
||||||
r = l[0]
|
r = l[0]
|
||||||
return r
|
return r
|
||||||
|
@ -567,14 +495,6 @@ class PageElement(object):
|
||||||
value =" ".join(value)
|
value =" ".join(value)
|
||||||
return 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=''):
|
def _attribute_checker(self, operator, attribute, value=''):
|
||||||
"""Create a function that performs a CSS selector operation.
|
"""Create a function that performs a CSS selector operation.
|
||||||
|
|
||||||
|
@ -616,6 +536,87 @@ class PageElement(object):
|
||||||
else:
|
else:
|
||||||
return lambda el: el.has_attr(attribute)
|
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
|
# Old non-property versions of the generators, for backwards
|
||||||
# compatibility with BS3.
|
# compatibility with BS3.
|
||||||
def nextGenerator(self):
|
def nextGenerator(self):
|
||||||
|
@ -651,9 +652,6 @@ class NavigableString(unicode, PageElement):
|
||||||
return unicode.__new__(cls, value)
|
return unicode.__new__(cls, value)
|
||||||
return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
|
return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
|
||||||
|
|
||||||
def __copy__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __getnewargs__(self):
|
def __getnewargs__(self):
|
||||||
return (unicode(self),)
|
return (unicode(self),)
|
||||||
|
|
||||||
|
@ -711,7 +709,7 @@ class Doctype(PreformattedString):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_name_and_ids(cls, name, pub_id, system_id):
|
def for_name_and_ids(cls, name, pub_id, system_id):
|
||||||
value = name or ''
|
value = name
|
||||||
if pub_id is not None:
|
if pub_id is not None:
|
||||||
value += ' PUBLIC "%s"' % pub_id
|
value += ' PUBLIC "%s"' % pub_id
|
||||||
if system_id is not None:
|
if system_id is not None:
|
||||||
|
@ -805,24 +803,16 @@ class Tag(PageElement):
|
||||||
self.clear()
|
self.clear()
|
||||||
self.append(string.__class__(string))
|
self.append(string.__class__(string))
|
||||||
|
|
||||||
def _all_strings(self, strip=False, types=(NavigableString, CData)):
|
def _all_strings(self, strip=False):
|
||||||
"""Yield all strings of certain classes, possibly stripping them.
|
"""Yield all child strings, possibly stripping them."""
|
||||||
|
|
||||||
By default, yields only NavigableString and CData objects. So
|
|
||||||
no comments, processing instructions, etc.
|
|
||||||
"""
|
|
||||||
for descendant in self.descendants:
|
for descendant in self.descendants:
|
||||||
if (
|
if not isinstance(descendant, NavigableString):
|
||||||
(types is None and not isinstance(descendant, NavigableString))
|
|
||||||
or
|
|
||||||
(types is not None and type(descendant) not in types)):
|
|
||||||
continue
|
continue
|
||||||
if strip:
|
if strip:
|
||||||
descendant = descendant.strip()
|
descendant = descendant.strip()
|
||||||
if len(descendant) == 0:
|
if len(descendant) == 0:
|
||||||
continue
|
continue
|
||||||
yield descendant
|
yield descendant
|
||||||
|
|
||||||
strings = property(_all_strings)
|
strings = property(_all_strings)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -830,13 +820,11 @@ class Tag(PageElement):
|
||||||
for string in self._all_strings(True):
|
for string in self._all_strings(True):
|
||||||
yield string
|
yield string
|
||||||
|
|
||||||
def get_text(self, separator=u"", strip=False,
|
def get_text(self, separator=u"", strip=False):
|
||||||
types=(NavigableString, CData)):
|
|
||||||
"""
|
"""
|
||||||
Get all child strings, concatenated using the given separator.
|
Get all child strings, concatenated using the given separator.
|
||||||
"""
|
"""
|
||||||
return separator.join([s for s in self._all_strings(
|
return separator.join([s for s in self._all_strings(strip)])
|
||||||
strip, types=types)])
|
|
||||||
getText = get_text
|
getText = get_text
|
||||||
text = property(get_text)
|
text = property(get_text)
|
||||||
|
|
||||||
|
@ -847,7 +835,6 @@ class Tag(PageElement):
|
||||||
while i is not None:
|
while i is not None:
|
||||||
next = i.next_element
|
next = i.next_element
|
||||||
i.__dict__.clear()
|
i.__dict__.clear()
|
||||||
i.contents = []
|
|
||||||
i = next
|
i = next
|
||||||
|
|
||||||
def clear(self, decompose=False):
|
def clear(self, decompose=False):
|
||||||
|
@ -979,13 +966,6 @@ class Tag(PageElement):
|
||||||
u = self.decode(indent_level, encoding, formatter)
|
u = self.decode(indent_level, encoding, formatter)
|
||||||
return u.encode(encoding, errors)
|
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,
|
def decode(self, indent_level=None,
|
||||||
eventual_encoding=DEFAULT_OUTPUT_ENCODING,
|
eventual_encoding=DEFAULT_OUTPUT_ENCODING,
|
||||||
formatter="minimal"):
|
formatter="minimal"):
|
||||||
|
@ -998,12 +978,6 @@ class Tag(PageElement):
|
||||||
document contains a <META> tag that mentions the document's
|
document contains a <META> tag that mentions the document's
|
||||||
encoding.
|
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 = []
|
attrs = []
|
||||||
if self.attrs:
|
if self.attrs:
|
||||||
for key, val in sorted(self.attrs.items()):
|
for key, val in sorted(self.attrs.items()):
|
||||||
|
@ -1036,15 +1010,12 @@ class Tag(PageElement):
|
||||||
else:
|
else:
|
||||||
closeTag = '</%s%s>' % (prefix, self.name)
|
closeTag = '</%s%s>' % (prefix, self.name)
|
||||||
|
|
||||||
pretty_print = self._should_pretty_print(indent_level)
|
pretty_print = (indent_level is not None)
|
||||||
space = ''
|
|
||||||
indent_space = ''
|
|
||||||
if indent_level is not None:
|
|
||||||
indent_space = (' ' * (indent_level - 1))
|
|
||||||
if pretty_print:
|
if pretty_print:
|
||||||
space = indent_space
|
space = (' ' * (indent_level - 1))
|
||||||
indent_contents = indent_level + 1
|
indent_contents = indent_level + 1
|
||||||
else:
|
else:
|
||||||
|
space = ''
|
||||||
indent_contents = None
|
indent_contents = None
|
||||||
contents = self.decode_contents(
|
contents = self.decode_contents(
|
||||||
indent_contents, eventual_encoding, formatter)
|
indent_contents, eventual_encoding, formatter)
|
||||||
|
@ -1057,10 +1028,8 @@ class Tag(PageElement):
|
||||||
attribute_string = ''
|
attribute_string = ''
|
||||||
if attrs:
|
if attrs:
|
||||||
attribute_string = ' ' + ' '.join(attrs)
|
attribute_string = ' ' + ' '.join(attrs)
|
||||||
if indent_level is not None:
|
if pretty_print:
|
||||||
# Even if this particular tag is not pretty-printed,
|
s.append(space)
|
||||||
# we should indent up to the start of the tag.
|
|
||||||
s.append(indent_space)
|
|
||||||
s.append('<%s%s%s%s>' % (
|
s.append('<%s%s%s%s>' % (
|
||||||
prefix, self.name, attribute_string, close))
|
prefix, self.name, attribute_string, close))
|
||||||
if pretty_print:
|
if pretty_print:
|
||||||
|
@ -1071,10 +1040,7 @@ class Tag(PageElement):
|
||||||
if pretty_print and closeTag:
|
if pretty_print and closeTag:
|
||||||
s.append(space)
|
s.append(space)
|
||||||
s.append(closeTag)
|
s.append(closeTag)
|
||||||
if indent_level is not None and closeTag and self.next_sibling:
|
if pretty_print 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.
|
|
||||||
s.append("\n")
|
s.append("\n")
|
||||||
s = ''.join(s)
|
s = ''.join(s)
|
||||||
return s
|
return s
|
||||||
|
@ -1097,11 +1063,6 @@ class Tag(PageElement):
|
||||||
document contains a <META> tag that mentions the document's
|
document contains a <META> tag that mentions the document's
|
||||||
encoding.
|
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)
|
pretty_print = (indent_level is not None)
|
||||||
s = []
|
s = []
|
||||||
for c in self:
|
for c in self:
|
||||||
|
@ -1111,13 +1072,13 @@ class Tag(PageElement):
|
||||||
elif isinstance(c, Tag):
|
elif isinstance(c, Tag):
|
||||||
s.append(c.decode(indent_level, eventual_encoding,
|
s.append(c.decode(indent_level, eventual_encoding,
|
||||||
formatter))
|
formatter))
|
||||||
if text and indent_level and not self.name == 'pre':
|
if text and indent_level:
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
if text:
|
if text:
|
||||||
if pretty_print and not self.name == 'pre':
|
if pretty_print:
|
||||||
s.append(" " * (indent_level - 1))
|
s.append(" " * (indent_level - 1))
|
||||||
s.append(text)
|
s.append(text)
|
||||||
if pretty_print and not self.name == 'pre':
|
if pretty_print:
|
||||||
s.append("\n")
|
s.append("\n")
|
||||||
return ''.join(s)
|
return ''.join(s)
|
||||||
|
|
||||||
|
@ -1184,207 +1145,6 @@ class Tag(PageElement):
|
||||||
yield current
|
yield current
|
||||||
current = current.next_element
|
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
|
# Old names for backwards compatibility
|
||||||
def childGenerator(self):
|
def childGenerator(self):
|
||||||
return self.children
|
return self.children
|
||||||
|
@ -1392,13 +1152,10 @@ class Tag(PageElement):
|
||||||
def recursiveChildGenerator(self):
|
def recursiveChildGenerator(self):
|
||||||
return self.descendants
|
return self.descendants
|
||||||
|
|
||||||
def has_key(self, key):
|
# This was kind of misleading because has_key() (attributes) was
|
||||||
"""This was kind of misleading because has_key() (attributes)
|
# different from __in__ (contents). has_key() is gone in Python 3,
|
||||||
was different from __in__ (contents). has_key() is gone in
|
# anyway.
|
||||||
Python 3, anyway."""
|
has_key = has_attr
|
||||||
warnings.warn('has_key is deprecated. Use has_attr("%s") instead.' % (
|
|
||||||
key))
|
|
||||||
return self.has_attr(key)
|
|
||||||
|
|
||||||
# Next, a couple classes to represent queries and their results.
|
# Next, a couple classes to represent queries and their results.
|
||||||
class SoupStrainer(object):
|
class SoupStrainer(object):
|
||||||
|
|
|
@ -81,11 +81,6 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||||
self.assertDoctypeHandled(
|
self.assertDoctypeHandled(
|
||||||
'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
|
'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):
|
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"'
|
doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
|
||||||
self.assertDoctypeHandled(doctype)
|
self.assertDoctypeHandled(doctype)
|
||||||
|
@ -164,12 +159,6 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||||
comment = soup.find(text="foobar")
|
comment = soup.find(text="foobar")
|
||||||
self.assertEqual(comment.__class__, Comment)
|
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):
|
def test_preserved_whitespace_in_pre_and_textarea(self):
|
||||||
"""Whitespace must be preserved in <pre> and <textarea> tags."""
|
"""Whitespace must be preserved in <pre> and <textarea> tags."""
|
||||||
self.assertSoupEquals("<pre> </pre>")
|
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>'
|
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)
|
||||||
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):
|
def test_entities_in_text_converted_to_unicode(self):
|
||||||
expect = u'<p>pi\N{LATIN SMALL LETTER N WITH TILDE}ata</p>'
|
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)
|
||||||
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):
|
def test_quot_entity_converted_to_quotation_mark(self):
|
||||||
|
@ -248,12 +235,6 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||||
self.assertSoupEquals("�", expect)
|
self.assertSoupEquals("�", expect)
|
||||||
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):
|
def test_basic_namespaces(self):
|
||||||
"""Parsers don't need to *understand* namespaces, but at the
|
"""Parsers don't need to *understand* namespaces, but at the
|
||||||
very least they should not choke on namespaces or lose
|
very least they should not choke on namespaces or lose
|
||||||
|
@ -472,18 +453,6 @@ class XMLTreeBuilderSmokeTest(object):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
soup.encode("utf-8"), markup)
|
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):
|
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>'
|
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)
|
soup = self.soup(markup)
|
||||||
|
@ -526,11 +495,6 @@ class XMLTreeBuilderSmokeTest(object):
|
||||||
soup = self.soup(markup)
|
soup = self.soup(markup)
|
||||||
self.assertEqual(unicode(soup.foo), 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):
|
class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest):
|
||||||
"""Smoke test for a tree builder that supports HTML5."""
|
"""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.math.namespace)
|
||||||
self.assertEqual(namespace, soup.msqrt.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 skipIf(condition, reason):
|
||||||
def nothing(test, *args, **kwargs):
|
def nothing(test, *args, **kwargs):
|
||||||
|
|
|
@ -56,17 +56,3 @@ class HTML5LibBuilderSmokeTest(SoupTest, HTML5TreeBuilderSmokeTest):
|
||||||
"<table><thead><tr><td>Foo</td></tr></thead>"
|
"<table><thead><tr><td>Foo</td></tr></thead>"
|
||||||
"<tbody><tr><td>Bar</td></tr></tbody>"
|
"<tbody><tr><td>Bar</td></tr></tbody>"
|
||||||
"<tfoot><tr><td>Baz</td></tr></tfoot></table>")
|
"<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 a new issue