changeset 72:f271fff3a713

MUC implementation: first draft /!\ the experimental muc branche of wokkel must be used - bridge: new roomJoined signal - wix: contact list widget is now in a separate file, and manage different kinds of presentation - wix: chat window now manage group chat (first draft, not working yet) - wix: constants are now in a separate class, so then can be accessible from everywhere - wix: new menu to join room (do nothing yet, except entering in a test room) - new plugin for xep 0045 (MUC), use wokkel experimental MUC branch - plugins: the profile is now given for get_handler, cause it can be used internally by a plugin (e.g.: xep-0045 plugin)
author Goffi <goffi@goffi.org>
date Sun, 21 Mar 2010 10:28:55 +1100
parents efe81b61673c
children 9d113b5471e6
files frontends/quick_frontend/quick_app.py frontends/quick_frontend/quick_chat.py frontends/quick_frontend/quick_contact_list.py frontends/sat_bridge_frontend/DBus.py frontends/wix/chat.py frontends/wix/constants.py frontends/wix/contact_list.py frontends/wix/main_window.py frontends/wix/profile_manager.py plugins/plugin_xep_0045.py plugins/plugin_xep_0054.py plugins/plugin_xep_0065.py plugins/plugin_xep_0096.py sat.tac sat_bridge/DBus.py
diffstat 15 files changed, 431 insertions(+), 193 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/quick_frontend/quick_app.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/quick_frontend/quick_app.py	Sun Mar 21 10:28:55 2010 +1100
@@ -42,6 +42,7 @@
         self.bridge.register("newContact", self.newContact)
         self.bridge.register("newMessage", self.newMessage)
         self.bridge.register("presenceUpdate", self.presenceUpdate)
+        self.bridge.register("roomJoined", self.roomJoined)
         self.bridge.register("subscribe", self.subscribe)
         self.bridge.register("paramUpdate", self.paramUpdate)
         self.bridge.register("contactDeleted", self.contactDeleted)
@@ -80,7 +81,7 @@
         ## misc ##
         self.profiles[profile]['onlineContact'] = set()  #FIXME: temporary
 
-        #TODO: managed multi-profiles here
+        #TODO: gof: managed multi-profiles here
         if self.bridge.isConnected(profile):
             self.setStatusOnline(True)
         else:
@@ -153,7 +154,7 @@
         if not self.__check_profile(profile):
             return
         print "check ok"
-        debug (_("presence update for %(jid)s (show=%(show)s, statuses=%(statuses)s)") % {'jid':jabber_id, 'show':show, 'statuses':statuses});
+        debug (_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]") % {'jid':jabber_id, 'show':show, 'priority':priority, 'statuses':statuses, 'profile':profile});
         from_jid=JID(jabber_id)
         debug ("from_jid.short=%(from_jid)s whoami.short=%(whoami)s" % {'from_jid':from_jid.short, 'whoami':self.profiles[profile]['whoami'].short})
 
@@ -187,13 +188,19 @@
                 self.CM.update(from_jid, 'nick', cache['nick'])
             if cache.has_key('avatar'): 
                 self.CM.update(from_jid, 'avatar', self.bridge.getAvatarFile(cache['avatar']))
-            self.contactList.replace(from_jid)
+            self.contactList.replace(from_jid, self.CM.getAttr(from_jid, 'groups'))
 
         if show=="unavailable" and from_jid in self.profiles[profile]['onlineContact']:
             self.profiles[profile]['onlineContact'].remove(from_jid)
             self.CM.remove(from_jid)
             if not self.CM.isConnected(from_jid):
                 self.contactList.disconnect(from_jid)
+    
+    def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile):
+        """Called when a MUC room is joined"""
+        debug (_("Room [%(room_name)s] joined by %(profile)s") % {'room_name':room_id+'@'+room_service, 'profile': profile})
+
+
 
     def subscribe(self, type, raw_jid, profile):
         """Called when a subsciption maangement signal is received"""
--- a/frontends/quick_frontend/quick_chat.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/quick_frontend/quick_chat.py	Sun Mar 21 10:28:55 2010 +1100
@@ -26,14 +26,21 @@
 
 class QuickChat():
     
-    def __init__(self, to_jid, host):
-        self.to_jid = to_jid
+    def __init__(self, target, host, type='one2one'):
+        self.target = target
         self.host = host
+        self.type = type
+
+    def setType(self, type):
+        """Set the type of the chat
+        @param type: can be 'one2one' for single conversation or 'group' for chat à la IRC
+        """
+        self.type = type
 
     def historyPrint(self, size=20, keep_last=False, profile='@NONE@'):
         """Print the initial history"""
         debug (_("now we print history"))
