PYTHON 10
Bot.py Guest on 31st July 2020 12:49:31 PM
  1. #!/usr/bin/python
  2. import logging
  3. from os import uname
  4.  
  5. from twisted.words.protocols.irc import IRCClient
  6. from twisted.internet import defer, threads, task
  7. from twisted import version as twisted_version
  8. from django import get_version as django_version
  9. from django.core import urlresolvers, exceptions
  10. from django.conf import settings
  11. from django.http import Http404
  12. from django.db import close_connection
  13.  
  14. from irc import IRCRequest, IRCResponse
  15. from signals import request_started, request_finished
  16. from shortcuts import render_quick_reply
  17. from version import VERSION
  18.  
  19. log = logging.getLogger('yardbird')
  20. log.setLevel(logging.DEBUG)
  21.  
  22. def report_error(failure, bot, request, *args, **kwargs):
  23.     """Specific errors are reacted to only if the bot is specifically
  24.    addressed.  These correspond roughly to the 4XX errors in HTTP."""
  25.     r = failure.trap(Http404, exceptions.PermissionDenied,
  26.             exceptions.ValidationError)
  27.     log.debug(failure)
  28.     if not request.addressed:
  29.         return
  30.     elif r in (Http404, exceptions.ValidationError):
  31.         res = render_quick_reply(request, "notfound.irc")
  32.     elif r == exceptions.PermissionDenied:
  33.         res = render_quick_reply(request, "permdenied.irc")
  34.     return bot.methods[res.method](res.recipient.encode('utf-8'),
  35.             res.data.encode('utf-8'))
  36.  
  37.  
  38. def unrecoverable_error(failure, bot, request, *args, **kwargs):
  39.     """Unrecoverable errors are logged and NOTICEd, unconditionally.
  40.    These correspond roughly to the 5XX errors in HTTP."""
  41.     close_connection() # prevent open transactions from wedging the bot
  42.     log.warn(failure)
  43.     e = str(failure.getErrorMessage())
  44.     res = IRCResponse(request.reply_recipient, e, method='NOTICE')
  45.     return bot.methods[res.method](res.recipient.encode('utf-8'),
  46.             res.data.encode('utf-8'))
  47.  
  48. class DjangoBot(IRCClient):
  49.     """DjangoBot subclasses the Twisted Python IRCClient class in order
  50.    to connect Twisted to Django with the smallest surface area
  51.    possible.  Incoming events are largely dispatched to a Django
  52.    urlresolver, which contains mappings from regular expressions to
  53.    dispatch functions.  The IRCRequest and IRCResponse objects are used
  54.    to communicate the incoming message and the desired response action
  55.    between this class and the Django view functions."""
  56.     def __init__(self):
  57.         self.methods = {'PRIVMSG':  self.msg,
  58.                         'ACTION':   self.me,
  59.                         'NOTICE':   self.notice,
  60.                         'TOPIC':    self.topic,
  61.                         'RESET':    self.reimport,
  62.                         'MULTIPLE': self.multiple,
  63.                        }
  64.         self.chanmodes = {}
  65.         self.whoreplies = {}
  66.         self.hostmask = '' # until we see ourselves speak, we do not know
  67.         self.servername = ''
  68.         self.lineRate = 1
  69.  
  70.         self.versionName = 'Yardbird'
  71.         self.versionNum = VERSION
  72.         udata = uname()
  73.         self.versionEnv = 'Twisted %s and Django %s on %s-%s' % \
  74.                 (twisted_version.short(), django_version(), udata[0],
  75.                  udata[4])
  76.         self.sourceURL = 'http://zork.net/~nick/yardbird/ '
  77.         self.realname = 'Charlie Parker Jr.'
  78.         self.fingerReply = str(settings.INSTALLED_APPS)
  79.         self.l = task.LoopingCall(self.PING)
  80.  
  81.  
  82.     ############### Connection management methods ###############
  83.     def myInfo(self, servername, version, umodes, cmodes):
  84.         """This function is run once the connection is complete and we
  85.        have information about what server we're talking to.  We fire
  86.        off a periodic PING request to regularly test the connection."""
  87.         self.servername = servername
  88.         log.info("Connected to %s" % self.servername)
  89.         self.l.start(60.0) # call every minute
  90.     def connectionMade(self):
  91.         """This function assumes that the factory was added to this
  92.        object by the calling script.  It may be more desirable to
  93.        implement this as an argument to the __init__"""
  94.         self.nickname = self.factory.nickname
  95.         self.password = self.factory.password
  96.         IRCClient.connectionMade(self)
  97.     def connectionLost(self, reason):
  98.         log.warn("Disconnected from %s (%s:%s): %s" % (self.servername,
  99.             self.factory.hostname, self.factory.port, reason))
  100.         IRCClient.connectionLost(self, reason)
  101.         try:
  102.             self.l.stop() # All done now.
  103.         except AssertionError:
  104.             pass # We never managed to connect in the first place!
  105.     def signedOn(self):
  106.         """Since we can't know what our hostmask will be in advance, the
  107.        DjangoBot sends itself a trivial PRIVMSG after sign-on.  The
  108.        privmsg() method watches for a message from the bot itself
  109.        (based on nickname) and stores the mask internally."""
  110.         self.msg(self.nickname, 'Watching for my own hostmask')
  111.         for channel in self.factory.channels:
  112.             self.join(channel)
  113.     def joined(self, channel):
  114.         log.info("[I have joined %s]" % channel)
  115.         self.who(channel)
  116.     def PING(self):
  117.         log.debug('PING %s' % self.servername)
  118.         self.sendLine('PING %s' % self.servername)
  119.  
  120.     ############### Event dispatch methods ###############
  121.     # Unfortunately, twisted is too difficult to reasonably stub out,
  122.     # and coverage.py isn't that smart about nested definitions or
  123.     # decorators or both, so I have to add the nocover pragma to each
  124.     # line in this method.
  125.     @defer.inlineCallbacks # pragma: nocover
  126.     def dispatch(self, req): # pragma: nocover
  127.         """This method invokes a django url resolver to detect
  128.        interesting messages and dispatch them to callback functions
  129.        based on regular expression matches."""
  130.         def asynchronous_work(request, args, kwargs): # pragma: nocover
  131.             """This function runs in a separate thread, as the signal
  132.            handlers and callback functions may take forever and a day
  133.            to execute."""
  134.             close_connection() # Get a new DB session
  135.             request_started.send(sender=self, request=request)
  136.             response = callback(req, *args, **kwargs)
  137.             request_finished.send(sender=self, request=request,
  138.                                   response=response)
  139.             return response
  140.  
  141.         resolver = urlresolvers.get_resolver('.'.join(
  142.             (settings.ROOT_MSGCONF, req.method.lower()))) # pragma: nocover
  143.         callback, args, kwargs = yield resolver.resolve('/' +
  144.                 req.message) # pragma: nocover
  145.         response = yield threads.deferToThread(asynchronous_work, req,
  146.                 args, kwargs) # pragma: nocover
  147.         if response.method == 'QUIET': # pragma: nocover
  148.             log.debug(response)
  149.             defer.returnValue(True)
  150.         elif response.method == 'PRIVMSG': # pragma: nocover
  151.             opts = {'length':
  152.                     510 - len(':! PRIVMSG  :' + self.nickname +
  153.                       response.recipient.encode('utf-8') + self.hostmask)}
  154.         elif response.method == 'MULTIPLE': # pragma: nocover
  155.             opts = {'responses': response.responses }
  156.         else: # pragma: nocover
  157.             opts = {}
  158.         log.info(unicode(response)) # pragma: nocover
  159.         defer.returnValue(
  160.             self.methods[response.method](
  161.                 response.recipient.encode('utf-8'),
  162.                 response.data.encode('utf-8'), **opts)) # pragma: nocover
  163.     def dispatchable_event(self, user, channel, msg, method):
  164.         """All events that can be handled by Django code construct an
  165.        IRCRequest representation and pass that on to the dispatch()
  166.        method."""
  167.         if user.split('!', 1)[0] != self.nickname:
  168.             req = IRCRequest(self, user, channel, msg, method,
  169.                     privileged_channels=self.factory.privchans)
  170.             log.info(unicode(req))
  171.             return self.dispatch(req
  172.                     ).addErrback(report_error, self, req
  173.                     ).addErrback(unrecoverable_error, self, req)
  174.         else:
  175.             self.hostmask = user.split('!', 1)[1]
  176.     def noticed(self, *args, **kwargs):
  177.         """Bots are required to ignore NOTICE events to avoid endless
  178.        loops.  This is largely irrelevant since bots use PRIVMSG these
  179.        days anyway, but you can't say we didn't try."""
  180.         pass # We're automatic for the people
  181.     def privmsg(self, user, channel, msg):
  182.         return self.dispatchable_event(user, channel, msg, 'privmsg')
  183.     def action(self, user, channel, msg):
  184.         return self.dispatchable_event(user, channel, msg, 'action')
  185.     def topicUpdated(self, user, channel, msg):
  186.         return self.dispatchable_event(user, channel, msg, 'topic')
  187.     def irc_NICK(self, user, params):
  188.         """When a user changes nickname, there is one dispatchable event
  189.        for each channel."""
  190.         old_nick, mask = user.split('!', 1)
  191.         new_nick = params[0]
  192.         if self.nickname not in (old_nick, new_nick):
  193.             for channel in self.chanmodes:
  194.                 if mask in self.chanmodes[channel]:
  195.                     return self.dispatchable_event(user, channel,
  196.                                                    new_nick, 'nick')
  197.  
  198.     def multiple(self, recipient, data, responses, **kwargs):
  199.         """The MULTIPLE response type contains a list of response
  200.        objects in the data parameter, but ultimately discards the
  201.        recipient or any kwargs."""
  202.         for response in responses:
  203.             if response.method == 'QUIET': # pragma: nocover
  204.                 log.debug(response)
  205.                 continue
  206.             elif response.method == 'PRIVMSG': # pragma: nocover
  207.                 opts = {'length':
  208.                         510 - len(':! PRIVMSG  :' + self.nickname +
  209.                           response.recipient.encode('utf-8') + self.hostmask)}
  210.             elif response.method == 'MULTIPLE': # pragma: nocover
  211.                 # If you're doing this, it's your own damn fault.
  212.                 opts = {'responses': response.responses }
  213.             else: # pragma: nocover
  214.                 opts = {}
  215.             self.methods[response.method](
  216.                 response.recipient.encode('utf-8'),
  217.                 response.data.encode('utf-8'), **opts) # pragma: nocover
  218.  
  219.     ############### Special methods ###############
  220.     def reimport(self, recipient, data, **kwargs):
  221.         """Since our interface with the django application code is
  222.        strictly limited to the urlresolver, we can be confident
  223.        that it will be refreshed if we:
  224.            * reimport all django apps listed in settings.py
  225.            * reset the urlresolver cache"""
  226.         import sys
  227.         for modname, module in sys.modules.iteritems():
  228.             # We reload yardbird library code as well as all known
  229.             # django apps.
  230.             for app in ['yardbird.'] + [ a for a in
  231.                     settings.INSTALLED_APPS if not
  232.                     a.startswith('django.')]:
  233.                 if module and modname.startswith(app):
  234.                     reload(module)
  235.                     break # On to next module
  236.         urlresolvers.clear_url_caches() # Drop stale references to apps
  237.         self.notice(recipient, data)
  238.  
  239.     ############### Channel user-mode tracking methods ###############
  240.     def who(self, channel):
  241.         """Send a WHO request.  Results will come back to the irc_*WHO*
  242.        handlers, which will update the information about user flags in
  243.        the specified channel. Note that we explicitly lower-case the
  244.        channel name, as some events come in with capitalization set the
  245.        way remote users used it (e.g: "/join #YaRdBiRd")."""
  246.         self.whoreplies[channel.lower()] = {}
  247.         self.sendLine('WHO %s' % channel.lower())
  248.     def irc_RPL_WHOREPLY(self, prefix, args):
  249.         """Parse each line of the WHO listing and plug it into a
  250.        temporary data structure."""
  251.         me, chan, uname, host, server, nick, modes, name = args
  252.         mask = '%[email protected]%s' % (uname, host)
  253.         if chan.lower() in self.whoreplies:
  254.             self.whoreplies[chan.lower()][mask] = modes
  255.     def irc_RPL_ENDOFWHO(self, prefix, args):
  256.         """All WHO data are received, and the newly-populated data
  257.        structure replaces a portion of the existing per-channel user
  258.        flags data."""
  259.         channel = args[1].lower()
  260.         self.chanmodes[channel] = self.whoreplies[channel]
  261.     def invalidate_chanmodes(self, user, channel, *args, **kwargs):
  262.         """Some events do not provide us with enough information about
  263.        the user affected, or do so in a different format (consider
  264.        modeChanged which uses letters instead of punctuation to
  265.        represent the varying operator status levels.  To simplify the
  266.        code, we just get a new listing for the relevant channel rather
  267.        than query the individual user and translate. At some point this
  268.        should be broken out so that the individual functions can
  269.        dispatch these events to user code."""
  270.         self.who(channel)
  271.     modeChanged = invalidate_chanmodes
  272.     userJoined = invalidate_chanmodes
  273.     userLeft = invalidate_chanmodes
  274.     userKicked = invalidate_chanmodes
  275.     def irc_QUIT(self, user, message):
  276.         """Users who quit must be removed from all channel mode
  277.        records."""
  278.         mask = user.split('!', 1)[1]
  279.         for channel in self.chanmodes:
  280.             if mask in self.chanmodes[channel]:
  281.                 del(self.chanmodes[channel][mask])
  282.  
  283.     ############### Custom IRC methods ###############
  284.     def me(self, channel, action):
  285.         """Hacking around broken CTCP ACTION stuff with PRIVMSG"""
  286.         return self.msg(channel, '\001ACTION %s\001' % action)

Paste is for source code and general debugging text.

Login or Register to edit, delete and keep track of your pastes and more.

Raw Paste

Login or Register to edit or fork this paste. It's free.