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