-        history=self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].short, self.to_jid, 20)
+        history=self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].short, self.target, 20)
         stamps=history.keys()
         stamps.sort()
         for stamp in stamps: 
--- a/frontends/quick_frontend/quick_contact_list.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/quick_frontend/quick_contact_list.py	Sun Mar 21 10:28:55 2010 +1100
@@ -49,6 +49,6 @@
         """remove a contact from the list"""
         raise NotImplementedError
     
-    def add(self, jid):
+    def add(self, jid, param_groups=None):
         """add a contact to the list"""
         raise NotImplementedError
--- a/frontends/sat_bridge_frontend/DBus.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/sat_bridge_frontend/DBus.py	Sun Mar 21 10:28:55 2010 +1100
@@ -110,6 +110,9 @@
         return self.db_req_iface.confirmationAnswer(id, accepted, data)
 
 #methods from plugins
+    def joinMUC(self, service, roomId, nick, profile_key='@DEFAULT@'):
+        return self.db_comm_iface.joinMUC(service, roomId, nick, profile_key)
+
     def sendFile(self, to, path, profile_key='@DEFAULT@'):
         return self.db_comm_iface.sendFile(to, path, profile_key)
 
--- a/frontends/wix/chat.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/wix/chat.py	Sun Mar 21 10:28:55 2010 +1100
@@ -27,6 +27,7 @@
 from logging import debug, info, error
 from tools.jid  import JID
 from quick_frontend.quick_chat import QuickChat
+from contact_list import ContactList
 
 
 idSEND           = 1
@@ -34,17 +35,22 @@
 class Chat(wx.Frame, QuickChat):
     """The chat Window for one to one conversations"""
 
-    def __init__(self, to_jid, host):
-        wx.Frame.__init__(self, None, title=to_jid, pos=(0,0), size=(400,200))
-        QuickChat.__init__(self, to_jid, host) 
+    def __init__(self, target, host, type='one2one'):
+        wx.Frame.__init__(self, None, title=target, pos=(0,0), size=(400,200))
+        QuickChat.__init__(self, target, host, type) 
 
-        self.chatWindow = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE | wx.TE_RICH | wx.TE_READONLY)
-        self.textBox = wx.TextCtrl(self, -1, style = wx.TE_PROCESS_ENTER)
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.sizer.Add(self.chatWindow, 1, flag=wx.EXPAND)
-        self.sizer.Add(self.textBox, flag=wx.EXPAND)
-        self.SetSizer(self.sizer)
-        self.SetAutoLayout(True)
+        self.splitter = wx.SplitterWindow(self, -1)
+        
+        self.conv_panel = wx.Panel(self.splitter)
+        self.conv_panel.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.chatWindow = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_MULTILINE | wx.TE_RICH | wx.TE_READONLY)
+        self.textBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_PROCESS_ENTER)
+        self.conv_panel.sizer.Add(self.chatWindow, 1, flag=wx.EXPAND)
+        self.conv_panel.sizer.Add(self.textBox, flag=wx.EXPAND)
+        self.conv_panel.SetSizer(self.conv_panel.sizer)
+        self.splitter.Initialize(self.conv_panel)
+        self.setType(self.type)
+
         self.createMenus()
         
         #events
@@ -61,7 +67,37 @@
         #misc
         self.textBox.SetFocus()
         self.Hide() #We hide because of the show toggle
-    
+   
+    def __createPresents(self):
+        """Create a list of present people in a group chat"""
+        self.present_panel = wx.Panel(self.splitter)
+        self.present_panel.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.present_panel.SetBackgroundColour(wx.BLUE)
+        self.present_panel.presents = ContactList(self.present_panel, self.host, type='nicks')
+        self.present_panel.presents.SetMinSize(wx.Size(80,20))
+        self.present_panel.sizer.Add(self.present_panel.presents, 1, wx.EXPAND)
+        self.present_panel.SetSizer(self.present_panel.sizer)
+        self.splitter.SplitVertically(self.present_panel, self.conv_panel, 80)
+
+    def setType(self, type):
+        QuickChat.setType(self, type)
+        if type is 'group' and not self.splitter.IsSplit():
+            self.__createPresents()
+        elif type is 'one2one' and self.splitter.IsSplit():
+            self.splitter.Unsplit(self.present_panel)
+            del self.present_panel
+
+    def setPresents(self, nicks):
+        """Set the users presents in the contact list for a group chat
+        @param nicks: list of nicknames
+        """
+        debug (_("Adding users %s to room") % nicks)
+        if self.type != "group":
+            error (_("[INTERNAL] trying to set presents nicks for a non group chat window"))
+            return
+        for nick in nicks:
+           self.present_panel.presents.replace(nick)
+
     def createMenus(self):
         info("Creating menus")
         actionMenu = wx.Menu()
