Imported from archive.
[mudpy.git] / lib / muff / muffuser.py
1 """User objects for the MUFF Engine"""
2
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.
5
6 # user accounts are stored in ini-style files supported by ConfigParser
7 import ConfigParser
8
9 # os is used to test for existence of the account dir and, if necessary, make it
10 import os
11
12 # string.replace is used to perform substitutions for color codes and the like
13 import string
14
15 # hack to load all modules in the muff package
16 import muff
17 for module in muff.__all__:
18         exec("import " + module)
19
20 class User:
21         """This is a connected user."""
22
23         def __init__(self):
24                 """Default values for the in-memory user variables."""
25
26                 # the account name
27                 self.name = ""
28
29                 # the password hash
30                 self.passhash = ""
31
32                 # the current client ip address
33                 self.address = ""
34
35                 # the previous client ip address
36                 self.last_address = ""
37
38                 # the current socket connection object
39                 self.connection = None
40
41                 # a flag to denote whether the user is authenticated
42                 self.authenticated = False
43
44                 # number of times password entry has failed during this session
45                 self.password_tries = 1
46
47                 # the current state of the user
48                 self.state = "entering_account_name"
49
50                 # flag to indicate whether a menu has been displayed
51                 self.menu_seen = False
52
53                 # current error condition, if any
54                 self.error = ""
55
56                 # fifo-style queue for lines of user input
57                 self.input_queue = []
58
59                 # fifo-style queue for blocks of user output
60                 self.output_queue = []
61
62                 # holding pen for unterminated user input
63                 self.partial_input = ""
64
65                 # flag to indicate the current echo status of the client
66                 self.echoing = True
67
68                 # the active avatar
69                 self.avatar = None
70
71                 # an object containing persistent account data
72                 self.record = ConfigParser.SafeConfigParser()
73
74         def quit(self):
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."
79                 muffmisc.log(message)
80                 self.save()
81                 self.connection.close()
82                 self.remove()
83
84         def reload(self):
85                 """Save, load a new user and relocate the connection."""
86
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"
91
92                 # authenticated users
93                 else:
94
95                         # save and get out of the list
96                         self.save()
97                         self.remove()
98
99                         # create a new user object
100                         new_user = muffuser.User()
101
102                         # give it the same name
103                         new_user.name = self.name
104
105                         # load from file
106                         new_user.load()
107
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
121
122                         # add it to the list
123                         muffvars.userlist.append(new_user)
124
125                         # get rid of the old user object
126                         del(self)
127
128         def replace_old_connections(self):
129                 """Disconnect active users with the same name."""
130
131                 # the default return value
132                 return_value = False
133
134                 # iterate over each user in the list
135                 for old_user in muffvars.userlist:
136
137                         # the name is the same but it's not us
138                         if old_user.name == self.name and old_user is not self:
139
140                                 # make a note of it
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)")
144
145                                 # close the old connection
146                                 old_user.connection.close()
147
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
153
154                                 # take this one out of the list and delete
155                                 self.remove()
156                                 del(self)
157                                 return_value = True
158                                 break
159
160                 # true if an old connection was replaced, false if not
161                 return return_value
162
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
168
169         def load(self):
170                 """Retrieve account data from cold storage."""
171
172                 # what the filename for the user account should be
173                 filename = muffconf.get("files", "accounts") + "/" + self.name
174
175                 # try to load the password hash and last connection ipa
176                 try:
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)
180
181                 # if we can't, that's okay too
182                 except:
183                         pass
184
185         def get_passhash(self):
186                 """Retrieve the user's account password hash from storage."""
187
188                 # what the filename for the user account could be
189                 filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
190
191                 # create a temporary account record object
192                 temporary_record = ConfigParser.SafeConfigParser()
193
194                 # try to load the indicated account and get a password hash
195                 try:
196                         temporary_record.read(filename)
197                         self.passhash = temporary_record.get("account", "passhash")
198                         return True
199
200                 # otherwise, the password hash is empty
201                 except:
202                         self.passhash = ""
203                         return False
204
205         def save(self):
206                 """Record account data to cold storage."""
207
208                 # the user account must be authenticated to save
209                 if self.authenticated:
210
211                         # create an account section if it doesn't exist
212                         if not self.record.has_section("account"):
213                                 self.record.add_section("account")
214
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)
219
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()
224
225                         # open the user account file for writing
226                         try:
227                                 record_file = file(filename, "w")
228
229                         # if the directory doesn't exist, create it first
230                         except IOError:
231                                 os.makedirs(account_path)
232                                 record_file = file(filename, "w")
233
234                         # dump the account data to it
235                         self.record.write(record_file)
236
237                         # close the user account file
238                         record_file.flush()
239                         record_file.close()
240
241                         # set the permissions to 0600
242                         os.chmod(filename, 0600)
243
244         def show_menu(self):
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
250                         self.error = False
251                         self.adjust_echoing()
252
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
257
258         def remove(self):
259                 """Remove a user from the list of connected users."""
260                 muffvars.userlist.remove(self)
261
262         def send(self, output, eol="$(eol)"):
263                 """Send arbitrary text to a connected user."""
264
265                 # only when there is actual output
266                 #if output:
267
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"
272
273                 # find and replace macros in the output
274                 output = muffmisc.replace_macros(self, output)
275
276                 # wrap the text at 80 characters
277                 # TODO: prompt user for preferred wrap width
278                 output = muffmisc.wrap_ansi_text(output, 80)
279
280                 # drop the formatted output into the output queue
281                 self.output_queue.append(output)
282
283                 # try to send the last item in the queue, remove it and
284                 # flag that menu display is not needed
285                 try:
286                         self.connection.send(self.output_queue[0])
287                         self.output_queue.remove(self.output_queue[0])
288                         self.menu_seen = False
289
290                 # but if we can't, that's okay too
291                 except:
292                         pass
293
294         def pulse(self):
295                 """All the things to do to the user per increment."""
296
297                 # if the world is terminating, disconnect
298                 if muffvars.terminate_world:
299                         self.state = "disconnecting"
300                         self.menu_seen = False
301
302                 # show the user a menu as needed
303                 self.show_menu()
304
305                 # disconnect users with the appropriate state
306                 if self.state == "disconnecting":
307                         self.quit()
308
309                 # the user is unique and not flagged to disconnect
310                 else:
311                 
312                         # check for input and add it to the queue
313                         self.enqueue_input()
314
315                         # there is input waiting in the queue
316                         if self.input_queue: muffcmds.handle_user_input(self)
317
318         def enqueue_input(self):
319                 """Process and enqueue any new input."""
320
321                 # check for some input
322                 try:
323                         input_data = self.connection.recv(1024)
324                 except:
325                         input_data = ""
326
327                 # we got something
328                 if input_data:
329
330                         # tack this on to any previous partial
331                         self.partial_input += input_data
332
333                         # separate multiple input lines
334                         new_input_lines = self.partial_input.split("\n")
335
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()
340
341                         # otherwise, chop off the extra null input and reset
342                         # the held partial input
343                         else:
344                                 new_input_lines.pop()
345                                 self.partial_input = ""
346
347                         # iterate over the remaining lines
348                         for line in new_input_lines:
349
350                                 # filter out non-printables
351                                 line = filter(lambda x: x>=' ' and x<='~', line)
352
353                                 # strip off extra whitespace
354                                 line = line.strip()
355
356                                 # put on the end of the queue
357                                 self.input_queue.append(line)
358
359         def new_avatar(self):
360                 """Instantiate a new, unconfigured avatar for this user."""
361                 try:
362                         counter = muffuniv.universe.internals["counters"].getint("next_actor")
363                 except:
364                         muffmisc.log("get next_actor failed")
365                         counter = 1
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)
369                 try:
370                         avatars = self.record.get("account", "avatars").split()
371                 except:
372                         avatars = []
373                 avatars.append(self.avatar.key)
374                 self.record.set("account", "avatars", " ".join(avatars))
375
376         def list_avatar_names(self):
377                 """A test function to list names of assigned avatars."""
378                 try:
379                         avatars = self.record.get("account", "avatars").split()
380                 except:
381                         avatars = []
382                 avatar_names = []
383                 for avatar in avatars:
384                         avatar_names.append(muffuniv.universe.contents[avatar].get("name"))
385                 return avatar_names
386