Secure, extensible, forward-compatable passwords.
[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 password
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 password.verify( input_data, user.account.get(u"passhash") ):
1793
1794       # if so, set the username and load from cold storage
1795       if not user.replace_old_connections():
1796          user.authenticate()
1797          user.state = u"main_utility"
1798
1799    # if at first your hashes don't match, try, try again
1800    elif user.password_tries < universe.categories[
1801       u"internal"
1802    ][
1803       u"limits"
1804    ].getint(
1805       u"password_tries"
1806    ) - 1:
1807       user.password_tries += 1
1808       user.error = u"incorrect"
1809
1810    # we've exceeded the maximum number of password failures, so disconnect
1811    else:
1812       user.send(
1813          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1814       )
1815       user.state = u"disconnecting"
1816
1817 def handler_entering_new_password(user):
1818    u"""Handle a new password entry."""
1819    import password
1820
1821    # get the next waiting line of input
1822    input_data = user.input_queue.pop(0)
1823
1824    # make sure the password is strong--at least one upper, one lower and
1825    # one digit, seven or more characters in length
1826    if len(input_data) > 6 and len(
1827       filter( lambda x: x>=u"0" and x<=u"9", input_data )
1828    ) and len(
1829       filter( lambda x: x>=u"A" and x<=u"Z", input_data )
1830    ) and len(
1831       filter( lambda x: x>=u"a" and x<=u"z", input_data )
1832    ):
1833
1834       # hash and store it, then move on to verification
1835       user.account.set( u"passhash", password.create(input_data) )
1836       user.state = u"verifying_new_password"
1837
1838    # the password was weak, try again if you haven't tried too many times
1839    elif user.password_tries < universe.categories[
1840       u"internal"
1841    ][
1842       u"limits"
1843    ].getint(
1844       u"password_tries"
1845    ) - 1:
1846       user.password_tries += 1
1847       user.error = u"weak"
1848
1849    # too many tries, so adios
1850    else:
1851       user.send(
1852          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1853       )
1854       user.account.destroy()
1855       user.state = u"disconnecting"
1856
1857 def handler_verifying_new_password(user):
1858    u"""Handle the re-entered new password for verification."""
1859    import password
1860
1861    # get the next waiting line of input
1862    input_data = user.input_queue.pop(0)
1863
1864    # hash the input and match it to storage
1865    if password.verify( input_data, user.account.get(u"passhash") ):
1866       user.authenticate()
1867
1868       # the hashes matched, so go active
1869       if not user.replace_old_connections(): user.state = u"main_utility"
1870
1871    # go back to entering the new password as long as you haven't tried
1872    # too many times
1873    elif user.password_tries < universe.categories[
1874       u"internal"
1875    ][
1876       u"limits"
1877    ].getint(
1878       u"password_tries"
1879    ) - 1:
1880       user.password_tries += 1
1881       user.error = u"differs"
1882       user.state = u"entering_new_password"
1883
1884    # otherwise, sayonara
1885    else:
1886       user.send(
1887          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1888       )
1889       user.account.destroy()
1890       user.state = u"disconnecting"
1891
1892 def handler_active(user):
1893    u"""Handle input for active users."""
1894
1895    # get the next waiting line of input
1896    input_data = user.input_queue.pop(0)
1897
1898    # is there input?
1899    if input_data:
1900
1901       # split out the command and parameters
1902       actor = user.avatar
1903       mode = actor.get(u"mode")
1904       if mode and input_data.startswith(u"!"):
1905          command_name, parameters = first_word(input_data[1:])
1906       elif mode == u"chat":
1907          command_name = u"say"
1908          parameters = input_data
1909       else:
1910          command_name, parameters = first_word(input_data)
1911
1912       # lowercase the command
1913       command_name = command_name.lower()
1914
1915       # the command matches a command word for which we have data
1916       if command_name in universe.categories[u"command"]:
1917          command = universe.categories[u"command"][command_name]
1918       else: command = None
1919
1920       # if it's allowed, do it
1921       if actor.can_run(command): exec(command.get(u"action"))
1922
1923       # otherwise, give an error
1924       elif command_name: command_error(actor, input_data)
1925
1926    # if no input, just idle back with a prompt
1927    else: user.send(u"", just_prompt=True)
1928    
1929 def command_halt(actor, parameters):
1930    u"""Halt the world."""
1931    if actor.owner:
1932
1933       # see if there's a message or use a generic one
1934       if parameters: message = u"Halting: " + parameters
1935       else:
1936          message = u"User " + actor.owner.account.get(
1937             u"name"
1938          ) + u" halted the world."
1939
1940       # let everyone know
1941       broadcast(message, add_prompt=False)
1942       log(message, 8)
1943
1944       # set a flag to terminate the world
1945       universe.terminate_flag = True
1946
1947 def command_reload(actor):
1948    u"""Reload all code modules, configs and data."""
1949    if actor.owner:
1950
1951       # let the user know and log
1952       actor.send(u"Reloading all code modules, configs and data.")
1953       log(
1954          u"User " + actor.owner.account.get(u"name") + u" reloaded the world.",
1955          8
1956       )
1957
1958       # set a flag to reload
1959       universe.reload_flag = True
1960
1961 def command_quit(actor):
1962    u"""Leave the world and go back to the main menu."""
1963    if actor.owner:
1964       actor.owner.state = u"main_utility"
1965       actor.owner.deactivate_avatar()
1966
1967 def command_help(actor, parameters):
1968    u"""List available commands and provide help for commands."""
1969
1970    # did the user ask for help on a specific command word?
1971    if parameters and actor.owner:
1972
1973       # is the command word one for which we have data?
1974       if parameters in universe.categories[u"command"]:
1975          command = universe.categories[u"command"][parameters]
1976       else: command = None
1977
1978       # only for allowed commands
1979       if actor.can_run(command):
1980
1981          # add a description if provided
1982          description = command.get(u"description")
1983          if not description:
1984             description = u"(no short description provided)"
1985          if command.getboolean(u"administrative"): output = u"$(red)"
1986          else: output = u"$(grn)"
1987          output += parameters + u"$(nrm) - " + description + u"$(eol)$(eol)"
1988
1989          # add the help text if provided
1990          help_text = command.get(u"help")
1991          if not help_text:
1992             help_text = u"No help is provided for this command."
1993          output += help_text
1994
1995          # list related commands
1996          see_also = command.getlist(u"see_also")
1997          if see_also:
1998             really_see_also = u""
1999             for item in see_also:
2000                if item in universe.categories[u"command"]:
2001                   command = universe.categories[u"command"][item]
2002                   if actor.can_run(command):
2003                      if really_see_also:
2004                         really_see_also += u", "
2005                      if command.getboolean(u"administrative"):
2006                         really_see_also += u"$(red)"
2007                      else:
2008                         really_see_also += u"$(grn)"
2009                      really_see_also += item + u"$(nrm)"
2010             if really_see_also:
2011                output += u"$(eol)$(eol)See also: " + really_see_also
2012
2013       # no data for the requested command word
2014       else:
2015          output = u"That is not an available command."
2016
2017    # no specific command word was indicated
2018    else:
2019
2020       # give a sorted list of commands with descriptions if provided
2021       output = u"These are the commands available to you:$(eol)$(eol)"
2022       sorted_commands = universe.categories[u"command"].keys()
2023       sorted_commands.sort()
2024       for item in sorted_commands:
2025          command = universe.categories[u"command"][item]
2026          if actor.can_run(command):
2027             description = command.get(u"description")
2028             if not description:
2029                description = u"(no short description provided)"
2030             if command.getboolean(u"administrative"): output += u"   $(red)"
2031             else: output += u"   $(grn)"
2032             output += item + u"$(nrm) - " + description + u"$(eol)"
2033       output += u"$(eol)Enter \"help COMMAND\" for help on a command " \
2034          + u"named \"COMMAND\"."
2035
2036    # send the accumulated output to the user
2037    actor.send(output)
2038
2039 def command_move(actor, parameters):
2040    u"""Move the avatar in a given direction."""
2041    if parameters in universe.contents[actor.get(u"location")].portals():
2042       actor.move_direction(parameters)
2043    else: actor.send(u"You cannot go that way.")
2044
2045 def command_look(actor, parameters):
2046    u"""Look around."""
2047    if parameters: actor.send(u"You can't look at or in anything yet.")
2048    else: actor.look_at(actor.get(u"location"))
2049
2050 def command_say(actor, parameters):
2051    u"""Speak to others in the same room."""
2052    import unicodedata
2053
2054    # check for replacement macros and escape them
2055    parameters = escape_macros(parameters)
2056
2057    # if the message is wrapped in quotes, remove them and leave contents intact
2058    if parameters.startswith(u"\"") and parameters.endswith(u"\""):
2059       message = parameters[1:-1]
2060       literal = True
2061
2062    # otherwise, get rid of stray quote marks on the ends of the message
2063    else:
2064       message = parameters.strip(u"\"'`")
2065       literal = False
2066
2067    # the user entered a message
2068    if message:
2069
2070       # match the punctuation used, if any, to an action
2071       actions = universe.categories[u"internal"][u"language"].getdict(
2072          u"actions"
2073       )
2074       default_punctuation = universe.categories[u"internal"][u"language"].get(
2075          u"default_punctuation"
2076       )
2077       action = u""
2078       for mark in actions.keys():
2079          if not literal and message.endswith(mark):
2080             action = actions[mark]
2081             break
2082
2083       # add punctuation if needed
2084       if not action:
2085          action = actions[default_punctuation]
2086          if message and not (
2087             literal or unicodedata.category(message[-1]) == u"Po"
2088          ):
2089             message += default_punctuation
2090
2091       # failsafe checks to avoid unwanted reformatting and null strings
2092       if message and not literal:
2093
2094          # decapitalize the first letter to improve matching
2095          message = message[0].lower() + message[1:]
2096
2097          # iterate over all words in message, replacing typos
2098          typos = universe.categories[u"internal"][u"language"].getdict(
2099             u"typos"
2100          )
2101          words = message.split()
2102          for index in range(len(words)):
2103             word = words[index]
2104             while unicodedata.category(word[0]) == u"Po":
2105                word = word[1:]
2106             while unicodedata.category(word[-1]) == u"Po":
2107                word = word[:-1]
2108             if word in typos.keys():
2109                words[index] = words[index].replace(word, typos[word])
2110          message = u" ".join(words)
2111
2112          # capitalize the first letter
2113          message = message[0].upper() + message[1:]
2114
2115    # tell the room
2116    if message:
2117       actor.echo_to_location(
2118          actor.get(u"name") + u" " + action + u"s, \"" + message + u"\""
2119       )
2120       actor.send(u"You " + action + u", \"" + message + u"\"")
2121
2122    # there was no message
2123    else:
2124       actor.send(u"What do you want to say?")
2125
2126 def command_chat(actor):
2127    u"""Toggle chat mode."""
2128    mode = actor.get(u"mode")
2129    if not mode:
2130       actor.set(u"mode", u"chat")
2131       actor.send(u"Entering chat mode (use $(grn)!chat$(nrm) to exit).")
2132    elif mode == u"chat":
2133       actor.remove_facet(u"mode")
2134       actor.send(u"Exiting chat mode.")
2135    else: actor.send(u"Sorry, but you're already busy with something else!")
2136
2137 def command_show(actor, parameters):
2138    u"""Show program data."""
2139    import re
2140    message = u""
2141    arguments = parameters.split()
2142    if not parameters: message = u"What do you want to show?"
2143    elif arguments[0] == u"time":
2144       message = universe.categories[u"internal"][u"counters"].get(
2145          u"elapsed"
2146       ) + u" increments elapsed since the world was created."
2147    elif arguments[0] == u"categories":
2148       message = u"These are the element categories:$(eol)"
2149       categories = universe.categories.keys()
2150       categories.sort()
2151       for category in categories:
2152          message += u"$(eol)   $(grn)" + category + u"$(nrm)"
2153    elif arguments[0] == u"files":
2154       message = u"These are the current files containing the universe:$(eol)"
2155       filenames = universe.files.keys()
2156       filenames.sort()
2157       for filename in filenames:
2158          if universe.files[filename].is_writeable(): status = u"rw"
2159          else: status = u"ro"
2160          message += u"$(eol)   $(red)(" + status + u") $(grn)" + filename \
2161             + u"$(nrm)"
2162    elif arguments[0] == u"category":
2163       if len(arguments) != 2: message = u"You must specify one category."
2164       elif arguments[1] in universe.categories:
2165          message = u"These are the elements in the \"" + arguments[1] \
2166             + u"\" category:$(eol)"
2167          elements = [
2168             (
2169                universe.categories[arguments[1]][x].key
2170             ) for x in universe.categories[arguments[1]].keys()
2171          ]
2172          elements.sort()
2173          for element in elements:
2174             message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2175       else: message = u"Category \"" + arguments[1] + u"\" does not exist."
2176    elif arguments[0] == u"file":
2177       if len(arguments) != 2: message = u"You must specify one file."
2178       elif arguments[1] in universe.files:
2179          message = u"These are the elements in the \"" + arguments[1] \
2180             + u"\" file:$(eol)"
2181          elements = universe.files[arguments[1]].data.sections()
2182          elements.sort()
2183          for element in elements:
2184             message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2185       else: message = u"Category \"" + arguments[1] + u"\" does not exist."
2186    elif arguments[0] == u"element":
2187       if len(arguments) != 2: message = u"You must specify one element."
2188       elif arguments[1] in universe.contents:
2189          element = universe.contents[arguments[1]]
2190          message = u"These are the properties of the \"" + arguments[1] \
2191             + u"\" element (in \"" + element.origin.filename + u"\"):$(eol)"
2192          facets = element.facets()
2193          facets.sort()
2194          for facet in facets:
2195             message += u"$(eol)   $(grn)" + facet + u": $(red)" \
2196                + escape_macros(element.get(facet)) + u"$(nrm)"
2197       else: message = u"Element \"" + arguments[1] + u"\" does not exist."
2198    elif arguments[0] == u"result":
2199       if len(arguments) < 2: message = u"You need to specify an expression."
2200       else:
2201          try:
2202             message = repr(eval(u" ".join(arguments[1:])))
2203          except:
2204             message = u"Your expression raised an exception!"
2205    elif arguments[0] == u"log":
2206       if len(arguments) == 4:
2207          if re.match(u"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
2208             stop = int(arguments[3])
2209          else: stop = -1
2210       else: stop = 0
2211       if len(arguments) >= 3:
2212          if re.match(u"^\d+$", arguments[2]) and int(arguments[2]) > 0:
2213             start = int(arguments[2])
2214          else: start = -1
2215       else: start = 10
2216       if len(arguments) >= 2:
2217          if re.match(u"^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9:
2218             level = int(arguments[1])
2219          else: level = -1
2220       elif 0 <= actor.owner.account.getint(u"loglevel") <= 9:
2221          level = actor.owner.account.getint(u"loglevel")
2222       else: level = 1
2223       if level > -1 and start > -1 and stop > -1:
2224          message = get_loglines(level, start, stop)
2225       else:
2226          message = u"When specified, level must be 0-9 (default 1), " \
2227             + u"start and stop must be >=1 (default 10 and 1)."
2228    else: message = u"I don't know what \"" + parameters + u"\" is."
2229    actor.send(message)
2230
2231 def command_create(actor, parameters):
2232    u"""Create an element if it does not exist."""
2233    if not parameters:
2234       message = u"You must at least specify an element to create."
2235    elif not actor.owner: message = u""
2236    else:
2237       arguments = parameters.split()
2238       if len(arguments) == 1: arguments.append(u"")
2239       if len(arguments) == 2:
2240          element, filename = arguments
2241          if element in universe.contents:
2242             message = u"The \"" + element + u"\" element already exists."
2243          else:
2244             message = u"You create \"" + element + u"\" within the universe."
2245             logline = actor.owner.account.get(
2246                u"name"
2247             ) + u" created an element: " + element
2248             if filename:
2249                logline += u" in file " + filename
2250                if filename not in universe.files:
2251                   message += u" Warning: \"" + filename \
2252                      + u"\" is not yet included in any other file and will " \
2253                      + u"not be read on startup unless this is remedied."
2254             Element(element, universe, filename)
2255             log(logline, 6)
2256       elif len(arguments) > 2:
2257          message = u"You can only specify an element and a filename."
2258    actor.send(message)
2259
2260 def command_destroy(actor, parameters):
2261    u"""Destroy an element if it exists."""
2262    if actor.owner:
2263       if not parameters: message = u"You must specify an element to destroy."
2264       else:
2265          if parameters not in universe.contents:
2266             message = u"The \"" + parameters + u"\" element does not exist."
2267          else:
2268             universe.contents[parameters].destroy()
2269             message = u"You destroy \"" + parameters \
2270                + u"\" within the universe."
2271             log(
2272                actor.owner.account.get(
2273                   u"name"
2274                ) + u" destroyed an element: " + parameters,
2275                6
2276             )
2277       actor.send(message)
2278
2279 def command_set(actor, parameters):
2280    u"""Set a facet of an element."""
2281    if not parameters:
2282       message = u"You must specify an element, a facet and a value."
2283    else:
2284       arguments = parameters.split(u" ", 2)
2285       if len(arguments) == 1:
2286          message = u"What facet of element \"" + arguments[0] \
2287             + u"\" would you like to set?"
2288       elif len(arguments) == 2:
2289          message = u"What value would you like to set for the \"" \
2290             + arguments[1] + u"\" facet of the \"" + arguments[0] \
2291             + u"\" element?"
2292       else:
2293          element, facet, value = arguments
2294          if element not in universe.contents:
2295             message = u"The \"" + element + u"\" element does not exist."
2296          else:
2297             universe.contents[element].set(facet, value)
2298             message = u"You have successfully (re)set the \"" + facet \
2299                + u"\" facet of element \"" + element \
2300                + u"\". Try \"show element " + element + u"\" for verification."
2301    actor.send(message)
2302
2303 def command_delete(actor, parameters):
2304    u"""Delete a facet from an element."""
2305    if not parameters: message = u"You must specify an element and a facet."
2306    else:
2307       arguments = parameters.split(u" ")
2308       if len(arguments) == 1:
2309          message = u"What facet of element \"" + arguments[0] \
2310          + u"\" would you like to delete?"
2311       elif len(arguments) != 2:
2312          message = u"You may only specify an element and a facet."
2313       else:
2314          element, facet = arguments
2315          if element not in universe.contents:
2316             message = u"The \"" + element + u"\" element does not exist."
2317          elif facet not in universe.contents[element].facets():
2318             message = u"The \"" + element + u"\" element has no \"" + facet \
2319                + u"\" facet."
2320          else:
2321             universe.contents[element].remove_facet(facet)
2322             message = u"You have successfully deleted the \"" + facet \
2323                + u"\" facet of element \"" + element \
2324                + u"\". Try \"show element " + element + u"\" for verification."
2325    actor.send(message)
2326
2327 def command_error(actor, input_data):
2328    u"""Generic error for an unrecognized command word."""
2329    import random
2330
2331    # 90% of the time use a generic error
2332    if random.randrange(10):
2333       message = u"I'm not sure what \"" + input_data + u"\" means..."
2334
2335    # 10% of the time use the classic diku error
2336    else:
2337       message = u"Arglebargle, glop-glyf!?!"
2338
2339    # send the error message
2340    actor.send(message)
2341
2342 def find_file(
2343    file_name=None,
2344    root_path=None,
2345    search_path=None,
2346    default_dir=None,
2347    relative=None,
2348    universe=None
2349 ):
2350    u"""Return an absolute file path based on configuration."""
2351    import os, os.path, sys
2352
2353    # make sure to get rid of any surrounding quotes first thing
2354    if file_name: file_name = file_name.strip(u"\"'")
2355
2356    # this is all unnecessary if it's already absolute
2357    if file_name and os.path.isabs(file_name):
2358       return os.path.realpath(file_name)
2359
2360    # when no file name is specified, look for <argv[0]>.conf
2361    elif not file_name: file_name = os.path.basename( sys.argv[0] ) + u".conf"
2362
2363    # if a universe was provided, try to get some defaults from there
2364    if universe:
2365
2366       if hasattr(
2367          universe,
2368          u"contents"
2369       ) and u"internal:storage" in universe.contents:
2370          storage = universe.categories[u"internal"][u"storage"]
2371          if not root_path: root_path = storage.get(u"root_path").strip("\"'")
2372          if not search_path: search_path = storage.getlist(u"search_path")
2373          if not default_dir: default_dir = storage.get(u"default_dir").strip("\"'")
2374
2375       # if there's only one file loaded, try to work around a chicken<egg
2376       elif hasattr(universe, u"files") and len(
2377          universe.files
2378       ) == 1 and not universe.files[universe.files.keys()[0]].is_writeable():
2379          data_file = universe.files[universe.files.keys()[0]].data
2380
2381          # try for a fallback default directory
2382          if not default_dir and data_file.has_option(
2383             u"internal:storage",
2384             u"default_dir"
2385          ):
2386             default_dir = data_file.get(
2387                u"internal:storage",
2388                u"default_dir"
2389             ).strip(u"\"'")
2390
2391          # try for a fallback root path
2392          if not root_path and data_file.has_option(
2393             u"internal:storage",
2394             u"root_path"
2395          ):
2396             root_path = data_file.get(
2397                u"internal:storage",
2398                u"root_path"
2399             ).strip(u"\"'")
2400
2401          # try for a fallback search path
2402          if not search_path and data_file.has_option(
2403             u"internal:storage",
2404             u"search_path"
2405          ):
2406             search_path = makelist(
2407                data_file.get(u"internal:storage", u"search_path").strip(u"\"'")
2408             )
2409
2410       # another fallback root path, this time from the universe startdir
2411       if not root_path and hasattr(universe, "startdir"):
2412          root_path = universe.startdir
2413
2414    # when no root path is specified, assume the current working directory
2415    if not root_path: root_path = os.getcwd()
2416
2417    # otherwise, make sure it's absolute
2418    elif not os.path.isabs(root_path): root_path = os.path.realpath(root_path)
2419
2420    # if there's no search path, just use the root path and etc
2421    if not search_path: search_path = [root_path, u"etc"]
2422
2423    # work on a copy of the search path, to avoid modifying the caller's
2424    else: search_path = search_path[:]
2425
2426    # if there's no default path, use the last element of the search path
2427    if not default_dir: default_dir = search_path[-1]
2428
2429    # if an existing file or directory reference was supplied, prepend it
2430    if relative:
2431       relative = relative.strip(u"\"'")
2432       if os.path.isdir(relative): search_path = [relative] + search_path
2433       else: search_path = [ os.path.dirname(relative) ] + search_path
2434
2435    # make the search path entries absolute and throw away any dupes
2436    clean_search_path = []
2437    for each_path in search_path:
2438       each_path = each_path.strip(u"\"'")
2439       if not os.path.isabs(each_path):
2440          each_path = os.path.realpath( os.path.join(root_path, each_path) )
2441       if each_path not in clean_search_path:
2442          clean_search_path.append(each_path)
2443
2444    # start hunting for the file now
2445    for each_path in clean_search_path:
2446
2447       # if the file exists and is readable, we're done
2448       if os.path.isfile( os.path.join(each_path, file_name) ):
2449          file_name = os.path.realpath( os.path.join(each_path, file_name) )
2450          break
2451
2452    # it didn't exist after all, so use the default path instead
2453    if not os.path.isabs(file_name):
2454       file_name = os.path.join(default_dir, file_name)
2455    if not os.path.isabs(file_name):
2456       file_name = os.path.join(root_path, file_name)
2457
2458    # and normalize it last thing before returning
2459    file_name = os.path.realpath(file_name)
2460
2461    # normalize the resulting file path and hand it back
2462    return file_name
2463
2464 def daemonize(universe):
2465    u"""Fork and disassociate from everything."""
2466    import codecs, ctypes, ctypes.util, os, os.path, sys
2467
2468    # only if this is what we're configured to do
2469    if universe.contents[u"internal:process"].getboolean(u"daemon"):
2470
2471       # if possible, we want to rename the process to the same as the script
2472       # (these will need to be byte type during 2to3 migration)
2473       new_argv = "\0".join(sys.argv) + "\0"
2474       new_short_argv0 = os.path.basename(sys.argv[0]) + "\0"
2475
2476       # attempt the linux way first
2477       try:
2478          argv_array = ctypes.POINTER(ctypes.c_char_p)
2479          ctypes.pythonapi.Py_GetArgcArgv.argtypes = (
2480             ctypes.POINTER(ctypes.c_int),
2481             ctypes.POINTER(argv_array)
2482          )
2483          argc = argv_array()
2484          ctypes.pythonapi.Py_GetArgcArgv(
2485             ctypes.c_int(0),
2486             ctypes.pointer(argc)
2487          )
2488          old_argv0_size = len(argc.contents.value)
2489          ctypes.memset( argc.contents, 0, len(new_argv)+old_argv0_size )
2490          ctypes.memmove( argc.contents, new_argv, len(new_argv) )
2491          ctypes.CDLL( ctypes.util.find_library(u"c") ).prctl(
2492             15,
2493             new_short_argv0,
2494             0,
2495             0,
2496             0
2497          )
2498
2499       except:
2500
2501          # since that failed, maybe it's bsd?
2502          try:
2503
2504             # much simpler, since bsd has a libc function call for this
2505             ctypes.CDLL( ctypes.util.find_library(u"c") ).setproctitle(
2506                new_argv
2507             )
2508
2509          except:
2510
2511             # that didn't work either, so just log that we couldn't
2512             log(u"Failed to rename the interpreter process (cosmetic).")
2513
2514       # log before we start forking around, so the terminal gets the message
2515       log(u"Disassociating from the controlling terminal.")
2516
2517       # fork off and die, so we free up the controlling terminal
2518       if os.fork(): os._exit(0)
2519
2520       # switch to a new process group
2521       os.setsid()
2522
2523       # fork some more, this time to free us from the old process group
2524       if os.fork(): os._exit(0)
2525
2526       # reset the working directory so we don't needlessly tie up mounts
2527       os.chdir(u"/")
2528
2529       # clear the file creation mask so we can bend it to our will later
2530       os.umask(0)
2531
2532       # redirect stdin/stdout/stderr and close off their former descriptors
2533       for stdpipe in range(3): os.close(stdpipe)
2534       sys.stdin = codecs.open(u"/dev/null", u"r", u"utf-8")
2535       sys.__stdin__ = codecs.open(u"/dev/null", u"r", u"utf-8")
2536       sys.stdout = codecs.open(u"/dev/null", u"w", u"utf-8")
2537       sys.stderr = codecs.open(u"/dev/null", u"w", u"utf-8")
2538       sys.__stdout__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2539       sys.__stderr__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2540
2541 def create_pidfile(universe):
2542    u"""Write a file containing the current process ID."""
2543    import codecs, os, os.path
2544    pid = unicode(os.getpid())
2545    log(u"Process ID: " + pid)
2546    file_name = universe.contents[u"internal:process"].get(u"pidfile")
2547    if file_name:
2548       if not os.path.isabs(file_name):
2549          file_name = os.path.join(universe.startdir, file_name)
2550       file_descriptor = codecs.open(file_name, u"w", u"utf-8")
2551       file_descriptor.write(pid + u"\n")
2552       file_descriptor.flush()
2553       file_descriptor.close()
2554
2555 def remove_pidfile(universe):
2556    u"""Remove the file containing the current process ID."""
2557    import os, os.path
2558    file_name = universe.contents[u"internal:process"].get(u"pidfile")
2559    if file_name:
2560       if not os.path.isabs(file_name):
2561          file_name = os.path.join(universe.startdir, file_name)
2562       if os.access(file_name, os.W_OK): os.remove(file_name)
2563
2564 def excepthook(excepttype, value, tracebackdata):
2565    u"""Handle uncaught exceptions."""
2566    import traceback
2567
2568    # assemble the list of errors into a single string
2569    message = u"".join(
2570       traceback.format_exception(excepttype, value, tracebackdata)
2571    )
2572
2573    # try to log it, if possible
2574    try: log(message, 9)
2575    except: pass
2576
2577    # try to write it to stderr, if possible
2578    try: sys.stderr.write(message)
2579    except: pass
2580
2581 def sighook(what, where):
2582    u"""Handle external signals."""
2583    import signal
2584
2585    # a generic message
2586    message = u"Caught signal: "
2587
2588    # for a hangup signal
2589    if what == signal.SIGHUP:
2590       message += u"hangup (reloading)"
2591       universe.reload_flag = True
2592
2593    # for a terminate signal
2594    elif what == signal.SIGTERM:
2595       message += u"terminate (halting)"
2596       universe.terminate_flag = True
2597
2598    # catchall for unexpected signals
2599    else: message += unicode(what) + u" (unhandled)"
2600
2601    # log what happened
2602    log(message, 8)
2603
2604 def override_excepthook():
2605    u"""Redefine sys.excepthook with our own."""
2606    import sys
2607    sys.excepthook = excepthook
2608
2609 def assign_sighook():
2610    u"""Assign a customized handler for some signals."""
2611    import signal
2612    signal.signal(signal.SIGHUP, sighook)
2613    signal.signal(signal.SIGTERM, sighook)
2614
2615 def setup():
2616    """This contains functions to be performed when starting the engine."""
2617    import sys
2618
2619    # see if a configuration file was specified
2620    if len(sys.argv) > 1: conffile = sys.argv[1]
2621    else: conffile = u""
2622
2623    # the big bang
2624    global universe
2625    universe = Universe(conffile, True)
2626
2627    # log an initial message
2628    log(u"Started mudpy with command line: " + u" ".join(sys.argv))
2629
2630    # fork and disassociate
2631    daemonize(universe)
2632
2633    # override the default exception handler so we get logging first thing
2634    override_excepthook()
2635
2636    # set up custom signal handlers
2637    assign_sighook()
2638
2639    # make the pidfile
2640    create_pidfile(universe)
2641
2642    # pass the initialized universe back
2643    return universe
2644
2645 def finish():
2646    """This contains functions to be performed when shutting down the engine."""
2647
2648    # the loop has terminated, so save persistent data
2649    universe.save()
2650
2651    # log a final message
2652    log(u"Shutting down now.")
2653
2654    # get rid of the pidfile
2655    remove_pidfile(universe)