@@ -86,7 +122,7 @@
 
     def onEnterPressed(self, event):
         """Behaviour when enter pressed in send line."""
-        self.host.bridge.sendMessage(self.to_jid, event.GetString(), profile_key=self.host.profile)
+        self.host.bridge.sendMessage(self.target, event.GetString(), profile_key=self.host.profile)
         self.textBox.Clear()
 
         
@@ -111,7 +147,7 @@
         filename = wx.FileSelector(_("Choose a file to send"), flags = wx.FD_FILE_MUST_EXIST)
         if filename:
             debug(_("filename: %s"),filename)
-            full_jid = self.host.CM.get_full(self.to_jid)
+            full_jid = self.host.CM.get_full(self.target)
             id = self.host.bridge.sendFile(full_jid, filename)
             self.host.waitProgress(id, _("File Transfer"), _("Copying %s") % os.path.basename(filename)) 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/wix/constants.py	Sun Mar 21 10:28:55 2010 +1100
@@ -0,0 +1,13 @@
+import sys
+import __builtin__
+
+__builtin__.__dict__['IMAGE_DIR'] = sys.path[0]+'/images'
+
+__builtin__.__dict__['msgOFFLINE']          = _("offline")
+__builtin__.__dict__['msgONLINE']           = _("online")
+__builtin__.__dict__['const_DEFAULT_GROUP'] = "Unclassed"
+__builtin__.__dict__['const_STATUS']        = [("", _("Online"), None),
+                                               ("chat", _("Free for chat"), "green"),
+                                               ("away", _("AFK"), "brown"),
+                                               ("dnd", _("DND"), "red"),
+                                               ("xa", _("Away"), "red")]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/wix/contact_list.py	Sun Mar 21 10:28:55 2010 +1100
@@ -0,0 +1,179 @@
+import wx
+from quick_frontend.quick_contact_list import QuickContactList
+from logging import debug, info, error
+from cgi import escape
+from tools.jid  import JID
+
+
+class Group(str):
+    """Class used to recognize groups"""
+
+class Contact(str):
+    """Class used to recognize groups"""
+
+class ContactList(wx.SimpleHtmlListBox, QuickContactList):
+    """Customized control to manage contacts."""
+
+    def __init__(self, parent, host, type="JID"):
+        """init the contact list
+        @param parent: WxWidgets parent of the widget
+        @param host: wix main app class
+        @param type: type of contact list: "JID" for the usual big jid contact list
+                                           "CUSTOM" for a customized contact list (self.__presentItem must then be overrided)
+        """
+        wx.SimpleHtmlListBox.__init__(self, parent, -1)
+        QuickContactList.__init__(self, host.CM)
+        self.host = host
+        self.type = type
+        self.__typeSwitch()
+        self.groups = {}  #list contacts in each groups, key = group
+        self.Bind(wx.EVT_LISTBOX, self.onSelected)
+        self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated)
+
+    def __typeSwitch(self):
+        if self.type == "JID":
+            self.__presentItem = self.__presentItemJID
+        elif type != "CUSTOM":
+            self.__presentItem = self.__presentItemDefault
+
+    def __find_idx(self, entity):
+        """Find indexes of given contact (or groups) in contact list, manage jid
+        @return: list of indexes"""
+        result=[]
+        for i in range(self.GetCount()):
+            if (type(entity) == JID and type(self.GetClientData(i)) == JID and self.GetClientData(i).short == entity.short) or\
+                self.GetClientData(i) == entity:
+                result.append(i)
+        return result
+
+    def replace(self, contact, groups=None):
+        debug(_("update %s") % contact)
+        if not self.__find_idx(contact):
+            self.add(contact, groups)
+        else:
+            for i in self.__find_idx(contact):
+                self.SetString(i, self.__presentItem(contact))
+
+    def disconnect(self, contact):
+        self.remove(contact) #for now, we only show online contacts
+    
+    def __eraseGroup(self, group):
+        """Erase all contacts in group
+        @param group: group to erase
+        @return: True if something as been erased"""
+        erased = False
+        indexes = self.__find_idx(group)
+        for idx in indexes:
+            while idx<self.GetCount()-1 and type(self.GetClientData(idx+1)) != Group:
+                erased = True
+                self.Delete(idx+1)
+        return erased
+
+
+    def __presentGroup(self, group):
+        """Make a nice presentation for the contact groups"""
+        html = """-- [%s] --""" % group
+
+        return html
+
+    def __presentItemDefault(self, contact):
+        """Make a basic presentation of string contacts in the list."""
+        return contact
+    
+    def __presentItemJID(self, jid):
+        """Make a nice presentation of the contact in the list for JID contacts."""
+        name = self.CM.getAttr(jid,'name')
+        nick = self.CM.getAttr(jid,'nick')
+        show =  filter(lambda x:x[0]==self.CM.getAttr(jid,'show'), const_STATUS)[0]
+        #show[0]==shortcut
+        #show[1]==human readable
+        #show[2]==color (or None)
+        show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else ""
+        status = self.CM.getAttr(jid,'status') or ''
+        avatar = self.CM.getAttr(jid,'avatar') or IMAGE_DIR+'/empty_avatar.png'
+        
+        #XXX: yes table I know :) but wxHTML* doesn't support CSS
+        html = """
+        <table border='0'>
+        <td>
+            <img  height='64' width='64' src='%s' />
+        </td>
+        <td>
+            <b>%s</b> %s<br />
+            <i>%s</i>
+        </td>
+        </table>
+        """ % (avatar,
+               escape(nick or name or jid.node or jid.short),
+               show_html,
+               escape(status)) 
+
+        return html
+
+    def clear_contacts(self):
+        """Clear all the contact list"""
+        self.Clear()
+
+    def add(self, contact, groups = None):
+        """add a contact to the list"""
+        debug (_("adding %s"),contact)
+        #gof: groups = param_groups or self.CM.getAttr(jid, 'groups')
+        if not groups:
+            idx = self.Insert(self.__presentItem(contact), 0, contact)
+        else:
+            for group in groups:
+                indexes = self.__find_idx(group)
+                gp_idx = 0
+                if not indexes:  #this is a new group, we have to create it
+                    gp_idx = self.Append(self.__presentGroup(group), Group(group))
+                else:
+                    gp_idx = indexes[0]
+
+                self.Insert(self.__presentItem(contact), gp_idx+1, contact)
+
+
+
+    def remove(self, contact):
+        """remove a contact from the list"""
+        debug (_("removing %s"), contact)
+        list_idx = self.__find_idx(contact)
+        list_idx.reverse()  #we me make some deletions, we have to reverse the order
+        for i in list_idx:
+            self.Delete(i)
+
+    def onSelected(self, event):
+        """Called when a contact is selected."""
+        data = self.getSelection()
+        if data == None: #we have a group
+            group = self.GetClientData(self.GetSelection())
+            erased = self.__eraseGroup(group)
+            if not erased: #the group was already erased, we can add again the contacts
+                contacts = self.CM.getContFromGroup(group)
+                contacts.sort()
+                id_insert = self.GetSelection()+1
+                for contact in contacts:
+                    self.Insert(self.__presentItem(contact), id_insert, contact)
+            self.SetSelection(wx.NOT_FOUND)
+            event.Skip(False)
+        else:
+            event.Skip()
+    
+    def onActivated(self, event):
+        """Called when a contact is clicked or activated with keyboard."""
+        data = self.getSelection()
+        self.onActivatedCB(data)
+        event.Skip()
+
+    def getSelection(self):
+        """Return the selected contact, or an empty string if there is not"""
+        if self.GetSelection() == wx.NOT_FOUND:
+            return None
+        data = self.GetClientData(self.GetSelection())
+        if type(data) == Group:
+            return None
+        return data
+
+    def registerActivatedCB(self, cb):
+        """Register a callback with manage contact activation."""
+        self.onActivatedCB=cb
+
--- a/frontends/wix/main_window.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/wix/main_window.py	Sun Mar 21 10:28:55 2010 +1100
@@ -22,9 +22,9 @@
 
 from quick_frontend.quick_chat_list import QuickChatList
 from quick_frontend.quick_app import QuickApp
