Partial implementation for LOC-21
Partial implementation for LOC-21

Users within the same room can now send each other direct messages

/msg [username] [msg]

However, the message is currently encrypted with the room key. Other users won't (and shouldn't) receive DMs not addressed to them back from {{pollMsg}}, and old DMs will be cleared at the same interval as room-wide messages.

Messages really should be encrypted with a key that's specific to the addressed user, but that can't be implemented until Roster support has been added.

--- a/client/LocalChatClient.py
+++ b/client/LocalChatClient.py
@@ -116,6 +116,10 @@
                     ]
             else:
                 
+                if i[4] != "0":
+                    color = 'brown'
+                    upstruser = 'DM %s' % (upstruser,)
+                
                 line = [
                     ts, # timestamp
                     "%s>" % (upstruser,), # To be replaced later
@@ -161,6 +165,43 @@
         
         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):
@@ -482,6 +523,25 @@
                 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":
                 
@@ -660,6 +720,8 @@
               ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), 
               ('yellow', urwid.YELLOW, urwid.BLACK), 
               ('cyan', urwid.LIGHT_CYAN, urwid.BLACK), 
+              ('brown', urwid.BROWN, urwid.BLACK), 
+              
               ]
     
     

--- a/server/LocalChat.py
+++ b/server/LocalChat.py
@@ -77,6 +77,7 @@
             ts INTEGER NOT NULL,
             room INTEGER NOT NULL,
             user NOT NULL,
+            touser TEXT DEFAULT '0',
             msg TEXT NOT NULL
         );
         
@@ -154,6 +155,9 @@
         
         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)
@@ -490,6 +494,53 @@
                 "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
@@ -510,11 +561,12 @@
         if not self.validateUser(reqjson['payload']):
             return self.returnFailure(403,reqjson['payload'],room)
         
-        self.cursor.execute("""SELECT id,msg,ts,user FROM messages
+        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']['mylast']))
+            """,(room,reqjson['payload']['user'],reqjson['payload']['mylast']))
         
         r = self.cursor.fetchall()