def __init__(self): self._escape_parser=EscapeParser() self.macros = {} self.log=open(r'c:\temp\log_process.log','w',0) self.log.write('Init MudProcessor\n') self.buffering = False self.buffer = '' self.missed_heartbeats = 0 self.heartbeat_lc = None self.settings_directory='' self.server_echo = True self.triggers = [] self.aliases = [] self.gmcp_events = [] self.gmcp={} self.last_command_sent = '' self.root=self self.tracing = False self.active_channels = ['main'] self.state={} self.event_handlers={} self.reactor = None self.block=[] self.queue_to_send=[] self.connected=False
def __init__(self, factory): self.factory = factory self.root=self self.telnet = None self.triggers = [] self.aliases = [] self.baked_in_macros = gui_macros.copy() self.macros = self.baked_in_macros.copy() self.modules_loaded = set() self._escape_parser = EscapeParser() self.tracing = False self.server_echo = False self.console_ns = {'realm': self} self.console = InteractiveConsole(self.console_ns) self._last_line_end = None self.wrapper = TextWrapper(width = 100, drop_whitespace = False) self.protocols = [] self._closing_down = False self.gmcp_handler = None self.gmcp_events=[] self.gmcp={} self.state={} self.module_settings_dir='' self.active_channels=['main']
class MudProcessor(LineReceiver): delimiter = "\n" MAX_LENGTH = 131072 * 8 * 100 def __init__(self): self._escape_parser=EscapeParser() self.macros = {} self.log=open(r'c:\temp\log_process.log','w',0) self.log.write('Init MudProcessor\n') self.buffering = False self.buffer = '' self.missed_heartbeats = 0 self.heartbeat_lc = None self.settings_directory='' self.server_echo = True self.triggers = [] self.aliases = [] self.gmcp_events = [] self.gmcp={} self.last_command_sent = '' self.root=self self.tracing = False self.active_channels = ['main'] self.state={} self.event_handlers={} self.reactor = None self.block=[] self.queue_to_send=[] self.connected=False def heartbeat(self): self.missed_heartbeats+=1 if self.missed_heartbeats == 3: self.stop() else: self.send_to_client('ping', []) def stop(self): self.send_to_client('bye', []) self.log.close() self.transport.loseConnection() from twisted.internet import reactor reactor.stop() def connectionMade(self): self.log.write('ConnectionMade\n') self.connected = True self.transport.write(json.dumps(["hello", [self.macros]]) + "\n") self.heartbeat_lc = LoopingCall(self.heartbeat) self.heartbeat_lc.start(10) for meth,params in self.queue_to_send: self.send_to_client(meth, params) def send_to_client(self, meth, params): #print('send to processor: %s, %s'%(meth, json.dumps(params))) if self.connected: line = json.dumps([meth, params]) if len(line)>1000: self.transport.write("%buff_begin%\n") while len(line)>1000: self.transport.write(line[:1000]+"\n") line=line[1000:] self.transport.write(line+"\n") self.transport.write("%buff_end%\n") else: self.transport.write(json.dumps([meth, params]) + "\n") else: self.queue_to_send.append((meth,params)) def lineReceived(self, line): self.log.write('Line: %s \n'%line) if line == '%buff_begin%': self.buffering = True self.buffer = '' elif line == '%buff_end%': if self.buffering == True: self.lineReceived_recomposed(self.buffer) self.buffering = False else: if self.buffering == True: self.buffer += line else: self.lineReceived_recomposed(line) def lineReceived_recomposed(self, line): meth,rest = json.loads(line) if meth == "close": self.stop() elif meth == "ping": self.missed_heartbeats=-1 elif meth == "hello": self.log.write('Hello received\n') name = rest[0] self.name = name elif meth == 'mud_line': metaline = json_to_metaline(rest[0]) display_line = bool(rest[1]) #self.match_triggers(metaline, display_line) #self.transport.write(json.dumps(['display_line', [metaline_to_json(metaline), 1]])+'\n') self.send_to_client('display_line', [metaline_to_json(metaline),1]) elif meth == 'user_line': line = rest[0] self.log.write('User Line: %s\n'%line) #self.transport.write(json.dumps(['send_to_mud',line])+'\n') self.send_to_client('send_to_mud', line) elif meth == 'do_block': lines = json.loads(rest[0]) block = [] for l in lines: block.append(json_to_metaline(l)) self.blockReceived(block) elif meth == 'do_triggers': metaline=json_to_metaline(rest[0]) display_line = bool(rest[1]) self.metalineReceived(metaline, display_line) elif meth == 'do_aliases': line = rest[0] self.server_echo = bool(rest[1]) echo = bool(rest[2]) self.parseSend(line, echo) elif meth == 'do_gmcp': pair = rest[0] gmcp_key,gmcp_data = pair self.gmcp[gmcp_key]=gmcp_data for gmcp_event in self.gmcp_events: gmcp_event(pair, self) else: raise ValueError("bad line: %s" % line) #self.transport.write(json.dumps(["ack", [meth,rest]]) + "\n") self.send_to_client('ack', [meth,rest]) def load_module(self, cls, _sort = True): """Load the triggers, aliases, macros and other modules of the given module. """ robmod = cls(self) for mod in robmod.modules: self.load_module(mod, _sort = False) if _sort: self.triggers.sort(key = attrgetter("sequence")) self.gmcp_events.sort(key = attrgetter("sequence")) self.aliases.sort(key = attrgetter("sequence")) return robmod def registerEventHandler(self, eventName, eventHandler): if not eventName in self.event_handlers: self.event_handlers[eventName]=[] self.event_handlers[eventName].append(eventHandler) def fireEvent(self, eventName, *args): self.send_to_client('event', [eventName,args]) self.fireEventLocal(eventName, *args) def fireEventLocal(self, eventName, *args): if eventName in self.event_handlers: for eh in self.event_handlers[eventName]: self.reactor.callLater(0, eh, *args) def get_state(self, item): if item in self.state: return self.state[item] else: return '' def set_state(self, name, value): self.state[name]=value self.send_to_client('set_state', [name,value]) def metalineReceived(self, metaline, display_line): realm = TriggerMatchingRealm(metaline, parent = self, root = self, display_line = display_line) try: realm.process() except: self.handle_exception(traceback.format_exc()) def cwrite(self, line, display_line=True, channels=['main']): ml=taggedml(line) ml.channels = channels self.write(ml, display_line) def write(self, line, display_line = True): if not isinstance(line, (basestring, Metaline)): line = str(line) if isinstance(line, basestring): metaline = simpleml(line, fg_code(WHITE, False), bg_code(BLACK)) metaline.wrap = False else: metaline = line self.send_to_client('display_line', [metaline_to_json(metaline),int(display_line)]) def send_mud(self, line): self.last_command_sent = line self.send_to_client('send_to_mud', line) def send(self, line, echo = True): """Match aliases against the line and perhaps send it to the MUD.""" echo = not self.server_echo and echo realm = AliasMatchingRealm(line, echo, parent = self, root = self) try: realm.process() except: self.handle_exception(traceback.format_exc()) def parseSend(self, string, echo): for line in list(self._escape_parser.parse(string + '\n')): self.send(line, echo) def setActiveChannels(self, channels): self.active_channels = channels self.send_to_client('set_active_channels', channels) def handle_exception(self, trace): self.send_to_client('error', trace) def blockReceived(self, block): self.block = block realm = TriggerBlockMatchingRealm(self.block, parent = self, root = self, display_group=True) realm.process() def set_timer(self, time, fn, *args, **kwargs): from twisted.internet import reactor realm = TimerRealm(self) timer = self.reactor.callLater(time, fn, realm, *args, **kwargs) return timer def debug(self, msg): self.send_to_client('debug', [msg]) def trace(self, line): """Write the argument to the screen if we are tracing, elsewise do nothing. """ if self.tracing: self.write("TRACE: " + line) def trace_thunk(self, thunk): """If we're tracing, call the thunk and write its result to the outputs. If not, do nothing. """ if self.tracing: self.write("TRACE: " + thunk())
class RootRealm(object): """The root of the realms hierarchy. This is what macros and top-level modules deal with. """ def __init__(self, factory): self.factory = factory self.root=self self.telnet = None self.triggers = [] self.aliases = [] self.baked_in_macros = gui_macros.copy() self.macros = self.baked_in_macros.copy() self.modules_loaded = set() self._escape_parser = EscapeParser() self.tracing = False self.server_echo = False self.console_ns = {'realm': self} self.console = InteractiveConsole(self.console_ns) self._last_line_end = None self.wrapper = TextWrapper(width = 100, drop_whitespace = False) self.protocols = [] self._closing_down = False self.gmcp_handler = None self.gmcp_events=[] self.block_handlers=[] self.gmcp={} self.state={} self.module_settings_dir='' self.active_channels=['main'] self.block=[] self.hide_lines=0 self.last_line=None self.gui=None self.accessibility_mode = False self.event_handlers={} self.safe_to_send=True #Bidirectional, or just ambivalent, functions. def registerEventHandler(self, eventName, eventHandler): if not eventName in self.event_handlers: self.event_handlers[eventName]=[] self.event_handlers[eventName].append(eventHandler) def fireEvent(self, eventName, *args): if eventName in self.event_handlers: for eh in self.event_handlers[eventName]: self.factory.reactor.callLater(0, eh, *args) def get_state(self, item): if item in self.state: return self.state[item] else: return '' def set_state(self, item, value): self.state[item]=value def hide_next_lines(self,num_lines): self.hide_lines+=num_lines def clear_modules(self): """Restore our state to a pristine (ie, blank) condition. """ #keep in place so references to these still work self.triggers[:] = [] self.aliases[:] = [] self.gmcp_events[:]=[] self.macros.clear() self.macros.update(self.baked_in_macros) self.modules_loaded = set() def reload_main_module(self): """Clear ourselves into a pristine state and load the main module again. """ self.clear_modules() cls = load_file(self.factory.main_module_name) self.load_module(cls) def load_module(self, cls, _sort = True): """Load the triggers, aliases, macros and other modules of the given module. """ if cls in self.modules_loaded: return #add now, so that we can avoid circular dependencies self.modules_loaded.add(cls) try: robmod = cls(self) for mod in robmod.modules: self.load_module(mod, _sort = False) if _sort: self.triggers.sort(key = attrgetter("sequence")) self.aliases.sort(key = attrgetter("sequence")) self.gmcp_events.sort(key = attrgetter("sequence")) except: self.modules_loaded.remove(cls) raise return robmod def close(self): """Close up our connection and shut up shop. It is guaranteed that, on registered connection event receivers, connection_lost will be called before close. """ if not self._closing_down: #lose the connection first. self.telnet.close() else: #connection's already lost, we don't need to wait for prot in self.protocols: prot.close() self._closing_down = True def addProtocol(self, protocol): self.protocols.append(protocol) def connectionLost(self): """The link to the MUD died. It is guaranteed that this will be called before close on connection event receivers. """ message = time.strftime("Connection closed at %H:%M:%S.") colour = HexFGCode(0xFF, 0xAA, 0x00) #lovely orange metaline = simpleml(message, colour, bg_code(BLACK)) self.write(metaline) for prot in self.protocols: prot.connectionLost() #we might be waiting on the connection to die before we send out #close events if self._closing_down: for prot in self.protocols: prot.close() self._closing_down = True def connectionMade(self): """The MUD's been connected to.""" message = time.strftime("Connection opened at %H:%M:%S.") colour = HexFGCode(0xFF, 0xAA, 0x00) #lovely orange metaline = simpleml(message, colour, bg_code(BLACK)) self.write(metaline) for prot in self.protocols: prot.connectionMade() def trace_on(self): """Turn tracing (verbose printing to the output screen) on.""" if not self.tracing: self.tracing = True self.trace("Tracing enabled!") def trace_off(self): """Turn tracing off.""" if self.tracing: self.trace("Tracing disabled!") self.tracing = False def maybe_do_macro(self, chord): """Try and run a macro against the given keychord. A return value of True means a macro was found and run, False means no macro was found, or a macro returned True (meaning allow the GUI to continue handling the keypress). """ if chord in self.macros: macro = self.macros[chord] allow_gui_continue = False try: allow_gui_continue = macro(self) except Exception: traceback.print_exc() return not allow_gui_continue else: return False #Going towards the screen def gmcpReceived(self, gmcp_pair): """Take GMCP data and do something with it""" for gmcp_event in self.gmcp_events: gmcp_event(gmcp_pair, self) def blockReceived(self, block): if len(block) > 0: realm = TriggerBlockMatchingRealm(block, parent = self, root = self, send_line_to_mud = self.telnet.sendLine) realm.process() def setActiveChannels(self, channels): self.active_channels = channels def metalineReceived(self, metaline): """Match a line against the triggers and perhaps display it on screen. """ realm = TriggerMatchingRealm(metaline, parent = self, root = self, send_line_to_mud = self.telnet.sendLine) realm.process() def cwrite(self, line, soft_line_start=False): ml=taggedml(line) self.write(ml, soft_line_start) def write(self, line, soft_line_start = False): if self.hide_lines>0: self.hide_lines-=1 return """Write a line to the screen. This forcibly converts its argument to a Metaline. """ if not isinstance(line, (basestring, Metaline)): line = str(line) if isinstance(line, basestring): metaline = simpleml(line, fg_code(WHITE, False), bg_code(BLACK)) metaline.wrap = False metaline.soft_line_start = soft_line_start else: metaline = line #we don't need to close off the ends of the note, because thanks to #the magic of the ColourCodeParser, each new line is started by the #implied colour, so notes can't bleed out into text (though the #reverse can be true). #this needs to be before the futzing with NLs and GA, because textwrap #obliterates all other newlines. metaline = metaline.wrapped(self.wrapper) #we don't actually append newlines at the end, but the start. This #simplifies things, because we don't use a newline where a soft line #end meets a soft line start, so there's only one place in this code #that can add newlines. if self._last_line_end is not None: if self._last_line_end == 'hard' or not metaline.soft_line_start: metaline.insert(0, '\n') for prot in self.protocols: prot.metalineReceived(metaline,self.active_channels) self._last_line_end = metaline.line_end def trace(self, line): """Write the argument to the screen if we are tracing, elsewise do nothing. """ if self.tracing: self.write("TRACE: " + line) def trace_thunk(self, thunk): """If we're tracing, call the thunk and write its result to the outputs. If not, do nothing. """ if self.tracing: self.write("TRACE: " + thunk()) #Going towards the MUD. def receive_gui_line(self, string): """Send lines input into the GUI to the MUD. NOTE: this may have the power to execute arbitrary Python code. Thus, triggers and aliases should avoid using this, as they may be vulnerable to injection from outside sources. Use send instead. """ if string.startswith('/'): self.console.push(string[1:]) else: for line in self._escape_parser.parse(string + '\n'): self.send(line) def safe_send(self, line, echo = True): if self.safe_to_send: self.send(line, echo) def send(self, line, echo = True): """Match aliases against the line and perhaps send it to the MUD.""" echo = not self.server_echo and (echo and not self.accessibility_mode) realm = AliasMatchingRealm(line, echo, parent = self, root = self, send_line_to_mud = self.telnet.sendLine) realm.process() def set_timer(self, seconds, f, realm): return self.factory.reactor.callLater(seconds, f, realm)
def setUp(self): self.eparser = EscapeParser()
class TestEscapes(object): def setUp(self): self.eparser = EscapeParser() tests = [('foo\n', #basic. ['foo']), ('\n', ['']), ('foo\\nbar\n', #multiple things on one input. ['foo', 'bar']), ('foo\\\nbar\n', ['foo\\', 'bar']), ('foo;bar\n', #semicolon linebreak this time ['foo', 'bar']), ('foo\\x0Abar\\012', #octal and hex escapes. ['foo', 'bar']), ('\\x57\n', ['\x57']), ('\\100\\10\\1\n', ['\100\10\1']), ('\\\\foo\n', #escaped backslashes. ['\\foo']), ('\\;bar\n', #escaped semicolons [';bar']), ('\\foo\n', #and unknown escapes. ['\\foo'])] error_tests = [('\\xoink\n', InvalidEscape), ('bar\\', InvalidInput), ('foo\\xb', InvalidEscape)] def test_normals(self): for test, expected in self.tests: yield self.do_one_normal, test, expected def do_one_normal(self, input, expected): res = list(self.eparser.parse(input)) assert res == expected, res def run_error_tests(self): for test, err in self.error_tests: yield self.do_one_error, test, err def do_one_error(self, input, expected_err): try: res = list(self.eparser.parse(input)) except expected_err: #I love Python so much. :) pass else: assert False, res def test_retain_if_no_newline(self): res = list(self.eparser.parse('foo')) assert res == [] res = list(self.eparser.parse('\n')) assert res == ['foo'] def test_chopped_octal_escape(self): res = list(self.eparser.parse('foo\\1')) assert res == [] res = list(self.eparser.parse('\n')) assert res == ['foo\1']