23
23
import tornado.ioloop
24
24
import tornado.iostream
25
25
import tornado.auth
26
import tornado.autoreload
27
import launchpadlib.launchpad
32
35
SERVER = "irc.freenode.net"
34
37
NICK = "JFLbot-dev"
36
CLASSROOM = "#PyTest-klas"
39
CLASSROOM = "##PyTest-klas"
43
ADMIN_TEAM = "ubuntu-nl-mwanzo-team"
46
SOURCE = "http://is.gd/1fdrpR" # https://code.launchpad.net/~jfl-developers/justforlearning/Development; short because of max length real name IRC
49
WEB_INTERFACE = "org.justforlearning.webinterface"
50
IRC_INTERFACE = "org.justforlearning.ircinterface"
53
def __init__(self, *args, **kwargs):
54
super(Event, self).__init__(*args, **kwargs)
56
self._handlers = set()
58
def handle(self, handler):
59
self._handlers.add(handler)
61
def unhandle(self, handler):
62
self._handlers.remove(handler)
64
def emit(self, *args, **kwargs):
65
for handler in self._handlers:
66
handler(*args, **kwargs)
39
68
class ChatHandler(object):
40
69
def __init__(self, *args, **kwargs):
41
70
super(ChatHandler, self).__init__(*args, **kwargs)
72
self.userUpdated = Event()
74
self._questionCounter = 1
45
77
self._clients = set()
46
self._logFile = open("log.txt", "a")
78
self._logFile = open(LOG_FILE, "a")
48
81
def _log(self, *args):
82
args = map(unicode, args)
50
83
args.append("---\n")
51
84
text = u"\n".join(args)
52
85
self._logFile.write(text.encode("UTF-8"))
54
def sendChatMessage(self, poster, message):
55
self._log(poster, message)
57
for client in self._clients:
58
client.sendChatMessage(poster, message)
60
def sendClassMessage(self, message):
63
for client in self._clients:
64
client.sendClassMessage(message)
66
def sendQuestion(self, poster, question):
67
self._log(poster, question)
69
for client in self._clients:
70
client.sendQuestion(self._counter, poster, question)
73
def answerQuestion(self, id, answer):
76
for client in self._clients:
77
client.answerQuestion(id, answer)
79
def sendLink(self, link):
82
for client in self._clients:
83
client.sendLink(id, answer)
87
def sendLog(self, client):
91
for line in open(LOG_FILE):
97
self._sendClient(client, methodName, *args)
103
def _sendClient(self, client, methodName, *args):
104
getattr(client, methodName)(*args)
106
def _send(self, methodName, *args):
107
self._log(methodName, *args)
109
for client in self._clients:
110
self._sendClient(client, methodName, *args)
112
def sendChatMessage(self, *args):
113
self._send("sendChatMessage", *args)
115
def sendClassMessage(self, *args):
116
self._send("sendClassMessage", *args)
118
def askQuestion(self, *args):
120
args.insert(1, self._questionCounter)
121
self._send("askQuestion", *args)
122
self._questionCounter += 1
124
def answerQuestion(self, *args):
125
self._send("answerQuestion", *args)
127
def sendLink(self, *args):
128
self._send("sendLink", *args)
130
def addUser(self, clientName, nickname, role):
131
userId = self._userCounter
132
self._users[userId] = {
133
"client": clientName,
134
"nickname": nickname,
137
self._userCounter += 1
140
def updateUser(self, userId, **kwargs):
141
self._users[userId].update(kwargs)
142
self.userUpdated.emit(userId, kwargs.keys())
144
def removeUser(self, userId):
145
del self._users[userId]
147
def getUser(self, userId):
148
return self._users[userId]
85
150
def addClient(self, client):
87
# file = open("log.txt")
92
# client.send(unicode(line.strip(), "UTF-8"))
93
151
self._clients.add(client)
152
self._users[client] = set()
95
154
def removeClient(self, client):
96
155
self._clients.remove(client)
156
del self._users[client]
98
158
class IrcSocketStream(tornado.iostream.IOStream):
99
def __init__(self, chatHandler, *args, **kwargs):
159
def __init__(self, chatHandler, authorisationHandler, *args, **kwargs):
100
160
s = socket.socket()
101
161
super(IrcSocketStream, self).__init__(s, *args, **kwargs)
103
163
self.chatHandler = chatHandler
105
self.connect((SERVER, PORT), self.connectToIrc)
107
def connectToIrc(self):
109
"PASS %s\r\n" % PASSWORD,
110
"NICK %s\r\n" % NICK,
111
"USER %s irc.freenode.net %s: %s\r\n" % (NICK.lower(), NICK, NICK),
164
self.authorisationHandler = authorisationHandler
166
self._activeNicks = {}
168
self._chatroomOp = False
169
self._classroomOp = False
171
self.connect((SERVER, PORT), self._connectToIrc)
173
def _connectToIrc(self):
174
self.chatHandler.userUpdated.handle(self._updateUser)
176
"PASS %s" % PASSWORD,
178
"USER %s 0 * :Source: %s" % (NICK, SOURCE),
179
"JOIN %s" % CLASSROOM,
180
"JOIN %s" % CHATROOM,
114
#FIXME: something nicer?
115
self.read_until(" MODE ", self.join)
117
def join(self, data):
118
self.write("JOIN %s\r\n" % CLASSROOM)
119
self.write("JOIN %s\r\n" % CHATROOM)
120
self.read_until("\n", self.handleLine)
121
184
self.chatHandler.addClient(self)
185
self.read_until("\n", self._handleLine)
123
def handleLine(self, line):
187
def _handleLine(self, line):
124
188
line = line.strip().split()
125
189
if line[0] == "PING":
126
self.write("PONG %s\r\n" % line[0])
127
if line[1] == "PRIVMSG" and line[2] in (CLASSROOM, CHATROOM):
128
data = " ".join(line[3:])
129
data = unicode(data[1:], "UTF-8") #[1:] strips the ':'
130
if line[2] == CLASSROOM:
131
self.parseClass(data)
134
self.read_until("\n", self.handleLine)
136
def parseChat(self, line):
190
self._handlePing(line)
191
elif line[1] == "MODE":
192
self._handleMode(line)
193
elif line[1] == "JOIN":
194
self._handleJoin(line)
195
elif line[1] == "PRIVMSG":
196
self._handlePrivMsg(line)
197
self.read_until("\n", self._handleLine)
199
def _handlePing(self, line):
200
self.write("PONG %s\r\n" % line[0])
202
def _handleMode(self, line):
204
line[2] in (CHATROOM, CLASSROOM) and
208
if aboutOp and "+" in line[3]:
209
if line[2] == CLASSROOM:
210
self._classroomOp = True
211
self._setupClassroom()
212
elif line[2] == CHATROOM:
213
self._chatroomOp = True
214
elif aboutOp and "-" in line[3]:
215
if line[2] == CLASSROOM:
216
self._classroomOp = False
217
elif line[2] == CHATROOM:
218
self._chatroomOp = False
222
return self._classroomOp and self._chatroomOp
224
def _handleJoin(self, line):
225
nick = line[0].split("!")[0][1:] #[1:] to strip ':'
226
if self._op and nick in self.authorisationHandler.adminIRCNicks:
227
self._voiceNewNick(nick)
229
def _setupClassroom(self):
230
for nick in self.authorisationHandler.adminIRCNicks:
231
self._voiceNewNick(nick)
233
self.write("MODE %s +m\r\n" % CLASSROOM)
235
def _voiceNewNick(self, nick):
236
userId = self._userId(nick)
237
self.chatHandler.updateUser(userId, role="admin")
239
def _userId(self, nick):
240
if nick not in self._activeNicks:
241
userId = self.chatHandler.addUser(IRC_INTERFACE, nick, "user")
242
self._activeNicks[nick] = userId
244
userId = self._activeNicks[nick]
247
def _handlePrivMsg(self, line):
250
nick = line[0].split("!")[0][1:] #[1:] to strip ':'
251
data = " ".join(line[3:])
252
data = unicode(data[1:], "UTF-8") #[1:] strips the ':'
254
userId = self._userId(nick)
256
if line[2] == CLASSROOM:
257
self._parseClass(userId, data)
258
elif line[2] == CHATROOM:
259
self._parseChat(userId, data)
260
elif line[2] == NICK:
261
self._parsePrivate(userId, data)
263
def _parsePrivate(self, userId, line):
264
nick = self.chatHandler.getUser(userId)["nickname"]
266
message = "No documentation yet!"
267
data = u"PRIVMSG %s :%s\r\n" % (nick, message)
268
self.write(data.encode("UTF-8"))
270
def _updateUser(self, userId, keys):
271
user = self.chatHandler.getUser(userId)
272
if not user["client"] == IRC_INTERFACE:
275
if user["role"] == "silenced":
276
text = (2 * u"MODE %s +q %s\r\n") % (CLASSROOM, user["nickname"], CHATROOM, user["nickname"])
277
elif user["role"] == "user":
278
text = (2 * u"MODE %s -q %s\r\n") % (CLASSROOM, user["nickname"], CHATROOM, user["nickname"])
279
text += u"MODE %s -v %s\r\n" % (CLASSROOM, user["nickname"])
280
elif user["role"] == "admin":
281
text = u"MODE %s +v %s\r\n" % (CLASSROOM, user["nickname"])
282
self.write(text.encode("UTF-8"))
284
def _parseChat(self, userId, line):
138
type, message = line.split(":")
286
type, message = line.split(":", 1)
139
287
except ValueError:
142
290
if type == "QUESTION":
143
self.chatHandler.sendQuestion("FIXNICK", message)
145
self.chatHandler.sendChatMessage("FIXNICK", line)
147
def parseClass(self, line):
148
self.chatHandler.sendClassMessage(line)
150
def _send(self, text):
151
self.write(text.encode("UTF-8"))
153
def sendClassMessage(self, message):
154
data = u"PRIVMSG %s :%s\r\n" % (CLASSROOM, message)
157
def sendChatMessage(self, poster, message):
158
data = u"PRIVMSG %s :%s wrote: %s\r\n" % (CHATROOM, poster, message)
161
def sendQuestion(self, id, poster, question):
162
data = u"PRIVMSG %s :#%s %s asked: %s\r\n" % (CLASSROOM, id, poster, question)
165
def answerQuestion(self, id, answer):
166
data = u"PRIVMSG %s :#%s answer: %s\r\n" % (CLASSROOM, id, answer)
169
def sendLink(self, link):
170
data = u"PRIVMSG %s :Link: %s\r\n" % (CLASSROOM, link)
291
self.chatHandler.askQuestion(userId, message)
293
self.chatHandler.sendChatMessage(userId, line)
295
def _parseClass(self, userId, line):
297
type, message = line.split(":", 1)
302
self.chatHandler.sendLink(userId, message)
304
elif type.startswith("ANSWER"):
305
#ANSWER 4: blabla blabla
307
id = type.split(" ", 1)[1]
311
self.chatHandler.answerQuestion(userId, id, message)
313
self.chatHandler.sendClassMessage(userId, line)
315
def _send(self, room, message):
316
message = message.replace(u"\r", u"").replace(u"\n", u"")
317
data = u"PRIVMSG %s :%s\r\n" % (room, message)
318
self.write(data.encode("UTF-8"))
320
def sendClassMessage(self, userId, message):
321
if self.chatHandler.getUser(userId)["client"] == IRC_INTERFACE:
323
self._send(CLASSROOM, message)
325
def sendChatMessage(self, userId, message):
326
user = self.chatHandler.getUser(userId)
327
if user["client"] == IRC_INTERFACE:
329
self._send(CHATROOM, u"%s wrote: %s" % (user["nickname"], message))
331
def askQuestion(self, userId, id, question):
332
user = self.chatHandler.getUser(userId)
333
self._send(CLASSROOM, u"#%s %s asked: %s" % (id, user["nickname"], question))
335
def answerQuestion(self, userId, id, answer):
336
if self.chatHandler.getUser(userId)["client"] == IRC_INTERFACE:
338
self._send(CLASSROOM, u"#%s answer: %s" % (id, answer))
340
def sendLink(self, userId, link):
341
if self.chatHandler.getUser(userId)["client"] == IRC_INTERFACE:
343
data = u"Link: %s\r\n" % (CLASSROOM, link)
172
346
class WebSocket(tornado.websocket.WebSocketHandler):
173
347
def __init__(self, *args, **kwargs):
178
352
self.chatHandler.addClient(self)
353
self.chatHandler.userUpdated.handle(self._updateUser)
180
355
def on_message(self, message):
181
poster = tornado.escape.json_decode(self.get_secure_cookie("user"))["nickname"]
356
userId = int(self.get_secure_cookie("userId"))
357
user = self.chatHandler.getUser(userId)
183
359
type, message = message.split("\n", 1)
184
360
if type == "question":
185
self.chatHandler.sendQuestion(poster, message)
361
if user["role"] in ("admin", "user"):
362
self.chatHandler.askQuestion(userId, message)
363
elif type == "answer":
364
if user["role"] == "admin":
365
self.chatHandler.answerQuestion(userId, -1, message) #FIXME: -1
186
366
elif type == "chat":
187
self.chatHandler.sendChatMessage(poster, message)
367
if user["role"] in ("admin", "user"):
368
self.chatHandler.sendChatMessage(userId, message)
369
elif type == "class":
370
if user["role"] == "admin":
371
self.chatHandler.sendClassMessage(userId, message)
189
373
def on_close(self):
190
374
self.chatHandler.removeClient(self)
376
def _updateUser(self, userId, keys):
377
if userId == self.get_secure_cookie("userId"):
192
380
def _send(self, *args):
193
381
args = map(unicode, args)
383
user = self.chatHandler.getUser(int(args.pop(1)))
384
args.insert(1, user["client"])
385
args.insert(2, user["nickname"])
194
387
data = tornado.escape.xhtml_escape("\n".join(args))
195
388
self.write_message(data)
197
390
sendChatMessage = lambda self, *args: self._send("chat", *args)
198
sendClassMessage = lambda self, *args: self._send(*args)
199
sendLink = lambda sef, *args: self._send("link", *args)
200
sendQuestion = lambda self, *args: self._send("question", *args)
391
sendClassMessage = lambda self, *args: self._send("class", *args)
392
sendLink = lambda self, *args: self._send("link", *args)
393
askQuestion = lambda self, *args: self._send("question", *args)
201
394
answerQuestion = lambda self, *args: self._send("answer", *args)
203
396
class ClientHandler(tornado.web.RequestHandler):
397
def __init__(self, *args, **kwargs):
398
self.chatHandler = kwargs.pop("chatHandler")
399
super(ClientHandler, self).__init__(*args, **kwargs)
205
if not self.get_secure_cookie("user"):
402
if not self.get_secure_cookie("userId"):
206
403
self.redirect("/")
208
user = tornado.escape.json_decode(self.get_secure_cookie("user"))
209
self.render("client.html", nick=user["nickname"])
405
user = self.chatHandler.getUser(int(self.get_secure_cookie("userId")))
406
self.render("pages/client.html", source=SOURCE, **user)
211
408
class NicknameLogin(tornado.web.RequestHandler):
409
def __init__(self, *args, **kwargs):
410
self.chatHandler = kwargs.pop("chatHandler")
411
super(NicknameLogin, self).__init__(*args, **kwargs)
213
user = tornado.escape.json_encode({
214
"nickname": self.get_argument("nickname"),
215
"authenticated": False,
217
self.set_secure_cookie("user", user)
414
nickname = self.get_argument("nickname")
415
userId = self.chatHandler.addUser(WEB_INTERFACE, nickname, "user")
416
self.set_secure_cookie("userId", unicode(userId))
218
418
self.redirect("/client")
220
420
class LaunchpadLogin(tornado.web.RequestHandler, tornado.auth.OpenIdMixin):
221
421
_OPENID_ENDPOINT = "https://login.launchpad.net/+openid"
423
def __init__(self, *args, **kwargs):
424
self.chatHandler = kwargs.pop("chatHandler")
425
self.authorisationHandler = kwargs.pop("authorisationHandler")
426
super(LaunchpadLogin, self).__init__(*args, **kwargs)
223
428
@tornado.web.asynchronous
225
430
if self.get_argument("openid.mode", None):