-from quick_frontend.quick_contact_list import QuickContactList
 from quick_frontend.quick_contact_management import QuickContactManagement
 import wx
+from contact_list import ContactList
 from chat import Chat
 from param import Param
 from form import Form
@@ -35,14 +35,9 @@
 import os.path
 import pdb
 from tools.jid  import JID
-from logging import debug, info, error
-from cgi import escape
-import sys
+from logging import debug, info, warning, error
+import constants
 
-IMAGE_DIR = sys.path[0]+'/images'
-
-msgOFFLINE          = "offline"
-msgONLINE           = "online"
 idCONNECT,\
 idDISCONNECT,\
 idEXIT,\
@@ -50,13 +45,8 @@
 idADD_CONTACT,\
 idREMOVE_CONTACT,\
 idSHOW_PROFILE,\
-idFIND_GATEWAYS = range(8)
-const_DEFAULT_GROUP = "Unclassed"
-const_STATUS        = [("", _("Online"), None),
-                       ("chat", _("Free for chat"), "green"),
-                       ("away", _("AFK"), "brown"),
-                       ("dnd", _("DND"), "red"),
-                       ("xa", _("Away"), "red")]
+idJOIN_ROOM,\
+idFIND_GATEWAYS = range(9)
 
 class ChatList(QuickChatList):
     """This class manage the list of chat windows"""
