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)
This commit is contained in:
parent
c9c7c483dc
commit
724a97b14d
15 changed files with 432 additions and 194 deletions
|
|
@ -42,6 +42,7 @@ class QuickApp():
|
|||
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 @@ class QuickApp():
|
|||
## 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 @@ class QuickApp():
|
|||
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 @@ class QuickApp():
|
|||
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"""
|
||||
|
|
|
|||
|
|
@ -26,14 +26,21 @@ from tools.jid import JID
|
|||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,6 @@ class QuickContactList():
|
|||
"""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
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@ class DBusBridgeFrontend(BridgeFrontend):
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import pdb
|
|||
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 @@ idSEND = 1
|
|||
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.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.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.createMenus()
|
||||
|
||||
#events
|
||||
|
|
@ -61,7 +67,37 @@ class Chat(wx.Frame, QuickChat):
|
|||
#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 @@ class Chat(wx.Frame, QuickChat):
|
|||
|
||||
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 @@ class Chat(wx.Frame, QuickChat):
|
|||
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))
|
||||
|
||||
|
|
|
|||
13
frontends/wix/constants.py
Normal file
13
frontends/wix/constants.py
Normal file
|
|
@ -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")]
|
||||
179
frontends/wix/contact_list.py
Normal file
179
frontends/wix/contact_list.py
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -22,9 +22,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
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 gobject
|
|||
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 @@ idPARAM,\
|
|||
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 @@ class ChatList(QuickChatList):
|
|||
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 @@ class MainWindow(wx.Frame, QuickApp):
|
|||
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 @@ class MainWindow(wx.Frame, QuickApp):
|
|||
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 @@ class MainWindow(wx.Frame, QuickApp):
|
|||
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 @@ class MainWindow(wx.Frame, QuickApp):
|
|||
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"))
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ class ProfileManager(wx.Panel):
|
|||
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 @@ class ProfileManager(wx.Panel):
|
|||
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)
|
||||
|
||||
|
|
|
|||
133
plugins/plugin_xep_0045.py
Executable file
133
plugins/plugin_xep_0045.py
Executable file
|
|
@ -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 []
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ class XEP_0054():
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ class XEP_0065():
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class XEP_0096():
|
|||
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):
|
||||
|
|
|
|||
5
sat.tac
5
sat.tac
|
|
@ -185,7 +185,7 @@ class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
|||
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 @@ class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
|||
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 @@ class SAT(service.Service):
|
|||
|
||||
for plugin in self.plugins.iteritems():
|
||||
if plugin[1].is_handler:
|
||||
plugin[1].getHandler().setHandlerParent(current)
|
||||
plugin[1].getHandler(profile).setHandlerParent(current)
|
||||
|
||||
current.startService()
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ class DbusObject(dbus.service.Object):
|
|||
def presenceUpdate(self, entity, show, priority, statuses, profile):
|
||||
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):
|
||||
|
|
@ -307,6 +312,9 @@ class DBusBridge(Bridge):
|
|||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue