Proper RFC 1143 Telnet option negotiation queue.
[mudpy.git] / lib / mudpy / misc.py
1 # -*- coding: utf-8 -*-
2 u"""Miscellaneous functions for the mudpy engine."""
3
4 # Copyright (c) 2004-2010 Jeremy Stanley <fungi@yuggoth.org>. Permission
5 # to use, copy, modify, and distribute this software is granted under
6 # terms provided in the LICENSE file distributed with this software.
7
8 class Element:
9    u"""An element of the universe."""
10    def __init__(self, key, universe, filename=None):
11       u"""Set up a new element."""
12       import os.path
13
14       # keep track of our key name
15       self.key = key
16
17       # keep track of what universe it's loading into
18       self.universe = universe
19
20       # clone attributes if this is replacing another element
21       if self.key in self.universe.contents:
22          old_element = self.universe.contents[self.key]
23          for attribute in vars(old_element).keys():
24             exec(u"self." + attribute + u" = old_element." + attribute)
25          if self.owner: self.owner.avatar = self
26
27       # i guess this is a new element then
28       else:
29
30          # not owned by a user by default (used for avatars)
31          self.owner = None
32
33          # no contents in here by default
34          self.contents = {}
35
36          # parse out appropriate category and subkey names, add to list
37          if self.key.find(u":") > 0:
38             self.category, self.subkey = self.key.split(u":", 1)
39          else:
40             self.category = u"other"
41             self.subkey = self.key
42          if not self.category in self.universe.categories:
43             self.category = u"other"
44             self.subkey = self.key
45
46          # get an appropriate filename for the origin
47          if not filename:
48             filename = self.universe.default_origins[self.category]
49          if not os.path.isabs(filename):
50             filename = os.path.abspath(filename)
51
52          # add the file if it doesn't exist yet
53          if not filename in self.universe.files:
54             DataFile(filename, self.universe)
55
56       # record or reset a pointer to the origin file
57       self.origin = self.universe.files[filename]
58
59       # add a data section to the origin if necessary
60       if not self.origin.data.has_section(self.key):
61          self.origin.data.add_section(self.key)
62
63       # add or replace this element in the universe
64       self.universe.contents[self.key] = self
65       self.universe.categories[self.category][self.subkey] = self
66
67    def reload(self):
68       u"""Create a new element and replace this one."""
69       new_element = Element(self.key, self.universe, self.origin.filename)
70       del(self)
71    def destroy(self):
72       u"""Remove an element from the universe and destroy it."""
73       self.origin.data.remove_section(self.key)
74       del self.universe.categories[self.category][self.subkey]
75       del self.universe.contents[self.key]
76       del self
77    def facets(self):
78       u"""Return a list of non-inherited facets for this element."""
79       if self.key in self.origin.data.sections():
80          return self.origin.data.options(self.key)
81       else: return []
82    def has_facet(self, facet):
83       u"""Return whether the non-inherited facet exists."""
84       return facet in self.facets()
85    def remove_facet(self, facet):
86       u"""Remove a facet from the element."""
87       if self.has_facet(facet):
88          self.origin.data.remove_option(self.key, facet)
89          self.origin.modified = True
90    def ancestry(self):
91       u"""Return a list of the element's inheritance lineage."""
92       if self.has_facet(u"inherit"):
93          ancestry = self.getlist(u"inherit")
94          for parent in ancestry[:]:
95             ancestors = self.universe.contents[parent].ancestry()
96             for ancestor in ancestors:
97                if ancestor not in ancestry: ancestry.append(ancestor)
98          return ancestry
99       else: return []
100    def get(self, facet, default=None):
101       u"""Retrieve values."""
102       if default is None: default = u""
103       if self.origin.data.has_option(self.key, facet):
104          raw_data = self.origin.data.get(self.key, facet)
105          if raw_data.startswith(u"u\"") or raw_data.startswith(u"u'"):
106             raw_data = raw_data[1:]
107          raw_data.strip(u"\"'")
108          if type(raw_data) == str: return unicode(raw_data, "utf-8")
109          else: return raw_data
110       elif self.has_facet(u"inherit"):
111          for ancestor in self.ancestry():
112             if self.universe.contents[ancestor].has_facet(facet):
113                return self.universe.contents[ancestor].get(facet)
114       else: return default
115    def getboolean(self, facet, default=None):
116       u"""Retrieve values as boolean type."""
117       if default is None: default=False
118       if self.origin.data.has_option(self.key, facet):
119          return self.origin.data.getboolean(self.key, facet)
120       elif self.has_facet(u"inherit"):
121          for ancestor in self.ancestry():
122             if self.universe.contents[ancestor].has_facet(facet):
123                return self.universe.contents[ancestor].getboolean(facet)
124       else: return default
125    def getint(self, facet, default=None):
126       u"""Return values as int/long type."""
127       if default is None: default = 0
128       if self.origin.data.has_option(self.key, facet):
129          return self.origin.data.getint(self.key, facet)
130       elif self.has_facet(u"inherit"):
131          for ancestor in self.ancestry():
132             if self.universe.contents[ancestor].has_facet(facet):
133                return self.universe.contents[ancestor].getint(facet)
134       else: return default
135    def getfloat(self, facet, default=None):
136       u"""Return values as float type."""
137       if default is None: default = 0.0
138       if self.origin.data.has_option(self.key, facet):
139          return self.origin.data.getfloat(self.key, facet)
140       elif self.has_facet(u"inherit"):
141          for ancestor in self.ancestry():
142             if self.universe.contents[ancestor].has_facet(facet):
143                return self.universe.contents[ancestor].getfloat(facet)
144       else: return default
145    def getlist(self, facet, default=None):
146       u"""Return values as list type."""
147       if default is None: default = []
148       value = self.get(facet)
149       if value: return makelist(value)
150       else: return default
151    def getdict(self, facet, default=None):
152       u"""Return values as dict type."""
153       if default is None: default = {}
154       value = self.get(facet)
155       if value: return makedict(value)
156       else: return default
157    def set(self, facet, value):
158       u"""Set values."""
159       if not self.has_facet(facet) or not self.get(facet) == value:
160          if type(value) is long: value = unicode(value)
161          elif not type(value) is unicode: value = repr(value)
162          self.origin.data.set(self.key, facet, value)
163          self.origin.modified = True
164    def append(self, facet, value):
165       u"""Append value tp a list."""
166       if type(value) is long: value = unicode(value)
167       elif not type(value) is unicode: value = repr(value)
168       newlist = self.getlist(facet)
169       newlist.append(value)
170       self.set(facet, newlist)
171
172    def new_event(self, action, when=None):
173       u"""Create, attach and enqueue an event element."""
174
175       # if when isn't specified, that means now
176       if not when: when = self.universe.get_time()
177
178       # events are elements themselves
179       event = Element(u"event:" + self.key + u":" + counter)
180
181    def send(
182       self,
183       message,
184       eol=u"$(eol)",
185       raw=False,
186       flush=False,
187       add_prompt=True,
188       just_prompt=False,
189       add_terminator=False,
190       prepend_padding=True
191    ):
192       u"""Convenience method to pass messages to an owner."""
193       if self.owner:
194          self.owner.send(
195             message,
196             eol,
197             raw,
198             flush,
199             add_prompt,
200             just_prompt,
201             add_terminator,
202             prepend_padding
203          )
204
205    def can_run(self, command):
206       u"""Check if the user can run this command object."""
207
208       # has to be in the commands category
209       if command not in self.universe.categories[u"command"].values():
210          result = False
211
212       # avatars of administrators can run any command
213       elif self.owner and self.owner.account.getboolean(u"administrator"):
214          result = True
215
216       # everyone can run non-administrative commands
217       elif not command.getboolean(u"administrative"):
218          result = True
219
220       # otherwise the command cannot be run by this actor
221       else:
222          result = False
223
224       # pass back the result
225       return result
226
227    def update_location(self):
228       u"""Make sure the location's contents contain this element."""
229       location = self.get(u"location")
230       if location in self.universe.contents:
231          self.universe.contents[location].contents[self.key] = self
232    def clean_contents(self):
233       u"""Make sure the element's contents aren't bogus."""
234       for element in self.contents.values():
235          if element.get(u"location") != self.key:
236             del self.contents[element.key]
237    def go_to(self, location):
238       u"""Relocate the element to a specific location."""
239       current = self.get(u"location")
240       if current and self.key in self.universe.contents[current].contents:
241          del universe.contents[current].contents[self.key]
242       if location in self.universe.contents: self.set(u"location", location)
243       self.universe.contents[location].contents[self.key] = self
244       self.look_at(location)
245    def go_home(self):
246       u"""Relocate the element to its default location."""
247       self.go_to(self.get(u"default_location"))
248       self.echo_to_location(
249          u"You suddenly realize that " + self.get(u"name") + u" is here."
250       )
251    def move_direction(self, direction):
252       u"""Relocate the element in a specified direction."""
253       self.echo_to_location(
254          self.get(
255             u"name"
256          ) + u" exits " + self.universe.categories[
257             u"internal"
258          ][
259             u"directions"
260          ].getdict(
261             direction
262          )[
263             u"exit"
264          ] + u"."
265       )
266       self.send(
267          u"You exit " + self.universe.categories[
268             u"internal"
269          ][
270             u"directions"
271          ].getdict(
272             direction
273          )[
274             u"exit"
275          ] + u".",
276          add_prompt=False
277       )
278       self.go_to(
279          self.universe.contents[self.get(u"location")].link_neighbor(direction)
280       )
281       self.echo_to_location(
282          self.get(
283             u"name"
284          ) + u" arrives from " + self.universe.categories[
285             u"internal"
286          ][
287             u"directions"
288          ].getdict(
289             direction
290          )[
291             u"enter"
292          ] + u"."
293       )
294    def look_at(self, key):
295       u"""Show an element to another element."""
296       if self.owner:
297          element = self.universe.contents[key]
298          message = u""
299          name = element.get(u"name")
300          if name: message += u"$(cyn)" + name + u"$(nrm)$(eol)"
301          description = element.get(u"description")
302          if description: message += description + u"$(eol)"
303          portal_list = element.portals().keys()
304          if portal_list:
305             portal_list.sort()
306             message += u"$(cyn)[ Exits: " + u", ".join(
307                portal_list
308             ) + u" ]$(nrm)$(eol)"
309          for element in self.universe.contents[
310             self.get(u"location")
311          ].contents.values():
312             if element.getboolean(u"is_actor") and element is not self:
313                message += u"$(yel)" + element.get(
314                   u"name"
315                ) + u" is here.$(nrm)$(eol)"
316             elif element is not self:
317                message += u"$(grn)" + element.get(
318                   u"impression"
319                ) + u"$(nrm)$(eol)"
320          self.send(message)
321    def portals(self):
322       u"""Map the portal directions for a room to neighbors."""
323       import re
324       portals = {}
325       if re.match(u"""^location:-?\d+,-?\d+,-?\d+$""", self.key):
326          coordinates = [(int(x)) for x in self.key.split(u":")[1].split(u",")]
327          directions = self.universe.categories[u"internal"][u"directions"]
328          offsets = dict(
329             [
330                (
331                   x, directions.getdict(x)[u"vector"]
332                ) for x in directions.facets()
333             ]
334          )
335          for portal in self.getlist(u"gridlinks"):
336             adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
337             neighbor = u"location:" + u",".join(
338                [(unicode(x)) for x in adjacent]
339             )
340             if neighbor in self.universe.contents: portals[portal] = neighbor
341       for facet in self.facets():
342          if facet.startswith(u"link_"):
343             neighbor = self.get(facet)
344             if neighbor in self.universe.contents:
345                portal = facet.split(u"_")[1]
346                portals[portal] = neighbor
347       return portals
348    def link_neighbor(self, direction):
349       u"""Return the element linked in a given direction."""
350       portals = self.portals()
351       if direction in portals: return portals[direction]
352    def echo_to_location(self, message):
353       u"""Show a message to other elements in the current location."""
354       for element in self.universe.contents[
355          self.get(u"location")
356       ].contents.values():
357          if element is not self: element.send(message)
358
359 class DataFile:
360    u"""A file containing universe elements."""
361    def __init__(self, filename, universe):
362       self.filename = filename
363       self.universe = universe
364       self.load()
365    def load(self):
366       u"""Read a file and create elements accordingly."""
367       import ConfigParser, os, os.path
368       self.data = ConfigParser.RawConfigParser()
369       self.modified = False
370       if os.access(self.filename, os.R_OK): self.data.read(self.filename)
371       if not hasattr(self.universe, u"files"): self.universe.files = {}
372       self.universe.files[self.filename] = self
373       includes = []
374       if self.data.has_option(u"__control__", u"include_files"):
375          for included in makelist(
376             self.data.get(u"__control__", u"include_files")
377          ):
378             included = find_file(
379                included,
380                relative=self.filename,
381                universe=self.universe
382             )
383             if included not in includes: includes.append(included)
384       if self.data.has_option(u"__control__", u"include_dirs"):
385          for included in [ os.path.join(x, u"__init__.mpy") for x in makelist(
386             self.data.get(u"__control__", u"include_dirs")
387          ) ]:
388             included = find_file(
389                included,
390                relative=self.filename,
391                universe=self.universe
392             )
393             if included not in includes: includes.append(included)
394       if self.data.has_option(u"__control__", u"default_files"):
395          origins = makedict(self.data.get(u"__control__", u"default_files"))
396          for key in origins.keys():
397             origins[key] = find_file(
398                origins[key],
399                relative=self.filename,
400                universe=self.universe
401             )
402             if origins[key] not in includes: includes.append(origins[key])
403             self.universe.default_origins[key] = origins[key]
404             if key not in self.universe.categories:
405                self.universe.categories[key] = {}
406       if self.data.has_option(u"__control__", u"private_files"):
407          for item in makelist(self.data.get(u"__control__", u"private_files")):
408             item = find_file(
409                item,
410                relative=self.filename,
411                universe=self.universe
412             )
413             if item not in includes: includes.append(item)
414             if item not in self.universe.private_files:
415                self.universe.private_files.append(item)
416       for section in self.data.sections():
417          if section != u"__control__":
418             Element(section, self.universe, self.filename)
419       for include_file in includes:
420          if not os.path.isabs(include_file):
421             include_file = find_file(
422                include_file,
423                relative=self.filename,
424                universe=self.universe
425             )
426          if include_file not in self.universe.files or not self.universe.files[
427             include_file
428          ].is_writeable():
429             DataFile(include_file, self.universe)
430    def save(self):
431       u"""Write the data, if necessary."""
432       import codecs, os, os.path, re, stat
433
434       # when modified, writeable and has content or the file exists
435       if self.modified and self.is_writeable() and (
436          self.data.sections() or os.path.exists(self.filename)
437       ):
438
439          # make parent directories if necessary
440          if not os.path.exists(os.path.dirname(self.filename)):
441             os.makedirs(os.path.dirname(self.filename))
442
443          # backup the file
444          if self.data.has_option(u"__control__", u"backup_count"):
445             max_count = self.data.has_option(u"__control__", u"backup_count")
446          else:
447             max_count = universe.categories[u"internal"][u"limits"].getint(
448                u"default_backup_count"
449             )
450          if os.path.exists(self.filename) and max_count:
451             backups = []
452             for candidate in os.listdir(os.path.dirname(self.filename)):
453                if re.match(
454                   os.path.basename(self.filename) + u"""\.\d+$""", candidate
455                ):
456                   backups.append(int(candidate.split(u".")[-1]))
457             backups.sort()
458             backups.reverse()
459             for old_backup in backups:
460                if old_backup >= max_count-1:
461                   os.remove(self.filename+u"."+unicode(old_backup))
462                elif not os.path.exists(
463                   self.filename+u"."+unicode(old_backup+1)
464                ):
465                   os.rename(
466                      self.filename + u"."+unicode(old_backup),
467                      self.filename + u"."+unicode( old_backup + 1 )
468                   )
469             if not os.path.exists(self.filename+u".0"):
470                os.rename( self.filename, self.filename + u".0" )
471
472          # our data file
473          file_descriptor = codecs.open(self.filename, u"w", u"utf-8")
474
475          # if it's marked private, chmod it appropriately
476          if self.filename in self.universe.private_files and oct(
477             stat.S_IMODE( os.stat(self.filename)[stat.ST_MODE] )
478          ) != 0600:
479             os.chmod(self.filename, 0600)
480
481          # write it back sorted, instead of using ConfigParser
482          sections = self.data.sections()
483          sections.sort()
484          for section in sections:
485             file_descriptor.write(u"[" + section + u"]\n")
486             options = self.data.options(section)
487             options.sort()
488             for option in options:
489                file_descriptor.write(
490                   option + u" = " + self.data.get(section, option) + u"\n"
491                )
492             file_descriptor.write(u"\n")
493
494          # flush and close the file
495          file_descriptor.flush()
496          file_descriptor.close()
497
498          # unset the modified flag
499          self.modified = False
500    def is_writeable(self):
501       u"""Returns True if the __control__ read_only is False."""
502       return not self.data.has_option(
503          u"__control__", u"read_only"
504       ) or not self.data.getboolean(
505          u"__control__", u"read_only"
506       )
507
508 class Universe:
509    u"""The universe."""
510
511    def __init__(self, filename=u"", load=False):
512       u"""Initialize the universe."""
513       import os, os.path
514       self.categories = {}
515       self.contents = {}
516       self.default_origins = {}
517       self.loglines = []
518       self.pending_events_long = {}
519       self.pending_events_short = {}
520       self.private_files = []
521       self.reload_flag = False
522       self.startdir = os.getcwd()
523       self.terminate_flag = False
524       self.userlist = []
525       if not filename:
526          possible_filenames = [
527             u".mudpyrc",
528             u".mudpy/mudpyrc",
529             u".mudpy/mudpy.conf",
530             u"mudpy.conf",
531             u"etc/mudpy.conf",
532             u"/usr/local/mudpy/mudpy.conf",
533             u"/usr/local/mudpy/etc/mudpy.conf",
534             u"/etc/mudpy/mudpy.conf",
535             u"/etc/mudpy.conf"
536             ]
537          for filename in possible_filenames:
538             if os.access(filename, os.R_OK): break
539       if not os.path.isabs(filename):
540          filename = os.path.join(self.startdir, filename)
541       self.filename = filename
542       if load: self.load()
543
544    def load(self):
545       u"""Load universe data from persistent storage."""
546
547       # the files dict must exist and filename needs to be read-only
548       if not hasattr(
549          self, u"files"
550       ) or not (
551          self.filename in self.files and self.files[
552             self.filename
553          ].is_writeable()
554       ):
555
556          # clear out all read-only files
557          if hasattr(self, u"files"):
558             for data_filename in self.files.keys():
559                if not self.files[data_filename].is_writeable():
560                   del self.files[data_filename]
561
562          # start loading from the initial file
563          DataFile(self.filename, self)
564
565       # make a list of inactive avatars
566       inactive_avatars = []
567       for account in self.categories[u"account"].values():
568          inactive_avatars += [
569             (self.contents[x]) for x in account.getlist(u"avatars")
570          ]
571       for user in self.userlist:
572          if user.avatar in inactive_avatars:
573             inactive_avatars.remove(user.avatar)
574
575       # go through all elements to clear out inactive avatar locations
576       for element in self.contents.values():
577          location = element.get(u"location")
578          if element in inactive_avatars and location:
579             if location in self.contents and element.key in self.contents[
580                location
581             ].contents:
582                del self.contents[location].contents[element.key]
583             element.set(u"default_location", location)
584             element.remove_facet(u"location")
585
586       # another pass to straighten out all the element contents
587       for element in self.contents.values():
588          element.update_location()
589          element.clean_contents()
590
591    def new(self):
592       u"""Create a new, empty Universe (the Big Bang)."""
593       new_universe = Universe()
594       for attribute in vars(self).keys():
595          exec(u"new_universe." + attribute + u" = self." + attribute)
596       new_universe.reload_flag = False
597       del self
598       return new_universe
599
600    def save(self):
601       u"""Save the universe to persistent storage."""
602       for key in self.files: self.files[key].save()
603
604    def initialize_server_socket(self):
605       u"""Create and open the listening socket."""
606       import socket
607
608       # need to know the local address and port number for the listener
609       host = self.categories[u"internal"][u"network"].get(u"host")
610       port = self.categories[u"internal"][u"network"].getint(u"port")
611
612       # if no host was specified, bind to all local addresses (preferring ipv6)
613       if not host:
614          if socket.has_ipv6: host = u"::"
615          else: host = u"0.0.0.0"
616
617       # figure out if this is ipv4 or v6
618       family = socket.getaddrinfo(host, port)[0][0]
619       if family is socket.AF_INET6 and not socket.has_ipv6:
620          log(u"No support for IPv6 address %s (use IPv4 instead)." % host)
621
622       # create a new stream-type socket object
623       self.listening_socket = socket.socket(family, socket.SOCK_STREAM)
624
625       # set the socket options to allow existing open ones to be
626       # reused (fixes a bug where the server can't bind for a minute
627       # when restarting on linux systems)
628       self.listening_socket.setsockopt(
629          socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
630       )
631
632       # bind the socket to to our desired server ipa and port
633       self.listening_socket.bind((host, port))
634
635       # disable blocking so we can proceed whether or not we can
636       # send/receive
637       self.listening_socket.setblocking(0)
638
639       # start listening on the socket
640       self.listening_socket.listen(1)
641
642       # note that we're now ready for user connections
643       log(
644          u"Listening for Telnet connections on: " + host + u":" + unicode(port)
645       )
646
647    def get_time(self):
648       u"""Convenience method to get the elapsed time counter."""
649       return self.categories[u"internal"][u"counters"].getint(u"elapsed")
650
651 class User:
652    u"""This is a connected user."""
653
654    def __init__(self):
655       u"""Default values for the in-memory user variables."""
656       import telnet
657       self.account = None
658       self.address = u""
659       self.authenticated = False
660       self.avatar = None
661       self.columns = 79
662       self.connection = None
663       self.error = u""
664       self.input_queue = []
665       self.last_address = u""
666       self.last_input = universe.get_time()
667       self.menu_choices = {}
668       self.menu_seen = False
669       self.negotiation_pause = 0
670       self.output_queue = []
671       self.partial_input = ""
672       self.password_tries = 0
673       self.state = u"initial"
674       self.telopts = {}
675
676    def quit(self):
677       u"""Log, close the connection and remove."""
678       if self.account: name = self.account.get(u"name")
679       else: name = u""
680       if name: message = u"User " + name
681       else: message = u"An unnamed user"
682       message += u" logged out."
683       log(message, 2)
684       self.deactivate_avatar()
685       self.connection.close()
686       self.remove()
687
688    def check_idle(self):
689       u"""Warn or disconnect idle users as appropriate."""
690       idletime = universe.get_time() - self.last_input
691       linkdead_dict = universe.categories[u"internal"][u"time"].getdict(
692          u"linkdead"
693       )
694       if self.state in linkdead_dict: linkdead_state = self.state
695       else: linkdead_state = u"default"
696       if idletime > linkdead_dict[linkdead_state]:
697          self.send(
698             u"$(eol)$(red)You've done nothing for far too long... goodbye!" \
699                + u"$(nrm)$(eol)",
700             flush=True,
701             add_prompt=False
702          )
703          logline = u"Disconnecting "
704          if self.account and self.account.get(u"name"):
705             logline += self.account.get(u"name")
706          else:
707             logline += u"an unknown user"
708          logline += u" after idling too long in the " + self.state + u" state."
709          log(logline, 2)
710          self.state = u"disconnecting"
711          self.menu_seen = False
712       idle_dict = universe.categories[u"internal"][u"time"].getdict(u"idle")
713       if self.state in idle_dict: idle_state = self.state
714       else: idle_state = u"default"
715       if idletime == idle_dict[idle_state]:
716          self.send(
717             u"$(eol)$(red)If you continue to be unproductive, " \
718                + u"you'll be shown the door...$(nrm)$(eol)"
719          )
720
721    def reload(self):
722       u"""Save, load a new user and relocate the connection."""
723
724       # get out of the list
725       self.remove()
726
727       # create a new user object
728       new_user = User()
729
730       # set everything equivalent
731       for attribute in vars(self).keys():
732          exec(u"new_user." + attribute + u" = self." + attribute)
733
734       # the avatar needs a new owner
735       if new_user.avatar: new_user.avatar.owner = new_user
736
737       # add it to the list
738       universe.userlist.append(new_user)
739
740       # get rid of the old user object
741       del(self)
742
743    def replace_old_connections(self):
744       u"""Disconnect active users with the same name."""
745
746       # the default return value
747       return_value = False
748
749       # iterate over each user in the list
750       for old_user in universe.userlist:
751
752          # the name is the same but it's not us
753          if hasattr(
754             old_user, u"account"
755          ) and old_user.account and old_user.account.get(
756             u"name"
757          ) == self.account.get(
758             u"name"
759          ) and old_user is not self:
760
761             # make a note of it
762             log(
763                u"User " + self.account.get(
764                   u"name"
765                ) + u" reconnected--closing old connection to " \
766                   + old_user.address + u".",
767                2
768             )
769             old_user.send(
770                u"$(eol)$(red)New connection from " + self.address \
771                   + u". Terminating old connection...$(nrm)$(eol)", \
772                flush=True,
773                add_prompt=False
774             )
775
776             # close the old connection
777             old_user.connection.close()
778
779             # replace the old connection with this one
780             old_user.send(
781                u"$(eol)$(red)Taking over old connection from " \
782                   + old_user.address + u".$(nrm)"
783             )
784             old_user.connection = self.connection
785             old_user.last_address = old_user.address
786             old_user.address = self.address
787
788             # take this one out of the list and delete
789             self.remove()
790             del(self)
791             return_value = True
792             break
793
794       # true if an old connection was replaced, false if not
795       return return_value
796
797    def authenticate(self):
798       u"""Flag the user as authenticated and disconnect duplicates."""
799       if not self.state is u"authenticated":
800          log(u"User " + self.account.get(u"name") + u" logged in.", 2)
801          self.authenticated = True
802          if self.account.subkey in universe.categories[
803             u"internal"
804          ][
805             u"limits"
806          ].getlist(
807             u"default_admins"
808          ):
809             self.account.set(u"administrator", u"True")
810
811    def show_menu(self):
812       u"""Send the user their current menu."""
813       if not self.menu_seen:
814          self.menu_choices = get_menu_choices(self)
815          self.send(
816             get_menu(self.state, self.error, self.menu_choices),
817             u"",
818             add_terminator=True
819          )
820          self.menu_seen = True
821          self.error = False
822          self.adjust_echoing()
823
824    def adjust_echoing(self):
825       u"""Adjust echoing to match state menu requirements."""
826       import telnet
827       if telnet.is_enabled(self, telnet.TELOPT_ECHO, telnet.US):
828          if menu_echo_on(self.state):
829             telnet.disable(self, telnet.TELOPT_ECHO, telnet.US)
830       elif not menu_echo_on(self.state):
831          telnet.enable(self, telnet.TELOPT_ECHO, telnet.US)
832
833    def remove(self):
834       u"""Remove a user from the list of connected users."""
835       universe.userlist.remove(self)
836
837    def send(
838       self,
839       output,
840       eol=u"$(eol)",
841       raw=False,
842       flush=False,
843       add_prompt=True,
844       just_prompt=False,
845       add_terminator=False,
846       prepend_padding=True
847    ):
848       u"""Send arbitrary text to a connected user."""
849       import telnet
850
851       # unless raw mode is on, clean it up all nice and pretty
852       if not raw:
853
854          # strip extra $(eol) off if present
855          while output.startswith(u"$(eol)"): output = output[6:]
856          while output.endswith(u"$(eol)"): output = output[:-6]
857          extra_lines = output.find(u"$(eol)$(eol)$(eol)")
858          while extra_lines > -1:
859             output = output[:extra_lines] + output[extra_lines+6:]
860             extra_lines = output.find(u"$(eol)$(eol)$(eol)")
861
862          # start with a newline, append the message, then end
863          # with the optional eol string passed to this function
864          # and the ansi escape to return to normal text
865          if not just_prompt and prepend_padding:
866             if not self.output_queue \
867                or not self.output_queue[-1].endswith("\r\n"):
868                output = u"$(eol)" + output
869             elif not self.output_queue[-1].endswith(
870                "\r\n\x1b[0m\r\n"
871             ) and not self.output_queue[-1].endswith(
872                "\r\n\r\n"
873             ):
874                output = u"$(eol)" + output
875          output += eol + unichr(27) + u"[0m"
876
877          # tack on a prompt if active
878          if self.state == u"active":
879             if not just_prompt: output += u"$(eol)"
880             if add_prompt:
881                output += u"> "
882                mode = self.avatar.get(u"mode")
883                if mode: output += u"(" + mode + u") "
884
885          # find and replace macros in the output
886          output = replace_macros(self, output)
887
888          # wrap the text at the client's width (min 40, 0 disables)
889          if self.columns:
890             if self.columns < 40: wrap = 40
891             else: wrap = self.columns
892             output = wrap_ansi_text(output, wrap)
893
894          # if supported by the client, encode it utf-8
895          if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.US):
896             encoded_output = output.encode(u"utf-8")
897
898          # otherwise just send ascii
899          else: encoded_output = output.encode(u"ascii", u"replace")
900
901          # end with a terminator if requested
902          if add_prompt or add_terminator:
903             if telnet.is_enabled(self, telnet.TELOPT_EOR, telnet.US):
904                encoded_output += telnet.telnet_proto(telnet.IAC, telnet.EOR)
905             elif not telnet.is_enabled(self, telnet.TELOPT_SGA, telnet.US):
906                encoded_output += telnet.telnet_proto(telnet.IAC, telnet.GA)
907
908          # and tack it onto the queue
909          self.output_queue.append(encoded_output)
910
911          # if this is urgent, flush all pending output
912          if flush: self.flush()
913
914       # just dump raw bytes as requested
915       else:
916          self.output_queue.append(output)
917          self.flush()
918
919    def pulse(self):
920       u"""All the things to do to the user per increment."""
921
922       # if the world is terminating, disconnect
923       if universe.terminate_flag:
924          self.state = u"disconnecting"
925          self.menu_seen = False
926
927       # check for an idle connection and act appropriately
928       else: self.check_idle()
929
930       # if output is paused, decrement the counter
931       if self.state == u"initial":
932          if self.negotiation_pause: self.negotiation_pause -= 1
933          else: self.state = u"entering_account_name"
934
935       # show the user a menu as needed
936       elif not self.state == u"active": self.show_menu()
937
938       # flush any pending output in the queue
939       self.flush()
940
941       # disconnect users with the appropriate state
942       if self.state == u"disconnecting": self.quit()
943
944       # check for input and add it to the queue
945       self.enqueue_input()
946
947       # there is input waiting in the queue
948       if self.input_queue:
949          handle_user_input(self)
950
951    def flush(self):
952       u"""Try to send the last item in the queue and remove it."""
953       if self.output_queue:
954          try:
955             self.connection.send(self.output_queue[0])
956             del self.output_queue[0]
957          except:
958             if self.account and self.account.get(u"name"):
959                account = self.account.get(u"name")
960             else: account = u"an unknown user"
961             log(
962                u"Sending to %s raised an exception (broken pipe?)." % account,
963                7
964             )
965             pass
966
967
968    def enqueue_input(self):
969       u"""Process and enqueue any new input."""
970       import telnet, unicodedata
971
972       # check for some input
973       try:
974          raw_input = self.connection.recv(1024)
975       except:
976          raw_input = ""
977
978       # we got something
979       if raw_input:
980
981          # tack this on to any previous partial
982          self.partial_input += raw_input
983
984          # reply to and remove any IAC negotiation codes
985          telnet.negotiate_telnet_options(self)
986
987          # separate multiple input lines
988          new_input_lines = self.partial_input.split("\n")
989
990          # if input doesn't end in a newline, replace the
991          # held partial input with the last line of it
992          if not self.partial_input.endswith("\n"):
993             self.partial_input = new_input_lines.pop()
994
995          # otherwise, chop off the extra null input and reset
996          # the held partial input
997          else:
998             new_input_lines.pop()
999             self.partial_input = ""
1000
1001          # iterate over the remaining lines
1002          for line in new_input_lines:
1003
1004             # strip off extra whitespace
1005             line = line.strip()
1006
1007             # make sure it's valid unicode (probably no longer needed)
1008             try: unicode(line, u"utf-8")
1009             except UnicodeDecodeError:
1010                logline = u"Non-unicode data from "
1011                if self.account and self.account.get(u"name"):
1012                   logline += self.account.get(u"name") + u": "
1013                else: logline += u"unknown user: "
1014                logline += repr(line)
1015                log(logline, 4)
1016                line = ""
1017
1018             # log non-printable characters remaining
1019             if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.HIM):
1020                asciiline = filter(lambda x: " " <= x <= "~", line)
1021                if line != asciiline:
1022                   logline = u"Non-ASCII characters from "
1023                   if self.account and self.account.get(u"name"):
1024                      logline += self.account.get(u"name") + u": "
1025                   else: logline += u"unknown user: "
1026                   logline += repr(line)
1027                   log(logline, 4)
1028                   line = asciiline
1029
1030             # put on the end of the queue
1031             self.input_queue.append(
1032                unicodedata.normalize( u"NFKC", unicode(line, u"utf-8") )
1033             )
1034
1035    def new_avatar(self):
1036       u"""Instantiate a new, unconfigured avatar for this user."""
1037       counter = 0
1038       while u"avatar:" + self.account.get(u"name") + u":" + unicode(
1039          counter
1040       ) in universe.categories[u"actor"].keys():
1041          counter += 1
1042       self.avatar = Element(
1043          u"actor:avatar:" + self.account.get(u"name") + u":" + unicode(
1044             counter
1045          ),
1046          universe
1047       )
1048       self.avatar.append(u"inherit", u"archetype:avatar")
1049       self.account.append(u"avatars", self.avatar.key)
1050
1051    def delete_avatar(self, avatar):
1052       u"""Remove an avatar from the world and from the user's list."""
1053       if self.avatar is universe.contents[avatar]: self.avatar = None
1054       universe.contents[avatar].destroy()
1055       avatars = self.account.getlist(u"avatars")
1056       avatars.remove(avatar)
1057       self.account.set(u"avatars", avatars)
1058
1059    def activate_avatar_by_index(self, index):
1060       u"""Enter the world with a particular indexed avatar."""
1061       self.avatar = universe.contents[self.account.getlist(u"avatars")[index]]
1062       self.avatar.owner = self
1063       self.state = u"active"
1064       self.avatar.go_home()
1065
1066    def deactivate_avatar(self):
1067       u"""Have the active avatar leave the world."""
1068       if self.avatar:
1069          current = self.avatar.get(u"location")
1070          if current:
1071             self.avatar.set(u"default_location", current)
1072             self.avatar.echo_to_location(
1073                u"You suddenly wonder where " + self.avatar.get(
1074                   u"name"
1075                ) + u" went."
1076             )
1077             del universe.contents[current].contents[self.avatar.key]
1078             self.avatar.remove_facet(u"location")
1079          self.avatar.owner = None
1080          self.avatar = None
1081
1082    def destroy(self):
1083       u"""Destroy the user and associated avatars."""
1084       for avatar in self.account.getlist(u"avatars"):
1085          self.delete_avatar(avatar)
1086       self.account.destroy()
1087
1088    def list_avatar_names(self):
1089       u"""List names of assigned avatars."""
1090       return [
1091          universe.contents[avatar].get(
1092             u"name"
1093          ) for avatar in self.account.getlist(u"avatars")
1094       ]
1095
1096 def makelist(value):
1097    u"""Turn string into list type."""
1098    if value[0] + value[-1] == u"[]": return eval(value)
1099    elif value[0] + value[-1] == u"\"\"": return [ value[1:-1] ]
1100    else: return [ value ]
1101
1102 def makedict(value):
1103    u"""Turn string into dict type."""
1104    if value[0] + value[-1] == u"{}": return eval(value)
1105    elif value.find(u":") > 0: return eval(u"{" + value + u"}")
1106    else: return { value: None }
1107
1108 def broadcast(message, add_prompt=True):
1109    u"""Send a message to all connected users."""
1110    for each_user in universe.userlist:
1111       each_user.send(u"$(eol)" + message, add_prompt=add_prompt)
1112
1113 def log(message, level=0):
1114    u"""Log a message."""
1115    import codecs, os.path, syslog, time
1116
1117    # a couple references we need
1118    file_name = universe.categories[u"internal"][u"logging"].get(u"file")
1119    max_log_lines = universe.categories[u"internal"][u"logging"].getint(
1120       u"max_log_lines"
1121    )
1122    syslog_name = universe.categories[u"internal"][u"logging"].get(u"syslog")
1123    timestamp = time.asctime()[4:19]
1124
1125    # turn the message into a list of lines
1126    lines = filter(
1127       lambda x: x!=u"", [ (x.rstrip()) for x in message.split(u"\n") ]
1128    )
1129
1130    # send the timestamp and line to a file
1131    if file_name:
1132       if not os.path.isabs(file_name):
1133          file_name = os.path.join(universe.startdir, file_name)
1134       file_descriptor = codecs.open(file_name, u"a", u"utf-8")
1135       for line in lines: file_descriptor.write(timestamp + u" " + line + u"\n")
1136       file_descriptor.flush()
1137       file_descriptor.close()
1138
1139    # send the timestamp and line to standard output
1140    if universe.categories[u"internal"][u"logging"].getboolean(u"stdout"):
1141       for line in lines: print(timestamp + u" " + line)
1142
1143    # send the line to the system log
1144    if syslog_name:
1145       syslog.openlog(
1146          syslog_name.encode("utf-8"),
1147          syslog.LOG_PID,
1148          syslog.LOG_INFO | syslog.LOG_DAEMON
1149       )
1150       for line in lines: syslog.syslog(line)
1151       syslog.closelog()
1152
1153    # display to connected administrators
1154    for user in universe.userlist:
1155       if user.state == u"active" and user.account.getboolean(
1156          u"administrator"
1157       ) and user.account.getint(u"loglevel") <= level:
1158          # iterate over every line in the message
1159          full_message = u""
1160          for line in lines:
1161             full_message += u"$(bld)$(red)" + timestamp + u" " + line.replace(
1162                u"$(", u"$_("
1163             ) + u"$(nrm)$(eol)"
1164          user.send(full_message, flush=True)
1165
1166    # add to the recent log list
1167    for line in lines:
1168       while 0 < len(universe.loglines) >= max_log_lines:
1169          del universe.loglines[0]
1170       universe.loglines.append((level, timestamp + u" " + line))
1171
1172 def get_loglines(level, start, stop):
1173    u"""Return a specific range of loglines filtered by level."""
1174
1175    # filter the log lines
1176    loglines = filter(lambda x: x[0]>=level, universe.loglines)
1177
1178    # we need these in several places
1179    total_count = unicode(len(universe.loglines))
1180    filtered_count = len(loglines)
1181
1182    # don't proceed if there are no lines
1183    if filtered_count:
1184
1185       # can't start before the begining or at the end
1186       if start > filtered_count: start = filtered_count
1187       if start < 1: start = 1
1188
1189       # can't stop before we start
1190       if stop > start: stop = start
1191       elif stop < 1: stop = 1
1192
1193       # some preamble
1194       message = u"There are " + unicode(total_count)
1195       message += u" log lines in memory and " + unicode(filtered_count)
1196       message += u" at or above level " + unicode(level) + u"."
1197       message += u" The matching lines from " + unicode(stop) + u" to "
1198       message += unicode(start) + u" are:$(eol)$(eol)"
1199
1200       # add the text from the selected lines
1201       if stop > 1: range_lines = loglines[-start:-(stop-1)]
1202       else: range_lines = loglines[-start:]
1203       for line in range_lines:
1204          message += u"   (" + unicode(line[0]) + u") " + line[1].replace(
1205             u"$(", u"$_("
1206          ) + u"$(eol)"
1207
1208    # there were no lines
1209    else:
1210       message = u"None of the " + unicode(total_count)
1211       message += u" lines in memory matches your request."
1212
1213    # pass it back
1214    return message
1215
1216 def glyph_columns(character):
1217    u"""Convenience function to return the column width of a glyph."""
1218    import unicodedata
1219    if unicodedata.east_asian_width(character) in u"FW": return 2
1220    else: return 1
1221
1222 def wrap_ansi_text(text, width):
1223    u"""Wrap text with arbitrary width while ignoring ANSI colors."""
1224    import unicodedata
1225
1226    # the current position in the entire text string, including all
1227    # characters, printable or otherwise
1228    absolute_position = 0
1229
1230    # the current text position relative to the begining of the line,
1231    # ignoring color escape sequences
1232    relative_position = 0
1233
1234    # the absolute position of the most recent whitespace character
1235    last_whitespace = 0
1236
1237    # whether the current character is part of a color escape sequence
1238    escape = False
1239
1240    # normalize any potentially composited unicode before we count it
1241    text = unicodedata.normalize(u"NFKC", text)
1242
1243    # iterate over each character from the begining of the text
1244    for each_character in text:
1245
1246       # the current character is the escape character
1247       if each_character == u"\x1b" and not escape:
1248          escape = True
1249
1250       # the current character is within an escape sequence
1251       elif escape:
1252
1253          # the current character is m, which terminates the
1254          # escape sequence
1255          if each_character == u"m":
1256             escape = False
1257
1258       # the current character is a newline, so reset the relative
1259       # position (start a new line)
1260       elif each_character == u"\n":
1261          relative_position = 0
1262          last_whitespace = absolute_position
1263
1264       # the current character meets the requested maximum line width,
1265       # so we need to backtrack and find a space at which to wrap;
1266       # special care is taken to avoid an off-by-one in case the
1267       # current character is a double-width glyph
1268       elif each_character != u"\r" and (
1269          relative_position >= width or (
1270             relative_position >= width -1 and glyph_columns(
1271                each_character
1272             ) == 2
1273          )
1274       ):
1275
1276          # it's always possible we landed on whitespace
1277          if unicodedata.category(each_character) in (u"Cc",u"Zs"):
1278             last_whitespace = absolute_position
1279
1280          # insert an eol in place of the space
1281          text = text[:last_whitespace] + u"\r\n" + text[last_whitespace + 1:]
1282
1283          # increase the absolute position because an eol is two
1284          # characters but the space it replaced was only one
1285          absolute_position += 1
1286
1287          # now we're at the begining of a new line, plus the
1288          # number of characters wrapped from the previous line
1289          relative_position = 0
1290          for remaining_characters in text[last_whitespace:absolute_position]:
1291             relative_position += glyph_columns(remaining_characters)
1292
1293       # as long as the character is not a carriage return and the
1294       # other above conditions haven't been met, count it as a
1295       # printable character
1296       elif each_character != u"\r":
1297          relative_position += glyph_columns(each_character)
1298          if unicodedata.category(each_character) in (u"Cc",u"Zs"):
1299             last_whitespace = absolute_position
1300
1301       # increase the absolute position for every character
1302       absolute_position += 1
1303
1304    # return the newly-wrapped text
1305    return text
1306
1307 def weighted_choice(data):
1308    u"""Takes a dict weighted by value and returns a random key."""
1309    import random
1310
1311    # this will hold our expanded list of keys from the data
1312    expanded = []
1313
1314    # create the expanded list of keys
1315    for key in data.keys():
1316       for count in range(data[key]):
1317          expanded.append(key)
1318
1319    # return one at random
1320    return random.choice(expanded)
1321
1322 def random_name():
1323    u"""Returns a random character name."""
1324    import random
1325
1326    # the vowels and consonants needed to create romaji syllables
1327    vowels = [
1328       u"a",
1329       u"i",
1330       u"u",
1331       u"e",
1332       u"o"
1333    ]
1334    consonants = [
1335       u"'",
1336       u"k",
1337       u"z",
1338       u"s",
1339       u"sh",
1340       u"z",
1341       u"j",
1342       u"t",
1343       u"ch",
1344       u"ts",
1345       u"d",
1346       u"n",
1347       u"h",
1348       u"f",
1349       u"m",
1350       u"y",
1351       u"r",
1352       u"w"
1353    ]
1354
1355    # this dict will hold our weighted list of syllables
1356    syllables = {}
1357
1358    # generate the list with an even weighting
1359    for consonant in consonants:
1360       for vowel in vowels:
1361          syllables[consonant + vowel] = 1
1362
1363    # we'll build the name into this string
1364    name = u""
1365
1366    # create a name of random length from the syllables
1367    for syllable in range(random.randrange(2, 6)):
1368       name += weighted_choice(syllables)
1369
1370    # strip any leading quotemark, capitalize and return the name
1371    return name.strip(u"'").capitalize()
1372
1373 def replace_macros(user, text, is_input=False):
1374    u"""Replaces macros in text output."""
1375    import codecs, os.path
1376
1377    # third person pronouns
1378    pronouns = {
1379       u"female": { u"obj": u"her", u"pos": u"hers", u"sub": u"she" },
1380       u"male": { u"obj": u"him", u"pos": u"his", u"sub": u"he" },
1381       u"neuter": { u"obj": u"it", u"pos": u"its", u"sub": u"it" }
1382       }
1383
1384    # a dict of replacement macros
1385    macros = {
1386       u"eol": u"\r\n",
1387       u"bld": unichr(27) + u"[1m",
1388       u"nrm": unichr(27) + u"[0m",
1389       u"blk": unichr(27) + u"[30m",
1390       u"blu": unichr(27) + u"[34m",
1391       u"cyn": unichr(27) + u"[36m",
1392       u"grn": unichr(27) + u"[32m",
1393       u"mgt": unichr(27) + u"[35m",
1394       u"red": unichr(27) + u"[31m",
1395       u"yel": unichr(27) + u"[33m",
1396       }
1397
1398    # add dynamic macros where possible
1399    if user.account:
1400       account_name = user.account.get(u"name")
1401       if account_name:
1402          macros[u"account"] = account_name
1403    if user.avatar:
1404       avatar_gender = user.avatar.get(u"gender")
1405       if avatar_gender:
1406          macros[u"tpop"] = pronouns[avatar_gender][u"obj"]
1407          macros[u"tppp"] = pronouns[avatar_gender][u"pos"]
1408          macros[u"tpsp"] = pronouns[avatar_gender][u"sub"]
1409
1410    # loop until broken
1411    while True:
1412
1413       # find and replace per the macros dict
1414       macro_start = text.find(u"$(")
1415       if macro_start == -1: break
1416       macro_end = text.find(u")", macro_start) + 1
1417       macro = text[macro_start+2:macro_end-1]
1418       if macro in macros.keys():
1419          replacement = macros[macro]
1420
1421       # this is how we handle local file inclusion (dangerous!)
1422       elif macro.startswith(u"inc:"):
1423          incfile = find_file(macro[4:], universe=universe)
1424          if os.path.exists(incfile):
1425             incfd = codecs.open(incfile, u"r", u"utf-8")
1426             replacement = u""
1427             for line in incfd:
1428                if line.endswith(u"\n") and not line.endswith(u"\r\n"):
1429                   line = line.replace(u"\n", u"\r\n")
1430                replacement += line
1431             # lose the trailing eol
1432             replacement = replacement[:-2]
1433          else:
1434             replacement = u""
1435             log(u"Couldn't read included " + incfile + u" file.", 6)
1436
1437       # if we get here, log and replace it with null
1438       else:
1439          replacement = u""
1440          if not is_input:
1441             log(u"Unexpected replacement macro " + macro + u" encountered.", 6)
1442
1443       # and now we act on the replacement
1444       text = text.replace(u"$(" + macro + u")", replacement)
1445
1446    # replace the look-like-a-macro sequence
1447    text = text.replace(u"$_(", u"$(")
1448
1449    return text
1450
1451 def escape_macros(text):
1452    u"""Escapes replacement macros in text."""
1453    return text.replace(u"$(", u"$_(")
1454
1455 def first_word(text, separator=u" "):
1456    u"""Returns a tuple of the first word and the rest."""
1457    if text:
1458       if text.find(separator) > 0: return text.split(separator, 1)
1459       else: return text, u""
1460    else: return u"", u""
1461
1462 def on_pulse():
1463    u"""The things which should happen on each pulse, aside from reloads."""
1464    import time
1465
1466    # open the listening socket if it hasn't been already
1467    if not hasattr(universe, u"listening_socket"):
1468       universe.initialize_server_socket()
1469
1470    # assign a user if a new connection is waiting
1471    user = check_for_connection(universe.listening_socket)
1472    if user: universe.userlist.append(user)
1473
1474    # iterate over the connected users
1475    for user in universe.userlist: user.pulse()
1476
1477    # add an element for counters if it doesn't exist
1478    if not u"counters" in universe.categories[u"internal"]:
1479       universe.categories[u"internal"][u"counters"] = Element(
1480          u"internal:counters", universe
1481       )
1482
1483    # update the log every now and then
1484    if not universe.categories[u"internal"][u"counters"].getint(u"mark"):
1485       log(unicode(len(universe.userlist)) + u" connection(s)")
1486       universe.categories[u"internal"][u"counters"].set(
1487          u"mark", universe.categories[u"internal"][u"time"].getint(
1488             u"frequency_log"
1489          )
1490       )
1491    else:
1492       universe.categories[u"internal"][u"counters"].set(
1493          u"mark", universe.categories[u"internal"][u"counters"].getint(
1494             u"mark"
1495          ) - 1
1496       )
1497
1498    # periodically save everything
1499    if not universe.categories[u"internal"][u"counters"].getint(u"save"):
1500       universe.save()
1501       universe.categories[u"internal"][u"counters"].set(
1502          u"save", universe.categories[u"internal"][u"time"].getint(
1503             u"frequency_save"
1504          )
1505       )
1506    else:
1507       universe.categories[u"internal"][u"counters"].set(
1508          u"save", universe.categories[u"internal"][u"counters"].getint(
1509             u"save"
1510          ) - 1
1511       )
1512
1513    # pause for a configurable amount of time (decimal seconds)
1514    time.sleep(universe.categories[u"internal"][u"time"].getfloat(u"increment"))
1515
1516    # increase the elapsed increment counter
1517    universe.categories[u"internal"][u"counters"].set(
1518       u"elapsed", universe.categories[u"internal"][u"counters"].getint(
1519          u"elapsed"
1520       ) + 1
1521    )
1522
1523 def reload_data():
1524    u"""Reload all relevant objects."""
1525    for user in universe.userlist[:]: user.reload()
1526    for element in universe.contents.values():
1527       if element.origin.is_writeable(): element.reload()
1528    universe.load()
1529
1530 def check_for_connection(listening_socket):
1531    u"""Check for a waiting connection and return a new user object."""
1532    import telnet
1533
1534    # try to accept a new connection
1535    try:
1536       connection, address = listening_socket.accept()
1537    except:
1538       return None
1539
1540    # note that we got one
1541    log(u"Connection from " + address[0], 2)
1542
1543    # disable blocking so we can proceed whether or not we can send/receive
1544    connection.setblocking(0)
1545
1546    # create a new user object
1547    user = User()
1548
1549    # associate this connection with it
1550    user.connection = connection
1551
1552    # set the user's ipa from the connection's ipa
1553    user.address = address[0]
1554
1555    # let the client know we WILL EOR (RFC 885)
1556    telnet.enable(user, telnet.TELOPT_EOR, telnet.US)
1557    user.negotiation_pause = 2
1558
1559    # return the new user object
1560    return user
1561
1562 def get_menu(state, error=None, choices=None):
1563    u"""Show the correct menu text to a user."""
1564
1565    # make sure we don't reuse a mutable sequence by default
1566    if choices is None: choices = {}
1567
1568    # get the description or error text
1569    message = get_menu_description(state, error)
1570
1571    # get menu choices for the current state
1572    message += get_formatted_menu_choices(state, choices)
1573
1574    # try to get a prompt, if it was defined
1575    message += get_menu_prompt(state)
1576
1577    # throw in the default choice, if it exists
1578    message += get_formatted_default_menu_choice(state)
1579
1580    # display a message indicating if echo is off
1581    message += get_echo_message(state)
1582
1583    # return the assembly of various strings defined above
1584    return message
1585
1586 def menu_echo_on(state):
1587    u"""True if echo is on, false if it is off."""
1588    return universe.categories[u"menu"][state].getboolean(u"echo", True)
1589
1590 def get_echo_message(state):
1591    u"""Return a message indicating that echo is off."""
1592    if menu_echo_on(state): return u""
1593    else: return u"(won't echo) "
1594
1595 def get_default_menu_choice(state):
1596    u"""Return the default choice for a menu."""
1597    return universe.categories[u"menu"][state].get(u"default")
1598
1599 def get_formatted_default_menu_choice(state):
1600    u"""Default menu choice foratted for inclusion in a prompt string."""
1601    default_choice = get_default_menu_choice(state)
1602    if default_choice: return u"[$(red)" + default_choice + u"$(nrm)] "
1603    else: return u""
1604
1605 def get_menu_description(state, error):
1606    u"""Get the description or error text."""
1607
1608    # an error condition was raised by the handler
1609    if error:
1610
1611       # try to get an error message matching the condition
1612       # and current state
1613       description = universe.categories[u"menu"][state].get(u"error_" + error)
1614       if not description: description = u"That is not a valid choice..."
1615       description = u"$(red)" + description + u"$(nrm)"
1616
1617    # there was no error condition
1618    else:
1619
1620       # try to get a menu description for the current state
1621       description = universe.categories[u"menu"][state].get(u"description")
1622
1623    # return the description or error message
1624    if description: description += u"$(eol)$(eol)"
1625    return description
1626
1627 def get_menu_prompt(state):
1628    u"""Try to get a prompt, if it was defined."""
1629    prompt = universe.categories[u"menu"][state].get(u"prompt")
1630    if prompt: prompt += u" "
1631    return prompt
1632
1633 def get_menu_choices(user):
1634    u"""Return a dict of choice:meaning."""
1635    menu = universe.categories[u"menu"][user.state]
1636    create_choices = menu.get(u"create")
1637    if create_choices: choices = eval(create_choices)
1638    else: choices = {}
1639    ignores = []
1640    options = {}
1641    creates = {}
1642    for facet in menu.facets():
1643       if facet.startswith(u"demand_") and not eval(
1644          universe.categories[u"menu"][user.state].get(facet)
1645       ):
1646          ignores.append(facet.split(u"_", 2)[1])
1647       elif facet.startswith(u"create_"):
1648          creates[facet] = facet.split(u"_", 2)[1]
1649       elif facet.startswith(u"choice_"):
1650          options[facet] = facet.split(u"_", 2)[1]
1651    for facet in creates.keys():
1652       if not creates[facet] in ignores:
1653          choices[creates[facet]] = eval(menu.get(facet))
1654    for facet in options.keys():
1655       if not options[facet] in ignores:
1656          choices[options[facet]] = menu.get(facet)
1657    return choices
1658
1659 def get_formatted_menu_choices(state, choices):
1660    u"""Returns a formatted string of menu choices."""
1661    choice_output = u""
1662    choice_keys = choices.keys()
1663    choice_keys.sort()
1664    for choice in choice_keys:
1665       choice_output += u"   [$(red)" + choice + u"$(nrm)]  " + choices[
1666          choice
1667       ] + u"$(eol)"
1668    if choice_output: choice_output += u"$(eol)"
1669    return choice_output
1670
1671 def get_menu_branches(state):
1672    u"""Return a dict of choice:branch."""
1673    branches = {}
1674    for facet in universe.categories[u"menu"][state].facets():
1675       if facet.startswith(u"branch_"):
1676          branches[
1677             facet.split(u"_", 2)[1]
1678          ] = universe.categories[u"menu"][state].get(facet)
1679    return branches
1680
1681 def get_default_branch(state):
1682    u"""Return the default branch."""
1683    return universe.categories[u"menu"][state].get(u"branch")
1684
1685 def get_choice_branch(user, choice):
1686    u"""Returns the new state matching the given choice."""
1687    branches = get_menu_branches(user.state)
1688    if choice in branches.keys(): return branches[choice]
1689    elif choice in user.menu_choices.keys():
1690       return get_default_branch(user.state)
1691    else: return u""
1692
1693 def get_menu_actions(state):
1694    u"""Return a dict of choice:branch."""
1695    actions = {}
1696    for facet in universe.categories[u"menu"][state].facets():
1697       if facet.startswith(u"action_"):
1698          actions[
1699             facet.split(u"_", 2)[1]
1700          ] = universe.categories[u"menu"][state].get(facet)
1701    return actions
1702
1703 def get_default_action(state):
1704    u"""Return the default action."""
1705    return universe.categories[u"menu"][state].get(u"action")
1706
1707 def get_choice_action(user, choice):
1708    u"""Run any indicated script for the given choice."""
1709    actions = get_menu_actions(user.state)
1710    if choice in actions.keys(): return actions[choice]
1711    elif choice in user.menu_choices.keys():
1712       return get_default_action(user.state)
1713    else: return u""
1714
1715 def handle_user_input(user):
1716    u"""The main handler, branches to a state-specific handler."""
1717    import telnet
1718
1719    # if the user's client echo is off, send a blank line for aesthetics
1720    if telnet.is_enabled(user, telnet.TELOPT_ECHO, telnet.US):
1721       user.send(u"", add_prompt=False, prepend_padding=False)
1722
1723    # check to make sure the state is expected, then call that handler
1724    if u"handler_" + user.state in globals():
1725       exec(u"handler_" + user.state + u"(user)")
1726    else:
1727       generic_menu_handler(user)
1728
1729    # since we got input, flag that the menu/prompt needs to be redisplayed
1730    user.menu_seen = False
1731
1732    # update the last_input timestamp while we're at it
1733    user.last_input = universe.get_time()
1734
1735 def generic_menu_handler(user):
1736    u"""A generic menu choice handler."""
1737
1738    # get a lower-case representation of the next line of input
1739    if user.input_queue:
1740       choice = user.input_queue.pop(0)
1741       if choice: choice = choice.lower()
1742    else: choice = u""
1743    if not choice: choice = get_default_menu_choice(user.state)
1744    if choice in user.menu_choices:
1745       exec(get_choice_action(user, choice))
1746       new_state = get_choice_branch(user, choice)
1747       if new_state: user.state = new_state
1748    else: user.error = u"default"
1749
1750 def handler_entering_account_name(user):
1751    u"""Handle the login account name."""
1752
1753    # get the next waiting line of input
1754    input_data = user.input_queue.pop(0)
1755
1756    # did the user enter anything?
1757    if input_data:
1758       
1759       # keep only the first word and convert to lower-case
1760       name = input_data.lower()
1761
1762       # fail if there are non-alphanumeric characters
1763       if name != filter(
1764          lambda x: x>=u"0" and x<=u"9" or x>=u"a" and x<=u"z", name
1765       ):
1766          user.error = u"bad_name"
1767
1768       # if that account exists, time to request a password
1769       elif name in universe.categories[u"account"]:
1770          user.account = universe.categories[u"account"][name]
1771          user.state = u"checking_password"
1772
1773       # otherwise, this could be a brand new user
1774       else:
1775          user.account = Element(u"account:" + name, universe)
1776          user.account.set(u"name", name)
1777          log(u"New user: " + name, 2)
1778          user.state = u"checking_new_account_name"
1779
1780    # if the user entered nothing for a name, then buhbye
1781    else:
1782       user.state = u"disconnecting"
1783
1784 def handler_checking_password(user):
1785    u"""Handle the login account password."""
1786    import md5
1787
1788    # get the next waiting line of input
1789    input_data = user.input_queue.pop(0)
1790
1791    # does the hashed input equal the stored hash?
1792    if unicode(
1793       md5.new(
1794          ( user.account.get(u"name") + input_data ).encode(u"utf-8")
1795       ).hexdigest()
1796    ) == user.account.get(u"passhash"):
1797
1798       # if so, set the username and load from cold storage
1799       if not user.replace_old_connections():
1800          user.authenticate()
1801          user.state = u"main_utility"
1802
1803    # if at first your hashes don't match, try, try again
1804    elif user.password_tries < universe.categories[
1805       u"internal"
1806    ][
1807       u"limits"
1808    ].getint(
1809       u"password_tries"
1810    ) - 1:
1811       user.password_tries += 1
1812       user.error = u"incorrect"
1813
1814    # we've exceeded the maximum number of password failures, so disconnect
1815    else:
1816       user.send(
1817          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1818       )
1819       user.state = u"disconnecting"
1820
1821 def handler_entering_new_password(user):
1822    u"""Handle a new password entry."""
1823    import md5
1824
1825    # get the next waiting line of input
1826    input_data = user.input_queue.pop(0)
1827
1828    # make sure the password is strong--at least one upper, one lower and
1829    # one digit, seven or more characters in length
1830    if len(input_data) > 6 and len(
1831       filter( lambda x: x>=u"0" and x<=u"9", input_data )
1832    ) and len(
1833       filter( lambda x: x>=u"A" and x<=u"Z", input_data )
1834    ) and len(
1835       filter( lambda x: x>=u"a" and x<=u"z", input_data )
1836    ):
1837
1838       # hash and store it, then move on to verification
1839       user.account.set(
1840          u"passhash",
1841          unicode(
1842             md5.new(
1843                ( user.account.get(u"name") + input_data ).encode(u"utf-8")
1844             ).hexdigest()
1845          )
1846       )
1847       user.state = u"verifying_new_password"
1848
1849    # the password was weak, try again if you haven't tried too many times
1850    elif user.password_tries < universe.categories[
1851       u"internal"
1852    ][
1853       u"limits"
1854    ].getint(
1855       u"password_tries"
1856    ) - 1:
1857       user.password_tries += 1
1858       user.error = u"weak"
1859
1860    # too many tries, so adios
1861    else:
1862       user.send(
1863          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1864       )
1865       user.account.destroy()
1866       user.state = u"disconnecting"
1867
1868 def handler_verifying_new_password(user):
1869    u"""Handle the re-entered new password for verification."""
1870    import md5
1871
1872    # get the next waiting line of input
1873    input_data = user.input_queue.pop(0)
1874
1875    # hash the input and match it to storage
1876    if unicode(
1877       md5.new(
1878          ( user.account.get(u"name") + input_data ).encode(u"utf-8")
1879       ).hexdigest()
1880    ) == user.account.get(u"passhash"):
1881       user.authenticate()
1882
1883       # the hashes matched, so go active
1884       if not user.replace_old_connections(): user.state = u"main_utility"
1885
1886    # go back to entering the new password as long as you haven't tried
1887    # too many times
1888    elif user.password_tries < universe.categories[
1889       u"internal"
1890    ][
1891       u"limits"
1892    ].getint(
1893       u"password_tries"
1894    ) - 1:
1895       user.password_tries += 1
1896       user.error = u"differs"
1897       user.state = u"entering_new_password"
1898
1899    # otherwise, sayonara
1900    else:
1901       user.send(
1902          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1903       )
1904       user.account.destroy()
1905       user.state = u"disconnecting"
1906
1907 def handler_active(user):
1908    u"""Handle input for active users."""
1909
1910    # get the next waiting line of input
1911    input_data = user.input_queue.pop(0)
1912
1913    # is there input?
1914    if input_data:
1915
1916       # split out the command and parameters
1917       actor = user.avatar
1918       mode = actor.get(u"mode")
1919       if mode and input_data.startswith(u"!"):
1920          command_name, parameters = first_word(input_data[1:])
1921       elif mode == u"chat":
1922          command_name = u"say"
1923          parameters = input_data
1924       else:
1925          command_name, parameters = first_word(input_data)
1926
1927       # lowercase the command
1928       command_name = command_name.lower()
1929
1930       # the command matches a command word for which we have data
1931       if command_name in universe.categories[u"command"]:
1932          command = universe.categories[u"command"][command_name]
1933       else: command = None
1934
1935       # if it's allowed, do it
1936       if actor.can_run(command): exec(command.get(u"action"))
1937
1938       # otherwise, give an error
1939       elif command_name: command_error(actor, input_data)
1940
1941    # if no input, just idle back with a prompt
1942    else: user.send(u"", just_prompt=True)
1943    
1944 def command_halt(actor, parameters):
1945    u"""Halt the world."""
1946    if actor.owner:
1947
1948       # see if there's a message or use a generic one
1949       if parameters: message = u"Halting: " + parameters
1950       else:
1951          message = u"User " + actor.owner.account.get(
1952             u"name"
1953          ) + u" halted the world."
1954
1955       # let everyone know
1956       broadcast(message, add_prompt=False)
1957       log(message, 8)
1958
1959       # set a flag to terminate the world
1960       universe.terminate_flag = True
1961
1962 def command_reload(actor):
1963    u"""Reload all code modules, configs and data."""
1964    if actor.owner:
1965
1966       # let the user know and log
1967       actor.send(u"Reloading all code modules, configs and data.")
1968       log(
1969          u"User " + actor.owner.account.get(u"name") + u" reloaded the world.",
1970          8
1971       )
1972
1973       # set a flag to reload
1974       universe.reload_flag = True
1975
1976 def command_quit(actor):
1977    u"""Leave the world and go back to the main menu."""
1978    if actor.owner:
1979       actor.owner.state = u"main_utility"
1980       actor.owner.deactivate_avatar()
1981
1982 def command_help(actor, parameters):
1983    u"""List available commands and provide help for commands."""
1984
1985    # did the user ask for help on a specific command word?
1986    if parameters and actor.owner:
1987
1988       # is the command word one for which we have data?
1989       if parameters in universe.categories[u"command"]:
1990          command = universe.categories[u"command"][parameters]
1991       else: command = None
1992
1993       # only for allowed commands
1994       if actor.can_run(command):
1995
1996          # add a description if provided
1997          description = command.get(u"description")
1998          if not description:
1999             description = u"(no short description provided)"
2000          if command.getboolean(u"administrative"): output = u"$(red)"
2001          else: output = u"$(grn)"
2002          output += parameters + u"$(nrm) - " + description + u"$(eol)$(eol)"
2003
2004          # add the help text if provided
2005          help_text = command.get(u"help")
2006          if not help_text:
2007             help_text = u"No help is provided for this command."
2008          output += help_text
2009
2010          # list related commands
2011          see_also = command.getlist(u"see_also")
2012          if see_also:
2013             really_see_also = u""
2014             for item in see_also:
2015                if item in universe.categories[u"command"]:
2016                   command = universe.categories[u"command"][item]
2017                   if actor.can_run(command):
2018                      if really_see_also:
2019                         really_see_also += u", "
2020                      if command.getboolean(u"administrative"):
2021                         really_see_also += u"$(red)"
2022                      else:
2023                         really_see_also += u"$(grn)"
2024                      really_see_also += item + u"$(nrm)"
2025             if really_see_also:
2026                output += u"$(eol)$(eol)See also: " + really_see_also
2027
2028       # no data for the requested command word
2029       else:
2030          output = u"That is not an available command."
2031
2032    # no specific command word was indicated
2033    else:
2034
2035       # give a sorted list of commands with descriptions if provided
2036       output = u"These are the commands available to you:$(eol)$(eol)"
2037       sorted_commands = universe.categories[u"command"].keys()
2038       sorted_commands.sort()
2039       for item in sorted_commands:
2040          command = universe.categories[u"command"][item]
2041          if actor.can_run(command):
2042             description = command.get(u"description")
2043             if not description:
2044                description = u"(no short description provided)"
2045             if command.getboolean(u"administrative"): output += u"   $(red)"
2046             else: output += u"   $(grn)"
2047             output += item + u"$(nrm) - " + description + u"$(eol)"
2048       output += u"$(eol)Enter \"help COMMAND\" for help on a command " \
2049          + u"named \"COMMAND\"."
2050
2051    # send the accumulated output to the user
2052    actor.send(output)
2053
2054 def command_move(actor, parameters):
2055    u"""Move the avatar in a given direction."""
2056    if parameters in universe.contents[actor.get(u"location")].portals():
2057       actor.move_direction(parameters)
2058    else: actor.send(u"You cannot go that way.")
2059
2060 def command_look(actor, parameters):
2061    u"""Look around."""
2062    if parameters: actor.send(u"You can't look at or in anything yet.")
2063    else: actor.look_at(actor.get(u"location"))
2064
2065 def command_say(actor, parameters):
2066    u"""Speak to others in the same room."""
2067    import unicodedata
2068
2069    # check for replacement macros and escape them
2070    parameters = escape_macros(parameters)
2071
2072    # if the message is wrapped in quotes, remove them and leave contents intact
2073    if parameters.startswith(u"\"") and parameters.endswith(u"\""):
2074       message = parameters[1:-1]
2075       literal = True
2076
2077    # otherwise, get rid of stray quote marks on the ends of the message
2078    else:
2079       message = parameters.strip(u"\"'`")
2080       literal = False
2081
2082    # the user entered a message
2083    if message:
2084
2085       # match the punctuation used, if any, to an action
2086       actions = universe.categories[u"internal"][u"language"].getdict(
2087          u"actions"
2088       )
2089       default_punctuation = universe.categories[u"internal"][u"language"].get(
2090          u"default_punctuation"
2091       )
2092       action = u""
2093       for mark in actions.keys():
2094          if not literal and message.endswith(mark):
2095             action = actions[mark]
2096             break
2097
2098       # add punctuation if needed
2099       if not action:
2100          action = actions[default_punctuation]
2101          if message and not (
2102             literal or unicodedata.category(message[-1]) == u"Po"
2103          ):
2104             message += default_punctuation
2105
2106       # failsafe checks to avoid unwanted reformatting and null strings
2107       if message and not literal:
2108
2109          # decapitalize the first letter to improve matching
2110          message = message[0].lower() + message[1:]
2111
2112          # iterate over all words in message, replacing typos
2113          typos = universe.categories[u"internal"][u"language"].getdict(
2114             u"typos"
2115          )
2116          words = message.split()
2117          for index in range(len(words)):
2118             word = words[index]
2119             while unicodedata.category(word[0]) == u"Po":
2120                word = word[1:]
2121             while unicodedata.category(word[-1]) == u"Po":
2122                word = word[:-1]
2123             if word in typos.keys():
2124                words[index] = words[index].replace(word, typos[word])
2125          message = u" ".join(words)
2126
2127          # capitalize the first letter
2128          message = message[0].upper() + message[1:]
2129
2130    # tell the room
2131    if message:
2132       actor.echo_to_location(
2133          actor.get(u"name") + u" " + action + u"s, \"" + message + u"\""
2134       )
2135       actor.send(u"You " + action + u", \"" + message + u"\"")
2136
2137    # there was no message
2138    else:
2139       actor.send(u"What do you want to say?")
2140
2141 def command_chat(actor):
2142    u"""Toggle chat mode."""
2143    mode = actor.get(u"mode")
2144    if not mode:
2145       actor.set(u"mode", u"chat")
2146       actor.send(u"Entering chat mode (use $(grn)!chat$(nrm) to exit).")
2147    elif mode == u"chat":
2148       actor.remove_facet(u"mode")
2149       actor.send(u"Exiting chat mode.")
2150    else: actor.send(u"Sorry, but you're already busy with something else!")
2151
2152 def command_show(actor, parameters):
2153    u"""Show program data."""
2154    import re
2155    message = u""
2156    arguments = parameters.split()
2157    if not parameters: message = u"What do you want to show?"
2158    elif arguments[0] == u"time":
2159       message = universe.categories[u"internal"][u"counters"].get(
2160          u"elapsed"
2161       ) + u" increments elapsed since the world was created."
2162    elif arguments[0] == u"categories":
2163       message = u"These are the element categories:$(eol)"
2164       categories = universe.categories.keys()
2165       categories.sort()
2166       for category in categories:
2167          message += u"$(eol)   $(grn)" + category + u"$(nrm)"
2168    elif arguments[0] == u"files":
2169       message = u"These are the current files containing the universe:$(eol)"
2170       filenames = universe.files.keys()
2171       filenames.sort()
2172       for filename in filenames:
2173          if universe.files[filename].is_writeable(): status = u"rw"
2174          else: status = u"ro"
2175          message += u"$(eol)   $(red)(" + status + u") $(grn)" + filename \
2176             + u"$(nrm)"
2177    elif arguments[0] == u"category":
2178       if len(arguments) != 2: message = u"You must specify one category."
2179       elif arguments[1] in universe.categories:
2180          message = u"These are the elements in the \"" + arguments[1] \
2181             + u"\" category:$(eol)"
2182          elements = [
2183             (
2184                universe.categories[arguments[1]][x].key
2185             ) for x in universe.categories[arguments[1]].keys()
2186          ]
2187          elements.sort()
2188          for element in elements:
2189             message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2190       else: message = u"Category \"" + arguments[1] + u"\" does not exist."
2191    elif arguments[0] == u"file":
2192       if len(arguments) != 2: message = u"You must specify one file."
2193       elif arguments[1] in universe.files:
2194          message = u"These are the elements in the \"" + arguments[1] \
2195             + u"\" file:$(eol)"
2196          elements = universe.files[arguments[1]].data.sections()
2197          elements.sort()
2198          for element in elements:
2199             message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2200       else: message = u"Category \"" + arguments[1] + u"\" does not exist."
2201    elif arguments[0] == u"element":
2202       if len(arguments) != 2: message = u"You must specify one element."
2203       elif arguments[1] in universe.contents:
2204          element = universe.contents[arguments[1]]
2205          message = u"These are the properties of the \"" + arguments[1] \
2206             + u"\" element (in \"" + element.origin.filename + u"\"):$(eol)"
2207          facets = element.facets()
2208          facets.sort()
2209          for facet in facets:
2210             message += u"$(eol)   $(grn)" + facet + u": $(red)" \
2211                + escape_macros(element.get(facet)) + u"$(nrm)"
2212       else: message = u"Element \"" + arguments[1] + u"\" does not exist."
2213    elif arguments[0] == u"result":
2214       if len(arguments) < 2: message = u"You need to specify an expression."
2215       else:
2216          try:
2217             message = repr(eval(u" ".join(arguments[1:])))
2218          except:
2219             message = u"Your expression raised an exception!"
2220    elif arguments[0] == u"log":
2221       if len(arguments) == 4:
2222          if re.match(u"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
2223             stop = int(arguments[3])
2224          else: stop = -1
2225       else: stop = 0
2226       if len(arguments) >= 3:
2227          if re.match(u"^\d+$", arguments[2]) and int(arguments[2]) > 0:
2228             start = int(arguments[2])
2229          else: start = -1
2230       else: start = 10
2231       if len(arguments) >= 2:
2232          if re.match(u"^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9:
2233             level = int(arguments[1])
2234          else: level = -1
2235       elif 0 <= actor.owner.account.getint(u"loglevel") <= 9:
2236          level = actor.owner.account.getint(u"loglevel")
2237       else: level = 1
2238       if level > -1 and start > -1 and stop > -1:
2239          message = get_loglines(level, start, stop)
2240       else:
2241          message = u"When specified, level must be 0-9 (default 1), " \
2242             + u"start and stop must be >=1 (default 10 and 1)."
2243    else: message = u"I don't know what \"" + parameters + u"\" is."
2244    actor.send(message)
2245
2246 def command_create(actor, parameters):
2247    u"""Create an element if it does not exist."""
2248    if not parameters:
2249       message = u"You must at least specify an element to create."
2250    elif not actor.owner: message = u""
2251    else:
2252       arguments = parameters.split()
2253       if len(arguments) == 1: arguments.append(u"")
2254       if len(arguments) == 2:
2255          element, filename = arguments
2256          if element in universe.contents:
2257             message = u"The \"" + element + u"\" element already exists."
2258          else:
2259             message = u"You create \"" + element + u"\" within the universe."
2260             logline = actor.owner.account.get(
2261                u"name"
2262             ) + u" created an element: " + element
2263             if filename:
2264                logline += u" in file " + filename
2265                if filename not in universe.files:
2266                   message += u" Warning: \"" + filename \
2267                      + u"\" is not yet included in any other file and will " \
2268                      + u"not be read on startup unless this is remedied."
2269             Element(element, universe, filename)
2270             log(logline, 6)
2271       elif len(arguments) > 2:
2272          message = u"You can only specify an element and a filename."
2273    actor.send(message)
2274
2275 def command_destroy(actor, parameters):
2276    u"""Destroy an element if it exists."""
2277    if actor.owner:
2278       if not parameters: message = u"You must specify an element to destroy."
2279       else:
2280          if parameters not in universe.contents:
2281             message = u"The \"" + parameters + u"\" element does not exist."
2282          else:
2283             universe.contents[parameters].destroy()
2284             message = u"You destroy \"" + parameters \
2285                + u"\" within the universe."
2286             log(
2287                actor.owner.account.get(
2288                   u"name"
2289                ) + u" destroyed an element: " + parameters,
2290                6
2291             )
2292       actor.send(message)
2293
2294 def command_set(actor, parameters):
2295    u"""Set a facet of an element."""
2296    if not parameters:
2297       message = u"You must specify an element, a facet and a value."
2298    else:
2299       arguments = parameters.split(u" ", 2)
2300       if len(arguments) == 1:
2301          message = u"What facet of element \"" + arguments[0] \
2302             + u"\" would you like to set?"
2303       elif len(arguments) == 2:
2304          message = u"What value would you like to set for the \"" \
2305             + arguments[1] + u"\" facet of the \"" + arguments[0] \
2306             + u"\" element?"
2307       else:
2308          element, facet, value = arguments
2309          if element not in universe.contents:
2310             message = u"The \"" + element + u"\" element does not exist."
2311          else:
2312             universe.contents[element].set(facet, value)
2313             message = u"You have successfully (re)set the \"" + facet \
2314                + u"\" facet of element \"" + element \
2315                + u"\". Try \"show element " + element + u"\" for verification."
2316    actor.send(message)
2317
2318 def command_delete(actor, parameters):
2319    u"""Delete a facet from an element."""
2320    if not parameters: message = u"You must specify an element and a facet."
2321    else:
2322       arguments = parameters.split(u" ")
2323       if len(arguments) == 1:
2324          message = u"What facet of element \"" + arguments[0] \
2325          + u"\" would you like to delete?"
2326       elif len(arguments) != 2:
2327          message = u"You may only specify an element and a facet."
2328       else:
2329          element, facet = arguments
2330          if element not in universe.contents:
2331             message = u"The \"" + element + u"\" element does not exist."
2332          elif facet not in universe.contents[element].facets():
2333             message = u"The \"" + element + u"\" element has no \"" + facet \
2334                + u"\" facet."
2335          else:
2336             universe.contents[element].remove_facet(facet)
2337             message = u"You have successfully deleted the \"" + facet \
2338                + u"\" facet of element \"" + element \
2339                + u"\". Try \"show element " + element + u"\" for verification."
2340    actor.send(message)
2341
2342 def command_error(actor, input_data):
2343    u"""Generic error for an unrecognized command word."""
2344    import random
2345
2346    # 90% of the time use a generic error
2347    if random.randrange(10):
2348       message = u"I'm not sure what \"" + input_data + u"\" means..."
2349
2350    # 10% of the time use the classic diku error
2351    else:
2352       message = u"Arglebargle, glop-glyf!?!"
2353
2354    # send the error message
2355    actor.send(message)
2356
2357 def find_file(
2358    file_name=None,
2359    root_path=None,
2360    search_path=None,
2361    default_dir=None,
2362    relative=None,
2363    universe=None
2364 ):
2365    u"""Return an absolute file path based on configuration."""
2366    import os, os.path, sys
2367
2368    # make sure to get rid of any surrounding quotes first thing
2369    if file_name: file_name = file_name.strip(u"\"'")
2370
2371    # this is all unnecessary if it's already absolute
2372    if file_name and os.path.isabs(file_name):
2373       return os.path.realpath(file_name)
2374
2375    # when no file name is specified, look for <argv[0]>.conf
2376    elif not file_name: file_name = os.path.basename( sys.argv[0] ) + u".conf"
2377
2378    # if a universe was provided, try to get some defaults from there
2379    if universe:
2380
2381       if hasattr(
2382          universe,
2383          u"contents"
2384       ) and u"internal:storage" in universe.contents:
2385          storage = universe.categories[u"internal"][u"storage"]
2386          if not root_path: root_path = storage.get(u"root_path").strip("\"'")
2387          if not search_path: search_path = storage.getlist(u"search_path")
2388          if not default_dir: default_dir = storage.get(u"default_dir").strip("\"'")
2389
2390       # if there's only one file loaded, try to work around a chicken<egg
2391       elif hasattr(universe, u"files") and len(
2392          universe.files
2393       ) == 1 and not universe.files[universe.files.keys()[0]].is_writeable():
2394          data_file = universe.files[universe.files.keys()[0]].data
2395
2396          # try for a fallback default directory
2397          if not default_dir and data_file.has_option(
2398             u"internal:storage",
2399             u"default_dir"
2400          ):
2401             default_dir = data_file.get(
2402                u"internal:storage",
2403                u"default_dir"
2404             ).strip(u"\"'")
2405
2406          # try for a fallback root path
2407          if not root_path and data_file.has_option(
2408             u"internal:storage",
2409             u"root_path"
2410          ):
2411             root_path = data_file.get(
2412                u"internal:storage",
2413                u"root_path"
2414             ).strip(u"\"'")
2415
2416          # try for a fallback search path
2417          if not search_path and data_file.has_option(
2418             u"internal:storage",
2419             u"search_path"
2420          ):
2421             search_path = makelist(
2422                data_file.get(u"internal:storage", u"search_path").strip(u"\"'")
2423             )
2424
2425       # another fallback root path, this time from the universe startdir
2426       if not root_path and hasattr(universe, "startdir"):
2427          root_path = universe.startdir
2428
2429    # when no root path is specified, assume the current working directory
2430    if not root_path: root_path = os.getcwd()
2431
2432    # otherwise, make sure it's absolute
2433    elif not os.path.isabs(root_path): root_path = os.path.realpath(root_path)
2434
2435    # if there's no search path, just use the root path and etc
2436    if not search_path: search_path = [root_path, u"etc"]
2437
2438    # work on a copy of the search path, to avoid modifying the caller's
2439    else: search_path = search_path[:]
2440
2441    # if there's no default path, use the last element of the search path
2442    if not default_dir: default_dir = search_path[-1]
2443
2444    # if an existing file or directory reference was supplied, prepend it
2445    if relative:
2446       relative = relative.strip(u"\"'")
2447       if os.path.isdir(relative): search_path = [relative] + search_path
2448       else: search_path = [ os.path.dirname(relative) ] + search_path
2449
2450    # make the search path entries absolute and throw away any dupes
2451    clean_search_path = []
2452    for each_path in search_path:
2453       each_path = each_path.strip(u"\"'")
2454       if not os.path.isabs(each_path):
2455          each_path = os.path.realpath( os.path.join(root_path, each_path) )
2456       if each_path not in clean_search_path:
2457          clean_search_path.append(each_path)
2458
2459    # start hunting for the file now
2460    for each_path in clean_search_path:
2461
2462       # if the file exists and is readable, we're done
2463       if os.path.isfile( os.path.join(each_path, file_name) ):
2464          file_name = os.path.realpath( os.path.join(each_path, file_name) )
2465          break
2466
2467    # it didn't exist after all, so use the default path instead
2468    if not os.path.isabs(file_name):
2469       file_name = os.path.join(default_dir, file_name)
2470    if not os.path.isabs(file_name):
2471       file_name = os.path.join(root_path, file_name)
2472
2473    # and normalize it last thing before returning
2474    file_name = os.path.realpath(file_name)
2475
2476    # normalize the resulting file path and hand it back
2477    return file_name
2478
2479 def daemonize(universe):
2480    u"""Fork and disassociate from everything."""
2481    import codecs, ctypes, ctypes.util, os, os.path, sys
2482
2483    # only if this is what we're configured to do
2484    if universe.contents[u"internal:process"].getboolean(u"daemon"):
2485
2486       # if possible, we want to rename the process to the same as the script
2487       # (these will need to be byte type during 2to3 migration)
2488       new_argv = "\0".join(sys.argv) + "\0"
2489       new_short_argv0 = os.path.basename(sys.argv[0]) + "\0"
2490
2491       # attempt the linux way first
2492       try:
2493          argv_array = ctypes.POINTER(ctypes.c_char_p)
2494          ctypes.pythonapi.Py_GetArgcArgv.argtypes = (
2495             ctypes.POINTER(ctypes.c_int),
2496             ctypes.POINTER(argv_array)
2497          )
2498          argc = argv_array()
2499          ctypes.pythonapi.Py_GetArgcArgv(
2500             ctypes.c_int(0),
2501             ctypes.pointer(argc)
2502          )
2503          old_argv0_size = len(argc.contents.value)
2504          ctypes.memset( argc.contents, 0, len(new_argv)+old_argv0_size )
2505          ctypes.memmove( argc.contents, new_argv, len(new_argv) )
2506          ctypes.CDLL( ctypes.util.find_library(u"c") ).prctl(
2507             15,
2508             new_short_argv0,
2509             0,
2510             0,
2511             0
2512          )
2513
2514       except:
2515
2516          # since that failed, maybe it's bsd?
2517          try:
2518
2519             # much simpler, since bsd has a libc function call for this
2520             ctypes.CDLL( ctypes.util.find_library(u"c") ).setproctitle(
2521                new_argv
2522             )
2523
2524          except:
2525
2526             # that didn't work either, so just log that we couldn't
2527             log(u"Failed to rename the interpreter process (cosmetic).")
2528
2529       # log before we start forking around, so the terminal gets the message
2530       log(u"Disassociating from the controlling terminal.")
2531
2532       # fork off and die, so we free up the controlling terminal
2533       if os.fork(): os._exit(0)
2534
2535       # switch to a new process group
2536       os.setsid()
2537
2538       # fork some more, this time to free us from the old process group
2539       if os.fork(): os._exit(0)
2540
2541       # reset the working directory so we don't needlessly tie up mounts
2542       os.chdir(u"/")
2543
2544       # clear the file creation mask so we can bend it to our will later
2545       os.umask(0)
2546
2547       # redirect stdin/stdout/stderr and close off their former descriptors
2548       for stdpipe in range(3): os.close(stdpipe)
2549       sys.stdin = codecs.open(u"/dev/null", u"r", u"utf-8")
2550       sys.__stdin__ = codecs.open(u"/dev/null", u"r", u"utf-8")
2551       sys.stdout = codecs.open(u"/dev/null", u"w", u"utf-8")
2552       sys.stderr = codecs.open(u"/dev/null", u"w", u"utf-8")
2553       sys.__stdout__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2554       sys.__stderr__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2555
2556 def create_pidfile(universe):
2557    u"""Write a file containing the current process ID."""
2558    import codecs, os, os.path
2559    pid = unicode(os.getpid())
2560    log(u"Process ID: " + pid)
2561    file_name = universe.contents[u"internal:process"].get(u"pidfile")
2562    if file_name:
2563       if not os.path.isabs(file_name):
2564          file_name = os.path.join(universe.startdir, file_name)
2565       file_descriptor = codecs.open(file_name, u"w", u"utf-8")
2566       file_descriptor.write(pid + u"\n")
2567       file_descriptor.flush()
2568       file_descriptor.close()
2569
2570 def remove_pidfile(universe):
2571    u"""Remove the file containing the current process ID."""
2572    import os, os.path
2573    file_name = universe.contents[u"internal:process"].get(u"pidfile")
2574    if file_name:
2575       if not os.path.isabs(file_name):
2576          file_name = os.path.join(universe.startdir, file_name)
2577       if os.access(file_name, os.W_OK): os.remove(file_name)
2578
2579 def excepthook(excepttype, value, tracebackdata):
2580    u"""Handle uncaught exceptions."""
2581    import traceback
2582
2583    # assemble the list of errors into a single string
2584    message = u"".join(
2585       traceback.format_exception(excepttype, value, tracebackdata)
2586    )
2587
2588    # try to log it, if possible
2589    try: log(message, 9)
2590    except: pass
2591
2592    # try to write it to stderr, if possible
2593    try: sys.stderr.write(message)
2594    except: pass
2595
2596 def sighook(what, where):
2597    u"""Handle external signals."""
2598    import signal
2599
2600    # a generic message
2601    message = u"Caught signal: "
2602
2603    # for a hangup signal
2604    if what == signal.SIGHUP:
2605       message += u"hangup (reloading)"
2606       universe.reload_flag = True
2607
2608    # for a terminate signal
2609    elif what == signal.SIGTERM:
2610       message += u"terminate (halting)"
2611       universe.terminate_flag = True
2612
2613    # catchall for unexpected signals
2614    else: message += unicode(what) + u" (unhandled)"
2615
2616    # log what happened
2617    log(message, 8)
2618
2619 def override_excepthook():
2620    u"""Redefine sys.excepthook with our own."""
2621    import sys
2622    sys.excepthook = excepthook
2623
2624 def assign_sighook():
2625    u"""Assign a customized handler for some signals."""
2626    import signal
2627    signal.signal(signal.SIGHUP, sighook)
2628    signal.signal(signal.SIGTERM, sighook)
2629
2630 def setup():
2631    """This contains functions to be performed when starting the engine."""
2632    import sys
2633
2634    # see if a configuration file was specified
2635    if len(sys.argv) > 1: conffile = sys.argv[1]
2636    else: conffile = u""
2637
2638    # the big bang
2639    global universe
2640    universe = Universe(conffile, True)
2641
2642    # log an initial message
2643    log(u"Started mudpy with command line: " + u" ".join(sys.argv))
2644
2645    # fork and disassociate
2646    daemonize(universe)
2647
2648    # override the default exception handler so we get logging first thing
2649    override_excepthook()
2650
2651    # set up custom signal handlers
2652    assign_sighook()
2653
2654    # make the pidfile
2655    create_pidfile(universe)
2656
2657    # pass the initialized universe back
2658    return universe
2659
2660 def finish():
2661    """This contains functions to be performed when shutting down the engine."""
2662
2663    # the loop has terminated, so save persistent data
2664    universe.save()
2665
2666    # log a final message
2667    log(u"Shutting down now.")
2668
2669    # get rid of the pidfile
2670    remove_pidfile(universe)