#! /usr/bin/python2.5 # Kilgame server as an IRC client. # Based on ircbot.py by bsmntbombdood. http://abacus.kwzs.be/~bsmnt_bot/ import socket import re import sys import traceback import thread import pprint import inspect import time import os import threading import copy import pickle import marshal import types import math from cStringIO import StringIO class StopHandlingCallbacks: pass class QuitIRC: pass class RemoveCallback: pass class IRCbot: def __init__(self, host, nick, ident, owner, realname="", port=6667, chan=None, verbose=True, exec_chans=[], command_char="#", threading=True, error_in_chan=True, ban_file="/bans"): self.host = host self.port = port self.nick = nick self.ident= ident self.realname = realname self.chan = chan self.verbose = verbose self.owner = owner self.error_in_chan = error_in_chan self.errorchan = "#%s_errors" % self.nick self.connected = False; self.raw_regex_queue = []; self.ban_file = ban_file self.banlist = self.read_bans() self.COMMAND_CHAR = command_char self.THREADING = threading self.commands_running = [] self.commands_running_lock = thread.allocate_lock() self.gameInited = False self.gameStarted = False self.gamePlayerDict = {} self.locals = {"self": self} self.message_re = re.compile(r"^:(?P\S+)!(?P\S+)@(?P\S+) (?P\S+) (?P\S+) :(?P.*)") #default callbacks: self.register_raw("^PING (.*)$", self.pong) self.register_raw( r"^:%s PRIVMSG \S* :\+%squit ?(.*)" % (self.owner, self.COMMAND_CHAR), self.do_quit) self.register_raw( r"^:%s PRIVMSG \S* :\+%sraw (.*)" % (self.owner, self.COMMAND_CHAR), self.do_raw) self.register_raw( r"^\S+ PRIVMSG \S+ :.%sctcp (\S+) (.+)" % self.COMMAND_CHAR, self.do_ctcp) self.register_raw( r"^:%s PRIVMSG (\S*) :\+%spexec (.*)" % (self.owner, self.COMMAND_CHAR), self.do_exec) self.register_raw( r"\S+ PRIVMSG (%s) :.%sexec (.*)" % ("|".join(exec_chans+[self.errorchan]), self.COMMAND_CHAR), self.do_exec) self.register_raw( r"\S+ PRIVMSG \S+ :.%sps" % self.COMMAND_CHAR, self.do_ps) self.register_raw( r"^\S+ PRIVMSG (%s) :.%skill (.*)" % ("|".join(exec_chans+[self.errorchan]), self.COMMAND_CHAR), self.do_kill) self.register_raw( r"^\S+ PRIVMSG %s :.%sinit" % (self.chan, self.COMMAND_CHAR), self.do_init) self.register_raw( r"^(\S+)!\S*@\S* PRIVMSG %s :(.)%sjoin" % (self.chan, self.COMMAND_CHAR), self.do_join) self.register_raw( r"^\S+ PRIVMSG %s :.%sstart" % (self.chan, self.COMMAND_CHAR), self.do_start) self.register_raw( r"^(\S+)!\S*@\S* PRIVMSG %s :(.)%sstatus" % (self.chan, self.COMMAND_CHAR), self.do_status) self.register_raw( r"^(\S+)!\S*@\S* PRIVMSG %s :(.)%sshoot (\S{1,16})" % (self.chan, self.COMMAND_CHAR), self.do_shoot) self.register_raw( r"^(\S+)!\S*@\S* PRIVMSG %s :(.)%sfriend (\S{1,16})" % (self.chan, self.COMMAND_CHAR), self.do_friend) self.register_raw( r"^\S+ PRIVMSG %s :.%send" % (self.chan, self.COMMAND_CHAR), self.do_end) self.register_raw(r"^ERROR :Closing Link:.*", lambda x : True) def raw(self, message): message += "\r\n" x = self.socket.send(message) while x < len(message): message = message[x:] x = self.socket.send(message) def connect(self): self.socket = socket.socket(); self.socket.connect((self.host, self.port)) self.raw("NICK %s" % self.nick) self.raw("USER %s %s bla :%s" % (self.ident, self.host, self.realname)) self.raw("CAPAB :IDENTIFY-MSG") if isinstance(self.chan, list): for i in self.chan: self.raw("JOIN :%s" % i) else: self.raw("JOIN :%s" % self.chan) self.raw("JOIN :%s" % self.errorchan) self.sockfile = self.socket.makefile("rw") self.connected = True; def listen(self): if not self.connected: self.connect() while self.connected: line = self.sockfile.readline() line = line.strip() if not line: continue if self.verbose: sys.__stdout__.write(line + "\n") if hasattr(self, "onmsg"): if self.onmsg(message): self.connected = False break; self.do_callbacks(line) def disconnect(self, message): self.raw("QUIT :%s" % message) self.socket.close() self.sockfile.close() self.connected = False return True def register_raw(self, regex, callback): if isinstance(regex, str): self.raw_regex_queue.append((re.compile(regex), callback)) else: self.raw_regex_queue.append((regex, callback)) def do_callbacks(self, message): # each entry in self.raw_regex_queue is (regex, callback) for ban in self.banlist: if ban.match(message): print "%s is banned by %r" % (message, ban.pattern) return for i in self.raw_regex_queue: if not self.connected: break m = i[0].match(message) if m: try: self.exec_execer("self.handle_callback(message, m, i)", (globals(), locals())) except StopHandlingCallbacks: break except RemoveCallback: self.raw_regex_queue.remove(i) except QuitIRC: self.disconnect("Disconnected.") return def handle_callback(self, message, m, i): try: retval = i[1](message, m) except TypeError: try: retval = i[1](message) except TypeError: self.raw("PRIVMSG %s :TypeError for callback %s, %r. Removing." % (self.errorchan, i[1].__name__, i[0].pattern)) self.raw_regex_queue.remove(i) return if retval: self.disconnect("Disconnected.") return def pong(self, message, r): self.raw("PONG %s" % r.group(1)) def do_quit(self, message, r): self.disconnect(r.group(1)) return True def do_raw(self, message, r): self.raw(r.group(1)) def do_ctcp(self, message, r): self.raw("PRIVMSG %s :\x01%s\x01" % (r.group(1), r.group(2))) def exec_execer(self, command, env): info = (command, time.time(), env, threading.Event()) self.commands_running_lock.acquire() self.commands_running.append(info) self.commands_running_lock.release() thread_info.info = info sys.settrace(exec_global_tracer) try: exec command in env[0], env[1] except SystemExit: pass except Exception: stuff = traceback.format_exc() if self.error_in_chan: env[0]["sys"].stdout.write(stuff.rstrip().split("\n")[-1]) env[0]["sys"].stderr.write(stuff) sys.settrace(None) self.commands_running_lock.acquire() self.commands_running.remove(info) self.commands_running_lock.release() def do_exec(self, message, r): command = r.group(2) # what to expose in exec env = (globals(), locals()) env[0]["sys"] = SysWrapper( stdout = IRCFileWrapper(self.raw, r.group(1)), stderr = IRCFileWrapper(self.raw, self.errorchan)) if self.THREADING: thread.start_new_thread(self.exec_execer, (command, env)) else: self.exec_execer(command, env) def get_message(self, line): return self.message_re.match(line).group("message") def print_callbacks(self, fd = sys.stdout): pprint.pprint( [(i[0].pattern, i[1].__name__) for i in self.raw_regex_queue], fd) def do_ps(self, message, r): channel = self.message_re.match(message).group("channel") for i in range(len(self.commands_running)): info = self.commands_running[i] if info[3].isSet(): killed = ", killed" else: killed = "" self.raw("PRIVMSG %s :%s: %r, %.2f seconds%s" % (channel, i, info[0], time.time() - info[1], killed)) if not self.commands_running and self.verbose: self.raw("PRIVMSG %s :None" % channel) def do_kill(self, message, r): channel = r.group(1) try: pid = int(r.group(2)) except ValueError: self.raw("PRIVMSG %s :Argument to kill must be a number" % channel) return self.commands_running_lock.acquire() try: self.commands_running[pid][3].set() except IndexError: self.commands_running_lock.release() self.raw("PRIVMSG %s :Proccess %s does not exist" % (channel, r.group(1))) return self.commands_running_lock.release() def do_init(self, message, r): if self.gameInited: sys.stdout("The game has already been initialized. Say #join to join; #start to start; #end to abort the game.") else: self.gameInited = True sys.stdout("Game initialized. Say #join to join; #start to start; #end to abort the game.") def do_join(self, message, r): if r.group(2) == "-": sys.stdout("%s, please identify to NickServ first." % r.group(1)) elif r.group(1) in self.gamePlayerDict: sys.stdout("%s, you are already in the game. Say #end to abort the game." % r.group(1)) elif not self.gameInited: sys.stdout("%s, the game has not yet been initialized. Say #init to initialize the game." % r.group(1)) elif self.gameStarted: sys.stdout("%s, the game has already started. Say #status to view status; #end to abort the game." % r.group(1)) else: self.gamePlayerDict[r.group(1)] = 2, datetime.min, [] sys.stdout("%s, you have joined the game. Say #start to start; #end to abort the game." % r.group(1)) def do_start(self, message, r): if not self.gameInited: sys.stdout("The game has not yet been initialized. Say #init to initialize the game.") elif self.gameStarted: sys.stdout("The game has already started. Say #status to view status; #end to abort the game.") elif len(self.gamePlayerDict) < 2: sys.stdout("The game doesn't yet have enough players. Say #join to join; #end to abort the game.") else: self.gameStarted = True sys.stdout("Game started. Say #status to view status; #end to abort the game.") def listInit(self, l): return reduce(lambda x, y: x + ", " + y, l[:-1]) def do_status(self, message, r): if not self.gameInited: sys.stdout("%s, the game has not yet been initialized. Say #init to initialize the game." % r.group(1)) if not self.gameStarted: if self.gamePlayerDict == []: sys.stdout("%s, currently there are no players. Say #join to join." % r.group(1)) elif len(self.gamePlayerDict) == 1: sys.stdout("%s, currently the only player is %s. Say #join to join." % (r.group(1), self.gamePlayerDict.keys()[0])) else: sys.stdout("%s, the current players are %s and %s. Say #join to join; #start to start." % (r.group(1), listInit(playerList), playerList[-1])) else: playerList = [] for name, (lives, nextShootTime, noShootList) in self.gamePlayerDict.iteritems(): timeToShoot = 0 if nextShootTime < datetime.now() else (nextShootTime - datetime.now()).seconds playerList.append("%s (%s lives, must wait %s seconds before shooting)" % (name, lives, timeToShoot)) sys.stdout("%s, the current players are %s and %s." % (r.group(1), self.listInit(playerList), playerList[-1])) noShootList = self.gamePlayerDict[r.group(1)][2] if noShootList == []: sys.stdout("%s, you are currently able to shoot anybody." % r.group(1)) elif len(noShootList) == 1: sys.stdout("%s, you are unable to shoot %s." % (r.group(1), noShootList[0])) sys.stdout("%s, you are unable to shoot %s and %s." % (r.group(1) + self.listInit(noShootList), noShootList[-1])) sys.stdout("%s, say #shoot to shoot a player; #friend to promise to not shoot a player; #end to abort the game." % r.group(1)) def do_shoot(self, message, r): sys.stdout("Not yet implemented.") def do_friend(self, message, r): sys.stdout("Not yet implemented.") def do_end(self, message, r): sys.stdout("Not yet implemented.") def save_callbacks(self, f): if not hasattr(f, "write"): f = open(f, "wb") data = [(i[0], marshal.dumps(i[1].func_code), inspect.ismethod(i[1])) for i in self.raw_regex_queue] pickle.dump(data, f) def load_callbacks(self, f): if not hasattr(f, "read"): f = open(f, "rb") callbacks = pickle.load(f) self.raw_regex_queue = [] for i in callbacks: fn = types.FunctionType(marshal.loads(i[1]), globals()) if i[2]: # method, this only works for methods on IRCbot callback = types.MethodType(fn, self, self.__class__) else: callback = fn self.raw_regex_queue.append((i[0], callback)) def read_bans(self): try: fd = open(self.ban_file, "r") except IOError: fd = open(self.ban_file, "w+") bans = fd.read() fd.close() self.banlist = [re.compile(i) for i in bans.split("\n") if i] return self.banlist def write_bans(self): fd = open(self.ban_file, "w+") fd.write("\n".join([i.pattern for i in self.banlist])) fd.close() def ban(self, pattern): self.banlist.append(re.compile(pattern)) self.write_bans() def unban(self, pattern): for i in self.banlist: if i.pattern == pattern: self.banlist.remove(i) self.write_bans() # to kill threads def exec_global_tracer(frame, event, arg): if event == "call": return exec_local_tracer else: return exec_global_tracer def exec_local_tracer(frame, event, arg): try: info=frame.f_globals["thread_info"].info except KeyError: return exec_local_tracer if info[3].isSet(): thread.exit() return exec_local_tracer class IRCFileWrapper: def __init__(self, raw, channel, rate_limit = None): """ rate_limit is function of one argument (the number of lines sent so far), and must return a number (the number of seconds to wait before sending the next line)""" self.raw = raw self.channel = channel self.rate_limit = rate_limit or (lambda x: (x > 2 and min([x/5., 2])) or 0) def write(self, s): if not self.channel: return s = str(s) s = s.split("\n") lines_sent = 0 for i in s: if i.isspace() or not i: continue sent = 0 while sent < len(i): self.raw("PRIVMSG %s :%s" % (self.channel, i[sent:sent+375])) sent += 375 lines_sent += 1 time.sleep(self.rate_limit(lines_sent)) __call__ = write class SysWrapper: """wrap the sys module with our own stdout and stderr""" def __init__(self, stdout, stderr): self.__sys = __import__("sys") self.stdout = stdout self.stderr = stderr def __getattr__(self, what): if what == "stdout": return self.stdout elif what == "stderr": return self.stderr else: # this is the only branch ever called return getattr(self.__sys, what) args = { "host": "85.188.1.26", "nick": "EagleBot", "ident": "bsmnt", "realname": "bsmntbombdood bot", "chan": "#kilbot", "exec_chans": ["#kilbot"], "owner": r"ihope!\S*@\S*" } if os.getuid() == 0: os.setgid(1005) os.setuid(1005) thread_info = threading.local() bot = IRCbot(**args) bot.listen() def hack_closure(func, locals, globals): code = func.func_code env = globals.copy() needed = [i for i in code.co_names if i not in code.co_varnames] print needed for var in needed: env[var] = locals[var] for k in locals: print env[k] return types.FunctionType(code, env)