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 # test for existence of the account dir with os.listdir and os.mkdir to 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                 # an object containing persistent account data
69                 self.record = ConfigParser.SafeConfigParser()
70
71         def quit(self):
72                 """Log, save, close the connection and remove."""
73                 if self.name: message = "User " + self.name
74                 else: message = "An unnamed user"
75                 message += " logged out."
76                 muffmisc.log(message)
77                 self.save()
78                 self.connection.close()
79                 self.remove()
80
81         def reload(self):
82                 """Save, load a new user and relocate the connection."""
83
84                 # unauthenticated connections get the boot
85                 if not self.authenticated:
86                         muffmisc.log("An unauthenticated user was disconnected during reload.")
87                         self.state = "disconnecting"
88
89                 # authenticated users
90                 else:
91
92                         # save and get out of the list
93                         self.save()
94                         self.remove()
95
96                         # create a new user object
97                         new_user = muffuser.User()
98
99                         # give it the same name
100                         new_user.name = self.name
101
102                         # load from file
103                         new_user.load()
104
105                         # set everything else equivalent
106                         new_user.address = self.address
107                         new_user.last_address = self.last_address
108                         new_user.connection = self.connection
109                         new_user.authenticated = self.authenticated
110                         new_user.password_tries = self.password_tries
111                         new_user.state = self.state
112                         new_user.menu_seen = self.menu_seen
113                         new_user.error = self.error
114                         new_user.input_queue = self.input_queue
115                         new_user.output_queue = self.output_queue
116                         new_user.partial_input = self.partial_input
117                         new_user.echoing = self.echoing
118
119                         # add it to the list
120                         muffvars.userlist.append(new_user)
121
122                         # get rid of the old user object
123                         del(self)
124
125         def replace_old_connections(self):
126                 """Disconnect active users with the same name."""
127
128                 # the default return value
129                 return_value = False
130
131                 # iterate over each user in the list
132                 for old_user in muffvars.userlist:
133
134                         # the name is the same but it's not us
135                         if old_user.name == self.name and old_user is not self:
136
137                                 # make a note of it
138                                 muffmisc.log("User " + self.name + " reconnected--closing old connection to " + old_user.address + ".")
139                                 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
140                                 self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
141
142                                 # close the old connection
143                                 old_user.connection.close()
144
145                                 # replace the old connection with this one
146                                 old_user.connection = self.connection
147                                 old_user.last_address = old_user.address
148                                 old_user.address = self.address
149                                 old_user.echoing = self.echoing
150
151                                 # take this one out of the list and delete
152                                 self.remove()
153                                 del(self)
154                                 return_value = True
155                                 break
156
157                 # true if an old connection was replaced, false if not
158                 return return_value
159
160         def authenticate(self):
161                 """Flag the user as authenticated and disconnect duplicates."""
162                 if not self.state is "authenticated":
163                         muffmisc.log("User " + self.name + " logged in.")
164                         self.authenticated = True
165
166         def load(self):
167                 """Retrieve account data from cold storage."""
168
169                 # what the filename for the user account should be
170                 filename = muffconf.get("files", "accounts") + "/" + self.name
171
172                 # try to load the password hash and last connection ipa
173                 try:
174                         self.record.read(filename)
175                         self.passhash = self.record.get("account", "passhash")
176                         self.last_address = self.record.get("account", "last_address", self.address)
177
178                 # if we can't, that's okay too
179                 except:
180                         pass
181
182         def get_passhash(self):
183                 """Retrieve the user's account password hash from storage."""
184
185                 # what the filename for the user account could be
186                 filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
187
188                 # create a temporary account record object
189                 temporary_record = ConfigParser.SafeConfigParser()
190
191                 # try to load the indicated account and get a password hash
192                 try:
193                         temporary_record.read(filename)
194                         self.passhash = temporary_record.get("account", "passhash")
195                         return True
196
197                 # otherwise, the password hash is empty
198                 except:
199                         self.passhash = ""
200                         return False
201
202         def save(self):
203                 """Record account data to cold storage."""
204
205                 # the user account must be authenticated to save
206                 if self.authenticated:
207
208                         # create an account section if it doesn't exist
209                         if not self.record.has_section("account"):
210                                 self.record.add_section("account")
211
212                         # write some in-memory data to the record
213                         self.record.set("account", "name", self.name)
214                         self.record.set("account", "passhash", self.passhash)
215                         self.record.set("account", "last_address", self.address)
216
217                         # the account files live here
218                         account_path = muffconf.get("files", "accounts")
219                         # the filename to which we'll write
220                         filename = account_path + "/" + self.name.lower()
221
222                         # if the directory doesn't exist, create it
223                         try:
224                                 if os.listdir(account_path): pass
225                         except:
226                                 os.mkdir(account_path)
227
228                         # open the user account file for writing
229                         record_file = file(filename, "w")
230
231                         # dump the account data to it
232                         self.record.write(record_file)
233
234                         # close the user account file
235                         record_file.flush()
236                         record_file.close()
237
238                         # set the permissions to 0600
239                         os.chmod(filename, 0600)
240
241         def show_menu(self):
242                 """Send the user their current menu."""
243                 self.send(muffmenu.get_menu(self))
244
245         def remove(self):
246                 """Remove a user from the list of connected users."""
247                 muffvars.userlist.remove(self)
248
249         def send(self, output, eol="$(eol)"):
250                 """Send arbitrary text to a connected user."""
251
252                 # only when there is actual output
253                 if output:
254
255                         # start with a newline, append the message, then end
256                         # with the optional eol string passed to this function
257                         # and the ansi escape to return to normal text
258                         output = "\r\n" + output + eol + chr(27) + "[0m"
259
260                         # find and replace macros in the output
261                         output = muffmisc.replace_macros(self, output)
262
263                         # wrap the text at 80 characters
264                         # TODO: prompt user for preferred wrap width
265                         output = muffmisc.wrap_ansi_text(output, 80)
266
267                         # drop the formatted output into the output queue
268                         self.output_queue.append(output)
269
270                         # try to send the last item in the queue, remove it and
271                         # flag that menu display is not needed
272                         try:
273                                 self.connection.send(self.output_queue[0])
274                                 self.output_queue.remove(self.output_queue[0])
275                                 self.menu_seen = False
276
277                         # but if we can't, that's okay too
278                         except:
279                                 pass
280
281         def pulse(self):
282                 """All the things to do to the user per increment."""
283
284                 # if the world is terminating, disconnect
285                 if muffvars.terminate_world:
286                         self.state = "disconnecting"
287                         self.menu_seen = False
288
289                 # show the user a menu as needed
290                 self.show_menu()
291
292                 # disconnect users with the appropriate state
293                 if self.state == "disconnecting":
294                         self.quit()
295
296                 # the user is unique and not flagged to disconnect
297                 else:
298                 
299                         # check for input and add it to the queue
300                         self.enqueue_input()
301
302                         # there is input waiting in the queue
303                         if self.input_queue: muffcmds.handle_user_input(self)
304
305         def enqueue_input(self):
306                 """Process and enqueue any new input."""
307
308                 # check for some input
309                 try:
310                         input_data = self.connection.recv(1024)
311                 except:
312                         input_data = ""
313
314                 # we got something
315                 if input_data:
316
317                         # tack this on to any previous partial
318                         self.partial_input += input_data
319
320                         # separate multiple input lines
321                         new_input_lines = self.partial_input.split("\n")
322
323                         # if input doesn't end in a newline, replace the
324                         # held partial input with the last line of it
325                         if not self.partial_input.endswith("\n"):
326                                 self.partial_input = new_input_lines.pop()
327
328                         # otherwise, chop off the extra null input and reset
329                         # the held partial input
330                         else:
331                                 new_input_lines.pop()
332                                 self.partial_input = ""
333
334                         # iterate over the remaining lines
335                         for line in new_input_lines:
336
337                                 # filter out non-printables
338                                 line = filter(lambda x: x>=' ' and x<='~', line)
339
340                                 # strip off extra whitespace
341                                 line = line.strip()
342
343                                 # put on the end of the queue
344                                 self.input_queue.append(line)
345