Advertisement
Guest User

Untitled

a guest
Jul 19th, 2017
134
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.08 KB | None | 0 0
  1.  
  2. '''
  3. Automated Python IRC bot structure.
  4.  
  5. This is a threaded bot class designed to make simple the creation of IRC
  6. bots with Python. It is inspired by the cmd standard library module and
  7. is very similar to it in many respects, including its setup.
  8.  
  9. Various security features are implemented, including a levels system,
  10. which allows individual commands to restrict access to groups of users,
  11. and an access system, which allows groups of commands to provide access
  12. to individual users.
  13.  
  14. An extension system allows new commands to be added. Extensions may be
  15. loaded with the bot or dynamically. Preprocessors and postprocessors can
  16. also be created.
  17.  
  18. '''
  19.  
  20. from __future__ import unicode_literals, print_function
  21. import sys
  22. import os
  23. # The threading module is not available on all platforms.
  24. try:
  25.     import threading
  26. except ImportError:
  27.     import dummy_threading as threading
  28. import socket
  29. import time
  30. import re
  31. import itertools
  32. # Compatibility stuff to make Python 2 behave more like py3k.
  33. if sys.version_info.major == 3:
  34.     import queue
  35.     import configparser
  36.     deutf = lambda s: str(s, 'utf-8')
  37.     basestring = (str, bytes)
  38. else:
  39.     import Queue as queue
  40.     import ConfigParser as configparser
  41.     deutf = lambda s: unicode(s, 'utf-8')
  42.  
  43. __all__ = ['Bot', 'User', 'Channel', 'deutf']
  44. __version__ = '0.0.0a1'
  45.  
  46.  
  47. class User(object):
  48.    
  49.     '''User object for IRC bots.
  50.  
  51.    This helps keep track of user information.
  52.    '''
  53.  
  54.     def __init__(self, nick, user, real, host, mode, channels):
  55.         '''Frobnicate the bizbang.
  56.  
  57.        nick, user, real, and host correspond to the nickname, username,
  58.        realname, and hostmask of the user, respectively.
  59.        mode is the user's mode, including op/voice/ban/&c status.
  60.        channels is a list of Channel objects for each channel in which
  61.        both the user and this bot are.
  62.        '''
  63.         # target is the target of Bot.send() calls.
  64.         self.target = self.nick = nick
  65.         self.user = user
  66.         self.real = real
  67.         self.host = host
  68.         self.mode = mode
  69.         self.channels = channels
  70.  
  71.     __slots__ = ('target', 'nick', 'user', 'real', 'host', 'mode', 'channels')
  72.  
  73. class Channel(object):
  74.  
  75.     '''Channel object for IRC bots.
  76.  
  77.    This helps keep track of users, modes, and topics of channels.
  78.    '''
  79.  
  80.     def __init__(self, name, users, mode, topic):
  81.         '''Catenifrob the bangbiz.
  82.  
  83.        name is the channel's name, including the initial # (or &).
  84.        users is a list of User objects for each user in the channel.
  85.        mode is the channel's mode, not including modes applying
  86.        specifically to individual users (op, voice, ban, &c).
  87.        topic is the channel's topic.
  88.        '''
  89.         # target is the target of Bot.send() calls.
  90.         self.target = self.name = name
  91.         self.users = users
  92.         self.mode = mode
  93.         self.topic = topic
  94.  
  95.     __slots__ = ('target', 'name', 'users', 'mode', 'topic')
  96.  
  97. class Bot(object):
  98.  
  99.     '''IRC bot class.
  100.  
  101.    New bots may subclass this or use only extension modules. The
  102.    default implementation provides the commands join, to have the bot
  103.    join a channel; part, to have it leave a channel; quit, to have it
  104.    disconnect from the server; load, to load an extension; unload, to
  105.    disable an extension; levels, to set a user's security level;
  106.    access, to modify a command's access list; identify, to give a user
  107.    owner-level priveleges; raw, to have the bot send a raw string to
  108.    the server (not run through any postprocessors); and eval, to have
  109.    the bot execute an arbitrary Python expression.
  110.  
  111.    To define a new command named, for example, `spam', define a method
  112.    named `do_spam(self, channel, sender, line, time)'. channel will be
  113.    a Channel object representing the originating channel or the sending
  114.    user if it was sent in a personal message; the sender argument will
  115.    be a User object corresponding to the user who sent the message;
  116.    line will be a string containing the line accompanying the command
  117.    call; and time will be the struct_time for the time when the
  118.    message was sent as returned by time.localtime(). For example, if a
  119.    user with nick `spam' attempts to call a command called `say' in
  120.    #foobar as follows:
  121.  
  122.        <spam> !say Ni.
  123.  
  124.    the bot will call
  125.    do_say(<Channel '#foobar'>, <User 'spam'>, 'Ni.', time.localtime()).
  126.  
  127.    The return value for a command must be a string which will be sent
  128.    to the server, unless it is empty. It will be run through any
  129.    postprocessors which have been defined.
  130.  
  131.    To specify security levels for a command named, for example, `eggs',
  132.    create an attribute named `level_eggs' with the numeric level. If
  133.    the level is None, only identified users will be allowed to use it;
  134.    if it is not defined, it will be treated as 0.
  135.    '''
  136.  
  137.     def __init__(self, nick, user=None, realname=None, server=None, port=None,
  138.                  pass_=None, channel=None, idpass=None, debug=None,
  139.                  eval_allowed=None, raw_allowed=None, configfile=None):
  140.         '''Spawn more overlords.
  141.  
  142.        nick, user, and realname are the bot's nickname, username, and
  143.        realname.
  144.        server is the server to which the bot should connect.
  145.        If port is not given, 6667 will be used.
  146.        pass_ is the password with which to connect. If not given, none
  147.        will be sent.
  148.        channel may be a single channel name or a sequence of them.
  149.        idpass should be a string used as a password for giving users
  150.        owner privileges via the identify command.
  151.        If debug is True, a lot of information you probably don't need
  152.        to know will be printed. self.debug will be set to True, too.
  153.        Setting eval_allowed to False will disable the eval command. Use
  154.        this to prevent the crazy people to whom you have given your
  155.        idpass from rm -rf /ing you.
  156.        raw_allowed is the same as eval_allowed but for raw instead of
  157.        eval.
  158.        configfile may be the path to a file containing these options in
  159.        the format used by the configfile standard library module. The
  160.        option names are the same, with the nick specifying the section
  161.        name. Options specified both here and in the config file use the
  162.        former. If the config file does not specify an option, the same
  163.        default is used as here.
  164.  
  165.        This method calls pre_init_hook() before doing anything and
  166.        post_init_hook() after it has finished. The former does nothing
  167.        by default; the latter calls self.check_init().
  168.        '''
  169.         self.pre_init_hook()
  170.         if configfile is not None:
  171.             # Note to self: Bo Burnham is an excellent comedian.
  172.             # Note to self: Sources are not the best places for notes to self.
  173.             cfg = configparser.SafeConfigParser(
  174.                 {
  175.                     'user': None, 'realname': None, 'server': None,
  176.                     'port': 6667, 'pass_': None, 'channel': None,
  177.                     'idpass': None, 'debug': False, 'eval_allowed': True,
  178.                     'raw_allowed': True
  179.                 }, dict)
  180.             with open(configfile) as f:
  181.                 cfg.readfp(f)
  182.             self.user = cfg.get(nick, 'user')
  183.             self.realname = cfg.get(nick, 'realname')
  184.             self.server = cfg.get(nick, 'server')
  185.             self.port = cfg.getint(nick, 'port')
  186.             self.pass_ = cfg.get(nick, 'pass')
  187.             c = cfg.get(nick, 'channel')
  188.             if c and ',' in c:
  189.                 self.channels = dict(zip(eval(c, {}, {}),
  190.                                          itertools.repeat(None)))
  191.             else:
  192.                 self.channels = c
  193.             self.idpass = cfg.get(nick, 'idpass')
  194.             self.debug = cfg.getboolean(nick, 'debug')
  195.             self.eval_allowed = cfg.getboolean(nick, 'eval_allowed')
  196.             self.raw_allowed = cfg.getboolean(nick, 'raw_allowed')
  197.         if user is not None:
  198.             self.user = user
  199.         if realname is not None:
  200.             self.realname = realname
  201.         if server is not None:
  202.             self.server = server
  203.         if port is not None:
  204.             self.port = port
  205.         if pass_ is not None:
  206.             self.pass_ = pass_
  207.         if channel is not None:
  208.             if isinstance(channel, basestring):
  209.                 self.channels = {channel: None}
  210.             else:
  211.                 self.channels = dict(zip(channel, itertools.repeat(None)))
  212.         if idpass is not None:
  213.             self.idpass = idpass
  214.         if debug is not None:
  215.             self.debug = debug
  216.         if eval_allowed is not None:
  217.             self.eval_allowed = eval_allowed
  218.         self.preprocessors = {'builtins': []}
  219.         self.commands = {'builtins': ['join', 'part', 'quit', 'load', 'unload',
  220.                                       'levels', 'access', 'raw', 'eval']}
  221.         self.postprocessors = {'builtins': []}
  222.         self.post_init_hook()
  223.  
  224.     def connect(self):
  225.         '''Connect to the server and begin processing.
  226.  
  227.        Once this is called, the bot becomes autonomous.
  228.        '''
  229.         self.pre_connect_hook()
  230.         if not getattr(self, 'params_checked', False):
  231.             raise AttributeError('parameters not checked')
  232.         # The send thread manages all messages to the server.
  233.         self.send_thread = threading.Thread(target=self.send_mgr, args=(self,))
  234.         # The recv thread delegates (or responds to) messages from the server.
  235.         self.recv_thread = threading.Thread(target=self.recv_mgr, args=(self,))
  236.         self.recv_thread.daemon = True #TODO: remember why this was a bad idea
  237.         # The work thread runs commands.
  238.         self.work_thread = threading.Thread(target=self.work_mgr, args=(self,))
  239.         self.work_thread.daemon = True
  240.         # The send queue is used by the send thread for unknown purposes.
  241.         self.send_queue = queue.Queue()
  242.         # The send timer is used by the send thread to prevent flooding.
  243.         self.send_timer = time.time()
  244.         # The work queue is used by the work thread for well-known purposes.
  245.         self.work_queue = queue.Queue()
  246.         # Initialize the send queue.
  247.         if self.pass_ is not None:
  248.             self.rawmsg('PASS ' + self.pass_)
  249.         self.rawmsg('NICK ' + self.nick)
  250.         self.rawmsg('USER ' + self.user + ' * * :' + self.realname)
  251.         # More hooks, please.
  252.         self.pre_connect_start_hook()
  253.         self.running = True # I'll just leave this here.
  254.         # Connection begins with YOU!!
  255.         self.socket = socket.create_connection((self.server, self.port), 180)
  256.         # Start the threads.
  257.         self.send_thread.start()
  258.         self.recv_thread.start()
  259.         self.work_thread.start()
  260.         self.post_connect_hook()
  261.         # Optionally block.
  262.         if getattr(self, 'join_send_thread', True):
  263.             self.send_thread.join()
  264.         return self.exit_hook()
  265.  
  266.     def reconnect(self):
  267.         '''Reconnect to the server.
  268.  
  269.        This is chiefly aimed at reconnecting after a ping timeout or
  270.        some other unexpected disconnection.
  271.        '''
  272.         self.pre_reconnect_hook()
  273.         # Close the connection in case it really is still active.
  274.         self.socket.close()
  275.         self.socket = socket.create_connect((self.server, self.port), 180)
  276.         if self.pass_ is not None:
  277.             self.rawmsg('PASS ' + self.pass_)
  278.         self.rawmsg('NICK ' + self.nick)
  279.         self.rawmsg('USER ' + self.user + ' * * :' + self.realname)
  280.         self.post_reconnect_hook()
  281.  
  282.     def send_wait(self):
  283.         '''Block until it is safe to send the next message.'''
  284.         if time.time() > self.send_timer:
  285.             self.send_timer = time.time()
  286.         elif time.time() < self.send_timer - 7.0:
  287.             time.sleep(2)
  288.  
  289.     def send_mgr(self):
  290.         '''Manage messages sent to the server.
  291.  
  292.        This should prevent the bot from flooding.
  293.        '''
  294.         while True:
  295.             m = self.send_queue.get().rstrip('\r\n\0')
  296.             if m:
  297.                 # If the message is too long, it must be split.
  298.                 if len(m) > 510: # This is the maximum in RFC 1459.
  299.                     # PRIVMSGs and NOTICEs need to be split such that they all
  300.                     # go where they are intended to go.
  301.                     s = m.split(' ', 2)
  302.                     # It is assumed that no message will contain no spaces yet
  303.                     # still be over the limit.
  304.                     if len(s) == 2:
  305.                         t, r = s
  306.                     else:
  307.                         t = ' '.join(s[:2])
  308.                         r = s[2]
  309.                     if r.startswith(':'):
  310.                         t += ' :'
  311.                         r = r[1:]
  312.                     else:
  313.                         t += ' '
  314.                     while len(r) > 510 - len(t):
  315.                         self.send_wait()
  316.                         #TODO: prevent any chance of paramaters being split
  317.                         n = self.socket.send(t + r[:511] + '\r\n')
  318.                         if not n:
  319.                             # The connection has been broken by this point.
  320.                             self.reconnect()
  321.                             continue
  322.                         #TODO: I don't think this is right.
  323.                         r = r[n:]
  324.                     self.send_wait()
  325.                     n = self.socket.send(t + r + '\r\n')
  326.                     while not n:
  327.                         self.reconnect()
  328.                         self.send_wait()
  329.                         n = self.socket.send(t + r + '\r\n')
  330.                 else:
  331.                     # The message may be sent in its entirety.
  332.                     while True:
  333.                         self.send_wait()
  334.                         n = self.socket.send(t + r + '\r\n')
  335.                         if n > 0:
  336.                             break
  337.                         self.reconnect()
  338.                 if m.startswith('QUIT'):
  339.                     # The bot has quit; end this thread.
  340.                     self.running = False
  341.                     self.socket.close()
  342.                     return
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement