All pastes #21612 Raw Edit

mm_ircbot.py

public text v1 · immutable
#21612 ·published 2005-08-30 16:12 UTC
rendered paste body
# vim: ts=4 sw=4 noexpandtab
"""IRC Bot module.

This is a IRC Bot ModManager module

===== Config =====
 # Sets option 1
 mm_sample.myOption1 1

 # Sets option 2
 mm_sample.myOption2 "hello there"

===== History =====
 v0.1 - 08/04/2005:
 Initial version

Copyright (c)2005 Multiplay UK
Author: Steven 'Killing' Hartland
"""

import bf2
import host
import mm_utils
import re

# Set the version of your module here
__version__ = 0.1

# Set the required module versions here
__required_modules__ = {
	'modmanager': 1.0
}

# Does this module support reload ( are all its reference closed on shutdown? )
__supports_reload__ = True

# Set the description of your module here
__description__ = "IRCBot v%s" % __version__

# Add all your configuration options here
configDefaults = {
	'serverAddress': 'irc.efnet.net',
	'serverPort': 6667,
	'localIp': '0.0.0.0',
	'localPort': 6668,
	'channelName': '#b0tty',
	'channelPassword': '',
	'nick': 'mmbot',
}

class IRCBot( object ):

	def __init__( self, modManager ):
		# ModManager reference
		self.mm = modManager

		# Internal shutdown state
		self.__state = 0

		# Add any static initialisation here.
		# Note: Handler registration should not be done here
		# but instead in the init() method

		# Your rcon commands go here:
		self.__cmds = {
			'sample': { 'method': self.cmdSample }
		}

	def cmdExec( self, ctx, cmd ):
		"""Execute a IRCBot sub command."""

		# Note: The Python doc above is used for help / description
		# messages in rcon if not overriden
		return mm_utils.exec_subcmd( self.mm, self.__cmds, ctx, cmd )

	def cmdSample( self, ctx, cmd ):
		"""Does XYZ.
		Details about this function
		"""
		# Note: The Python doc above is used for help / description
		# messages in rcon if not overriden
		self.mm.debug( 2, "Running cmdSample '%s'" % cmd )
		ctx.write( "Your arguments where '%s'" % cmd )
		return 1

	def onPlayerSpawn( self, player, soldier ):
		"""Do something when a player spawns."""
		if 1 != self.__state:
			return 0

		# Put your actions here

	def init( self ):
		"""Provides default initialisation."""

		# Load the configuration
		self.__config = self.mm.getModuleConfig( configDefaults )

		if 0 == self.__state:
			# Register your host handlers here
			host.registerHandler( 'PlayerKilled', self.onPlayerKilled, 1 )
			host.registerHandler( 'PlayerConnect', self.onPlayerConnect, 1 )
			host.registerHandler( 'PlayerDisconnect', self.onPlayerDisconnect, 1 )
			host.registerHandler( 'PlayerBanned', self.onPlayerBanned, 1 )
			host.registerHandler( 'PlayerKicked', self.onPlayerKicked, 1 )
			host.registerHandler( 'ControlPointChangedOwner', self.onControlPointChangedOwner, 1 )
			host.registerHandler( 'TicketLimitReached', self.onTicketLimitReached, 1 )
			host.registerHandler( 'TimeLimitReached', self.onTimeLimitReached, 1 )

		# Create out IRC Server Connection
		self.connectToServer()

		# Register our rcon command handlers
		#self.mm.registerRconCmdHandler( 'sample', { 'method': self.cmdExec, 'subcmds': self.__cmds } )

		self.mm.registerUpdates( self.update )

		# Update to the running state
		self.__state = 1

	def shutdown( self ):
		"""Shutdown and stop processing."""

		# Unregister game handlers and do any other
		# other actions to ensure your module no longer affects
		# the game in anyway

		# Shutdown our server connection
		if self.__socket:
			self.__socket.close();

		# Flag as shutdown as there is currently way to:
		# host.unregisterHandler
		self.__state = 2

	def update( self ):
		"""Process and update.
		Note: This is called VERY often processing in here should
		be kept to an absolute minimum.
		"""
		if self.__socket is None:
			# No socket, just return
			return

		# without blocking, check for new connections
		try:
			sock, peeraddr = self.__socket.accept()
			peer = AdminConnection( self, sock, peeraddr )
			self.__peers.append( peer )

			# inform the connect handlers of the new client
			ctx = CommandContext( peer )
			for handler in self.__connectHandlers:
				try:
					handler( ctx )
				except:
					self.mm.error( "Handler '%s' failed to process connect" % mm_utils.method_name( handler ), True )

		except socket.error, detail:
			if detail[0] != errno.EWOULDBLOCK:
				self.mm.error( "Rcon accept error (%s)" % detail, True )
				raise socket.error, detail

