1 """User objects for the MUFF Engine"""
3 # Copyright (c) 2005 mudpy, Jeremy Stanley <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."""
26 self.last_address = ""
27 self.connection = None
28 self.authenticated = False
29 self.password_tries = 1
30 self.state = "entering_account_name"
31 self.menu_seen = False
34 self.output_queue = []
35 self.partial_input = ""
41 """Log, save, close the connection and remove."""
42 name = self.account.get("name")
43 if name: message = "User " + name
44 else: message = "An unnamed user"
45 message += " logged out."
47 self.connection.close()
51 """Save, load a new user and relocate the connection."""
53 # unauthenticated connections get the boot
54 if not self.authenticated:
55 muffmisc.log("An unauthenticated user was disconnected during reload.")
56 self.state = "disconnecting"
61 # save and get out of the list
65 # create a new user object
66 new_user = muffuser.User()
68 # give it the same name
69 new_user.account.set("name", self.account.get("name"))
71 # set everything else equivalent
72 new_user.address = self.address
73 new_user.last_address = self.last_address
74 new_user.connection = self.connection
75 new_user.authenticated = self.authenticated
76 new_user.password_tries = self.password_tries
77 new_user.state = self.state
78 new_user.menu_seen = self.menu_seen
79 new_user.error = self.error
80 new_user.input_queue = self.input_queue
81 new_user.output_queue = self.output_queue
82 new_user.partial_input = self.partial_input
83 new_user.echoing = self.echoing
86 muffvars.userlist.append(new_user)
88 # get rid of the old user object
91 def replace_old_connections(self):
92 """Disconnect active users with the same name."""
94 # the default return value
97 # iterate over each user in the list
98 for old_user in muffvars.userlist:
100 # the name is the same but it's not us
101 if old_user.account.get("name") == self.account.get("name") and old_user is not self:
104 muffmisc.log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
105 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
106 self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
108 # close the old connection
109 old_user.connection.close()
111 # replace the old connection with this one
112 old_user.connection = self.connection
113 old_user.last_address = old_user.address
114 old_user.address = self.address
115 old_user.echoing = self.echoing
117 # take this one out of the list and delete
123 # true if an old connection was replaced, false if not
126 def authenticate(self):
127 """Flag the user as authenticated and disconnect duplicates."""
128 if not self.state is "authenticated":
129 muffmisc.log("User " + self.account.get("name") + " logged in.")
130 self.authenticated = True
133 """Send the user their current menu."""
134 if not self.menu_seen:
135 self.menu_choices = muffmenu.get_menu_choices(self)
136 self.send(muffmenu.get_menu(self.state, self.error, self.echoing, self.menu_choices), "")
137 self.menu_seen = True
139 self.adjust_echoing()
141 def adjust_echoing(self):
142 """Adjust echoing to match state menu requirements."""
143 if self.echoing and not muffmenu.menu_echo_on(self.state): self.echoing = False
144 elif not self.echoing and muffmenu.menu_echo_on(self.state): self.echoing = True
147 """Remove a user from the list of connected users."""
148 muffvars.userlist.remove(self)
150 def send(self, output, eol="$(eol)"):
151 """Send arbitrary text to a connected user."""
153 # only when there is actual output
156 # start with a newline, append the message, then end
157 # with the optional eol string passed to this function
158 # and the ansi escape to return to normal text
159 output = "\r\n" + output + eol + chr(27) + "[0m"
161 # find and replace macros in the output
162 output = muffmisc.replace_macros(self, output)
164 # wrap the text at 80 characters
165 # TODO: prompt user for preferred wrap width
166 output = muffmisc.wrap_ansi_text(output, 80)
168 # drop the formatted output into the output queue
169 self.output_queue.append(output)
171 # try to send the last item in the queue, remove it and
172 # flag that menu display is not needed
174 self.connection.send(self.output_queue[0])
175 self.output_queue.remove(self.output_queue[0])
176 self.menu_seen = False
178 # but if we can't, that's okay too
183 """All the things to do to the user per increment."""
185 # if the world is terminating, disconnect
186 if muffvars.terminate_world:
187 self.state = "disconnecting"
188 self.menu_seen = False
190 # show the user a menu as needed
193 # disconnect users with the appropriate state
194 if self.state == "disconnecting":
197 # the user is unique and not flagged to disconnect
200 # check for input and add it to the queue
203 # there is input waiting in the queue
204 if self.input_queue: muffcmds.handle_user_input(self)
206 def enqueue_input(self):
207 """Process and enqueue any new input."""
209 # check for some input
211 input_data = self.connection.recv(1024)
218 # tack this on to any previous partial
219 self.partial_input += input_data
221 # separate multiple input lines
222 new_input_lines = self.partial_input.split("\n")
224 # if input doesn't end in a newline, replace the
225 # held partial input with the last line of it
226 if not self.partial_input.endswith("\n"):
227 self.partial_input = new_input_lines.pop()
229 # otherwise, chop off the extra null input and reset
230 # the held partial input
232 new_input_lines.pop()
233 self.partial_input = ""
235 # iterate over the remaining lines
236 for line in new_input_lines:
238 # filter out non-printables
239 line = filter(lambda x: x>=' ' and x<='~', line)
241 # strip off extra whitespace
244 # put on the end of the queue
245 self.input_queue.append(line)
247 def new_avatar(self):
248 """Instantiate a new, unconfigured avatar for this user."""
249 counter = muffuniv.universe.categories["internal"]["counters"].getint("next_avatar")
250 while "avatar:" + repr(counter + 1) in muffuniv.universe.categories["actor"].keys(): counter += 1
251 muffuniv.universe.categories["internal"]["counters"].set("next_avatar", counter + 1)
252 self.avatar = muffuniv.Element("actor:avatar:" + repr(counter), muffuniv.universe)
253 avatars = self.account.get("avatars").split()
254 avatars.append(self.avatar.key)
255 self.account.set("avatars", " ".join(avatars))
257 def list_avatar_names(self):
258 """A test function to list names of assigned avatars."""
260 avatars = self.account.get("avatars").split()
264 for avatar in avatars:
265 avatar_names.append(muffuniv.universe.contents[avatar].get("name"))