Adding help output. SC-2 v0.0.1a
Adding help output. SC-2

This brings us (more or less) up to the point of a PoC for SC-2.

Although there's currently no E2E encryption and the auto-room purging isn't in place (apart from when a new user joins a room), messages can now flow back and forth.

It's not currently possible to close a room either, that needs to come soon.

But, there's enough functionality in place that it seems worth setting up a JIRA project for future adjustments/improvements. Auth is going to need doing at some point soon and it'd be good to have the decisions made on that properly documented.

This commit will be tagged as being v0.0.1a

--- a/client/LocalChatClient.py
+++ b/client/LocalChatClient.py
@@ -28,9 +28,9 @@
     
     def __init__(self):
         self.last = 0
-        self.user = USER
+        self.user = False
         self.server = SERVER
-        self.room = ROOMNAME
+        self.room = False
     
     
     def pollForMessage(self):
@@ -62,24 +62,28 @@
         # Otherwise, process the messages
         for i in resp["messages"]:
             self.last = i[0]
-            msgbody = self.decrypt(i[1])
+            msgbody = json.loads(self.decrypt(i[1]))
             
             # TODO - We'll need to json decode and extract the sending user's name
             # but not currently including that info in my curl tests. Also means test that part of the next block
             
             color = "green"
-            upstruser = 'foo' # Temporary placeholder
-            
-            if upstruser == USER:
+            upstruser = msgbody['user'] # Temporary placeholder
+            
+            if upstruser == self.user:
                 # One of our own, change the color
-                color = "red"
+                color = "blue"
+            
+            elif upstruser == "SYSTEM":
+                color = "magenta"
+                
             
             ts = dt.datetime.utcfromtimestamp(i[2]).strftime("[%H:%M:%S]")
             
             line = [
                 ts, # timestamp
                 "%s>" % (upstruser,), # To be replaced later
-                msgbody
+                msgbody['text']
                 ]
             
             to_print.append([color,' '.join(line)])
@@ -87,6 +91,152 @@
         return to_print
         
         
+    def sendMsg(self,line):
+        ''' Send a message 
+        '''
+        
+        if not self.room:
+            # We're not in a room. Abort
+            return False
+        
+        # Otherwise, build a payload
+        
+        
+        msg = {
+            'user': self.user,
+            'text': line
+            }
+        
+        payload = {"roomName": self.room, 
+                   "msg":self.encrypt(json.dumps(msg)),
+                   "user": self.user
+                   }
+        
+        request = {"action":"sendMsg",
+                   "payload": json.dumps(payload)
+                   }
+
+        resp = self.sendRequest(request)        
+        
+        if resp['status'] == "ok":
+            return True
+        
+        return False
+        
+
+
+    def joinRoom(self,user,room,passw):
+        ''' Join a room
+        '''
+        
+        # TODO - this functionality isn't on the 
+        # backend yet, so haven't defined the hashing mechanism etc
+        passhash = self.hashpw(passw)
+        
+        payload = {"roomName": room, 
+                   "passhash": passhash,
+                   "user": user
+                   }
+        
+        request = {"action":"joinRoom",
+                   "payload": json.dumps(payload)
+                   }        
+
+
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        
+        self.room = room
+        self.user = user
+        self.last = resp['last']
+        return True
+
+
+
+
+    def leaveRoom(self):
+        ''' Leave the current room
+        '''
+        if not self.room:
+            return False
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user
+                   }
+        
+        request = {"action":"leaveRoom",
+                   "payload": json.dumps(payload)
+                   }        
+
+
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        self.room = False
+        self.user = False
+        self.last = 0
+        
+        return True
+                
+
+
+    def createRoom(self,room,passw,user=False):
+        ''' Create a new room
+        '''
+        
+        if not user and not self.user:
+            return False
+        
+        if not user:
+            user = self.user
+        
+        # Room passwords may well go way at some point, but honour the
+        # api structure for now
+        passhash = self.hashpw(passw)
+        
+        payload = {"roomName": room, 
+                   "owner": user,
+                   "passhash": passhash
+                   }
+        
+        request = {"action":"createRoom",
+                   "payload": json.dumps(payload)
+                   }        
+        
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return resp['name']
+    
+
+
+    def inviteUser(self,room,passw,user):
+        ''' Invite a user into a room
+        
+        #TODO - Authentication
+        '''
+        
+        payload = {"roomName": room, 
+                   "user": user,
+                   }
+        
+        request = {"action":"inviteUser",
+                   "payload": json.dumps(payload)
+                   }    
+        
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return True
 
 
     def sendRequest(self,data):
