Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- '''
- Automated Python IRC bot structure.
- This is a threaded bot class designed to make simple the creation of IRC
- bots with Python. It is inspired by the cmd standard library module and
- is very similar to it in many respects, including its setup.
- Various security features are implemented, including a levels system,
- which allows individual commands to restrict access to groups of users,
- and an access system, which allows groups of commands to provide access
- to individual users.
- An extension system allows new commands to be added. Extensions may be
- loaded with the bot or dynamically. Preprocessors and postprocessors can
- also be created.
- '''
- from __future__ import unicode_literals, print_function
- import sys
- import os
- # The threading module is not available on all platforms.
- try:
- import threading
- except ImportError:
- import dummy_threading as threading
- import socket
- import time
- import re
- import itertools
- # Compatibility stuff to make Python 2 behave more like py3k.
- if sys.version_info.major == 3:
- import queue
- import configparser
- deutf = lambda s: str(s, 'utf-8')
- basestring = (str, bytes)
- else:
- import Queue as queue
- import ConfigParser as configparser
- deutf = lambda s: unicode(s, 'utf-8')
- __all__ = ['Bot', 'User', 'Channel', 'deutf']
- __version__ = '0.0.0a1'
- class User(object):
- '''User object for IRC bots.
- This helps keep track of user information.
- '''
- def __init__(self, nick, user, real, host, mode, channels):
- '''Frobnicate the bizbang.
- nick, user, real, and host correspond to the nickname, username,
- realname, and hostmask of the user, respectively.
- mode is the user's mode, including op/voice/ban/&c status.
- channels is a list of Channel objects for each channel in which
- both the user and this bot are.
- '''
- # target is the target of Bot.send() calls.
- self.target = self.nick = nick
- self.user = user
- self.real = real
- self.host = host
- self.mode = mode
- self.channels = channels
- __slots__ = ('target', 'nick', 'user', 'real', 'host', 'mode', 'channels')
- class Channel(object):
- '''Channel object for IRC bots.
- This helps keep track of users, modes, and topics of channels.
- '''
- def __init__(self, name, users, mode, topic):
- '''Catenifrob the bangbiz.
- name is the channel's name, including the initial # (or &).
- users is a list of User objects for each user in the channel.
- mode is the channel's mode, not including modes applying
- specifically to individual users (op, voice, ban, &c).
- topic is the channel's topic.
- '''
- # target is the target of Bot.send() calls.
- self.target = self.name = name
- self.users = users
- self.mode = mode
- self.topic = topic
- __slots__ = ('target', 'name', 'users', 'mode', 'topic')
- class Bot(object):
- '''IRC bot class.
- New bots may subclass this or use only extension modules. The
- default implementation provides the commands join, to have the bot
- join a channel; part, to have it leave a channel; quit, to have it
- disconnect from the server; load, to load an extension; unload, to
- disable an extension; levels, to set a user's security level;
- access, to modify a command's access list; identify, to give a user
- owner-level priveleges; raw, to have the bot send a raw string to
- the server (not run through any postprocessors); and eval, to have
- the bot execute an arbitrary Python expression.
- To define a new command named, for example, `spam', define a method
- named `do_spam(self, channel, sender, line, time)'. channel will be
- a Channel object representing the originating channel or the sending
- user if it was sent in a personal message; the sender argument will
- be a User object corresponding to the user who sent the message;
- line will be a string containing the line accompanying the command
- call; and time will be the struct_time for the time when the
- message was sent as returned by time.localtime(). For example, if a
- user with nick `spam' attempts to call a command called `say' in
- #foobar as follows:
- <spam> !say Ni.
- the bot will call
- do_say(<Channel '#foobar'>, <User 'spam'>, 'Ni.', time.localtime()).
- The return value for a command must be a string which will be sent
- to the server, unless it is empty. It will be run through any
- postprocessors which have been defined.
- To specify security levels for a command named, for example, `eggs',
- create an attribute named `level_eggs' with the numeric level. If
- the level is None, only identified users will be allowed to use it;
- if it is not defined, it will be treated as 0.
- '''
- def __init__(self, nick, user=None, realname=None, server=None, port=None,
- pass_=None, channel=None, idpass=None, debug=None,
- eval_allowed=None, raw_allowed=None, configfile=None):
- '''Spawn more overlords.
- nick, user, and realname are the bot's nickname, username, and
- realname.
- server is the server to which the bot should connect.
- If port is not given, 6667 will be used.
- pass_ is the password with which to connect. If not given, none
- will be sent.
- channel may be a single channel name or a sequence of them.
- idpass should be a string used as a password for giving users
- owner privileges via the identify command.
- If debug is True, a lot of information you probably don't need
- to know will be printed. self.debug will be set to True, too.
- Setting eval_allowed to False will disable the eval command. Use
- this to prevent the crazy people to whom you have given your
- idpass from rm -rf /ing you.
- raw_allowed is the same as eval_allowed but for raw instead of
- eval.
- configfile may be the path to a file containing these options in
- the format used by the configfile standard library module. The
- option names are the same, with the nick specifying the section
- name. Options specified both here and in the config file use the
- former. If the config file does not specify an option, the same
- default is used as here.
- This method calls pre_init_hook() before doing anything and
- post_init_hook() after it has finished. The former does nothing
- by default; the latter calls self.check_init().
- '''
- self.pre_init_hook()
- if configfile is not None:
- # Note to self: Bo Burnham is an excellent comedian.
- # Note to self: Sources are not the best places for notes to self.
- cfg = configparser.SafeConfigParser(
- {
- 'user': None, 'realname': None, 'server': None,
- 'port': 6667, 'pass_': None, 'channel': None,
- 'idpass': None, 'debug': False, 'eval_allowed': True,
- 'raw_allowed': True
- }, dict)
- with open(configfile) as f:
- cfg.readfp(f)
- self.user = cfg.get(nick, 'user')
- self.realname = cfg.get(nick, 'realname')
- self.server = cfg.get(nick, 'server')
- self.port = cfg.getint(nick, 'port')
- self.pass_ = cfg.get(nick, 'pass')
- c = cfg.get(nick, 'channel')
- if c and ',' in c:
- self.channels = dict(zip(eval(c, {}, {}),
- itertools.repeat(None)))
- else:
- self.channels = c
- self.idpass = cfg.get(nick, 'idpass')
- self.debug = cfg.getboolean(nick, 'debug')
- self.eval_allowed = cfg.getboolean(nick, 'eval_allowed')
- self.raw_allowed = cfg.getboolean(nick, 'raw_allowed')
- if user is not None:
- self.user = user
- if realname is not None:
- self.realname = realname
- if server is not None:
- self.server = server
- if port is not None:
- self.port = port
- if pass_ is not None:
- self.pass_ = pass_
- if channel is not None:
- if isinstance(channel, basestring):
- self.channels = {channel: None}
- else:
- self.channels = dict(zip(channel, itertools.repeat(None)))
- if idpass is not None:
- self.idpass = idpass
- if debug is not None:
- self.debug = debug
- if eval_allowed is not None:
- self.eval_allowed = eval_allowed
- self.preprocessors = {'builtins': []}
- self.commands = {'builtins': ['join', 'part', 'quit', 'load', 'unload',
- 'levels', 'access', 'raw', 'eval']}
- self.postprocessors = {'builtins': []}
- self.post_init_hook()
- def connect(self):
- '''Connect to the server and begin processing.
- Once this is called, the bot becomes autonomous.
- '''
- self.pre_connect_hook()
- if not getattr(self, 'params_checked', False):
- raise AttributeError('parameters not checked')
- # The send thread manages all messages to the server.
- self.send_thread = threading.Thread(target=self.send_mgr, args=(self,))
- # The recv thread delegates (or responds to) messages from the server.
- self.recv_thread = threading.Thread(target=self.recv_mgr, args=(self,))
- self.recv_thread.daemon = True #TODO: remember why this was a bad idea
- # The work thread runs commands.
- self.work_thread = threading.Thread(target=self.work_mgr, args=(self,))
- self.work_thread.daemon = True
- # The send queue is used by the send thread for unknown purposes.
- self.send_queue = queue.Queue()
- # The send timer is used by the send thread to prevent flooding.
- self.send_timer = time.time()
- # The work queue is used by the work thread for well-known purposes.
- self.work_queue = queue.Queue()
- # Initialize the send queue.
- if self.pass_ is not None:
- self.rawmsg('PASS ' + self.pass_)
- self.rawmsg('NICK ' + self.nick)
- self.rawmsg('USER ' + self.user + ' * * :' + self.realname)
- # More hooks, please.
- self.pre_connect_start_hook()
- self.running = True # I'll just leave this here.
- # Connection begins with YOU!!
- self.socket = socket.create_connection((self.server, self.port), 180)
- # Start the threads.
- self.send_thread.start()
- self.recv_thread.start()
- self.work_thread.start()
- self.post_connect_hook()
- # Optionally block.
- if getattr(self, 'join_send_thread', True):
- self.send_thread.join()
- return self.exit_hook()
- def reconnect(self):
- '''Reconnect to the server.
- This is chiefly aimed at reconnecting after a ping timeout or
- some other unexpected disconnection.
- '''
- self.pre_reconnect_hook()
- # Close the connection in case it really is still active.
- self.socket.close()
- self.socket = socket.create_connect((self.server, self.port), 180)
- if self.pass_ is not None:
- self.rawmsg('PASS ' + self.pass_)
- self.rawmsg('NICK ' + self.nick)
- self.rawmsg('USER ' + self.user + ' * * :' + self.realname)
- self.post_reconnect_hook()
- def send_wait(self):
- '''Block until it is safe to send the next message.'''
- if time.time() > self.send_timer:
- self.send_timer = time.time()
- elif time.time() < self.send_timer - 7.0:
- time.sleep(2)
- def send_mgr(self):
- '''Manage messages sent to the server.
- This should prevent the bot from flooding.
- '''
- while True:
- m = self.send_queue.get().rstrip('\r\n\0')
- if m:
- # If the message is too long, it must be split.
- if len(m) > 510: # This is the maximum in RFC 1459.
- # PRIVMSGs and NOTICEs need to be split such that they all
- # go where they are intended to go.
- s = m.split(' ', 2)
- # It is assumed that no message will contain no spaces yet
- # still be over the limit.
- if len(s) == 2:
- t, r = s
- else:
- t = ' '.join(s[:2])
- r = s[2]
- if r.startswith(':'):
- t += ' :'
- r = r[1:]
- else:
- t += ' '
- while len(r) > 510 - len(t):
- self.send_wait()
- #TODO: prevent any chance of paramaters being split
- n = self.socket.send(t + r[:511] + '\r\n')
- if not n:
- # The connection has been broken by this point.
- self.reconnect()
- continue
- #TODO: I don't think this is right.
- r = r[n:]
- self.send_wait()
- n = self.socket.send(t + r + '\r\n')
- while not n:
- self.reconnect()
- self.send_wait()
- n = self.socket.send(t + r + '\r\n')
- else:
- # The message may be sent in its entirety.
- while True:
- self.send_wait()
- n = self.socket.send(t + r + '\r\n')
- if n > 0:
- break
- self.reconnect()
- if m.startswith('QUIT'):
- # The bot has quit; end this thread.
- self.running = False
- self.socket.close()
- return
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement