Remove unneeded function 'test'
Remove unneeded function 'test'

Was used at the very beginning of development to check Flask would run on my system. Not called or needed

--- /dev/null
+++ b/client/LocalChatClient.py
@@ -1,1 +1,842 @@
-
+#!/usr/bin/env python
+#
+#
+# Interface based upon https://github.com/izderadicka/xmpp-tester/blob/master/commander.py
+#
+# apt-get install:
+#     python-urwid
+#     python-gnupg
+
+
+import urwid
+from collections import deque
+from threading import Thread
+import threading
+
+import json
+import urllib2
+import ssl
+import string
+import random
+
+import datetime as dt
+
+import gnupg
+
+
+
+# We'll get these from the commandline later
+USER='ben2'
+SERVER='https://127.0.0.1:8090'
+ROOMNAME='BenTest'
+
+
+class msgHandler(object):
+    
+    def __init__(self):
+        self.last = 0
+        self.user = False
+        self.server = SERVER
+        self.room = False
+        self.roompass = False
+        self.sesskey = False
+        self.syskey = False
+        self.gpg = gnupg.GPG()
+    
+    
+    def pollForMessage(self):
+        
+        if not self.room:
+            # We're not connected to a room
+            return False
+        
+        
+        payload = {"roomName": self.room, 
+                   "mylast":self.last,
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"pollMsg",
+                   "payload": json.dumps(payload)
+                   }
+
+        resp = self.sendRequest(request)
+        
+        if resp == "BROKENLINK":
+            return resp
+        
+        
+        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
+        for i in resp["messages"]:
+            self.last = i[0]
+            upstruser = i[3]
+            
+            try:
+                msgbody = json.loads(self.decrypt(i[1],upstruser))
+            except:
+                # Means an invalid message was received - LOC-8
+                to_print.append(['error','Received message which could not be decrypted'])
+                continue
+            
+            # 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"
+
+            if upstruser == self.user:
+                # One of our own, change the color
+                color = "blue"
+            
+            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]")
+            
+            if msgbody["verb"] == "do":
+                color = 'yellow'
+                line = [
+                    "        ** %s %s **" % (upstruser,msgbody['text'])
+                    ]
+            else:
+                
+                if i[4] != "0":
+                    color = 'brown'
+                    upstruser = 'DM %s' % (upstruser,)
+                
+                line = [
+                    ts, # timestamp
+                    "%s>" % (upstruser,), # To be replaced later
+                    msgbody['text']
+                    ]
+            
+            to_print.append([color,' '.join(line)])
+        
+        return to_print
+        
+        
+    def sendMsg(self,line,verb='say'):
+        ''' 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,
+            "verb": verb
+            }
+        
+        payload = {"roomName": self.room, 
+                   "msg":self.encrypt(json.dumps(msg)),
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"sendMsg",
+                   "payload": json.dumps(payload)
+                   }
+
+        resp = self.sendRequest(request)        
+        
+        if resp['status'] == "ok":
+            return True
+        
+        return False
+        
+
+
+    def sendDirectMsg(self,line,user,verb='say'):
+        ''' Send a direct message
+        '''
+        
+        if not self.room:
+            # We're not in a room. Abort
+            return False
+        
+        # Otherwise, build a payload
+        
+        msg = {
+            'user': self.user,
+            'text': line,
+            "verb": verb
+            }
+        
+        payload = {"roomName": self.room, 
+                   "msg":self.encrypt(json.dumps(msg)), # TODO this should use the user's key
+                   "to": user,
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"sendDirectMsg",
+                   "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
+        '''
+        
+        # We only want to send the user password section of the password
+        p = passw.split(":")
+        userpass = p[1]
+        
+        payload = {"roomName": room, 
+                   "userpass": userpass.encode('utf-8'),
+                   "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']
+        self.roompass = p[0] # The room password is the first section of the password
+        self.sesskey = resp['session']
+        self.syskey = resp['syskey']
+        return True
+
+
+
+
+    def leaveRoom(self):
+        ''' Leave the current room
+        '''
+        if not self.room:
+            return False
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        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
+        self.roompass = False
+        self.sesskey = False
+        
+        return True
+                
+
+
+    def createRoom(self,room,user=False):
+        ''' Create a new room
+        '''
+        
+        if not user and not self.user:
+            return False
+        
+        if not user:
+            user = self.user
+
+
+        # Generate a password for the admin
+        passw = self.genpassw()
+                
+        payload = {"roomName": room, 
+                   "owner": user,
+                   "pass": passw
+                   }
+        
+        request = {"action":"createRoom",
+                   "payload": json.dumps(payload)
+                   }        
+        
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return [resp['name'],passw]
+    
+
+    def closeRoom(self):
+        if not self.room:
+            return False
+
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"closeRoom",
+                   "payload": json.dumps(payload)
+                   }        
+
+        resp = self.sendRequest(request)
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return True
+        
+        
+
+    def inviteUser(self,user):
+        ''' Invite a user into a room
+        
+        #TODO - Authentication
+        '''
+        
+        
+        # Generate a password for the new user
+        passw = self.genpassw()
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "invite": user,
+                   "pass": passw,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"inviteUser",
+                   "payload": json.dumps(payload)
+                   }    
+        
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return [self.room,self.roompass,passw,user]
+
+
+
+    def kickUser(self,user,ban=False):
+        ''' Kick a user out of the room
+        '''
+        
+        action = 'banUser'
+        
+        if not ban:
+            action = 'kickUser'
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "kick": user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":action,
+                   "payload": json.dumps(payload)
+                   }    
+                
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return True
+
+        
+        
+
+    def sendRequest(self,data):
+        data = json.dumps(data)
+        
+        try:
+            # The cert the other end will be considered invalid
+            #
+            # Ignore it
+            ctx = ssl.create_default_context()
+            ctx.check_hostname = False
+            ctx.verify_mode = ssl.CERT_NONE
+
+            req = urllib2.Request(self.server, data, {'Content-Type': 'application/json'})
+            f = urllib2.urlopen(req,context=ctx)
+            response = f.read()
+            f.close()
+            return json.loads(response)
+        except:
+            return "BROKENLINK"
+
+
+
+    def decrypt(self,msg,sender):
+        ''' Placeholder
+        '''
+                
+        try:       
+            key = self.roompass
+            if sender == "SYSTEM":
+                key = self.syskey
+                
+            return str(self.gpg.decrypt(msg.decode("base64"),passphrase=key))
+        
+        except:
+            return False
+        
+    
+
+    def encrypt(self,msg):
+        ''' Placeholder
+        '''
+        
+        crypted = self.gpg.encrypt(msg,None,passphrase=self.roompass,symmetric="AES256",armor=False)
+        return crypted.data.encode('base64')
+
+
+    def hashpw(self,passw):
+        ''' Placeholder
+        '''
+        return passw
+
+
+    def genpassw(self,N=16):
+        ''' Generate a random string of chars to act as a password
+        '''
+        
+        return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits).encode('utf-8') for _ in range(N))
+
+
+
+class NotInRoom(Exception):
+    def __init__(self,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
+similar to cmd.Cmd in standard library
+just extend with do_something  method to handle your commands"""
+
+    def __init__(self,quit_commands=['q','quit','exit'], help_commands=['help','?', 'h']):
+        self._quit_cmd=quit_commands
+        self._help_cmd=help_commands
+        
+    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 == "me":
+                #/me [string]
+                r = msg.sendMsg(' '.join(args),'do')
+                if not r:
+                    raise NotInRoom(line)
+                return
+
+            
+            if cmd == "ban":
+                # /kick [user]
+                
+                if len(args) < 1:
+                    raise InvalidCommand(line)
+                
+                
+                m = msg.kickUser(args[0],True)
+                return
+                        
+            
+            if cmd == "join":
+                # /join [room] [password] [username] 
+                
+                if len(args) < 3:
+                    raise InvalidCommand(line)
+                    return
+                
+                if not msg.joinRoom(args[2],args[0],args[1]):
+                    raise UnableTo('join',line)
+                return
+            
+            
+            if cmd == "kick":
+                # /kick [user]
+                
+                if len(args) < 1:
+                    raise InvalidCommand(line)
+                
+                
+                m = msg.kickUser(args[0])
+                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 == 'msg':
+                # /msg ben Hello ben this is a DM
+                line = ' '.join(args[1:])
+                r = msg.sendDirectMsg(line,args[0])
+                if not r:
+                    raise NotInRoom(line)
+                
+                # Otherwise push a copy of the message to display
+                # cos we won't get this one back from pollMsg
+                global c
+                
+                m = "%s DM %s>%s" % (msg.user,args[0],line)
+                
+                c.output(m,'blue')
+                
+                return
+
+        
+            if cmd == "room":
+                
+                # /room close [roompass]
+                if args[0] == "close":
+                    if len(args) < 2:
+                        raise InvalidCommand('/room close [pass]')
+                    
+                    if not msg.closeRoom():
+                        raise UnableTo('Close Room','')
+                                        
+                    return
+                
+                
+                # /room create [roomname] [[user]]
+                if args[0] == "create":
+                    
+                    if len(args) < 3:
+                        args[2] = False
+                    
+                    n = msg.createRoom(args[1],args[2])
+                    if not n:
+                        raise UnableTo('create room',line)
+                        return
+                    
+                    # Seperate out the return value
+                    rm = n[0]
+                    up = n[1] # user specific password
+                    global c
+                    
+                    # Generate a room password
+                    p = msg.genpassw()
+                    c.output('Created Room %s' %(rm))
+                    c.output('To join the room, do /join %s %s:%s %s' %(args[1],p,up,args[2]))
+                    return
+                
+                elif args[0] == "invite":
+                    if len(args) < 2:
+                        raise InvalidCommand(line)
+                    
+                    n = msg.inviteUser(args[1])
+                    if not n:
+                        raise UnableTo('invite user',line)
+                        return
+                    
+                    global c
+                    c.output('User %s may now join room' %(args[1],))
+                    c.output('To join the room, they should do /join %s %s:%s %s' %(n[0],n[1],n[2],n[3]))
+                    return                                        
+                    
+
+        if cmd in self._quit_cmd:
+            return Commander.Exit
+        elif cmd in self._help_cmd:
+            return self.help(args[0] if args else None)
+        elif hasattr(self, 'do_'+cmd):
+            return getattr(self, 'do_'+cmd)(*args)
+        else:
+            # 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] to quit program\n' % qc
+            res += """Available commands: 
+            
+            /join [room] [password] [username]                          Join a room
+            /leave                                                      Leave current room
+            /room create [roomname] [roompass] [admin user]             New room management 
+            
+            
+            /room invite [user]                                         Invite a user into the current room
+            /me [string]                                                Send an 'action' instead of a message
+            
+            Room Admin commands:
+            
+            /kick [user]                                                Kick a user out of the room (they can return)
+            /ban [user]                                                 Kick a user out and disinvite them (they cannot return)
+            /room close [roompass]                                      Kick all users out and close the room
+            
+            Once in a room, to send a message, just type it.
+            
+            
+            """
+            return res
+        if not cmd:
+            return std_help()
+        else:
+            try:
+                fn=getattr(self,'do_'+cmd)
+                doc=fn.__doc__
+                return doc or 'No documentation available for %s'%cmd
+            except AttributeError:
+                return std_help()
+ 
+class FocusMixin(object):
+    def mouse_event(self, size, event, button, x, y, focus):
+        if focus and hasattr(self, '_got_focus') and self._got_focus:
+            self._got_focus()
+        return super(FocusMixin,self).mouse_event(size, event, button, x, y, focus)    
+    
+class ListView(FocusMixin, urwid.ListBox):
+    def __init__(self, model, got_focus, max_size=None):
+        urwid.ListBox.__init__(self,model)
+        self._got_focus=got_focus
+        self.max_size=max_size
+        self._lock=threading.Lock()
+        
+    def add(self,line):
+        with self._lock:
+            was_on_end=self.get_focus()[1] == len(self.body)-1
+            if self.max_size and len(self.body)>self.max_size:
+                del self.body[0]
+            self.body.append(urwid.Text(line))
+            last=len(self.body)-1
+            if was_on_end:
+                self.set_focus(last,'above')
+        
+    
+
+class Input(FocusMixin, urwid.Edit):
+    signals=['line_entered']
+    def __init__(self, got_focus=None):
+        urwid.Edit.__init__(self)
+        self.history=deque(maxlen=1000)
+        self._history_index=-1
+        self._got_focus=got_focus
+    
+    def keypress(self, size, key):
+        if key=='enter':
+            line=self.edit_text.strip()
+            if line:
+                urwid.emit_signal(self,'line_entered', line)
+                self.history.append(line)
+            self._history_index=len(self.history)
+            self.edit_text=u''
+        if key=='up':
+            
+            self._history_index-=1
+            if self._history_index< 0:
+                self._history_index= 0
+            else:
+                self.edit_text=self.history[self._history_index]
+        if key=='down':
+            self._history_index+=1
+            if self._history_index>=len(self.history):
+                self._history_index=len(self.history) 
+                self.edit_text=u''
+            else:
+                self.edit_text=self.history[self._history_index]
+        else:
+            urwid.Edit.keypress(self, size, key)
+        
+
+
+class Commander(urwid.Frame):
+    """ Simple terminal UI with command input on bottom line and display frame above
+similar to chat client etc.
+Initialize with your Command instance to execute commands
+and the start main loop Commander.loop().
+You can also asynchronously output messages with Commander.output('message') """
+
+    class Exit(object):
+        pass
+    
+    PALLETE=[('reversed', urwid.BLACK, urwid.LIGHT_GRAY),
+              ('normal', urwid.LIGHT_GRAY, urwid.BLACK),
+              ('error', urwid.LIGHT_RED, urwid.BLACK),
+              ('green', urwid.DARK_GREEN, urwid.BLACK),
+              ('blue', urwid.LIGHT_BLUE, urwid.BLACK),
+              ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), 
+              ('yellow', urwid.YELLOW, urwid.BLACK), 
+              ('cyan', urwid.LIGHT_CYAN, urwid.BLACK), 
+              ('brown', urwid.BROWN, 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):
+        self.header=urwid.Text(title)
+        self.model=urwid.SimpleListWalker([])
+        self.body=ListView(self.model, lambda: self._update_focus(False), max_size=max_size )
+        self.input=Input(lambda: self._update_focus(True))
+        foot=urwid.Pile([urwid.AttrMap(urwid.Text(command_caption), 'reversed'),
+                        urwid.AttrMap(self.input,'normal')])
+        urwid.Frame.__init__(self, 
+                             urwid.AttrWrap(self.body, 'normal'),
+                             urwid.AttrWrap(self.header, 'reversed'),
+                             foot)
+        self.set_focus_path(['footer',1])
+        self._focus=True
+        urwid.connect_signal(self.input,'line_entered',self.on_line_entered)
+        self._cmd=cmd_cb
+        self._output_styles=[s[0] for s in self.PALLETE]
+        self.eloop=None
+        
+    def loop(self, handle_mouse=False):
+        self.eloop=urwid.MainLoop(self, self.PALLETE, handle_mouse=handle_mouse)
+        self._eloop_thread=threading.current_thread()
+        self.eloop.run()
+        
+    def on_line_entered(self,line):
+        if self._cmd:
+            try:
+                res = self._cmd(line)
+            except Exception,e:
+                self.output('Error: %s'%e, 'error')
+                return
+            if res==Commander.Exit:
+                raise urwid.ExitMainLoop()
+            elif res:
+                self.output(str(res))
+        else:
+            if line in ('q','quit','exit'):
+                raise urwid.ExitMainLoop()
+            else:
+                self.output(line)
+    
+    def output(self, line, style=None):
+        if style and style in self._output_styles:
+                line=(style,line) 
+        self.body.add(line)
+        #since output could be called asynchronously form other threads we need to refresh screen in these cases
+        if self.eloop and self._eloop_thread != threading.current_thread():
+            self.eloop.draw_screen()
+        
+        
+    def _update_focus(self, focus):
+        self._focus=focus
+        
+    def switch_focus(self):
+        if self._focus:
+            self.set_focus('body')
+            self._focus=False
+        else:
+            self.set_focus_path(['footer',1])
+            self._focus=True
+        
+    def keypress(self, size, key):
+        if key=='tab':
+            self.switch_focus()
+        return urwid.Frame.keypress(self, size, key)
+        
+
+
+    
+if __name__=='__main__':
+    
+    
+    msg = msgHandler()
+    
+    
+    class TestCmd(Command):
+        def do_echo(self, *args):
+            '''echo - Just echos all arguments'''
+            return ' '.join(args)
+        def do_raise(self, *args):
+            raise Exception('Some Error')
+        
+    c=Commander('LocalChat', cmd_cb=TestCmd())
+    
+    #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":
+                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])
+                
+    t=Thread(target=run)
+    t.daemon=True
+    t.start()
+    
+    #start main loop
+    c.loop()
+        
+

--- a/server/LocalChat.py
+++ b/server/LocalChat.py
@@ -5,18 +5,25 @@
 #
 #
 # apt-get install:
-#    python-flask
+#   python-flask
+#   python-openssl
+#   python-bcrypt
 #
 
 from flask import Flask
 from flask import request, make_response
 
-
-
+import thread
+import urllib2
+import ssl
 import sqlite3
 import time
 import os
 import json
+import bcrypt
+import random
+import string
+import gnupg
 
 app = Flask(__name__)
 
@@ -38,21 +45,24 @@
     a = msghandler.processSubmission(reqjson)
     
     # Check the status
-    if a in [400]:
+    if a in [400,403,500]:
         response = make_response("",a)
         return response
         
     return json.dumps(a)
 
-    
-
 
 class MsgHandler(object):
 
 
-    def __init__(self):
+    def __init__(self,cronpass,bindpoint):
         self.conn = False
         self.cursor = False
+        # Generate a key for encryption of SYSTEM messages (LOC-13)
+        self.syskey = self.genpassw(16)
+        self.gpg = gnupg.GPG()
+        self.cronpass = cronpass
+        self.bindpoint = bindpoint
 
 
 
@@ -65,15 +75,16 @@
         sql = """ CREATE TABLE rooms (
             id INTEGER PRIMARY KEY,
             name TEXT NOT NULL UNIQUE,
-            owner TEXT NOT NULL,
-            pass TEXT NOT NULL
+            owner TEXT NOT NULL
         );
         
         
         CREATE TABLE messages (
-            id INTEGER PRIMARY KEY,
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
             ts INTEGER NOT NULL,
             room INTEGER NOT NULL,
+            user NOT NULL,
+            touser TEXT DEFAULT '0',
             msg TEXT NOT NULL
         );
         
@@ -81,12 +92,33 @@
         CREATE TABLE users (
             username TEXT NOT NULL,
             room INTEGER NOT NULL,
+            active INTEGER DEFAULT 0,
+            passhash TEXT NOT NULL,
             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)
+        
+        # We also need to start the scheduler thread (LOC-6)
+        thread.start_new_thread(taskScheduler,(self.cronpass,self.bindpoint))
 
 
 
@@ -101,8 +133,12 @@
         
         
         print reqjson
+        
+        if "action" in reqjson and reqjson['action'] == 'schedulerTrigger':
+            return self.triggerClean(reqjson)
+        
         if "action" not in reqjson or "payload" not in reqjson:
-            return 400
+            return self.returnFailure(400)
         
         
         # Decrypt the payload
@@ -111,14 +147,38 @@
         try:
             reqjson['payload'] = json.loads(reqjson['payload'])
         except:
-            return 400
+            return self.returnFailure(400)
         
         if reqjson['action'] == "createRoom":
             return self.createRoom(reqjson)
         
+        if reqjson['action'] == "closeRoom":
+            return self.closeRoom(reqjson)
+        
+        elif reqjson['action'] == "joinRoom":
+            return self.processjoinRoom(reqjson)
+        
+        elif reqjson['action'] == "leaveRoom":
+            return self.processleaveRoom(reqjson)
+
+        elif reqjson['action'] == "banUser":
+            return self.kickUser(reqjson,True)
+        
         elif reqjson['action'] == "inviteUser":
             return self.inviteUser(reqjson)
         
+        elif reqjson['action'] == "kickUser":
+            return self.kickUser(reqjson,False)
+        
+        elif reqjson['action'] == 'sendDirectMsg':
+            return self.sendDirectMsg(reqjson)
+        
+        elif reqjson['action'] == 'sendMsg':
+            return self.sendMsg(reqjson)
+        
+        elif reqjson['action'] == 'pollMsg':
+            return self.fetchMsgs(reqjson)
+         
         
         
         
@@ -152,19 +212,35 @@
         }'
         
         '''
+        
+        # Validate the request
+        #
+        # All validation snippets will change to this format soon
+        required = ['roomName','owner','pass']
+        for i in required:
+            if i not in reqjson['payload']:
+                return self.returnFailure(400)
+        
         print "Creating room %s" % (reqjson['payload'])
         
         # Create a tuple for sqlite3
         t = (reqjson['payload']['roomName'],
-             reqjson['payload']['owner'],
-             reqjson['payload']['passhash'])
+             reqjson['payload']['owner']
+             )
         
         try:
-            self.cursor.execute("INSERT INTO rooms (name,owner,pass) VALUES (?,?,?)",t)
+            self.cursor.execute("INSERT INTO rooms (name,owner) VALUES (?,?)",t)
             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
+        passhash = bcrypt.hashpw(reqjson['payload']['pass'].encode('utf-8'),bcrypt.gensalt())
+        
+        self.cursor.execute("INSERT INTO users (username,room,passhash) values (?,?,?)",(reqjson['payload']['owner'],roomid,passhash))
+        self.conn.commit()
         
         return {
                 'status':'ok',
@@ -172,33 +248,388 @@
                 'name' : reqjson['payload']['roomName']
             }
         
-    
+
+
+    def closeRoom(self,reqjson):
+        ''' Close a room.
+        
+        Means we need to
+        
+        - Ban all the users
+        - Scrub the message queue
+        - Remove the room record
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        
+        # Check the requesting user is the admin
+        self.cursor.execute("SELECT * from rooms where id=? and owner=?",(room,reqjson["payload"]["user"]))
+        n = self.cursor.fetchone()
+        
+        if not n:
+            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" }
+    
+        
+           
+        
 
 
     def inviteUser(self,reqjson):
         ''' Link a username into a room
-
-
-        curl -v -X POST http://127.0.0.1:8090/ -H "Content-Type: application/json" --data '{"action":"inviteUser","payload":"{\"roomName\":\"BenTest\",\"user\":\"ben2\"}"}'
-
-        '''
-        
-        if "roomName" not in reqjson['payload']:
-            return 400
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "pass" not in reqjson['payload'] or "invite" not in reqjson['payload']:
+            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 self.returnFailure(403,reqjson['payload'],room)
+        
+        
+        if reqjson['payload']['invite'] == "SYSTEM":
+            # Push a notification into the group
+            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
+        passhash = bcrypt.hashpw(reqjson['payload']['pass'].encode('utf-8'),bcrypt.gensalt())
         
         # Otherwise, link the user in
-        self.cursor.execute("INSERT INTO users (username,room) values (?,?)",(reqjson['payload']['user'],room))
+        self.cursor.execute("INSERT INTO users (username,room,passhash) values (?,?,?)",(reqjson['payload']['invite'],room,passhash))
+        
+        # Push a notification into the group
+        self.pushSystemMsg("User %s invited %s to the room" % (reqjson['payload']['user'],reqjson['payload']['invite']),room)
         
         return {
                 "status":'ok'
             }
         
-        
+    
+    
+    def kickUser(self,reqjson,ban=False):
+        ''' Kick a user out of room
+        
+        Default is just to boot them out, but can also remove their authorisation to enter
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload'] or "kick" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        
+        # Check the requesting user is the admin
+        self.cursor.execute("SELECT * from rooms where id=? and owner=?",(room,reqjson["payload"]["user"]))
+        n = self.cursor.fetchone()
+        
+        if not n:
+            return self.returnFailure(403)
+        
+        
+        
+        self.cursor.execute("UPDATE users set active=0 where room=? and username=?",(room,reqjson["payload"]["kick"]))
+        
+        # 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')
+        
+        self.pushFailureMessage(reqjson['payload']['kick'],room,'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"]))
+            self.pushSystemMsg("User %s banned %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick']),room,'syswarn')
+            
+        return { "status" : "ok" }
+    
+
+    
+    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
+        '''
+
+        # Check the required information is present
+        required = ['roomName','user','userpass']
+        for i in required:
+            if i not in reqjson['payload']:
+                return self.returnFailure(400)
+                
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        
+        if reqjson["payload"]["user"] == "SYSTEM":
+            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))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return { "status": "NOK" }
+        
+        
+        # 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 self.returnFailure(403)
+        
+            
+        # 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
+        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()
+        
+        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))
+        
+        
+        # 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,"session":sesskey,"syskey":self.syskey}
+        
+        
+    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 self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        # Check the user is actually in the room and authorised
+        if not self.validateUser(reqjson['payload']):
+            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
+        self.pushSystemMsg("User %s left the room" % (reqjson['payload']['user']),room)
+        return {"status":"ok"}
+    
+    
+    def sendMsg(self,reqjson):
+        ''' Push a message into a room
+        
+        curl -v -X POST http://127.0.0.1:8090/ -H "Content-Type: application/json" --data '{"action":"sendMsg","payload":"{\"roomName\":\"BenTest\", \"msg\":\"ENCRYPTED-DATA\",\"user\":\"ben2\"}"}'
+        
+        '''
+        
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403)
+        
+        
+        if "roomName" not in reqjson['payload'] or "msg" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        print room
+        if not room:
+            return self.returnFailure(400)
+
+            
+        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()
+        
+        # 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]
+        
+        return {
+                "status" : "ok",
+                "msgid" : msgid,
+                "last" : last
+            }
+        
+
+
+    def sendDirectMsg(self,reqjson):
+        ''' Push a message to a user in the same room
+        '''
+        
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403)
+                
+        required = ['roomName','msg','to']
+        for i in required:
+            if i not in reqjson['payload']:
+                return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        print room
+        if not room:
+            return self.returnFailure(400)
+
+        # Check the other user is in the room and active
+        self.cursor.execute("SELECT username from users where username=? and room=? and active=1",(reqjson['payload']['to'],room))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return self.returnFailure(400)
+            
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user,touser) VALUES (?,?,?,?,?)",(time.time(),room,reqjson['payload']['msg'],reqjson['payload']['user'],reqjson['payload']['to']))
+        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]
+        
+        return {
+                "status" : "ok",
+                "msgid" : msgid,
+                "last" : last
+            }
+        
+
+
+        
+    def fetchMsgs(self,reqjson):
+        ''' Check to see if there are any new messages in the room
+        
+        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 "mylast" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        print room
+        if not room:
+            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,touser FROM messages
+            WHERE room=? AND
+            (touser = 0 OR touser = ?) AND 
+            id > ?
+            ORDER BY ts ASC           
+            """,(room,reqjson['payload']['user'],reqjson['payload']['mylast']))
+        
+        r = self.cursor.fetchall()
+        
+        if not r:
+            # No changes
+            return {"status":"unchanged","last":reqjson['payload']['mylast']}
+        
+        # Otherwise, return the messages
+        return {"status":"updated",
+                "messages" : r
+                }
+    
+    
+        
+        
+        
+    
+    def validateUser(self,payload):
+        ''' Placeholder for now. Auth will be handled in more depth later
+        '''
+        if "user" not in payload or "roomName" not in payload:
+            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 False        
+        
+        
+        
+        # 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
         '''
