Repository: facebook/pyaib
Branch: master
Commit: 73a141f28503
Files: 31
Total size: 124.1 KB
Directory structure:
gitextract_6rpuxtkg/
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.markdown
├── example/
│ ├── botbot.conf
│ ├── botbot.py
│ └── plugins/
│ ├── __init__.py
│ ├── debug.py
│ ├── example.py
│ ├── jokes.py
│ └── karma.py
├── pyaib/
│ ├── __init__.py
│ ├── channels.py
│ ├── components.py
│ ├── config.py
│ ├── db.py
│ ├── dbd/
│ │ ├── __init__.py
│ │ └── sqlite.py
│ ├── events.py
│ ├── irc.py
│ ├── ircbot.py
│ ├── linesocket.py
│ ├── nickserv.py
│ ├── plugins.py
│ ├── timers.py
│ ├── triggers.py
│ └── util/
│ ├── __init__.py
│ ├── data.py
│ └── decorator.py
└── setup.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
build
dist
.idea
*.egg-info
*.pyc
botbot-facebook.conf
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
Facebook has adopted a Code of Conduct that we expect project participants to adhere to.
Please read the [full text](https://code.fb.com/codeofconduct/)
so that you can understand what actions will and will not be tolerated.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to pyaib
We want to make contributing to this project as easy and transparent as
possible.
## Pull Requests
We actively welcome your pull requests.
1. Fork the repo and create your branch from `master`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. If you haven't already, complete the Contributor License Agreement ("CLA").
## Contributor License Agreement ("CLA")
In order to accept your pull request, we need you to submit a CLA. You only need
to do this once to work on any of Facebook's open source projects.
Complete your CLA here: <https://code.facebook.com/cla>
## Issues
We use GitHub issues to track public bugs. Please ensure your description is
clear and has sufficient instructions to be able to reproduce the issue.
Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
disclosure of security bugs. In those cases, please go through the process
outlined on that page and do not file a public issue.
## Coding Style
* black
## License
By contributing to pyaib, you agree that your contributions will be licensed
under the LICENSE file in the root directory of this source tree.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.markdown
================================================
Python Async IrcBot framework (pyaib)
=====================================
pyaib is an easy to use framework for writing irc bots. pyaib uses gevent
for its Asynchronous bits.
Features
========
* SSL/IPv6
* YAML config
* plugin system
* simple nickserv auth
* simple abstract database system
Setup
=====
<pre><code>pip install pyaib</code></pre>
or
<pre><code>python setup.py build
python setup.py install</code></pre>
Example
========
Take a look at the example directory for an example bot called 'botbot'
Run:
<pre><code>python example/botbot.py</code></pre>
Try adding your own plugins in example/plugins.
Take a look at the [wiki](https://github.com/facebook/pyaib/wiki) for information about plugin writing and using the db component.
See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out.
License
=======
pyaib is Apache licensed, as found in the LICENSE file.
================================================
FILE: example/botbot.conf
================================================
######################
# IRC Config Section #
######################
IRC:
#Could be a yaml list or comma delimited value
servers: ssl://chat.freenode.net:6697
#Irc Nick Name
nick: botbot
#IRC User Name
user: botbot
#IRC Password
#password: testing
#IRC Whois Name
realname: "pyaib {version}"
#Auto ping: default 10 minutes 0 to disable
auto_ping: 300
##################
# Plugins Config #
##################
plugins:
#Package to look for plugins
base: plugins
#Load these plugins
# / means Absolute python path
load: debug example /plugins.jokes karma
#Load the nickserv component
components.load:
- db
#- nickserv
nickserv:
# If you've registered with the nickserv
password: mypassword
db:
backend: sqlite
driver.sqlite:
path: /tmp/botbot.sdb
channels:
db: true
autojoin:
- "#botbot"
plugin.karma:
scanner_refresh: 60
pronoun: his
plugin.jokes:
ballresp:
- "It is certain"
- "It is decidedly so"
- "Without a doubt"
- "Yes definitely"
- "You may rely on it"
- "As I see it yes"
- "Most likely"
- "Outlook good"
- "Yes"
- "Signs point to yes"
- "Reply hazy try again"
- "Ask again later"
- "Better not tell you now"
- "Cannot predict now"
- "Concentrate and ask again"
- "Don't count on it"
- "My reply is no"
- "My sources say no"
- "Outlook not so good"
- "Very doubtful"
================================================
FILE: example/botbot.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from pyaib.ircbot import IrcBot
import sys
argv = sys.argv[1:]
#Load 'botbot.conf' from the par
bot = IrcBot(argv[0] if argv else 'botbot.conf')
print("Config Dump: %s" % bot.config)
#Bot Take over
bot.run()
================================================
FILE: example/plugins/__init__.py
================================================
================================================
FILE: example/plugins/debug.py
================================================
""" Debug Plugin (botbot plugins.debug) """
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import time
from pyaib.plugins import observe, keyword, plugin_class, every
#Let pyaib know this is a plugin class and to
# Store the address of the class instance at
# 'debug' in the irc_context obj
@plugin_class('debug')
class Debug(object):
#Get a copy of the irc_context, and a copy of your config
# So for us it would be 'plugin.debug' in the bot config
def __init__(self, irc_context, config):
print("Debug Plugin Loaded!")
@observe('IRC_RAW_MSG', 'IRC_RAW_SEND')
def debug(self, irc_c, msg):
print("[%s] %r" % (time.strftime('%H:%M:%S'), msg))
@observe('IRC_MSG_PRIVMSG')
def auto_reply(self, irc_c, msg):
if msg.channel is None:
msg.reply(msg.message)
@keyword('die')
def die(self, irc_c, msg, trigger, args, kargs):
msg.reply('Ok :(')
irc_c.client.die()
@keyword('raw')
def raw(self, irc_c, msg, trigger, args, kargs):
irc_c.RAW(args)
@keyword('test')
def argtest(self, irc_c, msg, trigger, args, kargs):
msg.reply('Trigger: %r' % trigger)
msg.reply('ARGS: %r' % args)
msg.reply('KEYWORDS: %r' % kargs)
msg.reply('Unparsed: %r' % msg.unparsed)
@keyword('test')
@keyword.sub('sub')
def argsubtest(self, irc_c, msg, trigger, args, kargs):
msg.reply('Triggers: %r' % trigger)
msg.reply('ARGS: %s' % args)
msg.reply('KEYWORDS: %r' % kargs)
msg.reply('Unparsed: %r' % msg.unparsed)
@keyword('join')
def join(self, irc_c, msg, trigger, args, kargs):
if len(args) > 0:
irc_c.JOIN(args)
@keyword('part')
def part(self, irc_c, msg, trigger, args, kargs):
if len(args) > 0:
irc_c.PART(args, message='%s asked me to leave.' % msg.nick)
@keyword('invite')
def invite(self, irc_c, msg, trigger, args, kargs):
if len(args) > 0 and args[0].startswith('#'):
irc_c.RAW('INVITE %s :%s' % (msg.nick, args[0]))
@observe('IRC_MSG_INVITE')
def follow_invites(self, irc_c, msg):
if msg.target == irc_c.botnick: # Sanity
irc_c.JOIN(msg.message)
irc_c.PRIVMSG(msg.message, '%s: I have arrived' % msg.nick)
================================================
FILE: example/plugins/example.py
================================================
""" Example Plugin (dice roller) (botbot plugins.example) """
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import re
from pyaib.plugins import keyword
from random import SystemRandom
def statsCheck(stats):
total = sum([(s - 10) / 2 for s in stats])
avg = total / 6
return avg > 0 and max(stats) > 13
def statsGen():
rand = SystemRandom()
while True:
stats = []
for s in range(0, 6): # Six Stats
rolls = []
for d in range(0, 4): # Four Dice
roll = rand.randint(1, 6)
if roll == 1: # Reroll 1's once
roll = rand.randint(1, 6)
rolls.append(roll)
rolls.sort()
rolls.reverse()
stats.append(rolls[0] + rolls[1] + rolls[2])
if statsCheck(stats):
return stats
return None
@keyword('stats')
def stats(irc_c, msg, trigger, args, kargs):
msg.reply("%s: Set 1: %r" % (msg.nick, statsGen()))
msg.reply("%s: Set 2: %r" % (msg.nick, statsGen()))
rollRE = re.compile(r'((\d+)?d((?:\d+|%))([+-]\d+)?)', re.IGNORECASE)
modRE = re.compile(r'([+-]\d+)')
def roll(count, sides):
results = []
rand = SystemRandom()
for x in range(count):
if sides == 100 or sides == 1000:
#Special Case for 100 sized dice
results.append(rand.randint(1, 10))
results.append(rand.randrange(0, 100, 10))
if sides == 1000:
results.append(rand.randrange(0, 1000, 100))
else:
results.append(rand.randint(1, sides))
return results
@keyword('roll')
def diceroll(irc_c, msg, trigger, args, kargs):
def help():
txt = ("Dice expected in form [<count>]d<sides|'%'>[+-<modifer>] or "
"+-<modifier> for d20 roll. No argument rolls d20.")
msg.reply(txt)
if 'help' in kargs or 'h' in kargs:
help()
return
rolls = []
if not args:
rolls.append(['d20', 1, 20, 0])
else:
for dice in args:
m = rollRE.match(dice) or modRE.match(dice)
if m:
group = m.groups()
if len(group) == 1:
dice = ['d20%s' % group[0], 1, 20, int(group[0])]
rolls.append(dice)
else:
dice = [group[0], int(group[1] or 1),
100 if group[2] == '%' else int(group[2]),
int(group[3] or 0)]
rolls.append(dice)
if dice[1] > 100 or (dice[2] > 100 and dice[2] != 1000):
msg.reply("%s: I don't play with crazy power gamers!"
% msg.nick)
return
else:
help()
return
for dice in rolls:
results = roll(dice[1], dice[2])
total = sum(results) + int(dice[3])
if len(results) > 10:
srolls = '+'.join([str(x) for x in results[:10]])
srolls += '...'
else:
srolls = '+'.join([str(x) for x in results])
msg.reply("%s: (%s)[%s] = %d" % (
msg.nick, dice[0], srolls, total))
print("Example Plugin Done")
================================================
FILE: example/plugins/jokes.py
================================================
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import random
from pyaib.plugins import keyword, plugin_class
@plugin_class
class Jokes(object):
def __init__(self, irc_context, config):
self.r = Roulette()
self.ballresp = config.ballresp
print("Jokes Plugin Loaded!")
@keyword('roulette')
@keyword.nosubs
@keyword.autohelp_noargs
def roulette_root(self, irc_c, msg, trigger, args, kargs):
"""[spin|reload|stats|clearstats] :: Play russian roulette.
One round in a six chambered gun.
Take turns to spin the cylinder until somebody dies."""
pass
@keyword('roulette')
@keyword.sub('spin')
@keyword.autohelp
def roulette_spin(self, irc_c, msg, trigger, args, kargs):
''':: spins the cylinder'''
if self.r.fire(msg.nick):
msg.reply("BANG! %s %s" % (msg.nick, Roulette.unluckyMsg()))
else:
msg.reply("%s %s" % (msg.nick, Roulette.luckyMsg()))
@keyword('roulette')
@keyword.sub('reload')
@keyword.autohelp
def roulette_reload(self, irc_c, msg, trigger, args, kargs):
''':: force the gun to reload'''
self.r.reload()
@keyword('roulette')
@keyword.sub('stats')
@keyword.autohelp
def roulette_stats(self, irc_c, msg, trigger, args, kargs):
'''[player] :: show stats from all games'''
if len(args) == 0:
stats = self.r.getGlobalStats()
msg.reply("In all games there were %d misses and %d kills"
% (stats['misses'], stats['hits']))
else:
stats = self.r.getStats(args[0])
if stats:
msg.reply("%s dodged %d times, died %d times"
% (args[0], stats['misses'], stats['hits']))
@keyword('roulette')
@keyword.sub('clearstats')
@keyword.autohelp
def roulette_clearstats(self, irc_c, msg, trigger, args, kargs):
''':: clear stats'''
self.r.clear()
@keyword('8ball')
@keyword.autohelp_noargs
def magic_8ball(self, irc_c, msg, trigger, args, kargs):
"""[question]? :: Ask the magic 8 ball a question."""
if not msg.message.endswith('?'):
msg.reply("%s: that does not look like a question to me" %
msg.nick)
return
msg.reply("%s: %s" % (msg.nick, random.choice(self.ballresp)))
class Roulette(object):
luckyQuotes = [
"got lucky!",
"is safe... for now.",
"lived to see another day!"
]
unluckyQuotes = [
"swallowed a bullet!",
"snuffed it!",
"kicked the bucket!",
"just died!"
]
def __init__(self):
self.loaded = False
self.fired = False
self.chamber = None
self.position = 0
self.stats = {}
@staticmethod
def luckyMsg():
return random.choice(Roulette.luckyQuotes)
@staticmethod
def unluckyMsg():
return random.choice(Roulette.unluckyQuotes)
def clear(self):
self.stats = {}
def reload(self):
self.chamber = random.choice([0, 1, 2, 3, 4, 5])
self.position = 0
self.loaded = True
def getStats(self, nick):
if nick in self.stats:
return self.stats[nick]
def getGlobalStats(self):
stats = {'hits': 0, 'misses': 0}
for name in self.stats.keys():
stats['hits'] += self.stats[name]['hits']
stats['misses'] += self.stats[name]['misses']
return stats
def fire(self, nick):
if not self.loaded:
self.reload()
if nick not in self.stats:
self.stats[nick] = {'hits': 0, 'misses': 0}
if(self.position == self.chamber):
self.loaded = False
self.fired = True
self.stats[nick]['hits'] += 1
return True
else:
self.position += 1
self.stats[nick]['misses'] += 1
return False
================================================
FILE: example/plugins/karma.py
================================================
"""Karma Plugin (crushinator.plugins.karma)"""
# Copyright 2016 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from pyaib.plugins import observe, keyword, plugin_class
import re
import functools
# allow for yoda style karam
kregex = re.compile(r'^(\+\+|--)?(.+?)(\+\+|--)?$')
@plugin_class
@plugin_class.requires('db')
class Karma(object):
def __init__(self, irc_context, config):
self.db = irc_context.db.get('plugin.karma')
self.scanner = True
self.pronoun = config.get('pronoun', 'her')
self.scanner_refresh = config.get('scanner_refresh', 3600 * 12)
print("Karma Plugin Loaded!")
@keyword('karma')
def stats(self, irc_c, msg, trigger, args, kargs):
""" get karma for something / defaults to your karma lookup """
if not self.scanner:
msg.reply("Sorry {}, I crushed my karma scanner."
.format(msg.nick))
return
if not args:
karma = self.get_karma(msg.nick.user)
who = msg.nick
else:
karma = self.get_karma(args[0])
who = args[0]
if karma > 9000:
msg.reply("\001ACTION removes {} karma scanner.\001"
.format(self.pronoun))
msg.reply("It's Over 9000!")
msg.reply("\001ACTION crushes the karma scanner in {} clenched "
"fist.\001".format(self.pronoun))
self.scanner = False
self.where = msg.channel
irc_c.timers.set('ORDER-KARMA-SCANNER',
functools.partial(self.get_scanner, msg.reply),
at=msg.timestamp + self.scanner_refresh)
else:
msg.reply("Karma for {} is {}".format(who, karma))
def get_scanner(self, reply, irc_c, alarm):
if self.scanner:
return
self.scanner = True
reply('\001ACTION receives a karma scanner and equips it '
'over {} left eye.\001'.format(self.pronoun))
def get_karma(self, thing):
item = self.db.get(thing)
if item.value is None:
item.value = 0
item.commit()
return item.value
def set_karma(self, thing, value):
item = self.db.get(thing)
item.value = value
item.commit()
@observe('IRC_MSG_PRIVMSG')
def gift(self, irc_c, msg):
if not msg.channel:
return
p = '^\x01ACTION gives {} (?:a|his|her|its) karma scanner(?:.|!)?\x01$'
if re.search(p.format(irc_c.botnick), msg.message, re.IGNORECASE):
if self.scanner:
msg.reply("No Thanks {} I have one!".format(msg.nick))
else:
self.scanner = True
msg.reply("Thanks {} I needed that!".format(msg.nick))
@observe('IRC_MSG_PRIVMSG')
def log(self, irc_c, msg):
if not msg.channel:
return
# Collect all the karma changes
changes = {}
for word in re.split("\s", msg.message):
match = kregex.match(word)
if match:
pre, thing, post = match.groups()
for mod in [pre, post]:
if not mod:
continue
if mod == '++':
changes[thing] = changes.setdefault(thing, 0) + 1
else:
changes[thing] = changes.setdefault(thing, 0) - 1
# Apply the Karma
for thing, change in changes.items():
if msg.sender.user == thing: # Don't allow to bump your own karma
continue
self.set_karma(thing, self.get_karma(thing) + change)
================================================
FILE: pyaib/__init__.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from collections import namedtuple
# a namedtuple like that given by sys.version_info
__version_info__ = namedtuple(
'version_info',
'major minor micro releaselevel serial')(major=2,
minor=1,
micro=0,
releaselevel='final',
serial=0)
__version__ = '{v.major}.{v.minor}.{v.micro}'.format(v=__version_info__)
================================================
FILE: pyaib/channels.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import re
import sys
from .components import component_class, observes, msg_parser
if sys.version_info.major == 2:
str = unicode # noqa
@component_class('channels')
class Channels(object):
""" track channels and stuff """
def __init__(self, irc_c, config):
self.channels = set()
self.config = config
self.db = None
print("Channel Management Loaded")
#Provide a little bit of magic
def __contains__(self, channel):
return channel.lower() in self.channels
@observes('IRC_ONCONNECT')
def _autojoin(self, irc_c):
self.channels.clear()
if self.config.autojoin:
if isinstance(self.config.autojoin, str):
self.config.autojoin = self.config.autojoin.split(',')
if self.config.db and irc_c.db:
print("Loading Channels from DB")
self.db = irc_c.db.get('channels', 'autojoin')
if self.db.value:
merge = list(set(self.db.value + self.config.autojoin))
self.config.autojoin = merge
else:
self.db.value = []
self.db.value = sorted(self.config.autojoin)
self.db.commit()
print("Channels Auto Joining: %r" % self.config.autojoin)
irc_c.JOIN(self.config.autojoin)
@msg_parser('JOIN')
def _join_parser(self, msg, irc_c):
msg.raw_channel = re.sub(r'^:', '', msg.args.strip())
msg.channel = msg.raw_channel.lower()
msg.reply = lambda text: irc_c.PRIVMSG(msg.channel, text)
@msg_parser('PART')
def _part_parser(self, msg, irc_c):
msg.raw_channel, _, message = msg.args.strip().partition(' ')
msg.channel = msg.raw_channel.lower()
msg.message = re.sub(r'^:', '', message)
msg.reply = lambda text: irc_c.PRIVMSG(msg.channel, text)
@msg_parser('KICK')
def _kick_parser(self, msg, irc_c):
msg.raw_channel, msg.victim, message = msg.args.split(' ', 2)
msg.channel = msg.raw_channel.lower()
msg.message = re.sub(r'^:', '', message)
msg.reply = lambda text: irc_c.PRIVMSG(msg.channel, text)
@msg_parser('332')
def _topic_parser(self, msg, irc_c):
_, msg.raw_channel, message = msg.args.split(' ', 2)
msg.channel = msg.raw_channel.lower()
msg.message = re.sub(r'^:', '', message)
msg.reply = lambda text: irc_c.PRIVMSG(msg.channel, text)
@observes('IRC_MSG_JOIN')
def _join(self, irc_c, msg):
#Only Our Joins
if msg.nick.lower() == irc_c.botnick.lower():
self.channels.add(msg.channel)
if self.db and msg.channel not in self.db.value:
self.db.value.append(msg.channel)
self.db.value.sort()
self.db.commit()
@observes('IRC_MSG_PART')
def _part(self, irc_c, msg):
#Only Our Parts
if msg.nick.lower() == irc_c.botnick.lower():
self.channels.remove(msg.channel)
if self.db and msg.channel in self.db.value:
self.db.value.remove(msg.channel)
self.db.value.sort()
self.db.commit()
@observes('IRC_MSG_KICK')
def _kick(self, irc_c, msg):
if irc_c.botnick.lower() == msg.victim.lower():
self.channels.remove(msg.channel)
================================================
FILE: pyaib/components.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import inspect
import collections
import sys
from importlib import import_module
from gevent.event import AsyncResult
import gevent
from .util.decorator import EasyDecorator
from .irc import Message
if sys.version_info.major == 2:
str = unicode # noqa
__all__ = ['component_class',
'msg_parser',
'watches', 'observe', 'observes', 'handle', 'handles',
'every',
'triggers_on', 'keyword', 'keywords', 'trigger', 'triggers',
'ComponentManager']
#Used to mark classes for later inspection
CLASS_MARKER = '_PYAIB_COMPONENT'
def component_class(cls):
"""
Let the component loader know to load this class
If they pass a string argument to the decorator use it as a context
name for the instance
"""
if isinstance(cls, str):
context = cls
def wrapper(cls):
setattr(cls, CLASS_MARKER, context)
return cls
return wrapper
elif inspect.isclass(cls):
setattr(cls, CLASS_MARKER, True)
return cls
def _requires(*names):
def wrapper(cls):
cls.__requires__ = names
return cls
return wrapper
component_class.requires = _requires
def _get_plugs(method, kind):
""" Setup a place to put plugin hooks, allowing only one type per func """
if not hasattr(method, '__plugs__'):
method.__plugs__ = (kind, [])
elif method.__plugs__[0] != kind:
raise RuntimeError('Multiple Hook Types on a single method (%s)' %
method.__name__)
return method.__plugs__[1]
def msg_parser(*kinds, **kwargs):
"""
Defines that this method is a message type parser
@param kinds: List of IRC message types/numerics
@param kwargs: Accepts chain keyword, True or 'after' executes this after
the existing parser. 'before' execute before existing parsers.
default is to replace the existing parser
"""
chain = kwargs.pop('chain', False)
def wrapper(func):
parsers = _get_plugs(func, 'parsers')
parsers.extend([(kind, chain) for kind in kinds])
return func
return wrapper
def watches(*events):
""" Define a series of events to later be subscribed to """
def wrapper(func):
eplugs = _get_plugs(func, 'events')
eplugs.extend([event for event in events if event not in eplugs])
return func
return wrapper
observes = watches
observe = watches
handle = watches
handles = watches
class _Ignore(EasyDecorator):
"""Only pass if triggers is from user not ignored"""
def wrapper(dec, irc_c, msg, *args):
if dec.args and dec.kwargs.get('runtime'):
for attr in dec.args:
if hasattr(dec._instance, attr):
ignore_nicks = getattr(dec._instance, attr)
if isinstance(ignore_nicks, str)\
and msg.sender.nick == ignore_nicks:
return
elif isinstance(ignore_nicks, collections.Container)\
and msg.sender.nick in ignore_nicks:
return
elif dec.args and msg.sender.nick in dec.args:
return
return dec.call(irc_c, msg, *args)
watches.ignore = _Ignore
class _Channel(EasyDecorator):
"""Ignore triggers not in channels, or optionally a list of channels"""
def wrapper(dec, irc_c, msg, *args):
if msg.channel:
#Did they want to restrict which channels
#Should we lookup allowed channels at run time
if dec.args and dec.kwargs.get('runtime'):
ok = False
for attr in dec.args:
if hasattr(dec._instance, attr):
channel = getattr(dec._instance, attr)
if isinstance(channel, str)\
and msg.channel == channel:
ok = True
elif isinstance(channel, collections.Container)\
and msg.channel in channel:
ok = True
if not ok:
return
elif dec.args and msg.channel not in dec.args:
return
return dec.call(irc_c, msg, *args)
watches.channel = _Channel
def every(seconds, name=None):
""" Define a timer to execute every interval """
def wrapper(func):
timers = _get_plugs(func, 'timers')
timer = (name if name else func.__name__, seconds)
if timer not in timers:
timers.append(timer)
return func
return wrapper
class triggers_on(object):
"""Define a series of trigger words this method responds too"""
def __init__(self, *words):
self.words = words
def __call__(self, func):
triggers = _get_plugs(func, 'triggers')
triggers.extend(set([word for word in self.words
if word not in triggers]))
return func
class channel(EasyDecorator):
"""Ignore triggers not in channels, or optionally a list of channels"""
def wrapper(dec, irc_c, msg, trigger, args, kargs):
if msg.channel:
# Did they want to restrict which channels
# Should we lookup allowed channels at run time
if dec.args and dec.kwargs.get('runtime'):
ok = False
for attr in dec.args:
if hasattr(dec._instance, attr):
channel = getattr(dec._instance, attr)
if isinstance(channel, str)\
and msg.channel.lower() == channel:
ok = True
elif isinstance(channel, collections.Container)\
and msg.channel.lower() in channel:
ok = True
if not ok:
return
elif dec.args and msg.channel not in dec.args:
return
elif not dec.kwargs.get('private'):
return
return dec.call(irc_c, msg, trigger, args, kargs)
class private_or_channel(channel):
"""Allow either private or specified channel"""
def __init__(dec, *args, **kwargs):
kwargs['private'] = True
super(triggers_on.private_or_channel, dec).__init__(*args, **kwargs)
class private(EasyDecorator):
"""Only pass if triggers is from message not in a channel"""
def wrapper(dec, irc_c, msg, trigger, args, kargs):
if not msg.channel:
return dec.call(irc_c, msg, trigger, args, kargs)
class helponly(EasyDecorator):
"""Only provide help"""
def wrapper(dec, irc_c, msg, trigger, args, kargs):
msg.reply('%s %s' % (trigger,
irc_c.triggers._clean_doc(dec.__doc__)))
class autohelp(EasyDecorator):
"""Make --help trigger help"""
def wrapper(dec, irc_c, msg, trigger, args, kargs):
if 'help' in kargs or (args and args[0] == 'help'):
msg.reply('%s %s' % (trigger,
irc_c.triggers._clean_doc(dec.__doc__)))
else:
dec.call(irc_c, msg, trigger, args, kargs)
class autohelp_noargs(EasyDecorator):
"""Empty args / kargs trigger help"""
#It was impossible to call autohelp to decorate this method
def wrapper(dec, irc_c, msg, trigger, args, kargs):
if (not args and not kargs) or 'help' in kargs or (
args and args[0] == 'help'):
msg.reply('%s %s' % (trigger,
irc_c.triggers._clean_doc(dec.__doc__)))
else:
return dec.call(irc_c, msg, trigger, args, kargs)
class sub(EasyDecorator):
"""Handle only sub(words) for a given trigger"""
def __init__(dec, *words):
dec._subs = words
for word in words:
if not isinstance(word, str):
raise TypeError("sub word must be a string")
def wrapper(dec, irc_c, msg, trigger, args, kargs):
if args and args[0].lower() in dec._subs:
unparsed = msg.unparsed
msg = msg.copy(irc_c)
msg.unparsed = unparsed[len(args[0]) + 1:]
return dec.call(irc_c, msg, '%s %s' % (trigger,
args[0].lower()),
args[1:], kargs)
subs = sub
class nosub(EasyDecorator):
"""Prevent call if argument is present"""
def wrapper(dec, irc_c, msg, trigger, args, kargs):
if (not dec.args and args) or (dec.args and args
and args[0].lower() in dec.args):
return
else:
return dec.call(irc_c, msg, trigger, args, kargs)
nosubs = nosub
keyword = keywords = trigger = triggers = triggers_on
triggers.ignore = _Ignore
triggers.channel = _Channel
class ComponentManager(object):
""" Manage and Load all pyaib Components """
_loaded_components = collections.defaultdict(AsyncResult)
def __init__(self, context, config):
""" Needs a irc context and its config """
self.context = context
self.config = config
def load(self, name):
""" Load a python module as a component """
if self.is_loaded(name):
return
#Load top level config item matching component name
basename = name.split('.').pop()
config = self.context.config.setdefault(basename, {})
print("Loading Component %s..." % name)
ns = self._process_component(name, 'pyaib', CLASS_MARKER,
self.context, config)
self._loaded_components[basename].set(ns)
def _require(self, name):
self._loaded_components[name].wait()
def load_configured(self, autoload=None):
"""
Load all configured components autoload is a list of components
to always load
"""
components = []
if isinstance(autoload, (list, tuple, set)):
components.extend(autoload)
#Don't do duplicate loads
if self.config.load:
if not isinstance(self.config.load, list):
self.config.load = self.config.load.split(' ')
[components.append(comp) for comp in self.config.load
if comp not in components]
gevent.joinall([gevent.spawn(self.load, component)
for component in components])
def is_loaded(self, name):
""" Determine by name if a component is loaded """
return self._loaded_components[name].ready()
def _install_hooks(self, context, hooked_methods):
#Add All the hooks to the right place
for method in hooked_methods:
kind, args = method.__plugs__
if kind == 'events':
for event in args:
context.events(event).observe(method)
elif kind == 'triggers':
for word in args:
context.triggers(word).observe(method)
elif kind == 'timers':
for name, seconds in args:
context.timers.set(name, method, every=seconds)
elif kind == 'parsers':
for name, chain in args:
self._add_parsers(method, name, chain)
def _add_parsers(self, method, name, chain):
""" Handle Message parser adding and chaining """
if chain:
existing = Message.get_parser(name)
def _chain_after(msg, irc_c):
existing(msg, irc_c)
method(msg, irc_c)
def _chain_before(msg, irc_c):
method(msg, irc_c)
existing(msg, irc_c)
if existing and chain == 'before':
Message.add_parser(name, _chain_before)
elif existing:
Message.add_parser(name, _chain_after)
else:
Message.add_parser(name, method)
else:
Message.add_parser(name, method)
def _find_annotated_callables(self, class_marker, component_ns, config,
context):
annotated_callables = []
for name, member in inspect.getmembers(component_ns):
#Find Classes marked for loading
if inspect.isclass(member) and hasattr(member, class_marker):
#Handle Requirements
if hasattr(member, '__requires__'):
for req in member.__requires__:
self._require(req)
obj = member(context, config)
#Save the context for this obj if the class_marker is a str
context_name = getattr(obj, class_marker)
if isinstance(context_name, str):
context[context_name] = obj
#Search for hooked instance methods
for name, thing in inspect.getmembers(obj):
if (isinstance(thing, collections.Callable)
and hasattr(thing, '__plugs__')):
annotated_callables.append(thing)
#Find Functions with Hooks
if (isinstance(member, collections.Callable)
and hasattr(member, '__plugs__')):
annotated_callables.append(member)
return annotated_callables
def _process_component(self, name, path, class_marker, context, config):
if name.startswith('/'):
importname = name[1:]
path = None
else:
importname = '.'.join([path, name])
try:
component_ns = import_module(importname)
except ImportError as e:
raise ImportError('pyaib failed to load (%s): %r'
% (importname, e))
annotated_calls = self._find_annotated_callables(class_marker,
component_ns, config,
context)
self._install_hooks(context, annotated_calls)
return component_ns
================================================
FILE: pyaib/config.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import sys
import os
import yaml
from .util import data
if sys.version_info.major == 2:
def construct_yaml_str(self, node):
return self.construct_scalar(node)
yaml.SafeLoader.add_constructor('tag:yaml.org,2002:str',
construct_yaml_str)
class Config(object):
def __init__(self, configFile=None, configPath=None):
print("Config Module Loaded.")
if configFile is None:
raise RuntimeError("YOU MUST PASS 'configFile' DURING BOT INIT")
(config, searchpaths) = self.__load(configFile, configPath)
if config is None:
msg = ("You need a valid main config (searchpaths: %s)" %
searchpaths)
raise RuntimeError(msg)
#Wrap the config dict
self.config = data.CaseInsensitiveObject(config)
#Files can be loaded from the 'CONFIG' section
#Load the load statement if any
for section, file in self.config.setdefault('config.load', {}).items():
config = self.__load(file,
[configPath, self.config.get('config.path')])
#Badly syntax configs will be empty
if config is None:
config = {}
self.config.set(section, config)
#Attempt to load a config file name print exceptions
def __load(self, configFile, path=None):
data = None
(filepath, searchpaths) = self.__findfile(configFile, path)
if filepath: # If the file is found lets try to load it
try:
with open(filepath, 'r') as file:
data = yaml.safe_load(file)
print("Loaded Config from %s." % configFile)
except yaml.YAMLError as exc:
print("Error in configuration file (%s): %s" % (filepath, exc))
if hasattr(exc, 'problem_mark'):
mark = exc.problem_mark
print("Error position: (%s:%s)" % (mark.line + 1,
mark.column + 1))
return (data, searchpaths)
#Find the requested file in the path (for PARs)
#If configFile is a list then do lookup for each
#First Found is returned
def __findfile(self, configFile, path=None):
searchpaths = []
if isinstance(path, list):
searchpaths.extend(path) # Optional Config path
elif path:
searchpaths.append(path)
searchpaths.extend(sys.path)
for path in searchpaths:
if not os.path.isdir(path):
path = os.path.dirname(path)
if os.path.isdir(path):
for root, dirs, files in os.walk(path):
if configFile in files:
return (os.path.join(root, configFile), searchpaths)
return (None, searchpaths)
================================================
FILE: pyaib/db.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Generic DB Component
Provide a simple key value store.
The Backend data store can be changed out via a driver intermediate.
Must support the following methods, object is a dict or list or mixture
[key(plain text), payload] should be the return value for operations that
return objects
Driver Methods:
getObject(key=, bucket=)
setObject(object, key=, bucket=)
updateObject(object, key=, bucket=)
updateObjectKey(bucket=, oldkey=, newkey=)
updateObjectBucket(key=, oldbucket=, newbucket=)
getAllObjects(bucket=) (iter)
deleteObject(key=, bucket=) #One at a time for safety
"""
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import hashlib
import json
import inspect
from importlib import import_module
from .components import component_class
CLASS_MARKER = '_PYAIB_DB_DRIVER'
def sha256(msg):
""" return the hex digest for a givent msg """
if not isinstance(msg, bytes):
msg = msg.encode('utf-8')
return hashlib.sha256(msg).hexdigest()
hash = sha256
def jsonify(thing):
return json.dumps(thing, sort_keys=True, separators=(',', ':'))
def dejsonify(jsonstr):
return json.loads(jsonstr)
def db_driver(cls):
"""Mark a class def as a db driver"""
setattr(cls, CLASS_MARKER, True)
return cls
@component_class('db')
class ObjectStore(object):
""" Generic Key Value Store """
# Database Driver is not loaded
_driver = None
def __init__(self, irc_c, config):
self.config = config
self._load_driver()
# Small Sanity Test
if not self._driver:
raise RuntimeError('Can not load DB component driver not loaded')
def _load_driver(self):
""" Loads the configured driver config.db.backend """
name = self.config.backend
if not name:
#Raise some exception, bail out we are done.
raise RuntimeError('config item db.backend not set')
if '.' in name:
importname = name
else:
importname = 'pyaib.dbd.%s' % name
basename = name.split('.').pop()
driver_ns = import_module(importname)
for name, cls in inspect.getmembers(driver_ns, inspect.isclass):
if hasattr(cls, CLASS_MARKER):
#Load up the driver
self._driver = cls(self.config.driver.setdefault(basename, {}))
break
else:
raise RuntimeError('Unable to instance db driver %r' % name)
#Define easy data access methods
def get(self, bucket, key=None):
"""Get a Bucket or if key is provided get a Item from the db"""
if key is None:
return Bucket(self, bucket)
key, payload = self._driver.getObject(key, bucket)
return Item(self._driver, bucket, key, payload)
def getAll(self, bucket):
"""Get all items in the bucket ITERATOR"""
for key, payload in self._driver.getAllObjects(bucket):
yield Item(self._driver, bucket, key, payload)
def set(self, bucket, key, obj):
"""Store an object in the db by bucket and key, return an Item"""
self._driver.setObject(obj, key, bucket)
return Item(self._driver, bucket, key, obj)
def delete(self, bucket, key):
"""Delete an object in the store"""
self._driver.deleteObject(key, bucket)
class Item(object):
""" Represents a item stored in the key value store, with easy methods """
def __init__(self, driver, bucket, key, payload):
self._driver = driver
#Store some meta to determine changes for commit
self._meta = {'bucket': bucket, 'key': key,
'objectHash': hash(jsonify(payload))}
self.bucket = bucket
self.key = key
self.value = payload
def reload(self):
self.key, self.value = self._driver.getObject(self._meta['key'],
self._meta['bucket'])
self.bucket = self._meta['bucket']
def delete(self):
self._driver.deleteObject(self.key, self.bucket)
def commit(self):
if hash(jsonify(self.value)) != self._meta['objectHash']:
if not self.value:
self.delete()
else:
self._driver.updateObject(self.value, self._meta['key'],
self._meta['bucket'])
elif self._meta['bucket'] != self.bucket:
if not self.bucket:
self.delete()
else:
self._driver.updateObjectBucket(self._meta['key'],
self._meta['bucket'],
self.bucket)
elif self._meta['key'] != self.key:
if not self.key:
self.delete()
else:
self._driver.updateObjectKey(self._meta['bucket'],
self._meta['key'], self.key)
#Nothing left to commit
class Bucket(object):
""" An class tied to a bucket """
def __init__(self, db, bucket):
self._db = db
self._bucket = bucket
def __repr__(self):
return 'Bucket(%r)' % self._bucket
def get(self, key):
return self._db.get(self._bucket, key)
def getAll(self):
return self._db.getAll(self._bucket)
def set(self, key, obj):
return self._db.set(self._bucket, key, obj)
def delete(self, key):
return self._db.delete(self._bucket, key)
================================================
FILE: pyaib/dbd/__init__.py
================================================
================================================
FILE: pyaib/dbd/sqlite.py
================================================
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import sqlite3
import zlib
from pyaib.db import db_driver, hash
try:
#Try to make use of ujson if we have it
import ujson as json
import pyaib.db
pyaib.db.json = json
pyaib.db.jsonify = json.dumps
except ImportError:
pass
from pyaib.db import jsonify, dejsonify
def compress(message):
if not isinstance(message, bytes):
message = message.encode('utf-8')
return zlib.compress(message)
decompress = zlib.decompress
@db_driver
class SqliteDriver(object):
""" A Sqlite3 Pyaib DB Driver """
def __init__(self, config):
path = config.path
if not path:
raise RuntimeError('Missing "path" config for sqlite driver')
try:
self.conn = sqlite3.connect(path)
except sqlite3.OperationalError as e:
#Can't open DB
raise
print("Sqlite DB Driver Loaded!")
def _bucket_exists(self, bucket):
c = self.conn.execute("SELECT name from sqlite_master "
"WHERE type='table' and name=?",
(hash(bucket),))
if c.fetchone():
return True
else:
return False
def _has_keys(self, bucket):
c = self.conn.execute("SELECT count(*) from `{}`".format(hash(bucket)))
row = c.fetchone()
if row[0]:
return True
else:
return False
def _create_bucket(self, bucket):
self.conn.execute("CREATE TABLE `{}` (key blob UNIQUE, value blob)"
.format(hash(bucket)))
def getObject(self, key, bucket):
if not self._bucket_exists(bucket):
return key, None
c = self.conn.execute("SELECT key, value from `{}` WHERE key=?"
.format(hash(bucket)), (key,))
row = c.fetchone()
if row:
k, v = row
return (k, dejsonify(decompress(v).decode('utf-8')))
else:
return key, None
def setObject(self, obj, key, bucket):
if not self._bucket_exists(bucket):
self._create_bucket(bucket)
blob = sqlite3.Binary(compress(jsonify(obj).encode('utf-8')))
self.conn.execute("REPLACE INTO `{}` (key, value) VALUES (?, ?)"
.format(hash(bucket)),
(key, blob))
self.conn.commit()
def updateObject(self, obj, key, bucket):
self.setObject(obj, key, bucket)
def updateObjectKey(self, bucket, oldkey, newkey):
self.conn.execute("UPDATE `{}` set key = ? where key=?"
.format(hash(bucket)), (newkey, oldkey))
self.conn.commit()
def updateObjectBucket(self, key, oldbucket, newbucket):
_, v = self.getObject(key, oldbucket)
self.deleteObject(key, oldbucket, commit=False)
self.setObject(v, key, newbucket)
def getAllObjects(self, bucket):
if not self._bucket_exists(bucket):
return
for k, v in self.conn.execute("SELECT key, value from `{}`"
.format(hash(bucket))):
yield (k, dejsonify(decompress(v).decode('utf-8')))
def deleteObject(self, key, bucket, commit=True):
if self._bucket_exists(bucket):
self.conn.execute("DELETE from `{}` where key = ?"
.format(hash(bucket)), (key,))
if not self._has_keys(bucket):
self.conn.execute("DROP TABLE IF EXISTS `{}`"
.format(hash(bucket)))
if commit:
self.conn.commit()
================================================
FILE: pyaib/events.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import collections
import gevent
import gevent.pool
from . import irc
class Event(object):
""" An Event Handler """
def __init__(self):
self.__observers = []
def observe(self, observer):
if isinstance(observer, collections.Callable):
self.__observers.append(observer)
else:
print("Event Error: %s not callable" % repr(observer))
return self
def unobserve(self, observer):
self.__observers.remove(observer)
return self
def fire(self, *args, **keywargs):
#Pull the irc_c from the args
irc_c = args[0]
if not isinstance(irc_c, irc.Context):
print("Error first argument should be the irc context")
#Maybe DIE here
return
for observer in self.__observers:
if isinstance(observer, collections.Callable):
irc_c.bot_greenlets.spawn(observer, *args, **keywargs)
else:
print("Event Error: %s not callable" % repr(observer))
def clearObjectObservers(self, inObject):
for observer in self.__observers:
if observer.__self__ == inObject:
self.unobserve(observer)
def getObserverCount(self):
return len(self.__observers)
def observers(self):
return self.__observers
def __bool__(self):
return self.getObserverCount() > 0
__nonzero__ = __bool__ # 2.x compat
__iadd__ = observe
__isub__ = unobserve
__call__ = fire
__len__ = getObserverCount
class Events(object):
""" Manage events allow observers before events are defined"""
def __init__(self, irc_c):
self.__events = {}
self.__nullEvent = NullEvent()
#A place to track all the running events
#Events load first so this seems logical
irc_c.bot_greenlets = gevent.pool.Group()
def list(self):
return self.__events.keys()
def isEvent(self, name):
return name.lower() in self.__events
def getOrMake(self, name):
if not self.isEvent(name):
#Make Event if it does not exist
self.__events[name.lower()] = Event()
return self.get(name)
#Do not create the event on a simple get
#Return the null event on non existent events
def get(self, name):
event = self.__events.get(name.lower())
if event is None: # Only on undefined events
return self.__nullEvent
return event
__contains__ = isEvent
__call__ = getOrMake
__getitem__ = get
class NullEvent(object):
""" Null Object Pattern: Don't Do Anything Silently"""
def fire(self, *args, **keywargs):
pass
def clearObjectObservers(self, obj):
pass
def getObserverCount(self):
return 0
def __bool__(self):
return False
__nonzero__ = __bool__ # Diff between 3.x and 2.x
def observe(self, observer):
raise TypeError('Null Events can not have Observers!')
def unobserve(self, observer):
raise TypeError('Null Events do not have Observers!')
__iadd__ = observe
__isub__ = unobserve
__call__ = fire
__len__ = getObserverCount
================================================
FILE: pyaib/irc.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import re
import sys
from textwrap import wrap
import traceback
import time
import gevent
from .linesocket import LineSocket
from .util import data
from .util.decorator import raise_exceptions
from . import __version__ as pyaib_version
if sys.version_info.major == 2:
str = unicode # noqa
MAX_LENGTH = 510
#Class for storing irc related information
class Context(data.Object):
"""Dummy Object to hold irc data and send messages"""
# IRC COMMANDS are all CAPS for sanity with irc information
# TODO: MOVE irc commands into component and under irc_c.cmd
# Raw IRC Message
def RAW(self, message):
try:
#Join up the message parts
if isinstance(message, (list, tuple)):
message = ' '.join(message)
#Raw Send but don't allow empty spam
if message is not None:
#Clean up messages
message = re.sub(r'[\r\n]', '', message).expandtabs(4).rstrip()
if len(message):
self.client.socket.writeline(message)
#Fire raw send event for debug if exists [] instead of ()
self.events['IRC_RAW_SEND'](self, message)
except TypeError:
#Somebody tried to raw a None or something just print exception
print("Bad RAW message: %r" % repr(message))
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_tb(exc_traceback)
# Set our nick
def NICK(self, nick):
self.RAW('NICK %s' % nick)
if not self.registered:
#Assume we get the nick we want during registration
self.botnick = nick
# privmsg/notice with max line handling
def PRIVMSG(self, target, msg):
for line in self._wrap_command('PRIVMSG', target, msg):
self.RAW(line)
def NOTICE(self, target, msg):
for line in self._wrap_command('NOTICE', target, msg):
self.RAW(line)
def _wrap_command(self, command, target, msg):
if isinstance(msg, (list, tuple, set)):
msg = ' '.join(msg)
msgtemplate = '%s %s :%%s' % (command, target)
# length of self.botsender.raw is 0 when not set :P
# + 2 because of leading : and space after nickmask
prefix_length = len(self.botsender.raw) + 2 + len(msgtemplate % '')
for line in wrap(msg, MAX_LENGTH - prefix_length):
yield msgtemplate % line
def JOIN(self, channels):
if isinstance(channels, (list, set, tuple)):
channels = list(channels)
else:
channels = [channels]
join = 'JOIN '
msg = join
# Build up join messages (wrap won't work)
while channels:
channel = channels.pop() + ','
if len(msg + channel) > MAX_LENGTH:
self.RAW(msg.rstrip(','))
msg = join
msg += channel
self.RAW(msg.rstrip(','))
def PART(self, channels, message=None):
if isinstance(channels, list):
channels = ','.join(channels)
if message:
self.RAW('PART %s :%s' % (channels, message))
else:
self.RAW('PART %s' % channels)
class Client(object):
"""IRC Client contains irc logic"""
def __init__(self, irc_c):
self.config = irc_c.config.irc
self.servers = self.config.servers
self.irc_c = irc_c
irc_c.client = self
self.reconnect = True
self.__register_client_hooks(self.config)
# The IRC client Event Loop
# Call events for every irc message
def _try_connect(self):
for server in self.servers:
host, port, ssl = self.__parseserver(server)
sock = LineSocket(host, port, SSL=ssl)
if sock.connect():
self.socket = sock
return sock
return None
def _fire_msg_events(self, sock, irc_c):
while True: # Event still running
raw = sock.readline() # Yield
if raw:
#Fire RAW MSG if it has observers
irc_c.events['IRC_RAW_MSG'](irc_c, raw)
#Parse the RAW message
msg = Message(irc_c, raw)
if msg: # This is a valid message
#So we can do length calculations for PRIVMSG WRAPS
if (msg.nick == irc_c.botnick
and irc_c.botsender != msg.sender):
irc_c.botsender = msg.sender
#Event for kind of message [if exists]
eventKey = 'IRC_MSG_%s' % msg.kind
irc_c.events[eventKey](irc_c, msg)
#Event for parsed messages [if exists]
irc_c.events['IRC_MSG'](irc_c, msg)
def run(self):
irc_c = self.irc_c
#Function to Fire Timers
def _timers(irc_c):
print("Starting Timers Loop")
while True:
gevent.sleep(1)
irc_c.timers(irc_c)
#If servers is not a list make it one
if not isinstance(self.servers, list):
self.servers = self.servers.split(',')
while self.reconnect:
# Keep trying to reconnect going through the server list
sock = self._try_connect()
if sock is None:
gevent.sleep(10) # Wait 10 Seconds between retries
print("Retrying Server List...")
continue
#Catch when the socket has an exception
try:
#Have the line socket autofill its buffers
#Maybe this should be in socket.connect
gevent.spawn(raise_exceptions(self.socket.run))
gevent.sleep(0) # Yield
#Fire Socket Connect Event (Always)
irc_c.events('IRC_SOCKET_CONNECT')(irc_c)
irc_c.bot_greenlets.spawn(_timers, irc_c)
#Enter the irc event loop
self._fire_msg_events(sock, irc_c)
except LineSocket.SocketError:
try:
self.socket.close()
print("Giving Greenlets Time(1s) to die..")
irc_c.bot_greenlets.join(timeout=1)
except gevent.Timeout:
# We got a timeout kill the others
print("Killing Remaining Greenlets...")
irc_c.bot_greenlets.kill()
else:
print("Bot Dying.")
def die(self, message="Dying"):
self.irc_c.RAW("QUIT :%s" % message)
self.reconnect = False
def cycle(self):
self.irc_c.RAW("QUIT :Reconnecting")
def signal_handler(self, signum, frame):
""" Handle Ctrl+C """
self.irc_c.RAW("QUIT :Received a ctrl+c exiting")
self.reconnect = False
#Register our own hooks for basic protocol handling
def __register_client_hooks(self, options):
events = self.irc_c.events
timers = self.irc_c.timers
#AUTO_PING TIMER
def AUTO_PING(irc_c, msg):
irc_c.RAW('PING :%s' % irc_c.server)
#if auto_ping unless set to 0
if options.auto_ping != 0:
timers.set('AUTO_PING', AUTO_PING,
every=options.auto_ping or 600)
#Handle PINGs
def PONG(irc_c, msg):
irc_c.RAW('PONG :%s' % msg.args)
#On a ping from the server reset our timer for auto-ping
timers.reset('AUTO_PING', AUTO_PING)
events('IRC_MSG_PING').observe(PONG)
#On the socket connecting we should attempt to register
def REGISTER(irc_c):
irc_c.registered = False
if options.password: # Use a password if one is issued
#TODO allow password to be associated with server url
irc_c.RAW('PASS %s' % options.password)
irc_c.RAW('USER %s 8 * :%s'
% (options.user,
options.realname.format(version=pyaib_version)))
irc_c.NICK(options.nick)
events('IRC_SOCKET_CONNECT').observe(REGISTER)
#Trigger an IRC_ONCONNECT event on 001 msg's
def ONCONNECT(irc_c, msg):
irc_c.server = msg.sender
irc_c.registered = True
irc_c.events('IRC_ONCONNECT')(irc_c)
events('IRC_MSG_001').observe(ONCONNECT)
def NICK_INUSE(irc_c, msg):
if not irc_c.registered:
irc_c.NICK('%s_' % irc_c.botnick)
_, nick, _ = msg.args.split(' ', 2)
#Fire event for other modules [if its watched]
irc_c.events['IRC_NICK_INUSE'](irc_c, nick)
events('IRC_MSG_433').observe(NICK_INUSE)
#When we change nicks handle botnick updates
def NICK(irc_c, msg):
if msg.nick.lower() == irc_c.botnick.lower():
irc_c.botnick = msg.args
irc_c.events['IRC_NICK_CHANGE'](irc_c, msg.nick, msg.args)
events('IRC_MSG_NICK').observe(NICK)
#Parse Server Records
# (ssl:)?host(:port)? // after ssl: is optional
# TODO allow password@ in server strings
def __parseserver(self, server):
match = re.search(r'^(ssl:(?://)?)?([^:]+)(?::(\d+))?$',
server.lower())
if match is None:
print('BAD Server String: %s' % server)
sys.exit(1)
#Pull out the pieces of the server line
ssl = match.group(1) is not None
host = match.group(2)
port = int(match.group(3)) or 6667
return [host, port, ssl]
class Message (object):
"""Parse raw irc text into easy to use class"""
MSG_REGEX = re.compile(r'^(?::([^ ]+) )?([^ ]+) (.+)$')
DIRECT_REGEX = re.compile(r'^([^ ]+) :?(.+)$')
#Some Message prefixes for channel prefixes
PREFIX_OP = 1
PREFIX_HALFOP = 2
PREFIX_VOICE = 3
# Place to store parsers for complex message types
_parsers = {}
@classmethod
def add_parser(cls, kind, handler):
cls._parsers[kind] = handler
@classmethod
def get_parser(cls, kind):
return cls._parsers.get(kind)
def copy(self, irc_c):
return type(self)(irc_c, self.raw)
def __init__(self, irc_c, raw):
self.raw = raw
match = Message.MSG_REGEX.search(raw)
if match is None:
self._error_out('IRC Message')
#If the prefix is blank its the server
self.sender = Sender(match.group(1) or irc_c.server)
self.kind = match.group(2)
self.args = match.group(3)
self.nick = self.sender.nick
#Time Stamp every message (Floating Point is Fine)
self.timestamp = time.time()
#Handle more message types
if self.kind in Message._parsers:
Message._parsers[self.kind](self, irc_c)
#Be nice strip off the leading : on args
self.args = re.sub(r'^:', '', self.args)
def _error_out(self, text):
print('BAD %s: %s' % (text, self.raw))
self.kind = None
def __bool__(self):
return self.kind is not None
__nonzero__ = __bool__
def __str__(self):
return self.raw
#Friendly get that doesnt blow up on non-existent entries
def __getattr__(self, key):
return None
@staticmethod
def _directed_message(msg, irc_c):
match = Message.DIRECT_REGEX.search(msg.args)
if match is None:
return msg._error_out('PRIVMSG')
msg.target = match.group(1).lower()
msg.message = match.group(2)
#If the target is not the bot its a channel message
if msg.target != irc_c.botnick:
msg.reply_target = msg.target
#Strip off any message prefixes
msg.raw_channel = msg.target.lstrip('@%+')
msg.channel = msg.raw_channel.lower() # Normalized to lowercase
#Record the perfix
if msg.target.startswith('@'):
msg.channel_prefix = msg.PREFIX_OP
elif msg.target.startswith('%'):
msg.channel_prefix = msg.PREFIX_HALFOP
elif msg.target.startswith('+'):
msg.channel_prefix = msg.PREFIX_VOICE
else:
msg.reply_target = msg.nick
#Setup a reply method
def __reply(text):
irc_c.PRIVMSG(msg.reply_target, text)
msg.reply = __reply
#Install some common parsers
Message.add_parser('PRIVMSG', Message._directed_message)
Message.add_parser('NOTICE', Message._directed_message)
Message.add_parser('INVITE', Message._directed_message)
Message.add_parser('TOPIC', Message._directed_message)
class Sender(str):
"""all the logic one would need for understanding sender part of irc msg"""
def __new__(cls, sender):
#Pull out each of the pieces at instance time
if '!' in sender:
nick, _, usermask = sender.partition('!')
inst = str.__new__(cls, nick)
inst._user, _, inst._hostname = usermask.partition('@')
return inst
else:
return str.__new__(cls, sender)
@property
def raw(self):
""" get the raw sender string """
if self.nick:
return '%s!%s@%s' % (self, self._user, self._hostname)
else:
return self
@property
def nick(self):
""" get the nick """
if hasattr(self, '_hostname'):
return self
@property
def user(self):
""" get the user name """
if self.nick:
return self._user.lstrip('~')
@property
def hostname(self):
""" get the hostname """
if self.nick:
return self._hostname
else:
return self
@property
def usermask(self):
""" get the usermask user@hostname """
if self.nick:
return '%s@%s' % (self._user, self._hostname)
================================================
FILE: pyaib/ircbot.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
#WE want the ares resolver, screw thread-pool
import os
os.environ['GEVENT_RESOLVER'] = 'ares'
import gevent.monkey
gevent.monkey.patch_all()
#Screw you python, lets try this for unicode support
import sys
if sys.version_info.major == 2:
import imp
imp.reload(sys)
sys.setdefaultencoding('utf-8')
import signal
import gevent
from .config import Config
from .events import Events
from .timers import Timers
from .components import ComponentManager
from . import irc
class IrcBot(object):
""" A easy framework to make useful bots """
def __init__(self, *args, **kargs):
#Shortcut
install = self._install
#Irc Context the all purpose data structure
install('irc_c', irc.Context(), False)
#Load the Config
install('config', Config(*args, **kargs).config)
#Install most basic fundamental functionality
install('events', self._loadComponent(Events, False))
install('timers', self._loadComponent(Timers, False))
#Load the ComponentManager and load components
autoload = ['triggers', 'channels', 'plugins'] # Force these to load
install('components', self._loadComponent(ComponentManager))\
.load_configured(autoload)
def run(self):
""" Starts the Event loop for the bot """
client = irc.Client(self.irc_c)
#Tell the client to run inside a greenlit
signal.signal(signal.SIGINT, client.signal_handler)
gevent.spawn(client.run).join()
# Assign things to self and Context
def _install(self, name, thing, inContext=True):
setattr(self, name, thing)
if inContext:
self.irc_c[name] = thing
return thing
def _loadComponent(self, cname, passConfig=True):
""" Load a Component passing it the context and its config """
#I am using != instead of is not because of space limits :P
config = cname.__name__ if cname != ComponentManager else "Components"
if passConfig:
return cname(self.irc_c, self.config.setdefault(config, {}))
else:
return cname(self.irc_c)
================================================
FILE: pyaib/linesocket.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Line based socket using gevent
"""
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import errno
import gevent
from gevent import socket
from gevent import queue, select
from OpenSSL import SSL
from .util.decorator import utf8Encode, utf8Decode, raise_exceptions
class LineSocketBuffers(object):
def __init__(self):
self.readbuffer = bytearray()
self.writebuffer = bytearray()
def clear(self):
del self.readbuffer[0:]
del self.writebuffer[0:]
def readbuffer_mv(self):
return memoryview(self.readbuffer)
def writebuffer_mv(self):
return memoryview(self.writebuffer)
#We use this to end lines we send to the server its in the RFC
#Buffers don't support unicode just yet so 'encode'
LINEENDING = b'\r\n'
class LineSocket(object):
"""Line based socket impl takes a host and port"""
def __init__(self, host, port, SSL):
self.host, self.port, self.SSL = (host, port, SSL)
self._socket = None
self._buffer = LineSocketBuffers()
#Thread Safe Queues for
self._IN = queue.Queue()
self._OUT = queue.Queue()
#Exceptions for LineSockets
class SocketError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
# Connect to remote host
def connect(self):
host, port = (self.host, self.port)
#Clean out the buffers
self._buffer.clear()
#If the existing socket is not None close it
if self._socket is not None:
self.close()
# Resolve the hostname and connect (ipv6 ready)
sock = None
try:
for info in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM):
family, socktype, proto, canonname, sockaddr = info
#Validate the socket will make
try:
sock = socket.socket(family, socktype, proto)
#Set Keepalives
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
except socket.error as msg:
print('Socket Error: %s' % msg)
sock = None
continue
#Wrap in ssl if asked
if self.SSL:
print('Starting SSL')
try:
ctx = SSL.Context(SSL.SSLv23_METHOD)
sock = SSL.Connection(ctx, sock)
except SSL.Error as err:
print('Could not Initiate SSL: %s' % err)
sock = None
continue
#Try to establish the connection
try:
print('Trying Connect(%s)' % repr(sockaddr))
sock.settimeout(10)
sock.connect(sockaddr)
except socket.error as msg:
print('Socket Error: %s' % msg)
if self.SSL:
try:
sock.shutdown()
except SSL.Error as e:
print('Failed to shutdown SSL: %s' % e)
sock.close()
sock = None
continue
break
except Exception as e:
print('Some unknown exception: %s' % e)
#After all the connection attempts and sock is still none lets bomb out
if sock is None:
print('Could not open connection')
return False
#Set the socket to non_blocking
sock.setblocking(0)
print("Connection Open.")
self._socket = sock
return True
#Start up the read and write threads
def run(self):
#Fire off some greenlits to handing reading and writing
try:
print("Starting Read/Write Loops")
tasks = [gevent.spawn(raise_exceptions(self._read)),
gevent.spawn(raise_exceptions(self._write))]
#Wait for a socket exception and raise the flag
select.select([], [], [self._socket]) # Yield
raise self.SocketError('Socket Exception')
finally: # Make sure we kill the tasks
print("Killing read and write loops")
gevent.killall(tasks)
def close(self):
if self.SSL:
try:
self._socket.shutdown()
except:
pass
self._socket.close()
self._socket = None
#Read from the socket, split out lines into a queue for readline
def _read(self):
eof = False
while True:
try:
#Wait for when the socket is ready for read
select.select([self._socket], [], []) # Yield
data = self._socket.recv(4096)
if not data: # Disconnected Remote
eof = True
self._buffer.readbuffer.extend(data)
except SSL.WantReadError:
pass # Nonblocking ssl yo
except (SSL.ZeroReturnError, SSL.SysCallError):
eof = True
except socket.error as e:
if e.errno == errno.EAGAIN:
pass # Don't Care
else:
raise
#If there are lines to proccess do so
while LINEENDING in self._buffer.readbuffer:
#Find the buffer offset
size = self._buffer.readbuffer.find(LINEENDING)
#Get the string from the buffer
line = self._buffer.readbuffer_mv()[0:size].tobytes()
#Place the string the the queue for safe handling
#Also convert it to unicode
self._IN.put(line)
#Delete the line from the buffer + 2 for line endings
del self._buffer.readbuffer[0:size + 2]
# Make sure we parse our readbuffer before we return
if eof: # You would think reading from a disconnected socket would
# raise an excaption
raise self.SocketError('EOF')
#Read Operation (Block)
@utf8Decode.returnValue
def readline(self):
return self._IN.get()
#Write Operation
def _write(self):
while True:
line = self._OUT.get() # Yield Operation
self._buffer.writebuffer.extend(line + LINEENDING)
#If we have buffers to write lets write them all
while self._buffer.writebuffer:
try:
gevent.sleep(0) # This gets tight sometimes
#Try to dump 4096 bytes to the socket
count = self._socket.send(
self._buffer.writebuffer_mv()[0:4096])
#Remove sent len from buffer
del self._buffer.writebuffer[0:count]
except SSL.WantReadError:
gevent.sleep(0) # Yield so this is not tight
except socket.error as e:
if e.errno == errno.EPIPE:
raise self.SocketError('Broken Pipe')
else:
raise self.SocketError('Err Socket Code: ' + e.errno)
except SSL.SysCallError as e:
(errnum, errstr) = e
if errnum == errno.EPIPE:
raise self.SocketError(errstr)
else:
raise self.SocketError('SSL Syscall (%d) Error: %s'
% (errnum, errstr))
#writeline Operation [Blocking]
@utf8Encode
def writeline(self, data):
self._OUT.put(data)
================================================
FILE: pyaib/nickserv.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from .components import component_class, observes
@component_class('nickserv')
class Nickserv(object):
""" track channels and stuff """
def __init__(self, irc_c, config):
self.config = config
self.password = config.password
@observes('IRC_ONCONNECT')
def AUTO_IDENTIFY(self, irc_c):
if irc_c.config.debug:
return
self.identify(irc_c)
#Spawn off a watcher that makes sure we have the nick we want
irc_c.timers.clear('nickserv', self.watcher)
irc_c.timers.set('nickserv', self.watcher, every=90)
def watcher(self, irc_c, timertext):
if irc_c.botnick != irc_c.config.irc.nick:
self.identify(irc_c)
def identify(self, irc_c):
if irc_c.botnick != irc_c.config.irc.nick:
print("TRYING TO GET MY NICK BACK")
irc_c.PRIVMSG('nickserv', 'GHOST %s %s' % (irc_c.config.irc.nick,
self.password))
irc_c.NICK(irc_c.config.irc.nick)
#Identify
print("Identifying with nickserv")
irc_c.PRIVMSG('nickserv', 'IDENTIFY %s' % self.password)
================================================
FILE: pyaib/plugins.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import inspect
import sys
from .components import *
#Use this as an indicator of a class to inspect later
CLASS_MARKER = '_PYAIB_PLUGIN'
if sys.version_info.major == 2:
str = unicode # noqa
def plugin_class(cls):
"""
Let the component loader know to load this class
If they pass a string argument to the decorator use it as a context
name for the instance
"""
if isinstance(cls, str):
context = cls
def wrapper(cls):
setattr(cls, CLASS_MARKER, context)
return cls
return wrapper
elif inspect.isclass(cls):
setattr(cls, CLASS_MARKER, True)
return cls
plugin_class.requires = component_class.requires
@component_class('plugins')
@component_class.requires('triggers')
class PluginManager(ComponentManager):
def __init__(self, context, config):
ComponentManager.__init__(self, context, config)
#Load all configured plugins
self.load_configured()
def load(self, name):
#Pull from the global config
basename = name.split('.').pop()
config = self.context.config.setdefault("plugin.%s" % basename, {})
print("Loading Plugin %s..." % name)
ns = self._process_component(name, self.config.base, CLASS_MARKER,
self.context, config)
self._loaded_components["plugin.%s" % basename].set(ns)
================================================
FILE: pyaib/timers.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import collections
import time
#TODO Look into replacing timers with some kind of gevent construct
class Timers(object):
""" A Timers Handler """
def __init__(self, context):
self.__timers = []
def __call__(self, irc_c):
for timer in self.__timers:
timer(time.time(), irc_c)
if not timer:
self.__timers.remove(timer)
#Returns the timer
def set(self, *args, **keywargs):
timer = Timer(*args, **keywargs)
if timer:
self.__timers.append(timer)
return bool(timer)
def reset(self, message, callable):
for timer in self.__timers:
if timer.message == message and timer.callable == callable:
if timer.every:
timer.at = time.time() + timer.every
else:
self.__timers.remove(timer)
def clear(self, message, callable):
for timer in self.__timers:
if timer.message == message and timer.callable == callable:
self.__timers.remove(timer)
def __len__(self):
return len(self.__timers)
class Timer(object):
"""A Single Timer"""
# message = Message That gets passed to the callable
# at = Time when trigger will ring
# every = How long to push the 'at' time after timer rings
# count = Number of times the timer will fire before clearing
# callable = a callable object
def __init__(self, message, callable, at=None, every=None, count=None):
self.expired = False
self.message = message
if at is None:
self.at = time.time()
if every:
self.at += every
else:
self.at = at
self.count = count
self.every = every
if isinstance(callable, collections.Callable):
self.callable = callable
else:
print('Timer Error: %s not callable' % repr(callable))
self.expired = True
def __bool__(self):
return self.expired is False
__nonzero__ = __bool__
#Ring Check
def __call__(self, timestamp, irc_c):
if not isinstance(self.callable, collections.Callable):
print('Timer Error: (%r:%r) not callable'
% (self.message, callable))
return
if not self: # Sanity test for expired alarms
return
if timestamp >= self.at:
#Throw it into a greenlit
irc_c.bot_greenlets.spawn(self.callable, irc_c, self.message)
#Reset the timer
if self.every:
self.at = time.time() + self.every
if self.count:
if self.count <= 1:
self.expired = True
else:
self.count -= 1
else:
self.expired = True
================================================
FILE: pyaib/triggers.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import re
from .events import Events
from .components import component_class, observes, keyword
@component_class
class Triggers(Events):
""" Handle Trigger Words """
def __init__(self, irc_c, config):
Events.__init__(self, irc_c)
self.prefix = config.prefix or '!'
#Install self in context
irc_c['triggers'] = self
#How to parse trigger arguments
self._keywordRE = re.compile(r'^--?([a-z]\w*)(?:\s*(=))?\s*(.*)$',
re.I)
self._argRE = re.compile(r"""^(?:(['"])((?:\\\1|.)*?)\1"""
r"""|(\S+))\s*(.*)$""")
print("Triggers Loaded")
def _generate_command_words(self, commands, msg):
"""
Generate an array of arrays, of command words
Length of each array, is max irc messages length
"""
def _size(alist):
size = 0
for words in alist:
for word in words:
size += len(word) + 2 # Room for formating
return size
#Smarter Line Wrap
messages = [['Command List:']] # List of commands to send
prefix_len = len('PRVMSG %s :' % msg.nick)
for word in sorted(commands):
show = False
event_handler = self.get(word)
if event_handler:
for observer in event_handler.observers():
if observer.__doc__:
show = True
break
if show: # Hidden Commands Stay Hidden
if _size(messages[-1]) + len(word) + prefix_len <= 510:
messages[-1].append(word)
else:
messages.append([word])
return messages
def _clean_doc(self, doc):
""" Cleanup Multi-line Doc Strings """
return ' '.join([s.strip() for s in doc.strip().split('\n')])
def _generate_long_help(self, commands, msg):
for k in sorted(commands):
event_handler = self.get(k)
if event_handler:
for observer in event_handler.observers():
if observer.__doc__:
doc = self._clean_doc(observer.__doc__)
if hasattr(observer, '_subs'):
for sub in observer._subs:
msg.reply("%s %s %s"
% (k, sub, doc))
else:
msg.reply("%s %s" % (k, doc))
@keyword('help')
@keyword.autohelp
def autohelp(self, irc_c, msg, trigger, args, kargs):
"""[<command>]+ [--list|--full] :: get docs"""
if args:
commands = args
else:
commands = self.list()
if msg.channel and not args: # Was this issued in channel without args
#Force short mode
if 'full' in kargs: # If you ask for full we send your the list
msg.reply_target = msg.nick
else:
kargs['list'] = True
if 'list' in kargs and 'full' not in kargs:
messages = self._generate_command_words(commands, msg)
for words in messages:
msg.reply('%s' % ' '.join(words))
else:
self._generate_long_help(commands, msg)
def parse(self, next):
""" Take a string of arguments and parse them into args and kwargs """
args = []
kwargs = {}
while next:
getnext = None
keymatch = self._keywordRE.search(next)
if keymatch:
name, getnext, next = keymatch.groups()
kwargs[name] = True
if not getnext: # So keywords don't get lost
continue
argmatch = self._argRE.search(next)
if argmatch:
quotetype, quoted, naked, next = argmatch.groups()
#Could be a empty string
arg = quoted if quoted is not None else naked
#Get rid of any escaped strings
arg = re.sub(r"""\\(['"])""", r'\1', arg)
if getnext:
kwargs[name] = arg
else:
args.append(arg)
return [args, kwargs]
#Just privmsg, rfc forbids automatic responces to notice
@observes('IRC_MSG_PRIVMSG')
def _handler(self, irc_c, msg):
#Addressed Keywords like '<botnick>: keyword'
address = '%s:' % irc_c.botnick
#Cleanup the message for parsing
message = msg.message.strip()
if (message.startswith(self.prefix)
or message.lower().startswith(address)
or msg.channel is None):
#Lets strip directed addressed messages
if message.lower().startswith(address):
message = message[len(address):].strip()
#Get the trigger and everything else
parts = message.split(None, 1)
if parts:
word = parts.pop(0).lstrip(self.prefix)
else:
#WTF empty screw it
return
#Try to get the args
if parts:
allargs = parts.pop(0)
else:
allargs = '' # Empty NO ARGS provided
#Get the trigger if it exists
trigger = self.get(word)
if trigger:
args, keywords = self.parse(allargs)
#Call the trigger with parsed args
msg = msg.copy(irc_c)
msg.unparsed = allargs
trigger(irc_c, msg, word, args, keywords)
================================================
FILE: pyaib/util/__init__.py
================================================
================================================
FILE: pyaib/util/data.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import weakref
import sys
if sys.version_info.major == 2:
str = unicode # noqa
class Raw(object):
"""Wrapper to tell Object not to rewrap but just store the value"""
def __init__(self, value):
self.value = value
#A Sentinel value because None is a valid value
sentinel = object()
class Object(dict):
"""
Pretty DataStructure Objects with lots of magic
All Collections added to this object will be converted to
data.Collection if they are not already and instance of that type
All Dicts added to this class will be converted to data.Object's if
they are not currently instances of data.Object
To prevent any conversions from taking place in a value place in a
data.Object use data.Raw(myobject) to tell data.Object to store it
as is.
"""
#dir(self) causes these to be getattr'ed
#Its a weird python artifact
__members__ = None
__methods__ = None
def __init__(self, *args, **kwargs):
#Look to see if this object should be somebodies child once not empty
if kwargs.get('__PARENT__'):
self.__dict__['__PARENT__'] = kwargs.pop('__PARENT__')
super(Object, self).__init__(*args, **kwargs)
#A place to store future children before they are actually children
self.__dict__['__CACHE__'] = weakref.WeakValueDictionary()
#Read Only Keys
self.__dict__['__PROTECTED__'] = set()
#Make sure all children are Object not dict
#Also handle 'a.b.c' style keys
for k in list(self.keys()):
self[k] = self.pop(k)
def __wrap(self, value):
if isinstance(value, (tuple, set, frozenset)):
return type(value)([self.__wrap(v) for v in value])
elif isinstance(value, list) and not isinstance(value, Collection):
return Collection(value, self.__class__)
elif isinstance(value, Object):
return value # Don't Rewrap if already this class.
elif isinstance(value, Raw):
return value.value
elif isinstance(value, dict):
if isinstance(self, CaseInsensitiveObject):
return CaseInsensitiveObject(value)
else:
return Object(value)
else:
return value
def __protect__(self, key, value=sentinel):
"""Protected keys add its parents, not sure if useful"""
if not isinstance(key, list):
key = key.split('.') if isinstance(key, str) else [key]
key, path = key.pop(0), key
if len(path) > 0:
self.get(key).protect(path, value)
elif value is not sentinel:
self[key] = value
if key not in self:
raise KeyError('key %s has no value to protect' % key)
self.__PROTECTED__.add(key)
#Object.key sets
def __setattr__(self, name, value):
bad_ids = dir(self)
#Add some just for causion
bad_ids.append('__call__')
bad_ids.append('__dir__')
if name in self.__PROTECTED__:
raise KeyError('key %r is read only' % name)
if name not in bad_ids:
if self.__dict__.get('__PARENT__'):
#Do all the black magic with making sure my parents exist
parent, pname = self.__dict__.pop('__PARENT__')
parent[pname] = self
#Get rid of cached future children that match name
if name in self.__CACHE__:
del self.__CACHE__[name]
dict.__setitem__(self, name, self.__wrap(value))
else:
print("%s is an invalid identifier" % name)
print("identifiers can not be %r" % bad_ids)
raise KeyError('bad identifier')
#Object.key gets
def __getattr__(self, key):
return self.get(key)
#Dict like functionality and xpath like access
def __getitem__(self, key, default=sentinel):
if not isinstance(key, list):
key = key.split('.') if isinstance(key, str) else [key]
key, path = key.pop(0), key
if len(path) > 0:
return self.get(key).__getitem__(path, default)
elif key not in self:
if default is sentinel:
#Return a parentless object (this might be evil)
#CACHE it
return self.__CACHE__.setdefault(
key, self.__class__(__PARENT__=(self, key)))
else:
return default
else:
return dict.get(self, key)
get = __getitem__
def __contains__(self, key):
""" contains method with key paths support """
if not isinstance(key, list):
key = key.split('.') if isinstance(key, str) else [key]
this, next = key.pop(0), key
if this in self.keys():
if len(next) > 0:
return next in self.get(this)
else:
return True
else:
return False
has_key = __contains__
def setdefault(self, key, default=None):
if key not in self:
self[key] = default
return self.get(key)
#Allow address keys 'key.key.key'
def __setitem__(self, key, value):
if not isinstance(key, list):
key = key.split('.') if isinstance(key, str) else [key]
key, path = key.pop(0), key
if len(path) > 0:
self.setdefault(key, {}).__setitem__(path, value)
else:
self.__setattr__(key, value)
set = __setitem__
#Allow del by 'key.key.key'
def __delitem__(self, key):
if not isinstance(key, list):
key = key.split('.') if isinstance(key, str) else [key]
key, path = key.pop(0), key
if len(path) > 0:
self.get(key).__delitem__(path) # Pass the delete down
else:
if key not in self:
pass # This should handle itself
else:
dict.__delitem__(self, key)
__delattr__ = __delitem__
class CaseInsensitiveObject(Object):
"""A Case Insensitive Version of data.Object"""
def __protect__(self, key, value=sentinel):
Object.__protect__(self, key.lower(), value)
def __getitem__(self, key, default=sentinel):
if isinstance(key, list):
key = [x.lower() if isinstance(x, str) else x for x in key]
elif isinstance(key, str):
key = key.lower()
return Object.__getitem__(self, key, default)
get = __getitem__
def __setattr__(self, key, value):
if isinstance(key, str):
key = key.lower()
return Object.__setattr__(self, key, value)
def __contains__(self, key):
if not isinstance(key, list):
key = key.split('.') if isinstance(key, str) else [key]
if isinstance(key[0], str):
key[0] = key[0].lower()
return Object.__contains__(self, key)
has_key = __contains__
def __getattr__(self, key):
if key in self:
return self.get(key)
else:
return Object.__getattr__(self, key)
def __delattr__(self, key):
if isinstance(key, str):
key = key.lower()
return Object.__delattr__(self, key)
__delitem__ = __delattr__
class Collection(list):
"""Special Lists so [dicts,[dict,dict]] within get converted"""
def __init__(self, alist=None, default=Object):
if alist is None:
alist = ()
super(Collection, self).__init__(alist)
self.__default = default
#Makes sure all the conversions happen
for i in range(0, len(self)):
self[i] = self[i]
def __wrap(self, value):
if isinstance(value, dict):
return self.__default(value)
elif isinstance(value, self.__class__):
return value # Do Not Re-wrap
elif isinstance(value, list):
return self.__class__(value, self.__default)
else:
return value
def __setitem__(self, key, value):
super(Collection, self).__setitem__(key, self.__wrap(value))
def __getslice__(self, s, e):
return self.__class__(super(Collection, self).__getslice__(s, e),
self.__default)
def append(self, value):
list.append(self, self.__wrap(value))
def extend(self, alist):
for i in alist:
self.append(i)
def insert(self, key, value):
list.insert(self, key, self.__wrap(value))
def shift(self):
return self.pop(0)
def unshift(self, value):
self.insert(0, value)
push = append
================================================
FILE: pyaib/util/decorator.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import collections
import inspect
import gevent
import functools
import copy
import sys
if sys.version_info.major == 2:
str = unicode # noqa
class EasyDecorator(object):
"""An attempt to make Decorating stuff easier"""
_instance = None
_thing = _othing = None
def __init__(self, *args, **kwargs):
"""Figure how we are being called for decoration"""
#Default to empty
self.args = []
self.kwargs = {}
if len(args) == 1 and not kwargs \
and (inspect.isclass(args[0]) or isinstance(args[0],
collections.Callable)):
self._thing = args[0]
self._mimic()
else:
# Save args so wrappers could use them
self.args = args
self.kwargs = kwargs
def _mimic(self):
"""Mimic the base object so we have the same props"""
for n in set(dir(self._thing)) - set(dir(self)):
setattr(self, n, getattr(self._thing, n))
#These have to happen
self.__name__ = self._thing.__name__
self.__doc__ = self._thing.__doc__
def wrapper(self, *args, **kwargs):
"""Empty Wrapper: Overwride me"""
return self.call(*args, **kwargs)
def call(self, *args, **kwargs):
"""Call the decorated object"""
return self._thing(*args, **kwargs)
#Instance Methods
def __get__(self, instance, klass):
self._instance = instance
#Before we bind the method lets capture the original
if self._othing is None:
self._othing = self._thing
#Get a bound method from the original
self._thing = self._othing.__get__(instance, klass)
#Return a copy of self, for instance safety
return copy.copy(self)
#Functions / With args this gets the thing
def __call__(self, *args, **kwargs):
if self._thing:
return self.wrapper(*args, **kwargs)
else:
self._thing = args[0]
self._mimic()
return self
def filterintree(adict, block, stype=str, history=None):
"""Execute block filter for all strings in a dict/list recusive"""
if not adict: # Don't go through the proccess for empty containers
return adict
if history is None:
history = set()
if id(adict) in history:
return
else:
history.add(id(adict))
if isinstance(adict, list):
for i in range(len(adict)):
if isinstance(adict[i], stype):
adict[i] = block(adict[i])
elif isinstance(adict[i], (set, tuple)):
adict[i] = filterintree(adict[i], block, stype, history)
elif isinstance(adict[i], (list, dict)):
filterintree(adict[i], block, stype, history)
elif isinstance(adict, (set, tuple)):
c = list(adict)
filterintree(c, block, stype, history)
return type(adict)(c)
elif isinstance(adict, dict):
for k, v in adict.items():
if isinstance(v, stype):
adict[k] = block(v)
elif isinstance(v, (dict, list)):
filterintree(v, block, stype, history)
elif isinstance(v, (set, tuple)):
adict[k] = filterintree(v, block, stype, history)
class utf8Decode(EasyDecorator):
"""decode all arguments to unicode strings"""
def wrapper(self, *args, **kwargs):
def decode(s):
return s.decode('utf-8', 'ignore')
args = filterintree(args, decode, stype=bytes)
filterintree(kwargs, decode, stype=bytes)
#Call Method with converted args
return self.call(*args, **kwargs)
class returnValue(EasyDecorator):
"""decode the return value only"""
def wrapper(self, *args, **kwargs):
def decode(s):
return s.decode('utf-8', 'ignore')
value = [self.call(*args, **kwargs)]
filterintree(value, decode, stype=bytes)
return value[0]
class utf8Encode(EasyDecorator):
"""encode all unicode arguments to byte strings"""
def wrapper(self, *args, **kwargs):
def encode(s):
return s.encode('utf-8', 'backslashreplace')
args = filterintree(args, encode, stype=str)
filterintree(kwargs, encode, stype=str)
#Call Method with converted args
return self.call(*args, **kwargs)
class returnValue(EasyDecorator):
"""encode the return value"""
def wrapper(self, *args, **kwargs):
def encode(s):
return s.encode('utf-8', 'backslashreplace')
value = [self.call(*args, **kwargs)]
filterintree(value, encode, stype=str)
return value[0]
def raise_exceptions(func):
"""Wrap around for spawn to raise exceptions in current context"""
caller = gevent.getcurrent()
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as ex:
caller.throw(ex)
return wrapper
================================================
FILE: setup.py
================================================
#!/usr/bin/env python
#
# Copyright 2013 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
#Pull version out of the module
from pyaib import __version__
setup(name='pyaib',
version=__version__,
packages=['pyaib', 'pyaib.dbd', 'pyaib.util'],
url='http://github.com/facebook/pyaib',
license='Apache 2.0',
author='Jason Fried, Facebook',
author_email='fried@fb.com',
description='Python Framework for writing IRC Bots using gevent',
classifiers=[
'License :: OSI Approved :: Apache Software License',
'Topic :: Communications :: Chat :: Internet Relay Chat',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Intended Audience :: Developers',
'Development Status :: 5 - Production/Stable',
],
install_requires=[
'pyOpenSSL >= 0.12',
'gevent >= 1.1.0',
'PyYAML >= 3.09',
])
gitextract_6rpuxtkg/ ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.markdown ├── example/ │ ├── botbot.conf │ ├── botbot.py │ └── plugins/ │ ├── __init__.py │ ├── debug.py │ ├── example.py │ ├── jokes.py │ └── karma.py ├── pyaib/ │ ├── __init__.py │ ├── channels.py │ ├── components.py │ ├── config.py │ ├── db.py │ ├── dbd/ │ │ ├── __init__.py │ │ └── sqlite.py │ ├── events.py │ ├── irc.py │ ├── ircbot.py │ ├── linesocket.py │ ├── nickserv.py │ ├── plugins.py │ ├── timers.py │ ├── triggers.py │ └── util/ │ ├── __init__.py │ ├── data.py │ └── decorator.py └── setup.py
SYMBOL INDEX (288 symbols across 19 files)
FILE: example/plugins/debug.py
class Debug (line 28) | class Debug(object):
method __init__ (line 32) | def __init__(self, irc_context, config):
method debug (line 36) | def debug(self, irc_c, msg):
method auto_reply (line 40) | def auto_reply(self, irc_c, msg):
method die (line 45) | def die(self, irc_c, msg, trigger, args, kargs):
method raw (line 50) | def raw(self, irc_c, msg, trigger, args, kargs):
method argtest (line 54) | def argtest(self, irc_c, msg, trigger, args, kargs):
method argsubtest (line 62) | def argsubtest(self, irc_c, msg, trigger, args, kargs):
method join (line 69) | def join(self, irc_c, msg, trigger, args, kargs):
method part (line 74) | def part(self, irc_c, msg, trigger, args, kargs):
method invite (line 79) | def invite(self, irc_c, msg, trigger, args, kargs):
method follow_invites (line 84) | def follow_invites(self, irc_c, msg):
FILE: example/plugins/example.py
function statsCheck (line 25) | def statsCheck(stats):
function statsGen (line 31) | def statsGen():
function stats (line 51) | def stats(irc_c, msg, trigger, args, kargs):
function roll (line 59) | def roll(count, sides):
function diceroll (line 75) | def diceroll(irc_c, msg, trigger, args, kargs):
FILE: example/plugins/jokes.py
class Jokes (line 24) | class Jokes(object):
method __init__ (line 25) | def __init__(self, irc_context, config):
method roulette_root (line 33) | def roulette_root(self, irc_c, msg, trigger, args, kargs):
method roulette_spin (line 42) | def roulette_spin(self, irc_c, msg, trigger, args, kargs):
method roulette_reload (line 52) | def roulette_reload(self, irc_c, msg, trigger, args, kargs):
method roulette_stats (line 59) | def roulette_stats(self, irc_c, msg, trigger, args, kargs):
method roulette_clearstats (line 74) | def roulette_clearstats(self, irc_c, msg, trigger, args, kargs):
method magic_8ball (line 80) | def magic_8ball(self, irc_c, msg, trigger, args, kargs):
class Roulette (line 89) | class Roulette(object):
method __init__ (line 103) | def __init__(self):
method luckyMsg (line 111) | def luckyMsg():
method unluckyMsg (line 115) | def unluckyMsg():
method clear (line 118) | def clear(self):
method reload (line 121) | def reload(self):
method getStats (line 126) | def getStats(self, nick):
method getGlobalStats (line 130) | def getGlobalStats(self):
method fire (line 137) | def fire(self, nick):
FILE: example/plugins/karma.py
class Karma (line 29) | class Karma(object):
method __init__ (line 30) | def __init__(self, irc_context, config):
method stats (line 38) | def stats(self, irc_c, msg, trigger, args, kargs):
method get_scanner (line 67) | def get_scanner(self, reply, irc_c, alarm):
method get_karma (line 74) | def get_karma(self, thing):
method set_karma (line 81) | def set_karma(self, thing, value):
method gift (line 87) | def gift(self, irc_c, msg):
method log (line 99) | def log(self, irc_c, msg):
FILE: pyaib/channels.py
class Channels (line 28) | class Channels(object):
method __init__ (line 30) | def __init__(self, irc_c, config):
method __contains__ (line 37) | def __contains__(self, channel):
method _autojoin (line 41) | def _autojoin(self, irc_c):
method _join_parser (line 60) | def _join_parser(self, msg, irc_c):
method _part_parser (line 66) | def _part_parser(self, msg, irc_c):
method _kick_parser (line 73) | def _kick_parser(self, msg, irc_c):
method _topic_parser (line 80) | def _topic_parser(self, msg, irc_c):
method _join (line 87) | def _join(self, irc_c, msg):
method _part (line 97) | def _part(self, irc_c, msg):
method _kick (line 107) | def _kick(self, irc_c, msg):
FILE: pyaib/components.py
function component_class (line 45) | def component_class(cls):
function _requires (line 64) | def _requires(*names):
function _get_plugs (line 73) | def _get_plugs(method, kind):
function msg_parser (line 83) | def msg_parser(*kinds, **kwargs):
function watches (line 99) | def watches(*events):
class _Ignore (line 112) | class _Ignore(EasyDecorator):
method wrapper (line 114) | def wrapper(dec, irc_c, msg, *args):
class _Channel (line 131) | class _Channel(EasyDecorator):
method wrapper (line 133) | def wrapper(dec, irc_c, msg, *args):
function every (line 156) | def every(seconds, name=None):
class triggers_on (line 167) | class triggers_on(object):
method __init__ (line 169) | def __init__(self, *words):
method __call__ (line 172) | def __call__(self, func):
class channel (line 178) | class channel(EasyDecorator):
method wrapper (line 180) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class private_or_channel (line 203) | class private_or_channel(channel):
method __init__ (line 205) | def __init__(dec, *args, **kwargs):
class private (line 209) | class private(EasyDecorator):
method wrapper (line 211) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class helponly (line 215) | class helponly(EasyDecorator):
method wrapper (line 217) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class autohelp (line 221) | class autohelp(EasyDecorator):
method wrapper (line 223) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class autohelp_noargs (line 230) | class autohelp_noargs(EasyDecorator):
method wrapper (line 233) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class sub (line 241) | class sub(EasyDecorator):
method __init__ (line 243) | def __init__(dec, *words):
method wrapper (line 249) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class nosub (line 260) | class nosub(EasyDecorator):
method wrapper (line 262) | def wrapper(dec, irc_c, msg, trigger, args, kargs):
class ComponentManager (line 276) | class ComponentManager(object):
method __init__ (line 280) | def __init__(self, context, config):
method load (line 285) | def load(self, name):
method _require (line 297) | def _require(self, name):
method load_configured (line 300) | def load_configured(self, autoload=None):
method is_loaded (line 318) | def is_loaded(self, name):
method _install_hooks (line 322) | def _install_hooks(self, context, hooked_methods):
method _add_parsers (line 339) | def _add_parsers(self, method, name, chain):
method _find_annotated_callables (line 361) | def _find_annotated_callables(self, class_marker, component_ns, config,
method _process_component (line 387) | def _process_component(self, name, path, class_marker, context, config):
FILE: pyaib/config.py
function construct_yaml_str (line 27) | def construct_yaml_str(self, node):
class Config (line 34) | class Config(object):
method __init__ (line 35) | def __init__(self, configFile=None, configPath=None):
method __load (line 58) | def __load(self, configFile, path=None):
method __findfile (line 77) | def __findfile(self, configFile, path=None):
FILE: pyaib/db.py
function sha256 (line 49) | def sha256(msg):
function jsonify (line 58) | def jsonify(thing):
function dejsonify (line 62) | def dejsonify(jsonstr):
function db_driver (line 66) | def db_driver(cls):
class ObjectStore (line 73) | class ObjectStore(object):
method __init__ (line 79) | def __init__(self, irc_c, config):
method _load_driver (line 86) | def _load_driver(self):
method get (line 107) | def get(self, bucket, key=None):
method getAll (line 114) | def getAll(self, bucket):
method set (line 119) | def set(self, bucket, key, obj):
method delete (line 124) | def delete(self, bucket, key):
class Item (line 129) | class Item(object):
method __init__ (line 131) | def __init__(self, driver, bucket, key, payload):
method reload (line 140) | def reload(self):
method delete (line 145) | def delete(self):
method commit (line 148) | def commit(self):
class Bucket (line 171) | class Bucket(object):
method __init__ (line 173) | def __init__(self, db, bucket):
method __repr__ (line 177) | def __repr__(self):
method get (line 180) | def get(self, key):
method getAll (line 183) | def getAll(self):
method set (line 186) | def set(self, key, obj):
method delete (line 189) | def delete(self, key):
FILE: pyaib/dbd/sqlite.py
function compress (line 34) | def compress(message):
class SqliteDriver (line 44) | class SqliteDriver(object):
method __init__ (line 47) | def __init__(self, config):
method _bucket_exists (line 58) | def _bucket_exists(self, bucket):
method _has_keys (line 67) | def _has_keys(self, bucket):
method _create_bucket (line 75) | def _create_bucket(self, bucket):
method getObject (line 79) | def getObject(self, key, bucket):
method setObject (line 91) | def setObject(self, obj, key, bucket):
method updateObject (line 101) | def updateObject(self, obj, key, bucket):
method updateObjectKey (line 104) | def updateObjectKey(self, bucket, oldkey, newkey):
method updateObjectBucket (line 109) | def updateObjectBucket(self, key, oldbucket, newbucket):
method getAllObjects (line 114) | def getAllObjects(self, bucket):
method deleteObject (line 121) | def deleteObject(self, key, bucket, commit=True):
FILE: pyaib/events.py
class Event (line 26) | class Event(object):
method __init__ (line 28) | def __init__(self):
method observe (line 31) | def observe(self, observer):
method unobserve (line 38) | def unobserve(self, observer):
method fire (line 42) | def fire(self, *args, **keywargs):
method clearObjectObservers (line 56) | def clearObjectObservers(self, inObject):
method getObserverCount (line 61) | def getObserverCount(self):
method observers (line 64) | def observers(self):
method __bool__ (line 67) | def __bool__(self):
class Events (line 77) | class Events(object):
method __init__ (line 79) | def __init__(self, irc_c):
method list (line 86) | def list(self):
method isEvent (line 89) | def isEvent(self, name):
method getOrMake (line 92) | def getOrMake(self, name):
method get (line 100) | def get(self, name):
class NullEvent (line 111) | class NullEvent(object):
method fire (line 113) | def fire(self, *args, **keywargs):
method clearObjectObservers (line 116) | def clearObjectObservers(self, obj):
method getObserverCount (line 119) | def getObserverCount(self):
method __bool__ (line 122) | def __bool__(self):
method observe (line 127) | def observe(self, observer):
method unobserve (line 130) | def unobserve(self, observer):
FILE: pyaib/irc.py
class Context (line 38) | class Context(data.Object):
method RAW (line 44) | def RAW(self, message):
method NICK (line 64) | def NICK(self, nick):
method PRIVMSG (line 71) | def PRIVMSG(self, target, msg):
method NOTICE (line 75) | def NOTICE(self, target, msg):
method _wrap_command (line 79) | def _wrap_command(self, command, target, msg):
method JOIN (line 89) | def JOIN(self, channels):
method PART (line 108) | def PART(self, channels, message=None):
class Client (line 117) | class Client(object):
method __init__ (line 119) | def __init__(self, irc_c):
method _try_connect (line 129) | def _try_connect(self):
method _fire_msg_events (line 138) | def _fire_msg_events(self, sock, irc_c):
method run (line 157) | def run(self):
method die (line 200) | def die(self, message="Dying"):
method cycle (line 204) | def cycle(self):
method signal_handler (line 207) | def signal_handler(self, signum, frame):
method __register_client_hooks (line 213) | def __register_client_hooks(self, options):
method __parseserver (line 269) | def __parseserver(self, server):
class Message (line 282) | class Message (object):
method add_parser (line 297) | def add_parser(cls, kind, handler):
method get_parser (line 301) | def get_parser(cls, kind):
method copy (line 304) | def copy(self, irc_c):
method __init__ (line 307) | def __init__(self, irc_c, raw):
method _error_out (line 329) | def _error_out(self, text):
method __bool__ (line 333) | def __bool__(self):
method __str__ (line 338) | def __str__(self):
method __getattr__ (line 342) | def __getattr__(self, key):
method _directed_message (line 346) | def _directed_message(msg, irc_c):
class Sender (line 382) | class Sender(str):
method __new__ (line 384) | def __new__(cls, sender):
method raw (line 395) | def raw(self):
method nick (line 403) | def nick(self):
method user (line 409) | def user(self):
method hostname (line 415) | def hostname(self):
method usermask (line 423) | def usermask(self):
FILE: pyaib/ircbot.py
class IrcBot (line 42) | class IrcBot(object):
method __init__ (line 44) | def __init__(self, *args, **kargs):
method run (line 63) | def run(self):
method _install (line 72) | def _install(self, name, thing, inContext=True):
method _loadComponent (line 78) | def _loadComponent(self, cname, passConfig=True):
FILE: pyaib/linesocket.py
class LineSocketBuffers (line 31) | class LineSocketBuffers(object):
method __init__ (line 32) | def __init__(self):
method clear (line 36) | def clear(self):
method readbuffer_mv (line 40) | def readbuffer_mv(self):
method writebuffer_mv (line 43) | def writebuffer_mv(self):
class LineSocket (line 51) | class LineSocket(object):
method __init__ (line 53) | def __init__(self, host, port, SSL):
class SocketError (line 62) | class SocketError(Exception):
method __init__ (line 63) | def __init__(self, value):
method __str__ (line 66) | def __str__(self):
method connect (line 70) | def connect(self):
method run (line 141) | def run(self):
method close (line 154) | def close(self):
method _read (line 164) | def _read(self):
method readline (line 203) | def readline(self):
method _write (line 207) | def _write(self):
method writeline (line 238) | def writeline(self, data):
FILE: pyaib/nickserv.py
class Nickserv (line 23) | class Nickserv(object):
method __init__ (line 25) | def __init__(self, irc_c, config):
method AUTO_IDENTIFY (line 30) | def AUTO_IDENTIFY(self, irc_c):
method watcher (line 39) | def watcher(self, irc_c, timertext):
method identify (line 43) | def identify(self, irc_c):
FILE: pyaib/plugins.py
function plugin_class (line 31) | def plugin_class(cls):
class PluginManager (line 54) | class PluginManager(ComponentManager):
method __init__ (line 55) | def __init__(self, context, config):
method load (line 61) | def load(self, name):
FILE: pyaib/timers.py
class Timers (line 26) | class Timers(object):
method __init__ (line 28) | def __init__(self, context):
method __call__ (line 31) | def __call__(self, irc_c):
method set (line 38) | def set(self, *args, **keywargs):
method reset (line 44) | def reset(self, message, callable):
method clear (line 52) | def clear(self, message, callable):
method __len__ (line 57) | def __len__(self):
class Timer (line 61) | class Timer(object):
method __init__ (line 68) | def __init__(self, message, callable, at=None, every=None, count=None):
method __bool__ (line 85) | def __bool__(self):
method __call__ (line 91) | def __call__(self, timestamp, irc_c):
FILE: pyaib/triggers.py
class Triggers (line 26) | class Triggers(Events):
method __init__ (line 28) | def __init__(self, irc_c, config):
method _generate_command_words (line 43) | def _generate_command_words(self, commands, msg):
method _clean_doc (line 73) | def _clean_doc(self, doc):
method _generate_long_help (line 77) | def _generate_long_help(self, commands, msg):
method autohelp (line 93) | def autohelp(self, irc_c, msg, trigger, args, kargs):
method parse (line 114) | def parse(self, next):
method _handler (line 142) | def _handler(self, irc_c, msg):
FILE: pyaib/util/data.py
class Raw (line 27) | class Raw(object):
method __init__ (line 29) | def __init__(self, value):
class Object (line 36) | class Object(dict):
method __init__ (line 55) | def __init__(self, *args, **kwargs):
method __wrap (line 69) | def __wrap(self, value):
method __protect__ (line 86) | def __protect__(self, key, value=sentinel):
method __setattr__ (line 100) | def __setattr__(self, name, value):
method __getattr__ (line 126) | def __getattr__(self, key):
method __getitem__ (line 130) | def __getitem__(self, key, default=sentinel):
method __contains__ (line 149) | def __contains__(self, key):
method setdefault (line 164) | def setdefault(self, key, default=None):
method __setitem__ (line 170) | def __setitem__(self, key, value):
method __delitem__ (line 182) | def __delitem__(self, key):
class CaseInsensitiveObject (line 197) | class CaseInsensitiveObject(Object):
method __protect__ (line 199) | def __protect__(self, key, value=sentinel):
method __getitem__ (line 202) | def __getitem__(self, key, default=sentinel):
method __setattr__ (line 210) | def __setattr__(self, key, value):
method __contains__ (line 215) | def __contains__(self, key):
method __getattr__ (line 224) | def __getattr__(self, key):
method __delattr__ (line 230) | def __delattr__(self, key):
class Collection (line 238) | class Collection(list):
method __init__ (line 240) | def __init__(self, alist=None, default=Object):
method __wrap (line 249) | def __wrap(self, value):
method __setitem__ (line 259) | def __setitem__(self, key, value):
method __getslice__ (line 262) | def __getslice__(self, s, e):
method append (line 266) | def append(self, value):
method extend (line 269) | def extend(self, alist):
method insert (line 273) | def insert(self, key, value):
method shift (line 276) | def shift(self):
method unshift (line 279) | def unshift(self, value):
FILE: pyaib/util/decorator.py
class EasyDecorator (line 31) | class EasyDecorator(object):
method __init__ (line 36) | def __init__(self, *args, **kwargs):
method _mimic (line 52) | def _mimic(self):
method wrapper (line 60) | def wrapper(self, *args, **kwargs):
method call (line 64) | def call(self, *args, **kwargs):
method __get__ (line 69) | def __get__(self, instance, klass):
method __call__ (line 83) | def __call__(self, *args, **kwargs):
function filterintree (line 92) | def filterintree(adict, block, stype=str, history=None):
class utf8Decode (line 125) | class utf8Decode(EasyDecorator):
method wrapper (line 127) | def wrapper(self, *args, **kwargs):
class returnValue (line 136) | class returnValue(EasyDecorator):
method wrapper (line 138) | def wrapper(self, *args, **kwargs):
class utf8Encode (line 147) | class utf8Encode(EasyDecorator):
method wrapper (line 149) | def wrapper(self, *args, **kwargs):
class returnValue (line 158) | class returnValue(EasyDecorator):
method wrapper (line 160) | def wrapper(self, *args, **kwargs):
function raise_exceptions (line 169) | def raise_exceptions(func):
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (133K chars).
[
{
"path": ".gitignore",
"chars": 55,
"preview": "build\ndist\n.idea\n*.egg-info\n*.pyc\nbotbot-facebook.conf\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 244,
"preview": "# Code of Conduct\n\nFacebook has adopted a Code of Conduct that we expect project participants to adhere to.\nPlease read "
},
{
"path": "CONTRIBUTING.md",
"chars": 1274,
"preview": "# Contributing to pyaib\nWe want to make contributing to this project as easy and transparent as\npossible.\n\n## Pull Reque"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.markdown",
"chars": 893,
"preview": "Python Async IrcBot framework (pyaib)\n=====================================\n\npyaib is an easy to use framework for writi"
},
{
"path": "example/botbot.conf",
"chars": 1589,
"preview": "######################\n# IRC Config Section #\n######################\nIRC:\n #Could be a yaml list or comma delimited v"
},
{
"path": "example/botbot.py",
"chars": 915,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "example/plugins/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "example/plugins/debug.py",
"chars": 2940,
"preview": "\"\"\" Debug Plugin (botbot plugins.debug) \"\"\"\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0"
},
{
"path": "example/plugins/example.py",
"chars": 3877,
"preview": "\"\"\" Example Plugin (dice roller) (botbot plugins.example) \"\"\"\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache Li"
},
{
"path": "example/plugins/jokes.py",
"chars": 4615,
"preview": "# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); you may\n# not use this fil"
},
{
"path": "example/plugins/karma.py",
"chars": 4353,
"preview": "\"\"\"Karma Plugin (crushinator.plugins.karma)\"\"\"\n# Copyright 2016 Facebook\n#\n# Licensed under the Apache License, Version "
},
{
"path": "pyaib/__init__.py",
"chars": 1199,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/channels.py",
"chars": 4074,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/components.py",
"chars": 15159,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/config.py",
"chars": 3582,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/db.py",
"chars": 6134,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/dbd/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "pyaib/dbd/sqlite.py",
"chars": 4300,
"preview": "# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); you may\n# not use this fil"
},
{
"path": "pyaib/events.py",
"chars": 3907,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/irc.py",
"chars": 14640,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/ircbot.py",
"chars": 2840,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/linesocket.py",
"chars": 8491,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/nickserv.py",
"chars": 1863,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/plugins.py",
"chars": 2116,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/timers.py",
"chars": 3589,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/triggers.py",
"chars": 6402,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/util/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "pyaib/util/data.py",
"chars": 9350,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "pyaib/util/decorator.py",
"chars": 5810,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
},
{
"path": "setup.py",
"chars": 1558,
"preview": "#!/usr/bin/env python\n#\n# Copyright 2013 Facebook\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); yo"
}
]
About this extraction
This page contains the full source code of the facebook/pyaib GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (124.1 KB), approximately 29.3k tokens, and a symbol index with 288 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.