1ac01661fc786747b969cf71ea5d3527d037312d
[mudpy.git] / 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                 self.address = ""
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
32                 self.error = ""
33                 self.input_queue = []
34                 self.output_queue = []
35                 self.partial_input = ""
36                 self.echoing = True
37                 self.avatar = None
38                 self.account = None
39
40         def quit(self):
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."
46                 muffmisc.log(message)
47                 self.connection.close()
48                 self.remove()
49
50         def reload(self):
51                 """Save, load a new user and relocate the connection."""
52
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"
57
58                 # authenticated users
59                 else:
60
61                         # save and get out of the list
62                         self.save()
63                         self.remove()
64
65                         # create a new user object
66                         new_user = muffuser.User()
67
68                         # give it the same name
69                         new_user.account.set("name", self.account.get("name"))
70
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
84
85                         # add it to the list
86                         muffvars.userlist.append(new_user)
87
88                         # get rid of the old user object
89                         del(self)
90
91         def replace_old_connections(self):
92                 """Disconnect active users with the same name."""
93
94                 # the default return value
95                 return_value = False
96
97                 # iterate over each user in the list
98                 for old_user in muffvars.userlist:
99
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:
102
103                                 # make a note of it
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)")
107
108                                 # close the old connection
109                                 old_user.connection.close()
110
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
116
117                                 # take this one out of the list and delete
118                                 self.remove()
119                                 del(self)
120                                 return_value = True
121                                 break
122
123                 # true if an old connection was replaced, false if not
124                 return return_value
125
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
131
132         def show_menu(self):
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
138                         self.error = False
139                         self.adjust_echoing()
140
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
145
146         def remove(self):
147                 """Remove a user from the list of connected users."""
148                 muffvars.userlist.remove(self)
149
150         def send(self, output, eol="$(eol)"):
151                 """Send arbitrary text to a connected user."""
152
153                 # only when there is actual output
154                 #if output:
155
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"
160
161                 # find and replace macros in the output
162                 output = muffmisc.replace_macros(self, output)
163
164                 # wrap the text at 80 characters
165                 # TODO: prompt user for preferred wrap width
166                 output = muffmisc.wrap_ansi_text(output, 80)
167
168                 # drop the formatted output into the output queue
169                 self.output_queue.append(output)
170
171                 # try to send the last item in the queue, remove it and
172                 # flag that menu display is not needed
173                 try:
174                         self.connection.send(self.output_queue[0])
175                         self.output_queue.remove(self.output_queue[0])
176                         self.menu_seen = False
177
178                 # but if we can't, that's okay too
179                 except:
180                         pass
181
182         def pulse(self):
183                 """All the things to do to the user per increment."""
184
185                 # if the world is terminating, disconnect
186                 if muffvars.terminate_world:
187                         self.state = "disconnecting"
188                         self.menu_seen = False
189
190                 # show the user a menu as needed
191                 self.show_menu()
192
193                 # disconnect users with the appropriate state
194                 if self.state == "disconnecting":
195                         self.quit()
196
197                 # the user is unique and not flagged to disconnect
198                 else:
199                 
200                         # check for input and add it to the queue
201                         self.enqueue_input()
202
203                         # there is input waiting in the queue
204                         if self.input_queue: muffcmds.handle_user_input(self)
205
206         def enqueue_input(self):
207                 """Process and enqueue any new input."""
208
209                 # check for some input
210                 try:
211                         input_data = self.connection.recv(1024)
212                 except:
213                         input_data = ""
214
215                 # we got something
216                 if input_data:
217
218                         # tack this on to any previous partial
219                         self.partial_input += input_data
220
221                         # separate multiple input lines
222                         new_input_lines = self.partial_input.split("\n")
223
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()
228
229                         # otherwise, chop off the extra null input and reset
230                         # the held partial input
231                         else:
232                                 new_input_lines.pop()
233                                 self.partial_input = ""
234
235                         # iterate over the remaining lines
236                         for line in new_input_lines:
237
238                                 # filter out non-printables
239                                 line = filter(lambda x: x>=' ' and x<='~', line)
240
241                                 # strip off extra whitespace
242                                 line = line.strip()
243
244                                 # put on the end of the queue
245                                 self.input_queue.append(line)
246
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))
256
257         def list_avatar_names(self):
258                 """A test function to list names of assigned avatars."""
259                 try:
260                         avatars = self.account.get("avatars").split()
261                 except:
262                         avatars = []
263                 avatar_names = []
264                 for avatar in avatars:
265                         avatar_names.append(muffuniv.universe.contents[avatar].get("name"))
266                 return avatar_names
267