@@ -213,18 +644,178 @@
     
 
 
-    def test(self):
-        return ['foo']
-
-
-
-
-# Create a global instance of the wrapper so that state can be retained
-msghandler = MsgHandler()
+    def triggerClean(self,reqjson):
+        ''' Trigger a clean of old messages etc
+        '''
+        
+        if 'pass' not in reqjson:
+            # No need for failure messages here
+            return 403
+        
+        if reqjson['pass'] != self.cronpass:
+            return 403
+        
+        # Tidy messages older than 10 minutes
+        self.tidyMsgs(time.time() - 600);
+        
+        
+        return {'status':'ok'}
+        
+        
+
+    def tidyMsgs(self,thresholdtime,room=False):
+        ''' Remove messages older than the threshold time
+        '''
+        
+        print "Tidying"
+        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()
+
+
+        # Tidy away any failure messages
+        self.cursor.execute("DELETE FROM failuremsgs  where expires < ?",(time.time(),))
+
+
+
+    def pushSystemMsg(self,msg,room,verb="sysinfo"):
+        ''' Push a message from the SYSTEM user into the queue
+        '''
+        m = {
+            "text":msg,
+            "verb":verb
+        }
+        
+        m = self.encryptSysMsg(json.dumps(m))
+        
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user) VALUES (?,?,?,'SYSTEM')",(time.time(),room,m))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()
+        return msgid
+
+
+    def pushFailureMessage(self,user,room,msg):
+        ''' Record a failure message against a user
+        
+        See LOC-14
+        
+        '''
+        self.cursor.execute("INSERT INTO failuremsgs (username,room,expires,msg) values (?,?,?,?)",(user,room,time.time() + 300,msg))
+        self.conn.commit()
+        
+        
+
+
+    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 encryptSysMsg(self,msg):
+        ''' Encrypt a message from system - LOC-13
+        
+            This isn't so much for protection of the data in memory (as the key is also in memory) as it
+            is to protect against a couple of things you could otherwise do in the client. See LOC-13 for
+            more info.
+        
+        '''
+        
+        crypted = self.gpg.encrypt(msg,None,passphrase=self.syskey,symmetric="AES256",armor=False)
+        return crypted.data.encode('base64')
+        
+
+
+    def genSessionKey(self,N=48):
+        return ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits + '/=?&@#%^()+,.<>:!').encode('utf-8') for _ in range(N))
+
+
+
+    def genpassw(self,N=16):
+        ''' Generate a random string of chars to act as an encryption password
+        '''
+        
+        return ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits).encode('utf-8') for _ in range(N))
+
+
+
+# Create the scheduler function
+def taskScheduler(passw,bindpoint):
+    
+    
+    # Ignore cert errors
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    
+    data = json.dumps({"action":'schedulerTrigger',
+            "pass": passw
+            })
+    
+    while True:
+        time.sleep(60)
+        
+        try:
+            req = urllib2.Request(bindpoint, data, {'Content-Type': 'application/json'})
+            f = urllib2.urlopen(req,context=ctx)
+            response = f.read()
+            f.close()  
+        except:
+            # Don't let the thread abort just because one request went wrong
+            continue
+
+    
 
 
 if __name__ == '__main__':
+    
+    
+    # These will be handled properly later
+    passw = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits).encode('utf-8') for _ in range(64))
+    bindpoint = "https://127.0.0.1:8090" 
+
+
+    # Create a global instance of the wrapper so that state can be retained
+    #
+    # We pass in the password we generated for the scheduler thread to use
+    # as well as the URL it should POST to
+    msghandler = MsgHandler(passw,bindpoint)
+
+
     # Bind to PORT if defined, otherwise default to 8090.
     port = int(os.environ.get('PORT', 8090))
-    app.run(host='0.0.0.0', port=port,debug=True)
-
+    app.run(host='0.0.0.0', port=port,debug=True,ssl_context='adhoc')
+
+
+
+
+
+
+