@@ -67,155 +57,6 @@
     def createChat(self, target):
         return Chat(target, self.host)
     
-
-class ContactList(wx.SimpleHtmlListBox, QuickContactList):
-    """Customized control to manage contacts."""
-
-    def __init__(self, parent, CM):
-        wx.SimpleHtmlListBox.__init__(self, parent, -1)
-        QuickContactList.__init__(self, CM)
-        self.host = parent
-        self.groups = {}  #list contacts in each groups, key = group
-        self.Bind(wx.EVT_LISTBOX, self.onSelected)
-        self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated)
-
-    def __find_idx(self, entity, reverse=False):
-        """Find indexes of given jid (or groups) in contact list
-        @return: list of indexes"""
-        result=[]
-        for i in range(self.GetCount()):
-            if (type(entity) == JID and type(self.GetClientData(i)) == JID and self.GetClientData(i).short == entity.short) or\
-                self.GetClientData(i) == entity:
-                result.append(i)
-        return result
-
-    def replace(self, jid):
-        debug(_("update %s") % jid)
-        if not self.__find_idx(jid):
-            self.add(jid)
-        else:
-            for i in self.__find_idx(jid):
-                self.SetString(i, self.__presentItem(jid))
-
-    def disconnect(self, jid):
-        self.remove(jid) #for now, we only show online contacts
-    
-    def __eraseGroup(self, group):
-        """Erase all contacts in group
-        @param group: group to erase
-        @return: True if something as been erased"""
-        erased = False
-        indexes = self.__find_idx(group)
-        for idx in indexes:
-            while idx<self.GetCount()-1 and type(self.GetClientData(idx+1)) == JID:
-                erased = True
-                self.Delete(idx+1)
-        return erased
-
-
-    def __presentGroup(self, group):
-        """Make a nice presentation for the contact groups"""
-        html = """-- [%s] --""" % group
-
-        return html
-
-    def __presentItem(self, jid):
-        """Make a nice presentation of the contact in the list."""
-        name = self.CM.getAttr(jid,'name')
-        nick = self.CM.getAttr(jid,'nick')
-        show =  filter(lambda x:x[0]==self.CM.getAttr(jid,'show'), const_STATUS)[0]
-        #show[0]==shortcut
-        #show[1]==human readable
-        #show[2]==color (or None)
-        show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else ""
-        status = self.CM.getAttr(jid,'status') or ''
-        avatar = self.CM.getAttr(jid,'avatar') or IMAGE_DIR+'/empty_avatar.png'
-        
-        #XXX: yes table I know :) but wxHTML* doesn't support CSS
-        html = """
-        <table border='0'>
-        <td>
-            <img  height='64' width='64' src='%s' />
-        </td>
-        <td>
-            <b>%s</b> %s<br />
-            <i>%s</i>
-        </td>
-        </table>
-        """ % (avatar,
-               escape(nick or name or jid.node or jid.short),
-               show_html,
-               escape(status)) 
-
-        return html
-
-    def clear_contacts(self):
-        """Clear all the contact list"""
-        self.Clear()
-
-    def add(self, jid):
-        """add a contact to the list"""
-        debug (_("adding %s"),jid)
-        groups = self.CM.getAttr(jid, 'groups')
-        if not groups:
-            idx = self.Append(self.__presentItem(jid), jid)
-        else:
-            for group in groups:
-                indexes = self.__find_idx(group)
-                gp_idx = 0
-                if not indexes:  #this is a new group, we have to create it
-                    gp_idx = self.Append(self.__presentGroup(group), group)
-                else:
-                    gp_idx = indexes[0]
-
-                self.Insert(self.__presentItem(jid), gp_idx+1, jid)
-
-
-
-    def remove(self, jid):
-        """remove a contact from the list"""
-        debug (_("removing %s"),jid)
-        list_idx = self.__find_idx(jid)
-        list_idx.reverse()  #we me make some deletions, we have to reverse the order
-        for i in list_idx:
-            self.Delete(i)
-
-    def onSelected(self, event):
-        """Called when a contact is selected."""
-        data = self.getSelection()
-        if type(data) == JID:
-            event.Skip()
-        else:
-            group = self.GetClientData(self.GetSelection())
-            erased = self.__eraseGroup(group)
-            if not erased: #the group was already erased, we can add again the contacts
-                contacts = self.CM.getContFromGroup(group)
-                contacts.sort()
-                id_insert = self.GetSelection()+1
-                for contact in contacts:
-                    self.Insert(self.__presentItem(contact), id_insert, contact)
-            self.SetSelection(wx.NOT_FOUND)
-            event.Skip(False)
-    
-    def onActivated(self, event):
-        """Called when a contact is clicked or activated with keyboard."""
-        data = self.getSelection()
-        self.onActivatedCB(data)
-        event.Skip()
-
-    def getSelection(self):
-        """Return the selected contact, or an empty string if there is not"""
-        if self.GetSelection() == wx.NOT_FOUND:
-            return None
-        data = self.GetClientData(self.GetSelection())
-        if type(data) != JID:
-            return None
-        return data
-
-    def registerActivatedCB(self, cb):
-        """Register a callback with manage contact activation."""
-        self.onActivatedCB=cb
-
 class MainWindow(wx.Frame, QuickApp):
     """main app window"""
 
