LOC-14 System will now push a failuremessage when a user is kicked from a room
LOC-14 System will now push a failuremessage when a user is kicked from a room

The message will be returned to the client just once, and is valid for 5 minutes (though clearance hasn't been implemented yet)

--- a/client/LocalChatClient.py
+++ b/client/LocalChatClient.py
@@ -39,6 +39,7 @@
         self.server = SERVER
         self.room = False
         self.roompass = False
+        self.sesskey = False
         self.gpg = gnupg.GPG()
     
     
@@ -51,7 +52,8 @@
         
         payload = {"roomName": self.room, 
                    "mylast":self.last,
-                   "user": self.user
+                   "user": self.user,
+                   "sesskey": self.sesskey
                    }
         
         request = {"action":"pollMsg",
@@ -66,6 +68,14 @@
         
         if resp['status'] == "unchanged":
             return False
+        
+        
+        if resp['status'] == "errmessage":
+            # Got an error, we need to stop the poll and then return the text
+            self.room = False
+            self.roompass = False
+            self.sesskey = False
+            return [['reversed',resp['text']]]
         
         to_print = []
         # Otherwise, process the messages
@@ -83,7 +93,7 @@
             # but not currently including that info in my curl tests. Also means test that part of the next block
             
             color = "green"
-            upstruser = msgbody['user'] # Temporary placeholder
+            upstruser = i[3]
             
             if upstruser == self.user:
                 # One of our own, change the color
@@ -91,22 +101,33 @@
             
             elif upstruser == "SYSTEM":
                 color = "magenta"
+                if msgbody['verb'] == "sysalert":
+                    color = 'reversed'
+                elif msgbody['verb'] == 'syswarn':
+                    color = 'cyan'
                 
             
             ts = dt.datetime.utcfromtimestamp(i[2]).strftime("[%H:%M:%S]")
             
-            line = [
-                ts, # timestamp
-                "%s>" % (upstruser,), # To be replaced later
-                msgbody['text']
-                ]
+            if msgbody["verb"] == "do":
+                color = 'yellow'
+                line = [
+                    "        ** %s %s **" % (upstruser,msgbody['text'])
+                    ]
+            else:
+                
+                line = [
+                    ts, # timestamp
+                    "%s>" % (upstruser,), # To be replaced later
+                    msgbody['text']
+                    ]
             
             to_print.append([color,' '.join(line)])
         
         return to_print
         
         
-    def sendMsg(self,line):
+    def sendMsg(self,line,verb='say'):
         ''' Send a message 
         '''
         
@@ -119,12 +140,14 @@
         
         msg = {
             'user': self.user,
-            'text': line
+            'text': line,
+            "verb": verb
             }
         
         payload = {"roomName": self.room, 
                    "msg":self.encrypt(json.dumps(msg)),
-                   "user": self.user
+                   "user": self.user,
+                   "sesskey": self.sesskey
                    }
         
         request = {"action":"sendMsg",
@@ -168,6 +191,7 @@
         self.user = user
         self.last = resp['last']
         self.roompass = p[0] # The room password is the first section of the password
+        self.sesskey = resp['session']
         return True
 
 
@@ -180,7 +204,8 @@
             return False
         
         payload = {"roomName": self.room, 
-                   "user": self.user
+                   "user": self.user,
+                   "sesskey": self.sesskey
                    }
         
         request = {"action":"leaveRoom",
@@ -197,6 +222,7 @@
         self.user = False
         self.last = 0
         self.roompass = False
+        self.sesskey = False
         
         return True
                 
@@ -239,6 +265,7 @@
 
         payload = {"roomName": self.room, 
                    "user": self.user,
+                   "sesskey": self.sesskey
                    }
         
         request = {"action":"closeRoom",
@@ -266,7 +293,8 @@
         payload = {"roomName": self.room, 
                    "user": self.user,
                    "invite": user,
-                   "pass": passw
+                   "pass": passw,
+                   "sesskey": self.sesskey
                    }
         
         request = {"action":"inviteUser",
@@ -293,7 +321,8 @@
         
         payload = {"roomName": self.room, 
                    "user": self.user,
-                   "kick": user
+                   "kick": user,
+                   "sesskey": self.sesskey
                    }
         
         request = {"action":action,
@@ -401,6 +430,14 @@
             # It's a command
             cmd = cmd[1:]
             
+            
+            if cmd == "me":
+                #/me [string]
+                r = msg.sendMsg(' '.join(args),'do')
+                if not r:
+                    raise NotInRoom(line)
+                return
+
             
             if cmd == "ban":
                 # /kick [user]
@@ -522,7 +559,7 @@
             
             
             /room invite [user]                                         Invite a user into the current room
-            
+            /me [string]                                                Send an 'action' instead of a message
             
             Room Admin commands:
             
@@ -620,7 +657,10 @@
               ('error', urwid.LIGHT_RED, urwid.BLACK),
               ('green', urwid.DARK_GREEN, urwid.BLACK),
               ('blue', urwid.LIGHT_BLUE, urwid.BLACK),
-              ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), ]
+              ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), 
+              ('yellow', urwid.YELLOW, urwid.BLACK), 
+              ('cyan', urwid.LIGHT_CYAN, urwid.BLACK), 
+              ]
     
     
     def __init__(self, title, command_caption='Message:  (Tab to switch focus to upper frame, where you can scroll text)', cmd_cb=None, max_size=1000):

--- a/server/LocalChat.py
+++ b/server/LocalChat.py
@@ -20,6 +20,8 @@
 import os
 import json
 import bcrypt
+import random
+import string
 
 app = Flask(__name__)
 
@@ -74,6 +76,7 @@
             id INTEGER PRIMARY KEY AUTOINCREMENT,
             ts INTEGER NOT NULL,
             room INTEGER NOT NULL,
+            user NOT NULL,
             msg TEXT NOT NULL
         );
         
@@ -86,6 +89,22 @@
             PRIMARY KEY (username,room)
         );
         
+        
+        CREATE TABLE sessions (
+            username TEXT NOT NULL,
+            sesskey TEXT NOT NULL,
+            PRIMARY KEY(sesskey)
+        );
+        
+        
+        CREATE TABLE failuremsgs (
+            username TEXT NOT NULL,
+            room INTEGER NOT NULL,
+            expires INTEGER NOT NULL,
+            msg TEXT NOT NULL,
+            PRIMARY KEY (username,room)
+        );
+        
         """
         
         self.conn.executescript(sql)
@@ -104,7 +123,7 @@
         
         print reqjson
         if "action" not in reqjson or "payload" not in reqjson:
-            return 400
+            return self.returnFailure(400)
         
         
         # Decrypt the payload
@@ -113,7 +132,7 @@
         try:
             reqjson['payload'] = json.loads(reqjson['payload'])
         except:
-            return 400
+            return self.returnFailure(400)
         
         if reqjson['action'] == "createRoom":
             return self.createRoom(reqjson)
@@ -182,7 +201,7 @@
         required = ['roomName','owner','pass']
         for i in required:
             if i not in reqjson['payload']:
-                return 400
+                return self.returnFailure(400)
         
         print "Creating room %s" % (reqjson['payload'])
         
@@ -196,7 +215,7 @@
             roomid = self.cursor.lastrowid
         except:
             # Probably a duplicate name, but we don't want to give the other end a reason anyway
-            return 500
+            return self.returnFailure(500)
         
         
         # Generate a password hash for the owners password
@@ -224,12 +243,12 @@
         '''
         
         if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload']:
-            return 400
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         
         if not room:
-            return 400
+            return self.returnFailure(400)
         
         
         # Check the requesting user is the admin
@@ -237,18 +256,16 @@
         n = self.cursor.fetchone()
         
         if not n:
-            return 403
-        
-        m = {
-            "user":"SYSTEM",
-            "text":"Room has been closed. Buh-Bye"
-        }
-        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m))) 
-        self.conn.commit()
-        
+            return self.returnFailure(403)
+        
+
+        self.pushSystemMsg("Room has been closed. Buh-Bye",room,'syswarn')
+                
         self.cursor.execute("DELETE FROM users where room=?",(room,))
         self.cursor.execute("DELETE FROM rooms where id=?",(room,))
         self.cursor.execute("DELETE FROM messages where room=?",(room,))
+        self.cursor.execute("DELETE FROM sessions where sesskey like ?", (reqjson['payload']["roomName"] + '-%',))
+        
         self.conn.commit()
         
         return { "status" : "ok" }
@@ -263,26 +280,21 @@
         '''
         
         if "roomName" not in reqjson['payload'] or "pass" not in reqjson['payload'] or "invite" not in reqjson['payload']:
-            return 400
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         
         if not room:
-            return 400
+            return self.returnFailure(400)
         
         if not self.validateUser(reqjson['payload']):
-            return 403
+            return self.returnFailure(403,reqjson['payload'],room)
         
         
         if reqjson['payload']['invite'] == "SYSTEM":
             # Push a notification into the group
-            m = {
-                    "user":"SYSTEM",
-                    "text":"ALERT: User %s tried to invite SYSTEM" % (reqjson['payload']['user'])
-                }
-            self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m))) 
-            self.conn.commit()
-            return 403
+            self.pushSystemMsg("ALERT: User %s tried to invite SYSTEM" % (reqjson['payload']['user']),room,'sysalert')
+            return self.returnFailure(403)
        
         
         # Generate a hash of the submitted password
@@ -292,13 +304,7 @@
         self.cursor.execute("INSERT INTO users (username,room,passhash) values (?,?,?)",(reqjson['payload']['invite'],room,passhash))
         
         # Push a notification into the group
-        m = {
-                "user":"SYSTEM",
-                "text":"User %s invited %s to the room" % (reqjson['payload']['user'],reqjson['payload']['invite'])
-            }
-        
-        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))        
-        self.conn.commit()        
+        self.pushSystemMsg("User %s invited %s to the room" % (reqjson['payload']['user'],reqjson['payload']['invite']),room)
         
         return {
                 "status":'ok'
@@ -313,12 +319,12 @@
         '''
         
         if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload'] or "kick" not in reqjson['payload']:
-            return 400
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         
         if not room:
-            return 400
+            return self.returnFailure(400)
         
         
         # Check the requesting user is the admin
@@ -326,31 +332,26 @@
         n = self.cursor.fetchone()
         
         if not n:
-            return 403
+            return self.returnFailure(403)
         
         
         
         self.cursor.execute("UPDATE users set active=0 where room=? and username=?",(room,reqjson["payload"]["kick"]))
-        m = {
-                "user":"SYSTEM",
-                "text":"User %s kicked %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick'])
-            }
-        
-        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
-        msgid = self.cursor.lastrowid
-        self.conn.commit()
-                    
+        
+        # Delete their session
+        self.cursor.execute("DELETE FROM sessions where username=? and sesskey like ?", (reqjson['payload']['kick'],reqjson['payload']["roomName"] + '-%'))
+        
+        self.pushSystemMsg("User %s kicked %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick']),room,'syswarn')
+        
+        # Push a LOC-14 failure message
+        self.cursor.execute("INSERT INTO failuremsgs (username,room,expires,msg) values (?,?,?,?)",(reqjson['payload']['kick'],room,time.time() + 300,'You have been kicked from the room'))
+        
+        
         if ban:
             # If we're banning them, also need to disinvite them
             self.cursor.execute("DELETE from users where room=? and username=?",(room,reqjson["payload"]["kick"]))
-            m = {
-                    "user":"SYSTEM",
-                    "text":"User %s banned %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick'])
-                }
+            self.pushSystemMsg("User %s banned %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick']),room,'syswarn')
             
-            self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
-            self.conn.commit()
-        
         return { "status" : "ok" }
     
     
@@ -366,17 +367,17 @@
         required = ['roomName','user','userpass']
         for i in required:
             if i not in reqjson['payload']:
-                return 400
+                return self.returnFailure(400)
                 
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         
         if not room:
-            return 400
+            return self.returnFailure(400)
         
         
         if reqjson["payload"]["user"] == "SYSTEM":
-            return 403
+            return self.returnFailure(403)
         
         # Check whether that user is authorised to connect to that room
         self.cursor.execute("SELECT username, room,passhash from users where username=? and room=?",(reqjson['payload']['user'],room))
@@ -389,7 +390,7 @@
         # Now we need to verify they've supplied a correct password for that user
         stored = r[2].encode("utf-8")
         if stored != bcrypt.hashpw(reqjson['payload']['userpass'].encode('utf-8'),stored):
-            return 403
+            return self.returnFailure(403)
         
             
         # Tidy older messages away.
@@ -402,16 +403,8 @@
         
         
         # Push a message to the room to note that the user joined
-        
-        m = {
-                "user":"SYSTEM",
-                "text":"User %s joined the room" % (reqjson['payload']['user'])
-            }
-        
-        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
-        msgid = self.cursor.lastrowid
-        self.conn.commit()
-        
+        msgid = self.pushSystemMsg("User %s joined the room" % (reqjson['payload']['user']),room)
+
         # Check the latest message ID for that room
         self.cursor.execute("SELECT id from messages WHERE room=? and id != ? ORDER BY id DESC",(room,msgid))
         r = self.cursor.fetchone()
@@ -423,41 +416,40 @@
                 
         # Mark the user as active in the users table
         self.cursor.execute("UPDATE users set active=1 where username=? and room=?", (reqjson['payload']['user'],room))
+        
+        
+        # Create a session for the user
+        sesskey = "%s-%s" % (reqjson['payload']["roomName"],self.genSessionKey())
+        self.cursor.execute("INSERT INTO sessions (username,sesskey) values (?,?)", (reqjson['payload']['user'],sesskey))
         self.conn.commit()
-        
-        
-        return {"status":"ok","last":last}
+                
+        return {"status":"ok","last":last,"session":sesskey}
         
         
     def processleaveRoom(self,reqjson):
         ''' Process a user's request to leave a room
         '''
         if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload']:
-            return 400
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         
         if not room:
-            return 400
+            return self.returnFailure(400)
         
         # Check the user is actually in the room and authorised
         if not self.validateUser(reqjson['payload']):
-            return 400
+            return self.returnFailure(400)
         
         # Mark them as not in the room
         self.cursor.execute("UPDATE users set active=0 where username=? and room=?", (reqjson['payload']['user'],room))
         self.conn.commit()
         
+        # Delete their session
+        self.cursor.execute("DELETE FROM sessions where username=? and sesskey = ?", (reqjson['payload']['user'],reqjson['payload']["sesskey"]))
+        
         # Push a message to the room to note they left
-        m = {
-                "user":"SYSTEM",
-                "text":"User %s left the room" % (reqjson['payload']['user'])
-            }
-        
-        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
-        msgid = self.cursor.lastrowid
-        self.conn.commit()        
-        
+        self.pushSystemMsg("User %s left the room" % (reqjson['payload']['user']),room)
         return {"status":"ok"}
     
     
@@ -469,19 +461,19 @@
         '''
         
         if not self.validateUser(reqjson['payload']):
-            return 403
+            return self.returnFailure(403)
         
         
         if "roomName" not in reqjson['payload'] or "msg" not in reqjson['payload']:
-            return 400
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         print room
         if not room:
-            return 400
+            return self.returnFailure(400)
 
             
-        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,reqjson['payload']['msg']))
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user) VALUES (?,?,?,?)",(time.time(),room,reqjson['payload']['msg'],reqjson['payload']['user']))
         msgid = self.cursor.lastrowid
         self.conn.commit()
         
@@ -507,19 +499,20 @@
         curl -v -X POST http://127.0.0.1:8090/ -H "Content-Type: application/json" --data '{"action":"pollMsg","payload":"{\"roomName\":\"BenTest\", \"mylast\":1,\"user\":\"ben2\"}"}'
         
         '''
-        
-        if not self.validateUser(reqjson['payload']):
-            return 403
 
         if "mylast" not in reqjson['payload']:
-            return 400
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         print room
         if not room:
-            return 400        
-        
-        self.cursor.execute("""SELECT id,msg,ts FROM messages
+            return self.returnFailure(400)
+
+
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403,reqjson['payload'],room)
+        
+        self.cursor.execute("""SELECT id,msg,ts,user FROM messages
             WHERE room=? AND
             id > ?
             ORDER BY ts ASC           
@@ -548,9 +541,17 @@
             return False
         
         
+        # Validate the session information
+        self.cursor.execute("SELECT username from sessions where username=? and sesskey=?",(payload['user'],payload['sesskey']))
+        r = self.cursor.fetchone();
+        
+        if not r:
+            return False
+        
+        
         room = self.getRoomID(payload["roomName"])
         if not room:
-            return 400        
+            return False        
         
         
         
@@ -593,6 +594,51 @@
 
 
 
+    def pushSystemMsg(self,msg,room,verb="sysinfo"):
+        ''' Push a message from the SYSTEM user into the queue
+        '''
+        m = {
+            "text":msg,
+            "verb":verb
+        }
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user) VALUES (?,?,?,'SYSTEM')",(time.time(),room,json.dumps(m)))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()
+        return msgid
+
+
+    def returnFailure(self,status,reqjson=False,room=False):
+        ''' For whatever reason, a request isn't being actioned. We need to return a status code
+        
+        However, in some instances, we may allow a HTTP 200 just once in order to send the user
+        information on why their next request will fail 
+        '''
+        
+        # TODO - implement the failure handling stuff
+        
+        if reqjson and room:
+            # Check whether there's a failure message or not 
+            self.cursor.execute("SELECT msg from failuremsgs where username=? and room=?",(reqjson['user'],room))
+            r = self.cursor.fetchone()
+            
+            if not r:
+                # No message to return
+                return status
+            
+            # Otherwise, return the message and remove it
+            self.cursor.execute("DELETE from failuremsgs where username=? and room=?",(reqjson['user'],room))
+            self.conn.commit()
+            return {"status":"errmessage","text":r[0]}
+        
+        
+        return status
+        
+
+
+    def genSessionKey(self,N=128):
+        return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits).encode('utf-8') for _ in range(N))
+
+
     def test(self):
         return ['foo']