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