@@ -228,7 +69,7 @@
         self.SetSizer(self.sizer)
         
         #Frame elements
-        self.contactList = ContactList(self, self.CM)
+        self.contactList = ContactList(self, self)
         self.contactList.registerActivatedCB(self.onContactActivated)
         self.contactList.Hide()
         self.sizer.Add(self.contactList, 1, flag=wx.EXPAND)
@@ -296,6 +137,7 @@
         contactMenu.AppendSeparator()
         contactMenu.Append(idSHOW_PROFILE, _("&Show profile"), _(" Show contact's profile"))
         communicationMenu = wx.Menu()
+        communicationMenu.Append(idJOIN_ROOM, _("&Join Room"),_(" Join a Multi-User Chat room"))
         communicationMenu.Append(idFIND_GATEWAYS, _("&Find Gateways"),_(" Find gateways to legacy IM"))
         self.menuBar = wx.MenuBar()
         self.menuBar.Append(connectMenu,_("&General"))
@@ -311,12 +153,18 @@
         wx.EVT_MENU(self, idADD_CONTACT, self.onAddContact)
         wx.EVT_MENU(self, idREMOVE_CONTACT, self.onRemoveContact)
         wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile)
+        wx.EVT_MENU(self, idJOIN_ROOM, self.onJoinRoom)
         wx.EVT_MENU(self, idFIND_GATEWAYS, self.onFindGateways)
 
 
     def newMessage(self, from_jid, msg, type, to_jid, profile):
         QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile)
 
+    def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile):
+        super(MainWindow, self).roomJoined(room_id, room_service, room_nicks, user_nick, profile)
+        self.chat_wins[room_id+'@'+room_service].setType("group")
+        self.chat_wins[room_id+'@'+room_service].setPresents([user_nick]+room_nicks)
+
     def showAlert(self, message):
         # TODO: place this in a separate class
         popup=wx.PopupWindow(self)
@@ -552,6 +400,9 @@
         debug (_('Profile received: [%s]') % data)
         profile=Profile(self, data)
         
+    def onJoinRoom(self, e):
+        warning('FIXME: temporary menu, must be improved')
+        self.bridge.joinMUC("conference.necton2.int", "test", "Goffi \o/", self.profile)
 
     def onFindGateways(self, e):
         debug(_("Find Gateways request"))
--- a/frontends/wix/profile_manager.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/frontends/wix/profile_manager.py	Sun Mar 21 10:28:55 2010 +1100
@@ -132,7 +132,6 @@
         if name[0]=='@':
             wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal()
             return
-        profile = None # gof
         profile = self.host.bridge.getProfileName(name)
         if not profile:
             debug(_("The profile is new, we create it"))
@@ -141,10 +140,11 @@
         new_jid = self.login_jid.GetValue()
         new_pass = self.login_pass.GetValue()
         if old_jid != new_jid:
