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