1 """User objects for the MUFF Engine"""
3 # Copyright (c) 2005 MUDpy, The Fungi <fungi@yuggoth.org>, all rights reserved.
4 # Licensed per terms in the LICENSE file distributed with this software.
6 # user accounts are stored in ini-style files supported by ConfigParser
9 # os is used to test for existence of the account dir and, if necessary, make it
12 # string.replace is used to perform substitutions for color codes and the like
15 # hack to load all modules in the muff package
17 for module in muff.__all__:
18 exec("import " + module)
21 """This is a connected user."""
24 """Default values for the in-memory user variables."""
32 # the current client ip address
35 # the previous client ip address
36 self.last_address = ""
38 # the current socket connection object
39 self.connection = None
41 # a flag to denote whether the user is authenticated
42 self.authenticated = False
44 # number of times password entry has failed during this session
45 self.password_tries = 1
47 # the current state of the user
48 self.state = "entering_account_name"
50 # flag to indicate whether a menu has been displayed
51 self.menu_seen = False
53 # current error condition, if any
56 # fifo-style queue for lines of user input
59 # fifo-style queue for blocks of user output
60 self.output_queue = []
62 # holding pen for unterminated user input
63 self.partial_input = ""
65 # flag to indicate the current echo status of the client
71 # an object containing persistent account data
72 self.record = ConfigParser.SafeConfigParser()
75 """Log, save, close the connection and remove."""
76 if self.name: message = "User " + self.name
77 else: message = "An unnamed user"
78 message += " logged out."
81 self.connection.close()
85 """Save, load a new user and relocate the connection."""
87 # unauthenticated connections get the boot
88 if not self.authenticated:
89 muffmisc.log("An unauthenticated user was disconnected during reload.")
90 self.state = "disconnecting"
95 # save and get out of the list
99 # create a new user object
100 new_user = muffuser.User()
102 # give it the same name
103 new_user.name = self.name
108 # set everything else equivalent
109 new_user.address = self.address
110 new_user.last_address = self.last_address
111 new_user.connection = self.connection
112 new_user.authenticated = self.authenticated
113 new_user.password_tries = self.password_tries
114 new_user.state = self.state
115 new_user.menu_seen = self.menu_seen
116 new_user.error = self.error
117 new_user.input_queue = self.input_queue
118 new_user.output_queue = self.output_queue
119 new_user.partial_input = self.partial_input
120 new_user.echoing = self.echoing
123 muffvars.userlist.append(new_user)
125 # get rid of the old user object
128 def replace_old_connections(self):
129 """Disconnect active users with the same name."""
131 # the default return value
134 # iterate over each user in the list
135 for old_user in muffvars.userlist:
137 # the name is the same but it's not us
138 if old_user.name == self.name and old_user is not self:
141 muffmisc.log("User " + self.name + " reconnected--closing old connection to " + old_user.address + ".")
142 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
143 self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
145 # close the old connection
146 old_user.connection.close()
148 # replace the old connection with this one
149 old_user.connection = self.connection
150 old_user.last_address = old_user.address
151 old_user.address = self.address
152 old_user.echoing = self.echoing
154 # take this one out of the list and delete
160 # true if an old connection was replaced, false if not
163 def authenticate(self):
164 """Flag the user as authenticated and disconnect duplicates."""
165 if not self.state is "authenticated":
166 muffmisc.log("User " + self.name + " logged in.")
167 self.authenticated = True
170 """Retrieve account data from cold storage."""
172 # what the filename for the user account should be
173 filename = muffconf.get("files", "accounts") + "/" + self.name
175 # try to load the password hash and last connection ipa
177 self.record.read(filename)
178 self.passhash = self.record.get("account", "passhash")
179 self.last_address = self.record.get("account", "last_address", self.address)
181 # if we can't, that's okay too
185 def get_passhash(self):
186 """Retrieve the user's account password hash from storage."""
188 # what the filename for the user account could be
189 filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
191 # create a temporary account record object
192 temporary_record = ConfigParser.SafeConfigParser()
194 # try to load the indicated account and get a password hash
196 temporary_record.read(filename)
197 self.passhash = temporary_record.get("account", "passhash")
200 # otherwise, the password hash is empty
206 """Record account data to cold storage."""
208 # the user account must be authenticated to save
209 if self.authenticated:
211 # create an account section if it doesn't exist
212 if not self.record.has_section("account"):
213 self.record.add_section("account")
215 # write some in-memory data to the record
216 self.record.set("account", "name", self.name)
217 self.record.set("account", "passhash", self.passhash)
218 self.record.set("account", "last_address", self.address)
220 # the account files live here
221 account_path = muffconf.get("files", "accounts")
222 # the filename to which we'll write
223 filename = account_path + "/" + self.name.lower()
225 # open the user account file for writing
227 record_file = file(filename, "w")
229 # if the directory doesn't exist, create it first
231 os.makedirs(account_path)
232 record_file = file(filename, "w")
234 # dump the account data to it
235 self.record.write(record_file)
237 # close the user account file
241 # set the permissions to 0600
242 os.chmod(filename, 0600)
245 """Send the user their current menu."""
246 if not self.menu_seen:
247 self.menu_choices = muffmenu.get_menu_choices(self)
248 self.send(muffmenu.get_menu(self.state, self.error, self.echoing, self.menu_choices), "")
249 self.menu_seen = True
251 self.adjust_echoing()
253 def adjust_echoing(self):
254 """Adjust echoing to match state menu requirements."""
255 if self.echoing and not muffmenu.menu_echo_on(self.state): self.echoing = False
256 elif not self.echoing and muffmenu.menu_echo_on(self.state): self.echoing = True
259 """Remove a user from the list of connected users."""
260 muffvars.userlist.remove(self)
262 def send(self, output, eol="$(eol)"):
263 """Send arbitrary text to a connected user."""
265 # only when there is actual output
268 # start with a newline, append the message, then end
269 # with the optional eol string passed to this function
270 # and the ansi escape to return to normal text
271 output = "\r\n" + output + eol + chr(27) + "[0m"
273 # find and replace macros in the output
274 output = muffmisc.replace_macros(self, output)
276 # wrap the text at 80 characters
277 # TODO: prompt user for preferred wrap width
278 output = muffmisc.wrap_ansi_text(output, 80)
280 # drop the formatted output into the output queue
281 self.output_queue.append(output)
283 # try to send the last item in the queue, remove it and
284 # flag that menu display is not needed
286 self.connection.send(self.output_queue[0])
287 self.output_queue.remove(self.output_queue[0])
288 self.menu_seen = False
290 # but if we can't, that's okay too
295 """All the things to do to the user per increment."""
297 # if the world is terminating, disconnect
298 if muffvars.terminate_world:
299 self.state = "disconnecting"
300 self.menu_seen = False
302 # show the user a menu as needed
305 # disconnect users with the appropriate state
306 if self.state == "disconnecting":
309 # the user is unique and not flagged to disconnect
312 # check for input and add it to the queue
315 # there is input waiting in the queue
316 if self.input_queue: muffcmds.handle_user_input(self)
318 def enqueue_input(self):
319 """Process and enqueue any new input."""
321 # check for some input
323 input_data = self.connection.recv(1024)
330 # tack this on to any previous partial
331 self.partial_input += input_data
333 # separate multiple input lines
334 new_input_lines = self.partial_input.split("\n")
336 # if input doesn't end in a newline, replace the
337 # held partial input with the last line of it
338 if not self.partial_input.endswith("\n"):
339 self.partial_input = new_input_lines.pop()
341 # otherwise, chop off the extra null input and reset
342 # the held partial input
344 new_input_lines.pop()
345 self.partial_input = ""
347 # iterate over the remaining lines
348 for line in new_input_lines:
350 # filter out non-printables
351 line = filter(lambda x: x>=' ' and x<='~', line)
353 # strip off extra whitespace
356 # put on the end of the queue
357 self.input_queue.append(line)
359 def new_avatar(self):
360 """Instantiate a new, unconfigured avatar for this user."""
362 counter = muffuniv.universe.internals["counters"].getint("next_actor")
364 muffmisc.log("get next_actor failed")
366 while muffuniv.element_exists("actor:" + repr(counter)): counter += 1
367 muffuniv.universe.internals["counters"].set("next_actor", counter + 1)
368 self.avatar = muffuniv.Element("actor:" + repr(counter), muffconf.get("files", "avatars"), muffuniv.universe)
370 avatars = self.record.get("account", "avatars").split()
373 avatars.append(self.avatar.key)
374 self.record.set("account", "avatars", " ".join(avatars))
376 def list_avatar_names(self):
377 """A test function to list names of assigned avatars."""
379 avatars = self.record.get("account", "avatars").split()
383 for avatar in avatars:
384 avatar_names.append(muffuniv.universe.contents[avatar].get("name"))