-            debug(_('Saving new JID'))
+            debug(_('Saving new JID and server'))
             self.host.bridge.setParam("JabberID", new_jid, "Connection", profile)
+            self.host.bridge.setParam("Server", JID(new_jid).domain, "Connection", profile)
         if old_pass != new_pass:
             debug(_('Saving new password'))
-            self.host.bridge.setParam("JabberID", new_pass, "Connection", profile)
+            self.host.bridge.setParam("Password", new_pass, "Connection", profile)
         self.host.plug_profile(name)
         
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/plugin_xep_0045.py	Sun Mar 21 10:28:55 2010 +1100
@@ -0,0 +1,133 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+SAT plugin for managing xep-0045
+Copyright (C) 2009, 2010  Jérôme Poisson (goffi@goffi.org)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from logging import debug, info, warning, error
+from twisted.words.xish import domish
+from twisted.internet import protocol, defer, threads, reactor
+from twisted.words.protocols.jabber import client, jid, xmlstream
+from twisted.words.protocols.jabber import error as jab_error
+from twisted.words.protocols.jabber.xmlstream import IQ
+import os.path
+import pdb
+
+from zope.interface import implements
+
+from wokkel import disco, iwokkel, muc
+
+from base64 import b64decode
+from hashlib import sha1
+from time import sleep
+
+try:
+    from twisted.words.protocols.xmlstream import XMPPHandler
+except ImportError:
+    from wokkel.subprotocols import XMPPHandler
+
+AVATAR_PATH = "/avatars"
+
+IQ_GET = '/iq[@type="get"]'
+NS_VCARD = 'vcard-temp'
+VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]'  #TODO: manage requests
+
+PRESENCE = '/presence'
+NS_VCARD_UPDATE = 'vcard-temp:x:update'
+VCARD_UPDATE = PRESENCE + '/x[@xmlns="' +  NS_VCARD_UPDATE + '"]'
+
+PLUGIN_INFO = {
+"name": "XEP 0045 Plugin",
+"import_name": "XEP_0045",
+"type": "XEP",
+"protocols": ["XEP-0045"],
+"dependencies": [],
+"main": "XEP_0045",
+"handler": "yes",
+"description": _("""Implementation of Multi-User Chat""")
+}
+
+class XEP_0045():
+
+    def __init__(self, host):
+        info(_("Plugin XEP_0045 initialization"))
+        self.host = host
+        self.clients={}
+        host.bridge.addMethod("joinMUC", ".communication", in_sign='ssss', out_sign='', method=self.join)
+
+    def __check_profile(self, profile):
+        if not profile or not self.clients.has_key(profile) or not self.host.isConnected(profile):
+            error (_('Unknown or disconnected profile'))
+            if self.clients.has_key(profile):
+                del self.clients[profile]
+            return False
+        return True
+
+    def __room_joined(self, room, profile):
+        """Called when the user is in the requested room"""
+        print "room joined (profile = %s)" % profile
+        room_jid = room.roomIdentifier+'@'+room.service
+        self.clients[profile].joined_rooms[room_jid] = room
+        self.host.bridge.roomJoined(room.roomIdentifier, room.service, room.roster.keys(), room.nick, profile)
+
+    def __err_joining_room(self, failure, profile): #, profile):
+        """Called when something is going wrong when joining the room"""
+        error ("Error when joining the room")
+        pdb.set_trace()
+
+    def join(self, service, roomId, nick, profile_key='@DEFAULT@'):
+        profile = self.host.memory.getProfileName(profile_key)
+        if not self.__check_profile(profile):
+            return
+        room_jid = roomId+'@'+service
+        if self.clients[profile].joined_rooms.has_key(room_jid):
+            warning(_('%(profile)s is already in room %(room_jid)s') % {'profile':profile, 'room_jid':room_jid})
+            return
+        info (_("[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile':profile,'room':roomId+'@'+service, 'nick':nick})
+        self.clients[profile].join(service, roomId, nick).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile':profile}, errbackKeywords={'profile':profile})
+
+    def getHandler(self, profile):
+        #reactor.callLater(15,self.join,"conference.necton2.int", "test", "Goffi \o/", profile) 
+        self.clients[profile] = SatMUCClient(self)
+        return self.clients[profile]
+   
+
+
+class SatMUCClient (muc.MUCClient):
+    #implements(iwokkel.IDisco)
+   
+    def __init__(self, plugin_parent):
+        self.plugin_parent = plugin_parent
+        self.host = plugin_parent.host
+        muc.MUCClient.__init__(self)
+        self.joined_rooms = {} #FIXME gof: check if necessary
+        print "init SatMUCClient OK"
+    
+    def receivedGroupChat(self, room, user, body):
+        debug('receivedGroupChat: room=%s user=%s body=%s', room, user, body)
+
+
+    #def connectionInitialized(self):
+        #pass
+    
+    #def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
+        #return [disco.DiscoFeature(NS_VCARD)]
+
+    #def getDiscoItems(self, requestor, target, nodeIdentifier=''):
+        #return []
+    
--- a/plugins/plugin_xep_0054.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/plugins/plugin_xep_0054.py	Sun Mar 21 10:28:55 2010 +1100
@@ -75,7 +75,7 @@
         host.bridge.addMethod("getAvatarFile", ".communication", in_sign='s', out_sign='s', method=self.getAvatarFile)
         host.bridge.addMethod("getCardCache", ".communication", in_sign='s', out_sign='a{ss}', method=self.getCardCache)
 
-    def getHandler(self):
+    def getHandler(self, profile):
         return XEP_0054_handler(self)  
    
     def update_cache(self, jid, name, value):
--- a/plugins/plugin_xep_0065.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/plugins/plugin_xep_0065.py	Sun Mar 21 10:28:55 2010 +1100
@@ -480,7 +480,7 @@
         info(_("Launching Socks5 Stream server on port %d"), port)
         reactor.listenTCP(port, self.server_factory)
     
-    def getHandler(self):
+    def getHandler(self, profile):
         return XEP_0065_handler(self)  
    
     def getExternalIP(self):
--- a/plugins/plugin_xep_0096.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/plugins/plugin_xep_0096.py	Sun Mar 21 10:28:55 2010 +1100
@@ -60,7 +60,7 @@
         self._waiting_for_approval = {}
         host.bridge.addMethod("sendFile", ".communication", in_sign='sss', out_sign='s', method=self.sendFile)
     
-    def getHandler(self):
+    def getHandler(self, profile):
         return XEP_0096_handler(self)  
 
     def xep_96(self, IQ, profile):
--- a/sat.tac	Sat Mar 06 14:57:23 2010 +1100
+++ b/sat.tac	Sun Mar 21 10:28:55 2010 +1100
@@ -185,7 +185,7 @@
         self.host = host
     
     def availableReceived(self, entity, show=None, statuses=None, priority=0):
-        info (_("presence update for [%s]"), entity)
+        debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority})
         
         if statuses.has_key(None):   #we only want string keys
             statuses["default"] = statuses[None]