class IRCServer( object ):
	"""Each TCP connection is represented by an object of this class."""

	def __init__( self, bot ):
		self.mm = bot.mm
		self.__config = bot.__config
		self.__outputBuffer = []
		self.__inputBuffer = ''
		self.__connected = False
		self.__linesep_regexp = re.compile( "\r?\n" )
		self.__rfc_1459_command_regexp = re.compile( "^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?" )

		self.connect()

	def send( self, string ):
		"""Append an text to output."""
		try:
			self.__outputBuffer.append( string )
		except Exception, e:
			self.mm.error( "Failed to enqueue '%s' (%s)" % ( string, e ), True )

	def connect( self ):
		"""Connect to the IRC server."""
		try:
			self.__socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
			if self.__config['enableLinger']:
				linger = struct.pack( "ii", self.__config['lingerFor'], 0 )
				self.__socket.setsockopt( socket.SOL_SOCKET, socket.SO_LINGER, linger )
			if self.__config['reuseAddress']:
				self.__socket.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
		except socket.error, detail:
			self.mm.error( 'Failed setup IRC socket (%s)' % detail, True )
			self.__socket = None
			return False

		try:
			self.__socket.bind( ( self.__config['localIP'], self.__config['localPort'] ) )
			self.__socket.connect( ( self.__config['serverAddress'], self.__config['serverPort'] ) )
			self.__socket.setblocking( 0 )
		except socket.error, detail:
			self.__socket.close()
			self.__socket = None
			self.mm.error( "Couldn't connect to IRC server: %s" % detail, True )
			return False

		self.__connected = True

		return True

	def processInputLine( self, line ):
		"""Process an input line."""
		m = self.__rfc_1459_command_regexp.match( line )

		if m.group( "prefix" ):
			prefix = m.group( "prefix" )
			if not self.real_server_name:
				self.real_server_name = prefix

		if m.group( "command" ):
			cmd = string.lower( m.group( "command" ) )

		if m.group( "argument" ):
			a = string.split( m.group( "argument" ), " :", 1 )
			args = string.split( a[0] )
			if len( a ) == 2:
				args.append( a[1] )

		# Translate numerics into more readable strings.
		if numeric_events.has_key( command ):
			cmd = numeric_events[command]

		if cmd == "nick":
			if string.split(prefix, "!")[0] == self.__real_nick:
				self.__real_nick = args[0]
		elif cmd == "welcome":
			# Record the nickname in case the client changed nick
			# in a nicknameinuse callback.
			self.__real_nick = args[0]

		if "quit" == cmd:
			args = [args[0]]
		elif "ping" == cmd:
			target = args[0]
		else:
			target = args[0]
			args = args[1:]

		if cmd == "mode":
			if not is_channel( target ):
				cmd = "umode"

		self.mm.debug( 2, "cmd: %s, src: %s, target: %s, args: %s" % ( cmd, prefix, target, args ) )
		#self._handle_event(Event(command, prefix, target, arguments))

	def update( self ):
		"""Receive from server then send output."""
		if not self.__socket:
			# socket already closed e.g. DOS protection
			return False

		try:
			new_data = self.__socket.recv( 16384 )
		except socket.error, detail:
			# The server hung up.
			if detail[0] != errno.EWOULDBLOCK:
				self.disconnect( "Connection reset by peer" )
				return False
			else:
				# just nothing atm
				return True

		if not new_data:
			# Read nothing: connection must be down.
			self.disconnect( "Connection reset by peer" )
			return False

		lines = self.__linesep_regexp.split( self.__inputBuffer + new_data )

		# Save the last, unfinished line.
		self.__inputBuffer = lines[-1]
		lines = lines[:-1]

		for line in lines[:-1]:
			self.processInputLine( self, line )

		# Send our queued output
		while 0 < len( self__outputBuffer ):
			try:
				line = self__outputBuffer[0]
				scount = self.socket.send( line )
				if scount == len( line ):
					# all sent remove
					del self.__outputBuffer[0]
				else:
					# some remaining
					self.__outputBuffer[0] = line[scount:]
					break
			except socket.error, detail:
				if detail[0] != errno.EWOULDBLOCK:
					self.mm.error( "Failed to send", True )
					self.disconnect( detail[1] )
					return False

		return True

	def disconnect( self, err = 'unknown' ):
		"""Close the server connection."""
		self.mm.debug( 1, 'IRCServer: closing (%s)' % ( err ) )
		# ensure we close down as best we can
		if self.__socket is not None:
			try:
				self.__socket.shutdown(2)
			except:
				self.mm.error( 'IRCServer: failed to shutdown connection (%s)' % ( err ) )

			try:
				self.__socket.close()
			except:
				self.mm.error( 'IRCServer: failed to close connection (%s)' % ( err ), True )
			self.__socket = None
		return True

