# 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[^ ]+) +)?(?P[^ ]+)( *(?P .+))?" ) 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 )