@@ -199,6 +199,7 @@
                 int(priority), statuses, self.parent.profile)
     
     def unavailableReceived(self, entity, statuses=None):
+        debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses})
         if statuses and statuses.has_key(None):   #we only want string keys
             statuses["default"] = statuses[None]
             del statuses[None]
@@ -416,7 +417,7 @@
         
         for plugin in self.plugins.iteritems():
             if plugin[1].is_handler:
-                plugin[1].getHandler().setHandlerParent(current)
+                plugin[1].getHandler(profile).setHandlerParent(current)
 
         current.startService()
     
--- a/sat_bridge/DBus.py	Sat Mar 06 14:57:23 2010 +1100
+++ b/sat_bridge/DBus.py	Sun Mar 21 10:28:55 2010 +1100
@@ -69,6 +69,11 @@
         debug("presence update signal (from:%s show:%s priority:%d statuses:%s profile:%s) sended" , entity, show, priority, statuses, profile)
 
     @dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
+                         signature='ssasss')
+    def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile):
+        debug("room joined signal: id:%(room_id)s service:%(room_service)s nicks:%(room_nicks)s user:%(user_nick)s profile=%(profile)s" % {'room_id':room_id, 'room_service':room_service, 'room_nicks':room_nicks, 'user_nick':user_nick, 'profile':profile})
+
+    @dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
                          signature='sss')
     def subscribe(self, type, entity, profile):
         debug("subscribe (type: [%s] from:[%s] profile:[%s])" , type, entity, profile)
@@ -307,6 +312,9 @@
     def presenceUpdate(self, entity, show, priority, statuses, profile):
         debug("updating presence for %s",entity)
         self.dbus_bridge.presenceUpdate(entity, show, priority, statuses, profile)
+    
+    def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile):
+        self.dbus_bridge.roomJoined(room_id, room_service, room_nicks, user_nick, profile)
 
     def subscribe(self, type, entity, profile):
         debug("subscribe request for %s",entity)