# Numeric table mostly stolen from the Perl IRC module (Net::IRC).
numeric_events = {
    "001": "welcome",
    "002": "yourhost",
    "003": "created",
    "004": "myinfo",
    "005": "featurelist",  # XXX
    "200": "tracelink",
    "201": "traceconnecting",
    "202": "tracehandshake",
    "203": "traceunknown",
    "204": "traceoperator",
    "205": "traceuser",
    "206": "traceserver",
    "207": "traceservice",
    "208": "tracenewtype",
    "209": "traceclass",
    "210": "tracereconnect",
    "211": "statslinkinfo",
    "212": "statscommands",
    "213": "statscline",
    "214": "statsnline",
    "215": "statsiline",
    "216": "statskline",
    "217": "statsqline",
    "218": "statsyline",
    "219": "endofstats",
    "221": "umodeis",
    "231": "serviceinfo",
    "232": "endofservices",
    "233": "service",
    "234": "servlist",
    "235": "servlistend",
    "241": "statslline",
    "242": "statsuptime",
    "243": "statsoline",
    "244": "statshline",
    "250": "luserconns",
    "251": "luserclient",
    "252": "luserop",
    "253": "luserunknown",
    "254": "luserchannels",
    "255": "luserme",
    "256": "adminme",
    "257": "adminloc1",
    "258": "adminloc2",
    "259": "adminemail",
    "261": "tracelog",
    "262": "endoftrace",
    "263": "tryagain",
    "265": "n_local",
    "266": "n_global",
    "300": "none",
    "301": "away",
    "302": "userhost",
    "303": "ison",
    "305": "unaway",
    "306": "nowaway",
    "311": "whoisuser",
    "312": "whoisserver",
    "313": "whoisoperator",
    "314": "whowasuser",
    "315": "endofwho",
    "316": "whoischanop",
    "317": "whoisidle",
    "318": "endofwhois",
    "319": "whoischannels",
    "321": "liststart",
    "322": "list",
    "323": "listend",
    "324": "channelmodeis",
    "329": "channelcreate",
    "331": "notopic",
    "332": "topic",
    "333": "topicinfo",
    "341": "inviting",
    "342": "summoning",
    "346": "invitelist",
    "347": "endofinvitelist",
    "348": "exceptlist",
    "349": "endofexceptlist",
    "351": "version",
    "352": "whoreply",
    "353": "namreply",
    "361": "killdone",
    "362": "closing",
    "363": "closeend",
    "364": "links",
    "365": "endoflinks",
    "366": "endofnames",
    "367": "banlist",
    "368": "endofbanlist",
    "369": "endofwhowas",
    "371": "info",
    "372": "motd",
    "373": "infostart",
    "374": "endofinfo",
    "375": "motdstart",
    "376": "endofmotd",
    "377": "motd2",        # 1997-10-16 -- tkil
    "381": "youreoper",
    "382": "rehashing",
    "384": "myportis",
    "391": "time",
    "392": "usersstart",
    "393": "users",
    "394": "endofusers",
    "395": "nousers",
    "401": "nosuchnick",
    "402": "nosuchserver",
    "403": "nosuchchannel",
    "404": "cannotsendtochan",
    "405": "toomanychannels",
    "406": "wasnosuchnick",
    "407": "toomanytargets",
    "409": "noorigin",
    "411": "norecipient",
    "412": "notexttosend",
    "413": "notoplevel",
    "414": "wildtoplevel",
    "421": "unknowncommand",
    "422": "nomotd",
    "423": "noadmininfo",
    "424": "fileerror",
    "431": "nonicknamegiven",
    "432": "erroneusnickname", # Thiss iz how its speld in thee RFC.
    "433": "nicknameinuse",
    "436": "nickcollision",
    "437": "unavailresource",  # "Nick temporally unavailable"
    "441": "usernotinchannel",
    "442": "notonchannel",
    "443": "useronchannel",
    "444": "nologin",
    "445": "summondisabled",
    "446": "usersdisabled",
    "451": "notregistered",
    "461": "needmoreparams",
    "462": "alreadyregistered",
    "463": "nopermforhost",
    "464": "passwdmismatch",
    "465": "yourebannedcreep", # I love this one...
    "466": "youwillbebanned",
    "467": "keyset",
    "471": "channelisfull",
    "472": "unknownmode",
    "473": "inviteonlychan",
    "474": "bannedfromchan",
    "475": "badchannelkey",
    "476": "badchanmask",
    "477": "nochanmodes",  # "Channel doesn't supportmodes"
    "478": "banlistfull",
    "481": "noprivileges",
    "482": "chanoprivsneeded",
    "483": "cantkillserver",
    "484": "restricted",   # Connection is restricted
    "485": "uniqopprivsneeded",
    "491": "nooperhost",
    "492": "noservicehost",
    "501": "umodeunknownflag",
    "502": "usersdontmatch",
}

generated_events = [
    # Generated events
    "dcc_connect",
    "dcc_disconnect",
    "dccmsg",
    "disconnect",
    "ctcp",
    "ctcpreply",
]

protocol_events = [
    # IRC protocol events
    "error",
    "join",
    "kick",
    "mode",
    "part",
    "ping",
    "privmsg",
    "privnotice",
    "pubmsg",
    "pubnotice",
    "quit",
    "invite",
    "pong",
]

def mm_load( modManager ):
	"""Creates and returns your object."""
	return IRCBot( modManager )