@@ -109,12 +259,33 @@
         return msg
     
 
-
-
-
-class UnknownCommand(Exception):
+    def encrypt(self,msg):
+        ''' Placeholder
+        '''
+        return msg
+
+
+    def hashpw(self,passw):
+        ''' Placeholder
+        '''
+        return passw
+
+
+
+class NotInRoom(Exception):
     def __init__(self,cmd):
-        Exception.__init__(self,'Uknown command: %s'%cmd)
+        Exception.__init__(self,'Message not sent')
+
+
+class UnableTo(Exception):
+    def __init__(self,action,cmd):
+        Exception.__init__(self,'Could not %s: %s' % (action,cmd))
+
+
+class InvalidCommand(Exception):
+    def __init__(self,cmd):
+        Exception.__init__(self,'Command is invalid: %s' % (cmd,))
+
 
 class Command(object):
     """ Base class to manage commands in commander
@@ -124,11 +295,73 @@
     def __init__(self,quit_commands=['q','quit','exit'], help_commands=['help','?', 'h']):
         self._quit_cmd=quit_commands
         self._help_cmd=help_commands
+        self._controlcommands = [
+            "join","leave","room"
+            
+            
+            ]
         
     def __call__(self,line):
+        global msg
         tokens=line.split()
         cmd=tokens[0].lower()
         args=tokens[1:]
+        
+        if cmd[0] == "/":
+            # It's a command
+            cmd = cmd[1:]
+            
+            if cmd == "join":
+                # /join [username] [room] [password]
+                
+                if len(args) < 3:
+                    raise InvalidCommand(line)
+                    return
+                
+                if not msg.joinRoom(args[0],args[1],args[2]):
+                    raise UnableTo('join',line)
+                return
+            
+            if cmd == "leave":
+                # /leave
+                if not msg.leaveRoom():
+                    raise UnableTo('leave',line)
+                    return
+                
+                global c
+                c.output('Left the room','magenta')
+                return
+        
+            if cmd == "room":
+                # /room [create|invite] [roomname] [roompass] [[user]]
+                if args[0] == "create":
+                    
+                    if len(args) < 4:
+                        args[3] = False
+                    
+                    n = msg.createRoom(args[1],args[2],args[3])
+                    if not n:
+                        raise UnableTo('create room',line)
+                        return
+                    
+                    global c
+                    c.output('Created Room %s' %(n))
+                    return
+                
+                elif args[0] == "invite":
+                    if len(args) < 4:
+                        raise InvalidCommand(line)
+                    
+                    n = msg.inviteUser(args[1],args[2],args[3])
+                    if not n:
+                        raise UnableTo('invite user',line)
+                        return
+                    
+                    global c
+                    c.output('User %s may now join room %s' %(args[1],args[3]))
+                    return                                        
+                    
+
         if cmd in self._quit_cmd:
             return Commander.Exit
         elif cmd in self._help_cmd:
@@ -136,16 +369,27 @@
         elif hasattr(self, 'do_'+cmd):
             return getattr(self, 'do_'+cmd)(*args)
         else:
-            raise UnknownCommand(cmd)
+            # If it's not a command, then we're trying to send a message
+            r = msg.sendMsg(line)
+            if not r:
+                raise NotInRoom(line)
+            
         
     def help(self,cmd=None):
         def std_help():
             qc='|'.join(self._quit_cmd)
             hc ='|'.join(self._help_cmd)
-            res='Type [%s] command_name to get more help about particular command\n' % hc
-            res+='Type [%s] to quit program\n' % qc
-            cl=[name[3:] for name in dir(self) if name.startswith('do_') and len(name)>3]
-            res += 'Available commands: %s' %(' '.join(sorted(cl)))
+            res='Type [%s] to quit program\n' % qc
+            res += """Available commands: 
+            
+            /join [username] [room] [password]                          Join a room
+            /leave                                                      Leave current room
+            /room [create|invite] [roomname] [roompass] [[user]]        New room management 
+            
+            Once in a room, to send a message, just type it.
+            
+            
+            """
             return res
         if not cmd:
             return std_help()
@@ -235,7 +479,7 @@
               ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), ]
     
     
-    def __init__(self, title, command_caption='Command:  (Tab to switch focus to upper frame, where you can scroll text)', cmd_cb=None, max_size=1000):
+    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):
         self.header=urwid.Text(title)
         self.model=urwid.SimpleListWalker([])
         self.body=ListView(self.model, lambda: self._update_focus(False), max_size=max_size )
@@ -321,16 +565,20 @@
     #Test asynch output -  e.g. comming from different thread
     import time
     def run():
+        state=1
         while True:
             time.sleep(1)
             
             m = msg.pollForMessage()
             
             if m == "BROKENLINK":
-                c.output("Server went away", 'Red')
+                if state == 1:
+                    c.output("Server went away", 'Red')
+                    state = 0
                 continue
                 
             if m:
+                state = 1
                 for i in m:
                     c.output(i[1], i[0])
                 

--- a/server/LocalChat.py
+++ b/server/LocalChat.py
@@ -71,7 +71,7 @@
         
         
         CREATE TABLE messages (
-            id INTEGER PRIMARY KEY,
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
             ts INTEGER NOT NULL,
             room INTEGER NOT NULL,
             msg TEXT NOT NULL
@@ -81,6 +81,7 @@
         CREATE TABLE users (
             username TEXT NOT NULL,
             room INTEGER NOT NULL,
+            active INTEGER DEFAULT 0,
             PRIMARY KEY (username,room)
         );
         
@@ -115,6 +116,12 @@
         
         if reqjson['action'] == "createRoom":
             return self.createRoom(reqjson)
+        
+        elif reqjson['action'] == "joinRoom":
+            return self.processjoinRoom(reqjson)
+        
+        elif reqjson['action'] == "leaveRoom":
+            return self.processleaveRoom(reqjson)
         
         elif reqjson['action'] == "inviteUser":
             return self.inviteUser(reqjson)
@@ -210,6 +217,98 @@
         
     
     
+    def processjoinRoom(self,reqjson):
+        ''' Process a request from a user to login to a room
+        
+        Not yet defined the authentication mechanism to use, so that's a TODO
+        '''
+        if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload']:
+            return 400
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return 400
+        
+        
+        # Check whether that user is authorised to connect to that room
+        self.cursor.execute("SELECT username, room from users where username=? and room=?",(reqjson['payload']['user'],room))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return { "status": "NOK" }
+        else:
+            
+            
+            # Tidy older messages away.
+            #
+            # We do this so that a user who joins can't then send a poll with last:0 to retrieve the full history
+            #
+            # Basically, anything older than 10 seconds should go. Users who were already present will be able
+            # to scroll up and down in their client anyway
+            self.tidyMsgs(time.time()-10,room)
+            
+            
+            # 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()
+            
+            # 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()
+            
+            if not r:
+                last = 0
+            else:
+                last = r[0]
+                   
+            # 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))
+            self.conn.commit()
+            
+            
+            return {"status":"ok","last":last}
+        
+        
+    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
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return 400
+        
+        # Check the user is actually in the room and authorised
+        if not self.validateUser(reqjson['payload']):
+            return 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()
+        
+        # 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()        
+        
+        return {"status":"ok"}
+    
+    
     def sendMsg(self,reqjson):
         ''' Push a message into a room
         
@@ -268,7 +367,7 @@
         if not room:
             return 400        
         
-        self.cursor.execute("""SELECT id,msg FROM messages
+        self.cursor.execute("""SELECT id,msg,ts FROM messages
             WHERE room=? AND
             id > ?
             ORDER BY ts ASC           
@@ -291,13 +390,28 @@
         
     
     def validateUser(self,payload):
-        ''' Placeholder for now. Auth will be handled later
-        '''
-        if "user" not in payload:
+        ''' Placeholder for now. Auth will be handled in more depth later
+        '''
+        if "user" not in payload or "roomName" not in payload:
             return False
         
+        
+        room = self.getRoomID(payload["roomName"])
+        if not room:
+            return 400        
+        
+        
+        
+        # Check whether the user has been marked as active
+        self.cursor.execute("SELECT username, room from users where username=? and room=? and active=1",(payload['user'],room))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return False
+        
         return True
-        
+
+
     
     def getRoomID(self,roomname):
         ''' Get a room's ID from its name
@@ -311,6 +425,20 @@
         
         return r[0]
     
+
+    def tidyMsgs(self,thresholdtime,room=False):
+        ''' Remove messages older than the threshold time
+        '''
+        
+        if room:
+            # Tidy from a specific room
+            self.cursor.execute("DELETE FROM messages where ts < ? and room = ?",(thresholdtime,room))
+            self.conn.commit()
+            
+        else:
+            self.cursor.execute("DELETE FROM messages where ts < ?",(thresholdtime,))
+            self.conn.commit()
+
 
 
     def test(self):