-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmain.py
371 lines (346 loc) · 15.3 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
import os
import hashlib
import urllib
import logging
import re
import json
import webapp2
import jinja2
from google.appengine.api import channel as channel_api # 'channel' is kind of ambiguous in context
from google.appengine.ext import db
from google.appengine.api import memcache
# This section will eventually get moved to a Handler class
template_dir = os.path.join(
os.path.dirname(__file__), 'templates')
jinja_env = jinja2.Environment(
loader = jinja2.FileSystemLoader(template_dir),
autoescape = True)
def render_str(template, **params):
'''Returns a string of the rendered template'''
t = jinja_env.get_template(template)
return t.render(params)
def render(template, **kw):
'''Render using the template and parameters'''
return(render_str(template, **kw))
# End Handler
class ChatUser(db.Model):
'''A user'''
# key_name is username.lower()
username = db.StringProperty(required = True) # case SeNsItIvE
joined = db.DateTimeProperty(auto_now_add = True)
identifier = db.StringProperty(required = True) # Session specific
startingchannel = db.TextProperty(required = False)
channels = db.TextProperty(required = True) # JSON array
contacts = db.TextProperty(required = True) # JSON array
connected = db.BooleanProperty(default = False)
def store(self):
'''Store in memcache and datastore'''
memcache.set(user_key(self.key().name()), self)
self.put()
def get_contact_names(self):
'''Get the usernames of this user's contacts'''
# Remove nonexistent contacts
## contactnames = json.loads(self.contacts)
## changed = False
## for contactname in contactnames:
## contact = get_user(contactname)
## if not contact:
## contactnames = [c for c in contactnames if c != contactname]
## changed = True
## if changed:
## self.contacts = json.dumps(contactnames)
## self.store()
## return contactnames
return json.loads(self.contacts)
def add_contact(self, contactname):
'''Add a username to this user's contacts'''
contacts = json.loads(self.contacts)
if contactname not in contacts:
contacts.append(contactname)
self.contacts = json.dumps(contacts)
self.store()
def remove_contact(self, contactname):
'''Remove a username from this user's contacts'''
contacts = json.loads(self.contacts)
if contactname in contacts:
contacts.remove(contactname) # Should only ever be one contactname
self.contacts = json.dumps(contacts)
self.store()
def get_channel_names(self):
'''Returns a list of the names of all channels this user is in'''
return json.loads(self.channels)
def add_channel(self, channelname):
'''Adds a channel to this user's channel list'''
logging.info("user: %s joined channel %s"%(self.username, channelname))
channels = json.loads(self.channels)
if channelname not in channels:
channels.append(channelname)
self.channels = json.dumps(channels)
self.store()
def remove_channel(self, channelname):
'''Removes a channel from this user's channel list'''
channels = json.loads(self.channels)
if channelname in channels:
channels.remove(channelname)
self.channels = json.dumps(channels)
self.store()
class ChatChannel(db.Model):
'''A chat channel'''
# key_name is channelname.lower()
channelname = db.StringProperty(required = True) # case SeNsItIvE
created = db.DateTimeProperty(auto_now_add = True)
users = db.TextProperty(required = False) # JSON array
def store(self):
'''Store in memcache and datastore'''
memcache.set(channelkey(self.key().name()), self)
self.put()
def get_user_names(self):
'''Returns a list of all the users in this channel'''
return json.loads(self.users)
def add_user(self, username):
'''Adds a username to this channel's user list'''
users = json.loads(self.users)
if username not in users:
users.append(username)
self.users = json.dumps(users)
self.store()
def remove_user(self, username):
'''Removes a username from this channel's user list'''
users = json.loads(self.users)
if username in users:
users.remove(username)
self.users = json.dumps(users)
self.store()
class Communication(webapp2.RequestHandler):
'''Deals with chat traffic'''
def post(self):
# Client MUST ENFORCE correct syntax for commands, and ensure usernames and channelnames exist
# Perhaps that's a dangerous statement?
message = urllib.unquote(self.request.get('message'))
username = urllib.unquote(self.request.get('username'))
identifier = urllib.unquote(self.request.get('identifier'))
logging.info("Username: "+username+" Message: "+message)
if not (message and username and identifier):
return
user = get_user(username)
if not user or (user and identifier != user.identifier):
return
COMMANDS = {"JOIN": user_join,
"LEAVE": user_leave,
"PRIVMSG": user_privmsg,
"CHANNELMSG": user_channelmsg,
"QUIT": user_quit,
"PING": user_ping,
"PONG": user_pong
}
command = message.split(' ')[0].upper()
arg = '' if ' ' not in message else message[message.index(' ')+1:]
if command in COMMANDS:
COMMANDS[command](username, arg)
else:
channel_api.send_message(username, "NOTICE Command not supported by server: "+message) # Echo, for testing
def user_join(username, channelname):
user = get_user(username)
username = user.username # Use actual name
channel = get_channel(channelname)
if not channel:
if channelname and not re.compile(r'^#[\w]{3,20}$').match(channelname):
channelerror="Channel must consist of 3-20 alpha_numeric characters and start with a #"
channel_api.send_message(username, "NOTICE "+channelerror)
return
channel = ChatChannel(key_name=channelname.lower(),
channelname=channelname,
users=json.dumps([ ])
)
channelname = channel.channelname # Use actual name
channel.add_user(username)
user.add_channel(channelname)
userlist = ' '.join(channel.get_user_names())
channel_api.send_message(username, "USERS "+channelname+" "+userlist) # Tell the user who is in the channel
for u in channel.get_user_names():
# Tell the individual channel members that the new user joined
channel_api.send_message(u, "JOINED "+username+" "+channelname)
logging.warning("End of JOIN, %s's channels are: "%username+', '.join(get_user(username).get_channel_names()))
def user_leave(username, channelname):
user = get_user(username)
username = user.username # Use actual name
channel = get_channel(channelname)
channelname = channel.channelname # Use actual name
channel.remove_user(username)
# Do we inform the user they have successfully left?
# If we do, then that may cause a closed tab to reopen, better for client to handle that
for u in channel.get_user_names():
channel_api.send_message(u, "LEFT "+username+" "+channelname)
def user_quit(username, args):
'''User has quit'''
# This may take a while to execute
user = get_user(username)
username = user.username # Use actual name
user.connected = False
for channelname in user.get_channel_names():
# Remove the user from channel
channel = get_channel(channelname)
channel.remove_user(username) # Do this first to prevent infinite loops
for u in channel.get_user_names():
# Let the people in the channel know
channel_api.send_message(u, "QUIT "+username+" "+channelname)
for contactname in user.get_contact_names():
# Remove user from their contacts' contact lists
# Possible double-handling, but that's ok because remove_contact checks for that
contact = get_user(contact)
if contact:
contact.remove_contact(username)
try:
channel_api.send_message(username, "NOTICE You have quit")
except:
# POKEMON exception!
# Not really needed since send_message does not throw exceptions
pass
user.store()
# Let the disconnect handler deal with clearing the user object
def user_privmsg(username, args):
'''Private message from one user to another'''
recipientname = args.split(' ')[0]
message = args[args.index(' ')+1:]
recipient = get_user(recipientname)
user = get_user(username)
username = user.username # Use actual name
if recipient and user:
recipientname = recipient.username # Use actual name
channel_api.send_message(recipientname, "PRIVMSG "+username+" "+message)
# Add sender and recipient to each other's contact lists to inform of e.g. quittage
recipient.add_contact(username)
user.add_contact(recipientname)
else:
channel_api.send_message(username, "NOTICE "+recipientname+" is not a valid user. Maybe they disconnected "+
"or maybe you need to check your spelling")
def user_channelmsg(username, args):
channelname = args.split(' ')[0]
message = args[args.index(' ')+1:]
channel = get_channel(channelname)
user = get_user(username)
username = user.username # Use actual name
channelname = channel.channelname # Use actual name
if channel and username in channel.get_user_names():
for u in channel.get_user_names():
# Send message to all the users in the channel
channel_api.send_message(u, "CHANNELMSG "+channelname+" "+username+" "+message)
else:
channel_api.send_message(username, "NOTICE "+channelname+" does not appear to be a channel "+
"(or it is a channel, and you're not in it, somehow).")
def user_ping(username, args):
user = get_user(username)
username = user.username # Use actual name
channel_api.send_message(username, "PONG "+args)
def user_pong(username, args):
# Do nothing, since we have not yet implemented PING/PONG
pass
class Connect(webapp2.RequestHandler):
def post(self):
# Not everything causes a proper disconnect
username = self.request.get('from')
user = get_user(username)
if user and user.startingchannel:
user_join(username, user.startingchannel) # Join default channel
user = get_user(username) # To prevent overwriting
user.connected = True
user.store()
logging.info("Connected: "+username)
logging.warning("End of CONNECT, %s's channels are: "%username+', '.join(get_user(username).get_channel_names()))
class Disconnect(webapp2.RequestHandler):
def post(self):
# Have to propagate to all the channels the user was in
username = self.request.get("from")
user = get_user(username)
if user.connected:
user_quit(username, "")
clear_user(username)
logging.info("Disconnected: "+username)
def user_key(username):
'''user_key function is for key consistency'''
return "user/"+username.lower()
def get_user(username):
'''Get a user from memcache or datastore, returns None if user does not exist'''
key = user_key(username)
user = memcache.get(key)
if not user:
user = ChatUser.get_by_key_name(username.lower())
if user:
memcache.set(key, user)
else:
memcache.set(key, "placeholder to reduce memcache misses")
if isinstance(user, ChatUser):
return user
def clear_user(username):
'''Removes a user from datastore and from memcache'''
user = get_user(username)
if user:
user.delete()
memcache.set(user_key(username), "placeholder to reduce memcache misses")
logging.info("Removed "+username)
def channelkey(channelname):
'''For consistency'''
return "channel/"+channelname.lower()
def get_channel(channelname):
'''Get a channel from memcache or datastore, returns None if channel does not exist'''
key = channelkey(channelname)
channel = memcache.get(key)
if not channel:
channel = ChatChannel.get_by_key_name(channelname.lower())
if channel:
memcache.set(key, channel)
else:
memcache.set(key, "placeholder to reduce memcache misses")
if isinstance(channel, ChatChannel):
return channel
def clear_channel(channelname):
'''Removes a channel from datastore and from memcache'''
channel = get_channel(channelname)
if channel:
channel.delete()
memcache.set(channelkey(channelname), "placeholder to reduce memcache misses")
class Main(webapp2.RequestHandler):
def get(self):
'''Show connection page'''
self.response.out.write(render("main.html", channel="#udacity"))
def post(self):
'''Displays chat UI'''
username = self.request.get('username')
channelname = self.request.get('channel')
usernameerror = ""
if not username:
usernameerror="Please enter a username"
elif not re.compile(r'^[a-zA-Z0-9_-]{3,20}$').match(username):
usernameerror = "Username must consist of 3-20 alphanumeric characters."
elif get_user(username):
usernameerror="Username already in use"
channelerror = ""
if channelname and not re.compile(r'^#[\w]{3,20}$').match(channelname):
channelerror="Channel must consist of 3-20 alpha_numeric characters and start with a #"
if len(usernameerror+channelerror) > 0:
self.response.out.write(render("main.html",
username=username,
usernameerror=usernameerror,
channel=channelname,
channelerror=channelerror))
else:
token = channel_api.create_channel(username) # Expires after 120 minutes
identifier = os.urandom(16).encode('hex')
user = ChatUser(key_name=username.lower(),
username=username,
identifier=identifier,
startingchannel=channelname,
connected=True,
contacts=json.dumps([ ]),
channels=json.dumps([ ]))
user.store()
self.response.out.write(render("chat.html", token=token,
username=username,
identifier=identifier,
server="!AwesomeServer"))
app = webapp2.WSGIApplication([
('/', Main),
('/communication', Communication),
('/_ah/channel/connected/?', Connect),
('/_ah/channel/disconnected/?', Disconnect)
], debug=True)