Merging changes from trunk (6774:7465)

This commit is contained in:
Tomasz Melcer 2006-11-18 20:52:28 +00:00
parent 755964c3f1
commit 175a792cfe
69 changed files with 8647 additions and 2680 deletions

View file

@ -1,35 +0,0 @@
# Set the C flags to include the GTK+ and Python libraries
PYTHON ?= python
PYTHONVER = `$(PYTHON) -c 'import sys; print sys.version[:3]'`
gtk_CFLAGS = `pkg-config --cflags gtk+-2.0 pygtk-2.0` -fPIC -I/usr/include/python$(PYTHONVER) -I.
gtk_LDFLAGS = `pkg-config --libs gtk+-2.0 pygtk-2.0` -lpython$(PYTHONVER)
all: trayicon.so gtkspell.so
# Build the shared objects
trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o
$(CC) -shared $^ -o $@ $(LDFLAGS) $(gtk_LDFLAGS)
gtkspell.so:
$(CC) $(OPTFLAGS) $(CFLAGS) $(LDFLAGS) $(gtk_CFLAGS) $(gtk_LDFLAGS) `pkg-config --libs --cflags gtkspell-2.0` -shared gtkspellmodule.c $^ -o $@
# The path to the GTK+ python types
DEFS=`pkg-config --variable=defsdir pygtk-2.0`
%.o: %.c
$(CC) -o $@ -c $< $(CFLAGS) $(gtk_CFLAGS)
# Generate the C wrapper from the defs and our override file
trayicon.c: trayicon.defs trayicon.override
pygtk-codegen-2.0 --prefix trayicon \
--register $(DEFS)/gdk-types.defs \
--register $(DEFS)/gtk-types.defs \
--override trayicon.override \
trayicon.defs > $@
# A rule to clean the generated files
clean:
rm -f trayicon.so *.o trayicon.c gtkspell.so *~
.PHONY: clean

73
src/Makefile.am Normal file
View file

@ -0,0 +1,73 @@
SUBDIRS = common
CLEANFILES = \
trayicon.c
INCLUDES = \
$(PYTHON_INCLUDES)
if BUILD_GTKSPELL
gtkspelllib_LTLIBRARIES = gtkspell.la
gtkspelllibdir = $(libdir)/gajim
gtkspell_la_LIBADD = \
$(GTKSPELL_LIBS) $(PYGTK_LIBS)
gtkspell_la_SOURCES = \
gtkspellmodule.c
gtkspell_la_LDFLAGS = \
-module -avoid-version
gtkspell_la_CFLAGS = $(GTKSPELL_CFLAGS) $(PYGTK_CFLAGS)
endif
if BUILD_TRAYICON
trayiconlib_LTLIBRARIES = trayicon.la
trayiconlibdir = $(libdir)/gajim
trayicon_la_LIBADD = $(PYGTK_LIBS)
trayicon_la_SOURCES = \
eggtrayicon.c \
trayiconmodule.c
nodist_trayicon_la_SOURCES = \
trayicon.c
trayicon_la_LDFLAGS = \
-module -avoid-version
trayicon_la_CFLAGS = $(PYGTK_CFLAGS)
trayicon.c:
pygtk-codegen-2.0 --prefix trayicon \
--register $(PYGTK_DEFS)/gdk-types.defs \
--register $(PYGTK_DEFS)/gtk-types.defs \
--override $(srcdir)/trayicon.override \
$(srcdir)/trayicon.defs > $@
endif
gajimsrcdir = $(pkgdatadir)/src
gajimsrc_DATA = $(srcdir)/*.py
gajimsrc1dir = $(pkgdatadir)/src/common
gajimsrc1_DATA = \
$(srcdir)/common/*.py
gajimsrc2dir = $(pkgdatadir)/src/common/xmpp
gajimsrc2_DATA = \
$(srcdir)/common/xmpp/*.py
gajimsrc3dir = $(pkgdatadir)/src/common/zeroconf
gajimsrc3_DATA = \
$(srcdir)/common/zeroconf/*.py
DISTCLEANFILES =
EXTRA_DIST = $(gajimsrc_DATA) \
$(gajimsrc1_DATA) \
$(gajimsrc2_DATA) \
$(gajimsrc3_DATA) \
gtkspellmodule.c \
eggtrayicon.c \
trayiconmodule.c \
eggtrayicon.h \
trayicon.defs \
trayicon.override
MAINTAINERCLEANFILES = Makefile.in

View file

@ -1,18 +1,8 @@
## advanced.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Vincent Hanquez <tab@snarc.org>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005 Vincent Hanquez <tab@snarc.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
@ -42,7 +32,7 @@ C_TYPE
GTKGUI_GLADE = 'manage_accounts_window.glade'
class AdvancedConfigurationWindow:
class AdvancedConfigurationWindow(object):
def __init__(self):
self.xml = gtkgui_helpers.get_glade('advanced_configuration_window.glade')
self.window = self.xml.get_widget('advanced_configuration_window')

View file

@ -8,7 +8,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>

View file

@ -34,6 +34,8 @@ from message_textview import MessageTextView
from common.contacts import GC_Contact
from common.logger import Constants
constants = Constants()
from common.rst_xhtml_generator import create_xhtml
from common.xmpp.protocol import NS_XHTML
try:
import gtkspell
@ -112,7 +114,7 @@ class ChatControlBase(MessageControl):
acct, resource = None):
MessageControl.__init__(self, type_id, parent_win, widget_name,
display_names, contact, acct, resource = resource);
# when/if we do XHTML we will but formatting buttons back
# when/if we do XHTML we will put formatting buttons back
widget = self.xml.get_widget('emoticons_button')
id = widget.connect('clicked', self.on_emoticons_button_clicked)
self.handlers[id] = widget
@ -196,7 +198,6 @@ class ChatControlBase(MessageControl):
self.msg_textview.lang = lang
spell.set_language(lang)
except (gobject.GError, RuntimeError), msg:
#FIXME: add a ui for this use spell.set_language()
dialogs.ErrorDialog(unicode(msg), _('If that is not your language '
'for which you want to highlight misspelled words, then please '
'set your $LANG as appropriate. Eg. for French do export '
@ -229,6 +230,11 @@ class ChatControlBase(MessageControl):
item = gtk.SeparatorMenuItem()
menu.prepend(item)
item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
menu.prepend(item)
id = item.connect('activate', self.msg_textview.clear)
self.handlers[id] = item
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
item = gtk.MenuItem(_('Spelling language'))
menu.prepend(item)
@ -242,11 +248,6 @@ class ChatControlBase(MessageControl):
id = item.connect('activate', _on_select_dictionary, langs[lang])
self.handlers[id] = item
item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
menu.prepend(item)
id = item.connect('activate', self.msg_textview.clear)
self.handlers[id] = item
menu.show_all()
# moved from ChatControl
@ -425,7 +426,8 @@ class ChatControlBase(MessageControl):
message_textview = widget
message_buffer = message_textview.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8')
message = message_buffer.get_text(start_iter, end_iter, False).decode(
'utf-8')
# construct event instance from binding
event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
@ -471,10 +473,11 @@ class ChatControlBase(MessageControl):
self.send_message(message) # send the message
else:
# Give the control itself a chance to process
self.handle_message_textview_mykey_press(widget, event_keyval, event_keymod)
self.handle_message_textview_mykey_press(widget, event_keyval,
event_keymod)
def _process_command(self, message):
if not message:
if not message or message[0] != '/':
return False
message = message[1:]
@ -534,7 +537,7 @@ class ChatControlBase(MessageControl):
def print_conversation_line(self, text, kind, name, tim,
other_tags_for_name = [], other_tags_for_time = [],
other_tags_for_text = [], count_as_new = True,
subject = None, old_kind = None):
subject = None, old_kind = None, xhtml = None):
'''prints 'chat' type messages'''
jid = self.contact.jid
full_jid = self.get_full_jid()
@ -544,20 +547,26 @@ class ChatControlBase(MessageControl):
end = True
textview.print_conversation_line(text, jid, kind, name, tim,
other_tags_for_name, other_tags_for_time, other_tags_for_text,
subject, old_kind)
subject, old_kind, xhtml)
if not count_as_new:
return
if kind == 'incoming':
gajim.last_message_time[self.account][full_jid] = time.time()
urgent = True
if (not self.parent_win.get_active_jid() or \
full_jid != self.parent_win.get_active_jid() or \
not self.parent_win.is_active() or not end) and \
kind in ('incoming', 'incoming_queue'):
if self.notify_on_new_messages():
gc_message = False
if self.type_id == message_control.TYPE_GC:
gc_message = True
if not gc_message or \
(gc_message and (other_tags_for_text == ['marked'] or \
gajim.config.get('notify_on_all_muc_messages'))):
# we want to have save this message in events list
# other_tags_for_text == ['marked'] --> highlighted gc message
type_ = 'printed_' + self.type_id
if self.type_id == message_control.TYPE_GC:
if gc_message:
type_ = 'printed_gc_msg'
show_in_roster = notify.get_show_in_roster('message_received',
self.account, self.contact)
@ -572,10 +581,11 @@ class ChatControlBase(MessageControl):
gajim.interface.roster.draw_contact(self.contact.jid,
self.account)
self.parent_win.redraw_tab(self)
ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account)
if not self.parent_win.is_active():
ctrl = gajim.interface.msg_win_mgr.get_control(full_jid,
self.account)
self.parent_win.show_title(urgent, ctrl)
self.parent_win.show_title(True, ctrl) # Enabled Urgent hint
else:
self.parent_win.show_title(False, ctrl) # Disabled Urgent hint
def toggle_emoticons(self):
'''hide show emoticons_button and make sure emoticons_menu is always there
@ -647,17 +657,7 @@ class ChatControlBase(MessageControl):
if not gajim.events.remove_events(self.account, self.get_full_jid(),
types = [type_]):
# There were events to remove
self.parent_win.redraw_tab(self)
self.parent_win.show_title()
# redraw roster
if self.type_id == message_control.TYPE_PM:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
groupchat_control = gajim.interface.msg_win_mgr.get_control(
room_jid, self.account)
groupchat_control.draw_contact(nick)
else:
gajim.interface.roster.draw_contact(jid, self.account)
gajim.interface.roster.show_title()
self.redraw_after_event_removed(jid)
self.msg_textview.grab_focus()
# Note, we send None chatstate to preserve current
self.parent_win.redraw_tab(self)
@ -750,8 +750,22 @@ class ChatControlBase(MessageControl):
if not gajim.events.remove_events(self.account, self.get_full_jid(),
types = ['printed_' + type_, type_]):
# There were events to remove
self.parent_win.redraw_tab(self)
self.parent_win.show_title()
self.redraw_after_event_removed(jid)
def redraw_after_event_removed(self, jid):
''' We just removed a 'printed_*' event, redraw contact in roster or
gc_roster and titles in roster and msg_win '''
self.parent_win.redraw_tab(self)
self.parent_win.show_title()
# TODO : get the contact and check notify.get_show_in_roster()
if self.type_id == message_control.TYPE_PM:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
groupchat_control = gajim.interface.msg_win_mgr.get_control(
room_jid, self.account)
groupchat_control.draw_contact(nick)
else:
gajim.interface.roster.draw_contact(jid, self.account)
gajim.interface.roster.show_title()
def sent_messages_scroll(self, direction, conv_buf):
size = len(self.sent_history)
@ -760,7 +774,8 @@ class ChatControlBase(MessageControl):
#whatever is already typed
start_iter = conv_buf.get_start_iter()
end_iter = conv_buf.get_end_iter()
self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode('utf-8')
self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode(
'utf-8')
self.typing_new = False
if direction == 'up':
if self.sent_history_pos == 0:
@ -820,8 +835,8 @@ class ChatControl(ChatControlBase):
old_msg_kind = None # last kind of the printed message
def __init__(self, parent_win, contact, acct, resource = None):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox',
(_('Chat'), _('Chats')), contact, acct, resource)
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
'chat_child_vbox', (_('Chat'), _('Chats')), contact, acct, resource)
# for muc use:
# widget = self.xml.get_widget('muc_window_actions_button')
@ -829,13 +844,16 @@ class ChatControl(ChatControlBase):
id = widget.connect('clicked', self.on_actions_button_clicked)
self.handlers[id] = widget
self.hide_chat_buttons_always = gajim.config.get('always_hide_chat_buttons')
self.hide_chat_buttons_always = gajim.config.get(
'always_hide_chat_buttons')
self.chat_buttons_set_visible(self.hide_chat_buttons_always)
self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_chat_banner'))
self.widget_set_visible(self.xml.get_widget('banner_eventbox'),
gajim.config.get('hide_chat_banner'))
# Initialize drag-n-drop
self.TARGET_TYPE_URI_LIST = 80
self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ]
id = self.widget.connect('drag_data_received', self._on_drag_data_received)
id = self.widget.connect('drag_data_received',
self._on_drag_data_received)
self.handlers[id] = self.widget
self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT |
@ -857,17 +875,21 @@ class ChatControl(ChatControlBase):
self._on_window_motion_notify)
self.handlers[id] = self.parent_win.window
message_tv_buffer = self.msg_textview.get_buffer()
id = message_tv_buffer.connect('changed', self._on_message_tv_buffer_changed)
id = message_tv_buffer.connect('changed',
self._on_message_tv_buffer_changed)
self.handlers[id] = message_tv_buffer
widget = self.xml.get_widget('avatar_eventbox')
id = widget.connect('enter-notify-event', self.on_avatar_eventbox_enter_notify_event)
id = widget.connect('enter-notify-event',
self.on_avatar_eventbox_enter_notify_event)
self.handlers[id] = widget
id = widget.connect('leave-notify-event', self.on_avatar_eventbox_leave_notify_event)
id = widget.connect('leave-notify-event',
self.on_avatar_eventbox_leave_notify_event)
self.handlers[id] = widget
id = widget.connect('button-press-event', self.on_avatar_eventbox_button_press_event)
id = widget.connect('button-press-event',
self.on_avatar_eventbox_button_press_event)
self.handlers[id] = widget
widget = self.xml.get_widget('gpg_togglebutton')
@ -881,12 +903,7 @@ class ChatControl(ChatControlBase):
self.update_ui()
# restore previous conversation
self.restore_conversation()
# is account displayed after nick in banner ?
self.account_displayed= False
def notify_on_new_messages(self):
return gajim.config.get('trayicon_notification_on_new_messages')
def on_avatar_eventbox_enter_notify_event(self, widget, event):
'''we enter the eventbox area so we under conditions add a timeout
to show a bigger avatar after 0.5 sec'''
@ -924,7 +941,8 @@ class ChatControl(ChatControlBase):
menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
id = menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.contact.jid, self.account, self.contact.name + '.jpeg')
self.contact.jid, self.account, self.contact.get_shown_name() +
'.jpeg')
self.handlers[id] = menuitem
menu.append(menuitem)
menu.show_all()
@ -1001,39 +1019,33 @@ class ChatControl(ChatControlBase):
banner_name_label = self.xml.get_widget('banner_name_label')
name = contact.get_shown_name()
avoid_showing_account_too = False
if self.resource:
name += '/' + self.resource
avoid_showing_account_too = True
if self.TYPE_ID == message_control.TYPE_PM:
room_jid = self.contact.jid.split('/')[0]
room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid,
self.account)
name = _('%s from room %s') % (name, room_ctrl.name)
name = _('%(nickname)s from group chat %(room_name)s') %\
{'nickname': name, 'room_name': self.room_name}
name = gtkgui_helpers.escape_for_pango_markup(name)
# We know our contacts nick, but if there are any other controls
# with the same nick we need to also display the account
# We know our contacts nick, but if another contact has the same nick
# in another account we need to also display the account.
# except if we are talking to two different resources of the same contact
acct_info = ''
self.account_displayed = False
for ctrl in self.parent_win.controls():
if ctrl == self:
for account in gajim.contacts.get_accounts():
if account == self.account:
continue
if self.contact.get_shown_name() == ctrl.contact.get_shown_name()\
and not avoid_showing_account_too:
self.account_displayed = True
if not ctrl.account_displayed:
# do that after this instance exists
gobject.idle_add(ctrl.draw_banner)
acct_info = ' (%s)' % \
gtkgui_helpers.escape_for_pango_markup(self.account)
if acct_info: # We already found a contact with same nick
break
for jid in gajim.contacts.get_jid_list(account):
contact_ = gajim.contacts.get_first_contact_from_jid(account, jid)
if contact_.get_shown_name() == self.contact.get_shown_name():
acct_info = ' (%s)' % \
gtkgui_helpers.escape_for_pango_markup(self.account)
break
status = contact.status
if status is not None:
banner_name_label.set_ellipsize(pango.ELLIPSIZE_END)
status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 2)
status = helpers.reduce_chars_newlines(status, max_lines = 2)
status_escaped = gtkgui_helpers.escape_for_pango_markup(status)
font_attrs, font_attrs_small = self.get_font_attrs()
@ -1071,8 +1083,8 @@ class ChatControl(ChatControlBase):
banner_name_label.set_markup(label_text)
def on_toggle_gpg_togglebutton(self, widget):
gajim.config.set_per('contacts', self.contact.get_full_jid(),
'gpg_enabled', widget.get_active())
gajim.config.set_per('contacts', self.contact.jid, 'gpg_enabled',
widget.get_active())
def _update_gpg(self):
tb = self.xml.get_widget('gpg_togglebutton')
@ -1166,8 +1178,8 @@ class ChatControl(ChatControlBase):
if current_state == 'composing':
self.send_chatstate('paused') # pause composing
# assume no activity and let the motion-notify or 'insert-text' make them True
# refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
# assume no activity and let the motion-notify or 'insert-text' make them
# True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
@ -1186,11 +1198,12 @@ class ChatControl(ChatControlBase):
if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
return True # loop forever
if not self.mouse_over_in_last_30_secs or self.kbd_activity_in_last_30_secs:
if not self.mouse_over_in_last_30_secs or \
self.kbd_activity_in_last_30_secs:
self.send_chatstate('inactive', contact)
# assume no activity and let the motion-notify or 'insert-text' make them True
# refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
# assume no activity and let the motion-notify or 'insert-text' make them
# True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
@ -1201,7 +1214,7 @@ class ChatControl(ChatControlBase):
self.kbd_activity_in_last_30_secs = False
def print_conversation(self, text, frm = '', tim = None,
encrypted = False, subject = None):
encrypted = False, subject = None, xhtml = None):
'''Print a line in the conversation:
if contact is set to status: it's a status message
if contact is set to another value: it's an outgoing message
@ -1240,8 +1253,12 @@ class ChatControl(ChatControlBase):
else:
kind = 'outgoing'
name = gajim.nicks[self.account]
if not xhtml and not encrypted and gajim.config.get('rst_formatting_outgoing_messages'):
xhtml = create_xhtml(text)
if xhtml:
xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
ChatControlBase.print_conversation_line(self, text, kind, name, tim,
subject = subject, old_kind = self.old_msg_kind)
subject = subject, old_kind = self.old_msg_kind, xhtml = xhtml)
if text.startswith('/me ') or text.startswith('/me\n'):
self.old_msg_kind = None
else:
@ -1276,9 +1293,6 @@ class ChatControl(ChatControlBase):
elif chatstate == 'paused':
color = gajim.config.get_per('themes', theme,
'state_paused_color')
else:
color = gajim.config.get_per('themes', theme,
'state_active_color')
if color:
# We set the color for when it's the current tab or not
color = gtk.gdk.colormap_get_system().alloc_color(color)
@ -1287,6 +1301,9 @@ class ChatControl(ChatControlBase):
if chatstate in ('inactive', 'gone') and\
self.parent_win.get_active_control() != self:
color = self.lighten_color(color)
elif chatstate == 'active' : # active, get color from gtk
color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
name = self.contact.get_shown_name()
if self.resource:
@ -1459,18 +1476,20 @@ class ChatControl(ChatControlBase):
# prevent going paused if we we were not composing (JEP violation)
if state == 'paused' and not contact.our_chatstate == 'composing':
MessageControl.send_message(self, None, chatstate = 'active') # go active before
# go active before
MessageControl.send_message(self, None, chatstate = 'active')
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
# if we're inactive prevent composing (JEP violation)
elif contact.our_chatstate == 'inactive' and state == 'composing':
MessageControl.send_message(self, None, chatstate = 'active') # go active before
# go active before
MessageControl.send_message(self, None, chatstate = 'active')
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
MessageControl.send_message(self, None, chatstate = state, msg_id = contact.msg_id,
composing_jep = contact.composing_jep)
MessageControl.send_message(self, None, chatstate = state,
msg_id = contact.msg_id, composing_jep = contact.composing_jep)
contact.our_chatstate = state
if contact.our_chatstate == 'active':
self.reset_kbd_mouse_timeout_vars()
@ -1501,7 +1520,7 @@ class ChatControl(ChatControlBase):
self.msg_textview.destroy()
def allow_shutdown(self):
def allow_shutdown(self, method):
if time.time() - gajim.last_message_time[self.account]\
[self.get_full_jid()] < 2:
# 2 seconds
@ -1593,13 +1612,16 @@ class ChatControl(ChatControlBase):
return
timeout = gajim.config.get('restore_timeout') # in minutes
events = gajim.events.get_events(self.account, jid, ['chat'])
# number of messages that are in queue and are already logged, we want
# to avoid duplication
pending_how_many = len(events)
pending_how_many = len(gajim.events.get_events(self.account, jid,
['chat', 'pm']))
if self.resource:
pending_how_many += len(gajim.events.get_events(self.account,
self.contact.get_full_jid(), ['chat', 'pm']))
rows = gajim.logger.get_last_conversation_lines(jid, restore_how_many,
pending_how_many, timeout, self.account)
pending_how_many, timeout, self.account)
local_old_kind = None
for row in rows: # row[0] time, row[1] has kind, row[2] the message
if not row[2]: # message is empty, we don't print it
@ -1657,7 +1679,7 @@ class ChatControl(ChatControlBase):
else:
kind = 'print_queue'
self.print_conversation(data[0], kind, tim = data[3],
encrypted = data[4], subject = data[1])
encrypted = data[4], subject = data[1], xhtml = data[7])
if len(data) > 6 and isinstance(data[6], int):
message_ids.append(data[6])
if message_ids:
@ -1665,30 +1687,19 @@ class ChatControl(ChatControlBase):
gajim.events.remove_events(self.account, jid_with_resource,
types = [self.type_id])
self.parent_win.show_title()
self.parent_win.redraw_tab(self)
# redraw roster
gajim.interface.roster.show_title()
typ = 'chat' # Is it a normal chat or a pm ?
# reset to status image in gc if it is a pm
if is_pm:
control.update_ui()
typ = 'pm'
if is_pm:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
groupchat_control = gajim.interface.msg_win_mgr.get_control(
room_jid, self.account)
groupchat_control.draw_contact(nick)
else:
gajim.interface.roster.draw_contact(jid, self.account)
# Redraw parent too
gajim.interface.roster.draw_parent_contact(jid, self.account)
if (self.contact.show == 'offline' or self.contact.show == 'error'):
showOffline = gajim.config.get('showoffline')
if not showOffline and typ == 'chat' and \
len(gajim.contacts.get_contact(self.account, jid)) < 2:
self.redraw_after_event_removed(jid)
if (self.contact.show in ('offline', 'error')):
show_offline = gajim.config.get('showoffline')
show_transports = gajim.config.get('show_transports_group')
if (not show_transports and gajim.jid_is_transport(jid)) or \
(not show_offline and typ == 'chat' and \
len(gajim.contacts.get_contact(self.account, jid)) < 2):
gajim.interface.roster.really_remove_contact(self.contact,
self.account)
elif typ == 'pm':

View file

@ -2,13 +2,13 @@
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <nkour@jabber.org>
## - Nikos Kouremenos <kourem@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>

View file

@ -1,25 +0,0 @@
# Set the C flags to include the GTK+ and Python libraries
PYTHON ?= python
PYTHONVER = `$(PYTHON) -c 'import sys; print sys.version[:3]'`
HAVE_XSCRNSAVER = $(shell pkg-config --exists xscrnsaver && echo 'YES')
ifeq ($(HAVE_XSCRNSAVER),YES)
# We link with libXScrnsaver from modular X.Org X11
gtk_and_x_CFLAGS = `pkg-config --cflags gtk+-2.0 pygtk-2.0 xscrnsaver` -fpic -I/usr/include/python$(PYTHONVER) -I.
gtk_and_x_LDFLAGS = `pkg-config --libs gtk+-2.0 pygtk-2.0 xscrnsaver` -lpython$(PYTHONVER)
else
# # We link with libXScrnsaver from monolithic X.Org X11
gtk_and_x_CFLAGS = `pkg-config --cflags gtk+-2.0 pygtk-2.0` -fpic -I/usr/include/python$(PYTHONVER) -I.
gtk_and_x_LDFLAGS = `pkg-config --libs gtk+-2.0 pygtk-2.0` \
-L/usr/X11R6$(LIBDIR) -lX11 -lXss -lXext -lpython$(PYTHONVER)
endif
all: idle.so
idle.so:
$(CC) $(OPTFLAGS) $(CFLAGS) $(LDFLAGS) $(gtk_and_x_CFLAGS) $(gtk_and_x_LDFLAGS) -shared idle.c $^ -o $@
clean:
rm -f *.so
rm -rf build

22
src/common/Makefile.am Normal file
View file

@ -0,0 +1,22 @@
INCLUDES = \
$(PYTHON_INCLUDES)
if BUILD_IDLE
idlelib_LTLIBRARIES = idle.la
idlelibdir = $(libdir)/gajim
idle_la_LIBADD = $(XSCRNSAVER_LIBS)
idle_la_SOURCES = idle.c
idle_la_LDFLAGS = \
-module -avoid-version
idle_la_CFLAGS = $(XSCRNSAVER_CFLAGS) $(PYTHON_INCLUDES)
endif
DISTCLEANFILES =
EXTRA_DIST =
MAINTAINERCLEANFILES = Makefile.in

View file

@ -1,16 +1,7 @@
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Travis Shirk <travis@pobox.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -29,7 +20,14 @@ import stat
from common import gajim
import logger
from pysqlite2 import dbapi2 as sqlite # DO NOT MOVE ABOVE OF import gajim
# DO NOT MOVE ABOVE OF import gajim
try:
import sqlite3 as sqlite # python 2.5
except ImportError:
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
raise exceptions.PysqliteNotAvailable
def create_log_db():
print _('creating logs database')
@ -57,11 +55,13 @@ def create_log_db():
jid_id INTEGER
);
CREATE INDEX idx_unread_messages_jid_id ON unread_messages (jid_id);
CREATE TABLE transports_cache (
transport TEXT UNIQUE,
type INTEGER
);
CREATE TABLE logs(
log_line_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
jid_id INTEGER,
@ -72,6 +72,8 @@ def create_log_db():
message TEXT,
subject TEXT
);
CREATE INDEX idx_logs_jid_id_kind ON logs (jid_id, kind);
'''
)

View file

@ -6,7 +6,8 @@
## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005 Travis Shirk <travis@pobox.com>
## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za>
##
## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
##
## 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; version 2 only.
@ -20,6 +21,7 @@
import sre
import copy
import defs
(
@ -37,6 +39,8 @@ opt_bool = [ 'boolean', 0 ]
opt_color = [ 'color', '^(#[0-9a-fA-F]{6})|()$' ]
opt_one_window_types = ['never', 'always', 'peracct', 'pertype']
DEFAULT_ICONSET = 'dcraven'
class Config:
__options = {
@ -50,7 +54,8 @@ class Config:
'autopopupaway': [ opt_bool, False ],
'use_notif_daemon': [ opt_bool, True , _('Use D-Bus and Notification-Daemon to show notifications') ],
'ignore_unknown_contacts': [ opt_bool, False ],
'showoffline': [ opt_bool, False, '', True ],
'showoffline': [ opt_bool, False ],
'show_transports_group': [ opt_bool, True ],
'autoaway': [ opt_bool, True ],
'autoawaytime': [ opt_int, 5, _('Time in minutes, after which your status changes to away.') ],
'autoaway_message': [ opt_str, _('Away as a result of being idle') ],
@ -67,7 +72,7 @@ class Config:
'last_status_msg_invisible': [ opt_str, '' ],
'last_status_msg_offline': [ opt_str, '' ],
'trayicon': [ opt_bool, True, '', True ],
'iconset': [ opt_str, 'dcraven', '', True ],
'iconset': [ opt_str, DEFAULT_ICONSET, '', True ],
'use_transports_iconsets': [ opt_bool, True, '', True ],
'inmsgcolor': [ opt_color, '#a34526', '', True ],
'outmsgcolor': [ opt_color, '#164e6f', '', True ],
@ -79,15 +84,19 @@ class Config:
'saveposition': [ opt_bool, True ],
'mergeaccounts': [ opt_bool, False, '', True ],
'sort_by_show': [ opt_bool, True, '', True ],
'enable_zeroconf': [opt_bool, False, _('Enable link-local/zeroconf messaging')],
'use_speller': [ opt_bool, False, ],
'ignore_incoming_xhtml': [ opt_bool, False, ],
'speller_language': [ opt_str, '', _('Language used by speller')],
'print_time': [ opt_str, 'always', _('\'always\' - print time for every message.\n\'sometimes\' - print time every print_ichat_every_foo_minutes minute.\n\'never\' - never print time.')],
'print_time_fuzzy': [ opt_int, 0, _('Value of fuzziness from 1 to 4 or 0 to disable fuzzyclock. 1 is the most precise clock, 4 the less precise one.') ],
'print_time_fuzzy': [ opt_int, 0, _('Print time in chats using Fuzzy Clock. Value of fuzziness from 1 to 4, or 0 to disable fuzzyclock. 1 is the most precise clock, 4 the less precise one. This is used only if print_time is \'sometimes\'.') ],
'emoticons_theme': [opt_str, 'static', '', True ],
'ascii_formatting': [ opt_bool, True,
_('Treat * / _ pairs as possible formatting characters.'), True],
'show_ascii_formatting_chars': [ opt_bool, True , _('If True, do not '
'remove */_ . So *abc* will be bold but with * * not removed.')],
'rst_formatting_outgoing_messages': [ opt_bool, False,
_('Uses ReStructured text markup for HTML, plus ascii formatting if selected. (If you want to use this, install docutils)')],
'sounds_on': [ opt_bool, True ],
# 'aplay', 'play', 'esdplay', 'artsplay' detected first time only
'soundplayer': [ opt_str, '' ],
@ -125,6 +134,7 @@ class Config:
'before_nickname': [ opt_str, '' ],
'after_nickname': [ opt_str, ':' ],
'send_os_info': [ opt_bool, True ],
'set_status_msg_from_current_music_track': [ opt_bool, False ],
'notify_on_new_gmail_email': [ opt_bool, True ],
'notify_on_new_gmail_email_extra': [ opt_bool, False ],
'usegpg': [ opt_bool, False, '', True ],
@ -135,25 +145,26 @@ class Config:
'send_on_ctrl_enter': [opt_bool, False, _('Send message on Ctrl+Enter and with Enter make new line (Mirabilis ICQ Client default behaviour).')],
'show_roster_on_startup': [opt_bool, True],
'key_up_lines': [opt_int, 25, _('How many lines to store for Ctrl+KeyUP.')],
'version': [ opt_str, '0.10.1.3' ], # which version created the config
'version': [ opt_str, defs.version ], # which version created the config
'search_engine': [opt_str, 'http://www.google.com/search?&q=%s&sourceid=gajim'],
'dictionary_url': [opt_str, 'WIKTIONARY', _("Either custom url with %s in it where %s is the word/phrase or 'WIKTIONARY' which means use wiktionary.")],
'always_english_wikipedia': [opt_bool, False],
'always_english_wiktionary': [opt_bool, True],
'remote_control': [opt_bool, True, _('If checked, Gajim can be controlled remotely using gajim-remote.'), True],
'networkmanager_support': [opt_bool, True, _('If True, listen to D-Bus signals from NetworkManager and change the status of accounts (provided they do not have listen_to_network_manager set to False and they sync with global status) based upon the status of the network connection.'), True],
'chat_state_notifications': [opt_str, 'all'], # 'all', 'composing_only', 'disabled'
'autodetect_browser_mailer': [opt_bool, False, '', True],
'print_ichat_every_foo_minutes': [opt_int, 5, _('When not printing time for every message (print_time==sometimes), print it every x minutes.')],
'confirm_close_muc': [opt_bool, True, _('Ask before closing a group chat tab/window.')],
'confirm_close_muc_rooms': [opt_str, '', _('Always ask before closing group chat tab/window in this space separated list of room jids.')],
'noconfirm_close_muc_rooms': [opt_str, '', _('Never ask before closing group chat tab/window in this space separated list of room jids.')],
'confirm_close_muc_rooms': [opt_str, '', _('Always ask before closing group chat tab/window in this space separated list of group chat jids.')],
'noconfirm_close_muc_rooms': [opt_str, '', _('Never ask before closing group chat tab/window in this space separated list of group chat jids.')],
'notify_on_file_complete': [opt_bool, True],
'file_transfers_port': [opt_int, 28011],
'ft_override_host_to_send': [opt_str, '', _('Overrides the host we send for File Transfer in case of address translation/port forwarding.')],
'conversation_font': [opt_str, ''],
'use_kib_mib': [opt_bool, False, _('IEC standard says KiB = 1024 bytes, KB = 1000 bytes.')],
'notify_on_all_muc_messages': [opt_bool, False],
'trayicon_notification_on_new_messages': [opt_bool, True],
'trayicon_notification_on_events': [opt_bool, True, _('Notify of events in the system trayicon.')],
'last_save_dir': [opt_str, ''],
'last_send_dir': [opt_str, ''],
'last_emoticons_dir': [opt_str, ''],
@ -174,7 +185,7 @@ class Config:
'notification_position_y': [opt_int, -1],
'notification_avatar_width': [opt_int, 48],
'notification_avatar_height': [opt_int, 48],
'muc_highlight_words': [opt_str, '', _('A semicolon-separated list of words that will be highlighted in multi-user chat.')],
'muc_highlight_words': [opt_str, '', _('A semicolon-separated list of words that will be highlighted in group chats.')],
'quit_on_roster_x_button': [opt_bool, False, _('If True, quits Gajim when X button of Window Manager is clicked. This setting is taken into account only if trayicon is used.')],
'set_xmpp://_handler_everytime': [opt_bool, False, _('If True, Gajim registers for xmpp:// on each startup.')],
'show_unread_tab_icon': [opt_bool, False, _('If True, Gajim will display an icon on each tab containing unread messages. Depending on the theme, this icon may be animated.')],
@ -182,15 +193,15 @@ class Config:
'show_avatars_in_roster': [opt_bool, True, '', True],
'ask_avatars_on_startup': [opt_bool, True, _('If True, Gajim will ask for avatar each contact that did not have an avatar last time or has one cached that is too old.')],
'print_status_in_chats': [opt_bool, True, _('If False, Gajim will no longer print status line in chats when a contact changes his or her status and/or his or her status message.')],
'print_status_in_muc': [opt_str, 'in_and_out', _('can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes his or her status and/or his or her status message. If "all" Gajim will print all status messages. If "in_and_out", gajim will only print FOO enters/leaves room.')],
'print_status_in_muc': [opt_str, 'in_and_out', _('can be "none", "all" or "in_and_out". If "none", Gajim will no longer print status line in groupchats when a member changes his or her status and/or his or her status message. If "all" Gajim will print all status messages. If "in_and_out", gajim will only print FOO enters/leaves group chat.')],
'log_contact_status_changes': [opt_bool, False],
'restored_messages_color': [opt_str, 'grey'],
'restored_messages_small': [opt_bool, True, _('If True, restored messages will use a smaller font than the default one.')],
'hide_avatar_of_transport': [opt_bool, False, _('Don\'t show avatar for the transport itself.')],
'roster_window_skip_taskbar': [opt_bool, False],
'roster_window_skip_taskbar': [opt_bool, False, _('Don\'t show roster in the system taskbar.')],
'use_urgency_hint': [opt_bool, True, _('If True and installed GTK+ and PyGTK versions are at least 2.8, make the window flash (the default behaviour in most Window Managers) when holding pending events.')],
'notification_timeout': [opt_int, 5],
'send_sha_in_gc_presence': [opt_bool, True, _('Jabberd1.4 does not like sha info when one join a password protected room. Turn this option to False to stop sending sha info in group chat presences.')],
'send_sha_in_gc_presence': [opt_bool, True, _('Jabberd1.4 does not like sha info when one join a password protected group chat. Turn this option to False to stop sending sha info in group chat presences.')],
'one_message_window': [opt_str, 'always',
#always, never, peracct, pertype should not be translated
_('Controls the window where new messages are placed.\n\'always\' - All messages are sent to a single window.\n\'never\' - All messages get their own window.\n\'peracct\' - Messages for each account are sent to a specific window.\n\'pertype\' - Each message type (e.g., chats vs. groupchats) are sent to a specific window. Note, changing this option requires restarting Gajim before the changes will take effect.')],
@ -200,11 +211,12 @@ class Config:
'always_hide_chat_buttons': [opt_bool, False, _('Hides the buttons in two persons chat window.')],
'hide_groupchat_banner': [opt_bool, False, _('Hides the banner in a group chat window')],
'hide_chat_banner': [opt_bool, False, _('Hides the banner in two persons chat window')],
'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the room occupants list in group chat window.')],
'chat_merge_consecutive_nickname': [opt_bool, False, _('Merge consecutive nickname in chat window.')],
'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the group chat occupants list in group chat window.')],
'chat_merge_consecutive_nickname': [opt_bool, False, _('In a chat, show the nickname at the beginning of a line only when it\'s not the same person talking than in previous message.')],
'chat_merge_consecutive_nickname_indent': [opt_str, ' ', _('Indentation when using merge consecutive nickame.')],
'gc_nicknames_colors': [ opt_str, '#a34526:#c000ff:#0012ff:#388a99:#38995d:#519938:#ff8a00:#94452d:#244b5a:#32645a', _('List of colors that will be used to color nicknames in group chats.'), True ],
'ctrl_tab_go_to_next_composing': [opt_bool, True, _('Ctrl-Tab go to next composing tab when none is unread.')],
'confirm_metacontacts': [ opt_str, '', _('Should we show the confirm metacontacts creation dialog or not? Empty string means we never show the dialog.')],
}
__options_per_key = {
@ -215,6 +227,13 @@ class Config:
'password': [ opt_str, '' ],
'resource': [ opt_str, 'gajim', '', True ],
'priority': [ opt_int, 5, '', True ],
'adjust_priority_with_status': [ opt_bool, True, _('Priority will change automatically according to your status. Priorities are defined in autopriority_* options.') ],
'autopriority_online': [ opt_int, 50],
'autopriority_chat': [ opt_int, 50],
'autopriority_away': [ opt_int, 40],
'autopriority_xa': [ opt_int, 30],
'autopriority_dnd': [ opt_int, 20],
'autopriority_invisible': [ opt_int, 10],
'autoconnect': [ opt_bool, False, '', True ],
'autoreconnect': [ opt_bool, True ],
'active': [ opt_bool, True],
@ -246,6 +265,12 @@ class Config:
'msgwin-y-position': [opt_int, -1], # Default is to let the wm decide
'msgwin-width': [opt_int, 480],
'msgwin-height': [opt_int, 440],
'listen_to_network_manager' : [opt_bool, True],
'is_zeroconf': [opt_bool, False],
'zeroconf_first_name': [ opt_str, '', '', True ],
'zeroconf_last_name': [ opt_str, '', '', True ],
'zeroconf_jabber_id': [ opt_str, '', '', True ],
'zeroconf_email': [ opt_str, '', '', True ],
}, {}),
'statusmsg': ({
'message': [ opt_str, '' ],
@ -284,8 +309,6 @@ class Config:
'bannerfontattrs': [ opt_str, 'B', '', True ],
# http://www.pitt.edu/~nisg/cis/web/cgi/rgb.html
# FIXME: not black but the default color from gtk+ theme
'state_active_color': [ opt_color, 'black' ],
'state_inactive_color': [ opt_color, 'grey62' ],
'state_composing_color': [ opt_color, 'green4' ],
'state_paused_color': [ opt_color, 'mediumblue' ],
@ -327,15 +350,15 @@ class Config:
_('Movie'): _("I'm watching a movie."),
_('Working'): _("I'm working."),
_('Phone'): _("I'm on the phone."),
_('Out'): _("I'm out enjoying life"),
_('Out'): _("I'm out enjoying life."),
}
defaultstatusmsg_default = {
'online': [ False, _("I'm available") ],
'chat': [ False, _("I'm free for chat") ],
'away': [ False, _('Be right back') ],
'xa': [ False, _("I'm not available") ],
'dnd': [ False, _('Do not disturb') ],
'online': [ False, _("I'm available.") ],
'chat': [ False, _("I'm free for chat.") ],
'away': [ False, _('Be right back.') ],
'xa': [ False, _("I'm not available.") ],
'dnd': [ False, _('Do not disturb.') ],
'invisible': [ False, _('Bye!') ],
'offline': [ False, _('Bye!') ],
}
@ -346,8 +369,8 @@ class Config:
'contact_connected': [ True, '../data/sounds/connected.wav' ],
'contact_disconnected': [ True, '../data/sounds/disconnected.wav' ],
'message_sent': [ True, '../data/sounds/sent.wav' ],
'muc_message_highlight': [ True, '../data/sounds/gc_message1.wav', _('Sound to play when a MUC message contains one of the words in muc_highlight_words, or when a MUC message contains your nickname.')],
'muc_message_received': [ True, '../data/sounds/gc_message2.wav', _('Sound to play when any MUC message arrives. (This setting is taken into account only if notify_on_all_muc_messages is True)') ],
'muc_message_highlight': [ True, '../data/sounds/gc_message1.wav', _('Sound to play when a group chat message contains one of the words in muc_highlight_words, or when a group chat message contains your nickname.')],
'muc_message_received': [ False, '../data/sounds/gc_message2.wav', _('Sound to play when any MUC message arrives.') ],
}
themes_default = {

115
src/common/configpaths.py Normal file
View file

@ -0,0 +1,115 @@
import os
import sys
import tempfile
# Note on path and filename encodings:
#
# In general it is very difficult to do this correctly.
# We may pull information from environment variables, and what encoding that is
# in is anyone's guess. Any information we request directly from the file
# system will be in filesystemencoding, and (parts of) paths that we write in
# this source code will be in whatever encoding the source is in. (I hereby
# declare this file to be UTF-8 encoded.)
#
# To make things more complicated, modern Windows filesystems use UTF-16, but
# the API tends to hide this from us.
#
# I tried to minimize problems by passing Unicode strings to OS functions as
# much as possible. Hopefully this makes the function return an Unicode string
# as well. If not, we get an 8-bit string in filesystemencoding, which we can
# happily pass to functions that operate on files and directories, so we can
# just leave it as is. Since these paths are meant to be internal to Gajim and
# not displayed to the user, Unicode is not really necessary here.
def fse(s):
'''Convert from filesystem encoding if not already Unicode'''
return unicode(s, sys.getfilesystemencoding())
class ConfigPaths:
def __init__(this, root=None):
this.root = root
this.paths = {}
if this.root is None:
if os.name == 'nt':
try:
# Documents and Settings\[User Name]\Application Data\Gajim
# How are we supposed to know what encoding the environment
# variable 'appdata' is in? Assuming it to be in filesystem
# encoding.
this.root = os.path.join(fse(os.environ[u'appdata']), u'Gajim')
except KeyError:
# win9x, in cwd
this.root = u''
else: # Unices
# Pass in an Unicode string, and hopefully get one back.
this.root = os.path.expanduser(u'~/.gajim')
def add_from_root(this, name, path):
this.paths[name] = (True, path)
def add(this, name, path):
this.paths[name] = (False, path)
def __getitem__(this, key):
relative, path = this.paths[key]
if not relative:
return path
return os.path.join(this.root, path)
def get(this, key, default=None):
try:
return this[key]
except KeyError:
return default
def iteritems(this):
for key in this.paths.iterkeys():
yield (key, this[key])
def windowsify(s):
if os.name == 'nt':
return s.capitalize()
return s
def init():
paths = ConfigPaths()
# LOG is deprecated
k = ( 'LOG', 'LOG_DB', 'VCARD', 'AVATAR', 'MY_EMOTS' )
v = (u'logs', u'logs.db', u'vcards', u'avatars', u'emoticons')
if os.name == 'nt':
v = map(lambda x: x.capitalize(), v)
for n, p in zip(k, v):
paths.add_from_root(n, p)
paths.add('DATA', os.path.join(u'..', windowsify(u'data')))
paths.add('HOME', os.path.expanduser(u'~'))
paths.add('TMP', fse(tempfile.gettempdir()))
try:
import svn_config
svn_config.configure(paths)
except (ImportError, AttributeError):
pass
#for k, v in paths.iteritems():
# print "%s: %s" % (k, v)
return paths
gajimpaths = init()
def init_profile(profile, paths=gajimpaths):
conffile = windowsify(u'config')
pidfile = windowsify(u'gajim')
if len(profile) > 0:
conffile += u'.' + profile
pidfile += u'.' + profile
pidfile += u'.pid'
paths.add_from_root('CONFIG_FILE', conffile)
paths.add_from_root('PID_FILE', pidfile)

View file

@ -1,19 +1,11 @@
## common/connection.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <nkour@jabber.org>
## - Dimitur Kirov <dkirov@gmail.com>
## - Travis Shirk <travis@pobox.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -38,10 +30,13 @@ import common.xmpp
from common import helpers
from common import gajim
from common import GnuPG
from common import passwords
from connection_handlers import *
USE_GPG = GnuPG.USE_GPG
from common.rst_xhtml_generator import create_xhtml
class Connection(ConnectionHandlers):
'''Connection class'''
def __init__(self, name):
@ -51,8 +46,10 @@ class Connection(ConnectionHandlers):
self.connection = None # xmpppy ClientCommon instance
# this property is used to prevent double connections
self.last_connection = None # last ClientCommon instance
self.is_zeroconf = False
self.gpg = None
self.status = ''
self.priority = gajim.get_priority(name, 'offline')
self.old_show = ''
# increase/decrease default timeout for server responses
self.try_connecting_for_foo_secs = 45
@ -61,11 +58,12 @@ class Connection(ConnectionHandlers):
self.time_to_reconnect = None
self.new_account_info = None
self.bookmarks = []
self.annotations = {}
self.on_purpose = False
self.last_io = gajim.idlequeue.current_time()
self.last_sent = []
self.last_history_line = {}
self.password = gajim.config.get_per('accounts', name, 'password')
self.password = passwords.get_password(name)
self.server_resource = gajim.config.get_per('accounts', name, 'resource')
if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'):
self.keepalives = gajim.config.get_per('accounts', self.name,'keep_alive_every_foo_secs')
@ -88,6 +86,7 @@ class Connection(ConnectionHandlers):
self.available_transports = {} # list of available transports on this
# server {'icq': ['icq.server.com', 'icq2.server.com'], }
self.vcard_supported = True
self.metacontacts_supported = True
# END __init__
def put_event(self, ev):
@ -118,6 +117,7 @@ class Connection(ConnectionHandlers):
self.on_purpose = on_purpose
self.connected = 0
self.time_to_reconnect = None
self.privacy_rules_supported = False
if self.connection:
# make sure previous connection is completely closed
gajim.proxy65_manager.disconnect(self.connection)
@ -128,23 +128,22 @@ class Connection(ConnectionHandlers):
def _disconnectedReconnCB(self):
'''Called when we are disconnected'''
gajim.log.debug('disconnectedReconnCB')
if self.connected > 1:
# we cannot change our status to offline or connectiong
if gajim.account_is_connected(self.name):
# we cannot change our status to offline or connecting
# after we auth to server
self.old_show = STATUS_LIST[self.connected]
self.connected = 0
self.dispatch('STATUS', 'offline')
if not self.on_purpose:
self.disconnect()
if gajim.config.get_per('accounts', self.name, 'autoreconnect') \
and self.retrycount <= 10:
if gajim.config.get_per('accounts', self.name, 'autoreconnect'):
self.connected = 1
self.dispatch('STATUS', 'connecting')
# this check has moved from _reconnect method
if self.retrycount > 5:
self.time_to_reconnect = 20
self.time_to_reconnect = random.randint(15, 25)
else:
self.time_to_reconnect = 10
self.time_to_reconnect = random.randint(5, 15)
gajim.idlequeue.set_alarm(self._reconnect_alarm,
self.time_to_reconnect)
elif self.on_connect_failure:
@ -163,7 +162,7 @@ class Connection(ConnectionHandlers):
self.dispatch('STATUS', 'offline')
self.dispatch('CONNECTION_LOST',
(_('Connection with account "%s" has been lost') % self.name,
_('To continue sending and receiving messages, you will need to reconnect.')))
_('Reconnect manually.')))
def _event_dispatcher(self, realm, event, data):
if realm == common.xmpp.NS_REGISTER:
@ -185,7 +184,6 @@ class Connection(ConnectionHandlers):
if not common.xmpp.isResultNode(result):
self.dispatch('ACC_NOT_OK', (result.getError()))
return
self.connected = 0
self.password = self.new_account_info['password']
if USE_GPG:
self.gpg = GnuPG.GnuPG()
@ -195,7 +193,9 @@ class Connection(ConnectionHandlers):
gajim.connections[self.name] = self
self.dispatch('ACC_OK', (self.new_account_info))
self.new_account_info = None
self.connection = None
if self.connection:
self.connection.UnregisterDisconnectHandler(self._on_new_account)
self.disconnect(on_purpose=True)
common.xmpp.features_nb.register(self.connection, data[0],
req, _on_register_result)
return
@ -368,8 +368,7 @@ class Connection(ConnectionHandlers):
secure = self._secure)
return
else:
if not retry or self.retrycount > 10:
self.retrycount = 0
if not retry and self.retrycount == 0:
self.time_to_reconnect = None
if self.on_connect_failure:
self.on_connect_failure()
@ -404,8 +403,6 @@ class Connection(ConnectionHandlers):
con.RegisterDisconnectHandler(self._disconnectedReconnCB)
gajim.log.debug(_('Connected to server %s:%s with %s') % (self._current_host['host'],
self._current_host['port'], con_type))
# Ask metacontacts before roster
self.get_metacontacts()
self._register_handlers(con, con_type)
return True
@ -459,7 +456,7 @@ class Connection(ConnectionHandlers):
# END connect
def quit(self, kill_core):
if kill_core and self.connected > 1:
if kill_core and gajim.account_is_connected(self.name):
self.disconnect(on_purpose = True)
def get_privacy_lists(self):
@ -531,14 +528,15 @@ class Connection(ConnectionHandlers):
# active the privacy rule
self.privacy_rules_supported = True
self.activate_privacy_rule('invisible')
prio = unicode(gajim.config.get_per('accounts', self.name, 'priority'))
p = common.xmpp.Presence(typ = ptype, priority = prio, show = show)
priority = unicode(gajim.get_priority(self.name, show))
p = common.xmpp.Presence(typ = ptype, priority = priority, show = show)
p = self.add_sha(p, ptype != 'unavailable')
if msg:
p.setStatus(msg)
if signed:
p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed)
self.connection.send(p)
self.priority = priority
self.dispatch('STATUS', 'invisible')
if initial:
#ask our VCard
@ -546,6 +544,9 @@ class Connection(ConnectionHandlers):
#Get bookmarks from private namespace
self.get_bookmarks()
#Get annotations
self.get_annotations()
#Inform GUI we just signed in
self.dispatch('SIGNED_IN', ())
@ -591,8 +592,11 @@ class Connection(ConnectionHandlers):
if self.connection:
con.set_send_timeout(self.keepalives, self.send_keepalive)
self.connection.onreceive(None)
# Ask metacontacts before roster
self.get_metacontacts()
iq = common.xmpp.Iq('get', common.xmpp.NS_PRIVACY, xmlns = '')
id = self.connection.getAnID()
iq.setID(id)
self.awaiting_answers[id] = (PRIVACY_ARRIVED, )
self.connection.send(iq)
def change_status(self, show, msg, auto = False):
if not show in STATUS_LIST:
@ -646,9 +650,8 @@ class Connection(ConnectionHandlers):
iq = self.build_privacy_rule('visible', 'allow')
self.connection.send(iq)
self.activate_privacy_rule('visible')
prio = unicode(gajim.config.get_per('accounts', self.name,
'priority'))
p = common.xmpp.Presence(typ = None, priority = prio, show = sshow)
priority = unicode(gajim.get_priority(self.name, sshow))
p = common.xmpp.Presence(typ = None, priority = priority, show = sshow)
p = self.add_sha(p)
if msg:
p.setStatus(msg)
@ -656,6 +659,7 @@ class Connection(ConnectionHandlers):
p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed)
if self.connection:
self.connection.send(p)
self.priority = priority
self.dispatch('STATUS', show)
def _on_disconnected(self):
@ -666,17 +670,22 @@ class Connection(ConnectionHandlers):
def get_status(self):
return STATUS_LIST[self.connected]
def send_motd(self, jid, subject = '', msg = ''):
def send_motd(self, jid, subject = '', msg = '', xhtml = None):
if not self.connection:
return
msg_iq = common.xmpp.Message(to = jid, body = msg, subject = subject)
msg_iq = common.xmpp.Message(to = jid, body = msg, subject = subject,
xhtml = xhtml)
self.connection.send(msg_iq)
def send_message(self, jid, msg, keyID, type = 'chat', subject='',
chatstate = None, msg_id = None, composing_jep = None, resource = None,
user_nick = None):
user_nick = None, xhtml = None):
if not self.connection:
return
if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'):
xhtml = create_xhtml(msg)
if not msg and chatstate is None:
return
fjid = jid
@ -690,18 +699,24 @@ class Connection(ConnectionHandlers):
if msgenc:
msgtxt = '[This message is encrypted]'
lang = os.getenv('LANG')
if lang is not None or lang != 'en': # we're not english
msgtxt = _('[This message is encrypted]') +\
' ([This message is encrypted])' # one in locale and one en
if lang is not None and lang != 'en': # we're not english
# one in locale and one en
msgtxt = _('[This message is *encrypted* (See :JEP:`27`]') +\
' ([This message is *encrypted* (See :JEP:`27`])'
if msgtxt and not xhtml and gajim.config.get(
'rst_formatting_outgoing_messages'):
# Generate a XHTML part using reStructured text markup
xhtml = create_xhtml(msgtxt)
if type == 'chat':
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type)
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type,
xhtml = xhtml)
else:
if subject:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
typ = 'normal', subject = subject)
typ = 'normal', subject = subject, xhtml = xhtml)
else:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
typ = 'normal')
typ = 'normal', xhtml = xhtml)
if msgenc:
msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc)
@ -719,7 +734,8 @@ class Connection(ConnectionHandlers):
msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES)
if composing_jep == 'JEP-0022' or not composing_jep:
# JEP-0022
chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT)
chatstate_node = msg_iq.setTag('x',
namespace = common.xmpp.NS_EVENT)
if not msgtxt: # when no <body>, add <id>
if not msg_id: # avoid putting 'None' in <id> tag
msg_id = ''
@ -729,7 +745,8 @@ class Connection(ConnectionHandlers):
chatstate_node.addChild(name = 'composing')
self.connection.send(msg_iq)
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\
.split()
ji = gajim.get_jid_without_resource(jid)
if self.name not in no_log_for and ji not in no_log_for:
log_msg = msg
@ -832,7 +849,7 @@ class Connection(ConnectionHandlers):
if self.connection:
self.connection.getRoster().setItem(jid = jid, name = name,
groups = groups)
def new_account(self, name, config, sync = False):
# If a connection already exist we cannot create a new account
if self.connection:
@ -851,6 +868,8 @@ class Connection(ConnectionHandlers):
return
self.on_connect_failure = None
self.connection = con
if con:
con.RegisterDisconnectHandler(self._on_new_account)
common.xmpp.features_nb.getRegInfo(con, self._hostname)
def account_changed(self, new_name):
@ -914,14 +933,39 @@ class Connection(ConnectionHandlers):
# Only add optional elements if not empty
# Note: need to handle both None and '' as empty
# thus shouldn't use "is not None"
if bm['nick']:
if bm.get('nick', None):
iq5 = iq4.setTagData('nick', bm['nick'])
if bm['password']:
if bm.get('password', None):
iq5 = iq4.setTagData('password', bm['password'])
if bm['print_status']:
if bm.get('print_status', None):
iq5 = iq4.setTagData('print_status', bm['print_status'])
self.connection.send(iq)
def get_annotations(self):
'''Get Annonations from storage as described in XEP 0048, and XEP 0145'''
self.annotations = {}
if not self.connection:
return
iq = common.xmpp.Iq(typ='get')
iq2 = iq.addChild(name='query', namespace='jabber:iq:private')
iq2.addChild(name='storage', namespace='storage:rosternotes')
self.connection.send(iq)
def store_annotations(self):
'''Set Annonations in private storage as described in XEP 0048, and XEP 0145'''
if not self.connection:
return
iq = common.xmpp.Iq(typ='set')
iq2 = iq.addChild(name='query', namespace='jabber:iq:private')
iq3 = iq2.addChild(name='storage', namespace='storage:rosternotes')
for jid in self.annotations.keys():
if self.annotations[jid]:
iq4 = iq3.addChild(name = "note")
iq4.setAttr('jid', jid)
iq4.setData(self.annotations[jid])
self.connection.send(iq)
def get_metacontacts(self):
'''Get metacontacts list from storage as described in JEP 0049'''
if not self.connection:
@ -958,14 +1002,15 @@ class Connection(ConnectionHandlers):
p = self.add_sha(p, ptype != 'unavailable')
self.connection.send(p)
def join_gc(self, nick, room, server, password):
def join_gc(self, nick, room_jid, password):
# FIXME: This room JID needs to be normalized; see #1364
if not self.connection:
return
show = helpers.get_xmpp_show(STATUS_LIST[self.connected])
if show == 'invisible':
# Never join a room when invisible
return
p = common.xmpp.Presence(to = '%s@%s/%s' % (room, server, nick),
p = common.xmpp.Presence(to = '%s/%s' % (room_jid, nick),
show = show, status = self.status)
if gajim.config.get('send_sha_in_gc_presence'):
p = self.add_sha(p)
@ -974,17 +1019,18 @@ class Connection(ConnectionHandlers):
t.setTagData('password', password)
self.connection.send(p)
#last date/time in history to avoid duplicate
# FIXME: This JID needs to be normalized; see #1364
jid='%s@%s' % (room, server)
last_log = gajim.logger.get_last_date_that_has_logs(jid, is_room = True)
last_log = gajim.logger.get_last_date_that_has_logs(room_jid,
is_room = True)
if last_log is None:
last_log = 0
self.last_history_line[jid]= last_log
self.last_history_line[room_jid]= last_log
def send_gc_message(self, jid, msg):
def send_gc_message(self, jid, msg, xhtml = None):
if not self.connection:
return
msg_iq = common.xmpp.Message(jid, msg, typ = 'groupchat')
if not xhtml and gajim.config.get('rst_formatting_outgoing_messages'):
xhtml = create_xhtml(msg)
msg_iq = common.xmpp.Message(jid, msg, typ = 'groupchat', xhtml = xhtml)
self.connection.send(msg_iq)
self.dispatch('MSGSENT', (jid, msg))
@ -1110,11 +1156,11 @@ class Connection(ConnectionHandlers):
self.connection.send(iq)
def unregister_account(self, on_remove_success):
# no need to write this as a class method and keep the value of on_remove_success
# as a class property as pass it as an argument
# no need to write this as a class method and keep the value of
# on_remove_success as a class property as pass it as an argument
def _on_unregister_account_connect(con):
self.on_connect_auth = None
if self.connected > 1:
if gajim.account_is_connected(self.name):
hostname = gajim.config.get_per('accounts', self.name, 'hostname')
iq = common.xmpp.Iq(typ = 'set', to = hostname)
q = iq.setTag(common.xmpp.NS_REGISTER + ' query').setTag('remove')

View file

@ -3,7 +3,7 @@
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <nkour@jabber.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
## - Travis Shirk <travis@pobox.com>
##
@ -45,15 +45,13 @@ VCARD_PUBLISHED = 'vcard_published'
VCARD_ARRIVED = 'vcard_arrived'
AGENT_REMOVED = 'agent_removed'
METACONTACTS_ARRIVED = 'metacontacts_arrived'
PRIVACY_ARRIVED = 'privacy_arrived'
HAS_IDLE = True
try:
import common.idle as idle # when we launch gajim from sources
import idle
except:
try:
import idle # when Gajim is installed
except:
gajim.log.debug(_('Unable to load idle module'))
HAS_IDLE = False
gajim.log.debug(_('Unable to load idle module'))
HAS_IDLE = False
class ConnectionBytestream:
def __init__(self):
@ -94,7 +92,7 @@ class ConnectionBytestream:
if contact.jid == receiver_jid:
file_props['error'] = -5
self.remove_transfer(file_props)
self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props))
self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, ''))
sender_jid = unicode(file_props['sender']).split('/')[0]
if contact.jid == sender_jid:
file_props['error'] = -3
@ -179,11 +177,12 @@ class ConnectionBytestream:
except socket.gaierror:
self.dispatch('ERROR', (_('Wrong host'), _('The host you configured as the ft_override_host_to_send advanced option is not valid, so ignored.')))
ft_override_host_to_send = self.peerhost[0]
listener = gajim.socks5queue.start_listener(self.peerhost[0], port,
listener = gajim.socks5queue.start_listener(port,
sha_str, self._result_socks5_sid, file_props['sid'])
if listener == None:
file_props['error'] = -5
self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props))
self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props,
''))
self._connect_error(unicode(receiver), file_props['sid'],
file_props['sid'], code = 406)
return
@ -225,8 +224,8 @@ class ConnectionBytestream:
iq = common.xmpp.Protocol(name = 'iq',
to = unicode(file_props['sender']), typ = 'error')
iq.setAttr('id', file_props['request-id'])
err = common.xmpp.ErrorNode(code = '406', typ = 'auth', name =
'not-acceptable')
err = common.xmpp.ErrorNode(code = '403', typ = 'cancel', name =
'forbidden', text = 'Offer Declined')
iq.addChild(node=err)
self.connection.send(iq)
@ -318,8 +317,8 @@ class ConnectionBytestream:
if file_props is not None:
self.disconnect_transfer(file_props)
file_props['error'] = -3
self.dispatch('FILE_REQUEST_ERROR', (to, file_props))
self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg))
def _proxy_auth_ok(self, proxy):
'''cb, called after authentication to proxy server '''
file_props = self.files_props[proxy['sid']]
@ -348,7 +347,7 @@ class ConnectionBytestream:
return
file_props = self.files_props[id]
file_props['error'] = -4
self.dispatch('FILE_REQUEST_ERROR', (jid, file_props))
self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
raise common.xmpp.NodeProcessed
def _bytestreamSetCB(self, con, iq_obj):
@ -565,7 +564,7 @@ class ConnectionBytestream:
return
jid = helpers.get_jid_from_iq(iq_obj)
file_props['error'] = -3
self.dispatch('FILE_REQUEST_ERROR', (jid, file_props))
self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
raise common.xmpp.NodeProcessed
class ConnectionDisco:
@ -696,6 +695,13 @@ class ConnectionDisco:
attr = {}
for key in i.getAttrs():
attr[key] = i.getAttrs()[key]
if 'jid' not in attr:
continue
try:
helpers.parse_jid(attr['jid'])
except common.helpers.InvalidFormat:
# jid is not conform
continue
items.append(attr)
jid = helpers.get_full_jid_from_iq(iq_obj)
hostname = gajim.config.get_per('accounts', self.name,
@ -732,6 +738,7 @@ class ConnectionDisco:
q.addChild('feature', attrs = {'var': common.xmpp.NS_SI})
q.addChild('feature', attrs = {'var': common.xmpp.NS_FILE})
q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC})
q.addChild('feature', attrs = {'var': common.xmpp.NS_XHTML_IM})
self.connection.send(iq)
raise common.xmpp.NodeProcessed
@ -839,6 +846,8 @@ class ConnectionVcard:
puny_jid = helpers.sanitize_filename(jid)
path = os.path.join(gajim.VCARD_PATH, puny_jid)
if jid in self.room_jids or os.path.isdir(path):
if not nick:
return
# remove room_jid file if needed
if os.path.isfile(path):
os.remove(path)
@ -976,9 +985,7 @@ class ConnectionVcard:
'invisible':
self.vcard_sha = new_sha
sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected])
prio = unicode(gajim.config.get_per('accounts', self.name,
'priority'))
p = common.xmpp.Presence(typ = None, priority = prio,
p = common.xmpp.Presence(typ = None, priority = self.priority,
show = sshow, status = self.status)
p = self.add_sha(p)
self.connection.send(p)
@ -1023,8 +1030,15 @@ class ConnectionVcard:
else:
meta_list[tag] = [data]
self.dispatch('METACONTACTS', meta_list)
else:
self.metacontacts_supported = False
# We can now continue connection by requesting the roster
self.connection.initRoster()
elif self.awaiting_answers[id][0] == PRIVACY_ARRIVED:
if iq_obj.getType() != 'error':
self.privacy_rules_supported = True
# Ask metacontacts before roster
self.get_metacontacts()
del self.awaiting_answers[id]
@ -1104,10 +1118,8 @@ class ConnectionVcard:
if STATUS_LIST[self.connected] == 'invisible':
return
sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected])
prio = unicode(gajim.config.get_per('accounts', self.name,
'priority'))
p = common.xmpp.Presence(typ = None, priority = prio, show = sshow,
status = self.status)
p = common.xmpp.Presence(typ = None, priority = self.priority,
show = sshow, status = self.status)
p = self.add_sha(p)
self.connection.send(p)
else:
@ -1137,11 +1149,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
def build_http_auth_answer(self, iq_obj, answer):
if answer == 'yes':
iq = iq_obj.buildReply('result')
self.connection.send(iq_obj.buildReply('result'))
elif answer == 'no':
iq = iq_obj.buildReply('error')
iq.setError('not-authorized', 401)
self.connection.send(iq)
err = common.xmpp.Error(iq_obj,
common.xmpp.protocol.ERR_NOT_AUTHORIZED)
self.connection.send(err)
def _HttpAuthCB(self, con, iq_obj):
gajim.log.debug('HttpAuthCB')
@ -1156,7 +1168,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
raise common.xmpp.NodeProcessed
def _ErrorCB(self, con, iq_obj):
errmsg = iq_obj.getError()
gajim.log.debug('ErrorCB')
if iq_obj.getQueryNS() == common.xmpp.NS_VERSION:
who = helpers.get_full_jid_from_iq(iq_obj)
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
self.dispatch('OS_INFO', (jid_stripped, resource, '', ''))
return
errmsg = iq_obj.getErrorMsg()
errcode = iq_obj.getErrorCode()
jid_from = helpers.get_full_jid_from_iq(iq_obj)
id = unicode(iq_obj.getID())
@ -1197,6 +1215,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
# http://www.jabber.org/jeps/jep-0049.html
#TODO: implement this
pass
elif ns == 'storage:rosternotes':
# Annotations
# http://www.xmpp.org/extensions/xep-0145.html
notes = storage.getTags('note')
for note in notes:
jid = note.getAttr('jid')
annotation = note.getData()
self.annotations[jid] = annotation
def _PrivateErrorCB(self, con, iq_obj):
gajim.log.debug('PrivateErrorCB')
@ -1205,6 +1231,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if storage_tag:
ns = storage_tag.getNamespace()
if ns == 'storage:metacontacts':
self.metacontacts_supported = False
# Private XML Storage (JEP49) is not supported by server
# Continue connecting
self.connection.initRoster()
@ -1312,7 +1339,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if gm.getTag('mailbox').getTag('mail-thread-info'):
gmail_messages = gm.getTag('mailbox').getTags('mail-thread-info')
for gmessage in gmail_messages:
gmail_from = gmessage.getTag('senders').getTag('sender').getAttr('address')
sender = gmessage.getTag('senders').getTag('sender')
if not sender:
continue
gmail_from = sender.getAttr('address')
gmail_subject = gmessage.getTag('subject').getData()
gmail_snippet = gmessage.getTag('snippet').getData()
gmail_messages_list.append({ \
@ -1331,6 +1361,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
self._pubsubEventCB(con, msg)
return
msgtxt = msg.getBody()
msghtml = msg.getXHTML()
mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None
tim = msg.getTimestamp()
@ -1405,15 +1436,18 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt,
tim))
elif mtype == 'groupchat':
has_timestamp = False
if msg.timestamp:
has_timestamp = True
if subject:
self.dispatch('GC_SUBJECT', (frm, subject, msgtxt))
self.dispatch('GC_SUBJECT', (frm, subject, msgtxt, has_timestamp))
else:
if not msg.getTag('body'): #no <body>
return
# Ignore message from room in which we are not
if not self.last_history_line.has_key(jid):
return
self.dispatch('GC_MSG', (frm, msgtxt, tim))
self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msghtml))
if self.name not in no_log_for and not int(float(time.mktime(tim))) <= \
self.last_history_line[jid] and msgtxt:
gajim.logger.write('gc_msg', frm, msgtxt, tim = tim)
@ -1425,11 +1459,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
chatstate, msg_id, composing_jep, user_nick))
chatstate, msg_id, composing_jep, user_nick, msghtml))
else: # it's single message
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
if invite is not None:
item = invite.getTag('invite')
jid_from = item.getAttr('from')
@ -1437,9 +1468,12 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
item = invite.getTag('password')
password = invite.getTagData('password')
self.dispatch('GC_INVITATION',(frm, jid_from, reason, password))
else:
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
subject, chatstate, msg_id, composing_jep, user_nick))
return
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
subject, chatstate, msg_id, composing_jep, user_nick, msghtml))
# END messageCB
def _pubsubEventCB(self, con, msg):
@ -1482,8 +1516,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
# one
who = str(prs.getFrom())
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
self.dispatch('GC_MSG', (jid_stripped, _('Nickname not allowed: %s') % \
resource, None))
self.dispatch('GC_MSG', (jid_stripped,
_('Nickname not allowed: %s') % resource, None, False, None))
return
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
timestamp = None
@ -1545,22 +1579,22 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
prio, keyID, timestamp))
elif errcode == '401': # password required to join
self.dispatch('ERROR', (_('Unable to join room'),
_('A password is required to join this room.')))
self.dispatch('ERROR', (_('Unable to join group chat'),
_('A password is required to join this group chat.')))
elif errcode == '403': # we are banned
self.dispatch('ERROR', (_('Unable to join room'),
_('You are banned from this room.')))
elif errcode == '404': # room does not exist
self.dispatch('ERROR', (_('Unable to join room'),
_('Such room does not exist.')))
self.dispatch('ERROR', (_('Unable to join group chat'),
_('You are banned from this group chat.')))
elif errcode == '404': # group chat does not exist
self.dispatch('ERROR', (_('Unable to join group chat'),
_('Such group chat does not exist.')))
elif errcode == '405':
self.dispatch('ERROR', (_('Unable to join room'),
_('Room creation is restricted.')))
self.dispatch('ERROR', (_('Unable to join group chat'),
_('Group chat creation is restricted.')))
elif errcode == '406':
self.dispatch('ERROR', (_('Unable to join room'),
self.dispatch('ERROR', (_('Unable to join group chat'),
_('Your registered nickname must be used.')))
elif errcode == '407':
self.dispatch('ERROR', (_('Unable to join room'),
self.dispatch('ERROR', (_('Unable to join group chat'),
_('You are not in the members list.')))
elif errcode == '409': # nick conflict
# the jid_from in this case is FAKE JID: room_jid/nick
@ -1568,7 +1602,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
proposed_nickname = resource + \
gajim.config.get('gc_proposed_nick_char')
room_jid = gajim.get_room_from_fjid(who)
self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join room'),
self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join group chat'),
_('Your desired nickname is in use or registered by another occupant.\nPlease specify another nickname below:'), proposed_nickname))
else: # print in the window the error
self.dispatch('ERROR_ANSWER', ('', jid_stripped,
@ -1839,12 +1873,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if show == 'invisible':
self.send_invisible_presence(msg, signed, True)
return
prio = unicode(gajim.config.get_per('accounts', self.name,
'priority'))
priority = gajim.get_priority(self.name, sshow)
vcard = self.get_cached_vcard(jid)
if vcard and vcard.has_key('PHOTO') and vcard['PHOTO'].has_key('SHA'):
self.vcard_sha = vcard['PHOTO']['SHA']
p = common.xmpp.Presence(typ = None, priority = prio, show = sshow)
p = common.xmpp.Presence(typ = None, priority = priority, show = sshow)
p = self.add_sha(p)
if msg:
p.setStatus(msg)
@ -1853,6 +1886,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if self.connection:
self.connection.send(p)
self.priority = priority
self.dispatch('STATUS', show)
# ask our VCard
self.request_vcard(None)
@ -1860,6 +1894,9 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
# Get bookmarks from private namespace
self.get_bookmarks()
# Get annotations from private namespace
self.get_annotations()
# If it's a gmail account,
# inform the server that we want e-mail notifications
if gajim.get_server_from_jid(our_jid) in gajim.gmail_domains:

View file

@ -1,16 +1,8 @@
## common/contacts.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -27,8 +19,8 @@ import common.gajim
class Contact:
'''Information concerning each contact'''
def __init__(self, jid='', name='', groups=[], show='', status='', sub='',
ask='', resource='', priority=0, keyID='', our_chatstate=None,
chatstate=None, last_status_time=None, msg_id = None, composing_jep = None):
ask='', resource='', priority=0, keyID='', our_chatstate=None,
chatstate=None, last_status_time=None, msg_id = None, composing_jep = None):
self.jid = jid
self.name = name
self.groups = groups
@ -67,10 +59,40 @@ class Contact:
return self.name
return self.jid.split('@')[0]
def is_hidden_from_roster(self):
'''if contact should not be visible in roster'''
# XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
if self.is_transport():
return False
if self.sub in ('both', 'to'):
return False
if self.sub in ('none', 'from') and self.ask == 'subscribe':
return False
if self.sub in ('none', 'from') and (self.name or len(self.groups)):
return False
if _('Not in Roster') in self.groups:
return False
return True
def is_observer(self):
# XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
is_observer = False
if self.sub == 'from' and not self.is_transport()\
and self.is_hidden_from_roster():
is_observer = True
return is_observer
def is_transport(self):
# if not '@' or '@' starts the jid then contact is transport
if self.jid.find('@') <= 0:
return True
return False
class GC_Contact:
'''Information concerning each groupchat contact'''
def __init__(self, room_jid='', name='', show='', status='', role='',
affiliation='', jid = '', resource = ''):
affiliation='', jid = '', resource = ''):
self.room_jid = room_jid
self.name = name
self.show = show
@ -220,6 +242,51 @@ class Contacts:
return self._contacts[account][jid][0]
return None
def get_contacts_from_group(self, account, group):
'''Returns all contacts in the given group'''
group_contacts = []
for jid in self._contacts[account]:
contacts = self.get_contacts_from_jid(account, jid)
if group in contacts[0].groups:
group_contacts += contacts
return group_contacts
def get_nb_online_total_contacts(self, accounts = [], groups = []):
'''Returns the number of online contacts and the total number of
contacts'''
if accounts == []:
accounts = self.get_accounts()
nbr_online = 0
nbr_total = 0
for account in accounts:
our_jid = common.gajim.get_jid_from_account(account)
for jid in self.get_jid_list(account):
if jid == our_jid:
continue
if common.gajim.jid_is_transport(jid) and not \
common.gajim.config.get('show_transports_group'):
# do not count transports
continue
contact = self.get_contact_with_highest_priority(account, jid)
in_groups = False
if groups == []:
in_groups = True
else:
contact_groups = contact.groups
if not contact_groups:
# Contact is not in a group, so count it in General group
contact_groups.append(_('General'))
for group in groups:
if group in contact_groups:
in_groups = True
break
if in_groups:
if contact.show not in ('offline', 'error'):
nbr_online += 1
nbr_total += 1
return nbr_online, nbr_total
def define_metacontacts(self, account, tags_list):
self._metacontacts_tags[account] = tags_list

View file

@ -16,32 +16,56 @@
##
import os
import sys
from common import gajim
from common import exceptions
_GAJIM_ERROR_IFACE = 'org.gajim.dbus.Error'
try:
import dbus
version = getattr(dbus, 'version', (0, 20, 0))
supported = True
import dbus.service
import dbus.glib
supported = True # does use have D-Bus bindings?
except ImportError:
version = (0, 0, 0)
supported = False
if not os.name == 'nt': # only say that to non Windows users
print _('D-Bus python bindings are missing in this computer')
print _('D-Bus capabilities of Gajim cannot be used')
# dbus 0.23 leads to segfault with threads_init()
if sys.version[:4] >= '2.4' and version[1] < 30:
supported = False
if version >= (0, 41, 0):
import dbus.service
import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it
class SystemBus:
'''A Singleton for the DBus SystemBus'''
def __init__(self):
self.system_bus = None
def SystemBus(self):
if not supported:
raise exceptions.DbusNotSupported
if not self.present():
raise exceptions.SystemBusNotPresent
return self.system_bus
def bus(self):
return self.SystemBus()
def present(self):
if not supported:
return False
if self.system_bus is None:
try:
self.system_bus = dbus.SystemBus()
except dbus.dbus_bindings.DBusException:
self.system_bus = None
return False
if self.system_bus is None:
return False
return True
system_bus = SystemBus()
class SessionBus:
'''A Singleton for the DBus SessionBus'''
'''A Singleton for the D-Bus SessionBus'''
def __init__(self):
self.session_bus = None
@ -102,4 +126,13 @@ def get_interface(interface, path):
def get_notifications_interface():
'''Returns the notifications interface.'''
return get_interface('org.freedesktop.Notifications','/org/freedesktop/Notifications')
return get_interface('org.freedesktop.Notifications',
'/org/freedesktop/Notifications')
if supported:
class MissingArgument(dbus.DBusException):
_dbus_error_name = _GAJIM_ERROR_IFACE + '.MissingArgument'
class InvalidArgument(dbus.DBusException):
'''Raised when one of the provided arguments is invalid.'''
_dbus_error_name = _GAJIM_ERROR_IFACE + '.InvalidArgument'

9
src/common/defs.py Normal file
View file

@ -0,0 +1,9 @@
docdir = '../'
datadir = '../'
version = '0.10.1.7'
import sys, os.path
for base in ('.', 'common'):
sys.path.append(os.path.join(base, '.libs'))

View file

@ -5,7 +5,7 @@
##
## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -81,7 +81,7 @@ class Events:
gajim.interface.systray.set_img()
def remove_events(self, account, jid, event = None, types = []):
'''if event is not speficied, remove all events from this jid,
'''if event is not specified, remove all events from this jid,
optionnaly only from given type
return True if no such event found'''
if not self._events.has_key(account):
@ -118,11 +118,11 @@ class Events:
if gajim.interface.systray_capabilities:
gajim.interface.systray.set_img()
def get_nb_events(self, types = []):
return self._get_nb_events(types = types)
def get_nb_events(self, types = [], account = None):
return self._get_nb_events(types = types, account = account)
def get_events(self, account, jid = None, types = []):
'''if event is not speficied, remove all events from this jid,
'''if event is not specified, get all events from this jid,
optionnaly only from given type'''
if not self._events.has_key(account):
return []
@ -149,7 +149,7 @@ class Events:
return first_event
def _get_nb_events(self, account = None, jid = None, attribute = None, types = []):
'''return the number of events'''
'''return the number of pending events'''
nb = 0
if account:
accounts = [account]
@ -223,7 +223,7 @@ class Events:
return self._get_first_event_with_attribute(events)
def get_nb_roster_events(self, account = None, jid = None, types = []):
'''returns the number of events displayedin roster'''
'''returns the number of events displayed in roster'''
return self._get_nb_events(attribute = 'roster', account = account,
jid = jid, types = types)

View file

@ -1,17 +1,7 @@
## exceptions.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -54,3 +44,12 @@ class SessionBusNotPresent(Exception):
def __str__(self):
return _('Session bus is not available.\nTry reading http://trac.gajim.org/wiki/GajimDBus')
class GajimGeneralException(Exception):
'''This exception ir our general exception'''
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text

View file

@ -15,9 +15,7 @@
## GNU General Public License for more details.
##
import os
import sys
import tempfile
import logging
import locale
@ -25,10 +23,36 @@ import config
from contacts import Contacts
from events import Events
try:
import defs
except ImportError:
print >> sys.stderr, '''defs.py is missing!
If you start gajim from svn:
* Make sure you have GNU autotools installed.
This includes the following packages:
automake >= 1.8
autoconf >= 2.59
intltool-0.35
libtool
* Run
$ sh autogen.sh
* Optionally, install gajim
$ make
$ sudo make install
**** Note for translators ****
You can get the latest string updates, by running:
$ cd po/
$ make update-po
'''
sys.exit(1)
interface = None # The actual interface (the gtk one for the moment)
config = config.Config()
version = config.get('version')
connections = {}
connections = {} # 'account name': 'account (connection.Connection) instance'
verbose = False
h = logging.StreamHandler()
@ -40,38 +64,17 @@ log.addHandler(h)
import logger
logger = logger.Logger() # init the logger
if os.name == 'nt':
DATA_DIR = os.path.join('..', 'data')
try:
# Documents and Settings\[User Name]\Application Data\Gajim
LOGPATH = os.path.join(os.environ['appdata'], 'Gajim', 'Logs') # deprecated
VCARD_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'Vcards')
AVATAR_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'Avatars')
MY_EMOTS_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'Emoticons')
except KeyError:
# win9x, in cwd
LOGPATH = 'Logs' # deprecated
VCARD_PATH = 'Vcards'
AVATAR_PATH = 'Avatars'
MY_EMOTS_PATH = 'Emoticons'
else: # Unices
DATA_DIR = '../data'
LOGPATH = os.path.expanduser('~/.gajim/logs') # deprecated
VCARD_PATH = os.path.expanduser('~/.gajim/vcards')
AVATAR_PATH = os.path.expanduser('~/.gajim/avatars')
MY_EMOTS_PATH = os.path.expanduser('~/.gajim/emoticons')
import configpaths
gajimpaths = configpaths.gajimpaths
HOME_DIR = os.path.expanduser('~')
TMP = tempfile.gettempdir()
LOGPATH = gajimpaths['LOG'] # deprecated
VCARD_PATH = gajimpaths['VCARD']
AVATAR_PATH = gajimpaths['AVATAR']
MY_EMOTS_PATH = gajimpaths['MY_EMOTS']
TMP = gajimpaths['TMP']
DATA_DIR = gajimpaths['DATA']
HOME_DIR = gajimpaths['HOME']
try:
LOGPATH = LOGPATH.decode(sys.getfilesystemencoding())
VCARD_PATH = VCARD_PATH.decode(sys.getfilesystemencoding())
TMP = TMP.decode(sys.getfilesystemencoding())
AVATAR_PATH = AVATAR_PATH.decode(sys.getfilesystemencoding())
MY_EMOTS_PATH = MY_EMOTS_PATH.decode(sys.getfilesystemencoding())
except:
pass
try:
LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc..
except (ValueError, locale.Error):
@ -120,6 +123,12 @@ status_before_autoaway = {}
SHOW_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible']
# zeroconf account name
ZEROCONF_ACC_NAME = 'Local'
priority_dict = {}
for status in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
priority_dict[status] = config.get('autopriority' + status)
def get_nick_from_jid(jid):
pos = jid.find('@')
return jid[:pos]
@ -133,10 +142,10 @@ def get_nick_from_fjid(jid):
# gaim@conference.jabber.no/nick/nick-continued
return jid.split('/', 1)[1]
def get_room_name_and_server_from_room_jid(jid):
room_name = get_nick_from_jid(jid)
def get_name_and_server_from_jid(jid):
name = get_nick_from_jid(jid)
server = get_server_from_jid(jid)
return room_name, server
return name, server
def get_room_and_nick_from_fjid(jid):
# fake jid is the jid for a contact in a room
@ -207,11 +216,36 @@ def get_number_of_connected_accounts(accounts_list = None):
accounts = connections.keys()
else:
accounts = accounts_list
for acct in accounts:
if connections[acct].connected > 1:
for account in accounts:
if account_is_connected(account):
connected_accounts = connected_accounts + 1
return connected_accounts
def account_is_connected(account):
if account not in connections:
return False
if connections[account].connected > 1: # 0 is offline, 1 is connecting
return True
else:
return False
def account_is_disconnected(account):
return not account_is_connected(account)
def get_number_of_securely_connected_accounts():
'''returns the number of the accounts that are SSL/TLS connected'''
num_of_secured = 0
for account in connections:
if account_is_securely_connected(account):
num_of_secured += 1
return num_of_secured
def account_is_securely_connected(account):
if account in con_types and con_types[account] in ('tls', 'ssl'):
return True
else:
return False
def get_transport_name_from_jid(jid, use_config_setting = True):
'''returns 'aim', 'gg', 'irc' etc
if JID is not from transport returns None'''
@ -299,3 +333,13 @@ def get_name_from_jid(account, jid):
else:
actor = jid
return actor
def get_priority(account, show):
'''return the priority an account must have'''
if not show:
show = 'online'
if show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible') and \
config.get_per('accounts', account, 'adjust_priority_with_status'):
return config.get_per('accounts', account, 'autopriority_' + show)
return config.get_per('accounts', account, 'priority')

View file

@ -17,19 +17,21 @@
##
import sre
import locale
import os
import subprocess
import urllib
import errno
import select
import sys
import sha
from encodings.punycode import punycode_encode
import gajim
from i18n import Q_
from i18n import ngettext
from xmpp_stringprep import nodeprep, resourceprep, nameprep
try:
import winsound # windows-only built-in module for playing wav
import win32api
@ -290,6 +292,21 @@ def get_uf_role(role, plural = False):
else:
role_name = _('Visitor')
return role_name
def get_uf_affiliation(affiliation):
'''Get a nice and translated affilition for muc'''
if affiliation == 'none':
affiliation_name = Q_('?Group Chat Contact Affiliation:None')
elif affiliation == 'owner':
affiliation_name = _('Owner')
elif affiliation == 'admin':
affiliation_name = _('Administrator')
elif affiliation == 'member':
affiliation_name = _('Member')
else: # Argl ! An unknown affiliation !
affiliation_name = affiliation.capitalize()
return affiliation_name
def get_sorted_keys(adict):
keys = adict.keys()
@ -362,9 +379,14 @@ def is_in_path(name_of_command, return_abs_path = False):
return is_in_dir
def exec_command(command):
'''command is a string that contain arguments'''
# os.system(command)
subprocess.Popen(command.split())
subprocess.Popen(command, shell = True)
def build_command(executable, parameter):
# we add to the parameter (can hold path with spaces)
# "" so we have good parsing from shell
parameter = parameter.replace('"', '\\"') # but first escape "
command = '%s "%s"' % (executable, parameter)
return command
def launch_browser_mailer(kind, uri):
#kind = 'url' or 'mail'
@ -382,6 +404,8 @@ def launch_browser_mailer(kind, uri):
command = 'gnome-open'
elif gajim.config.get('openwith') == 'kfmclient exec':
command = 'kfmclient exec'
elif gajim.config.get('openwith') == 'exo-open':
command = 'exo-open'
elif gajim.config.get('openwith') == 'custom':
if kind == 'url':
command = gajim.config.get('custombrowser')
@ -389,7 +413,8 @@ def launch_browser_mailer(kind, uri):
command = gajim.config.get('custommailapp')
if command == '': # if no app is configured
return
command = command + ' ' + uri
command = build_command(command, uri)
try:
exec_command(command)
except:
@ -406,11 +431,13 @@ def launch_file_manager(path_to_open):
command = 'gnome-open'
elif gajim.config.get('openwith') == 'kfmclient exec':
command = 'kfmclient exec'
elif gajim.config.get('openwith') == 'exo-open':
command = 'exo-open'
elif gajim.config.get('openwith') == 'custom':
command = gajim.config.get('custom_file_manager')
if command == '': # if no app is configured
return
command = command + ' ' + path_to_open
command = build_command(command, path_to_open)
try:
exec_command(command)
except:
@ -438,7 +465,7 @@ def play_sound_file(path_to_soundfile):
if gajim.config.get('soundplayer') == '':
return
player = gajim.config.get('soundplayer')
command = player + ' ' + path_to_soundfile
command = build_command(player, path_to_soundfile)
exec_command(command)
def get_file_path_from_dnd_dropped_uri(uri):
@ -523,8 +550,10 @@ def get_icon_name_to_show(contact, account = None):
def decode_string(string):
'''try to decode (to make it Unicode instance) given string'''
if isinstance(string, unicode):
return string
# by the time we go to iso15 it better be the one else we show bad characters
encodings = (sys.getfilesystemencoding(), 'utf-8', 'iso-8859-15')
encodings = (locale.getpreferredencoding(), 'utf-8', 'iso-8859-15')
for encoding in encodings:
try:
string = string.decode(encoding)
@ -599,7 +628,6 @@ def get_documents_path():
path = os.path.expanduser('~')
return path
# moved from connection.py
def get_full_jid_from_iq(iq_obj):
'''return the full jid (with resource) from an iq as unicode'''
return parse_jid(str(iq_obj.getFrom()))
@ -674,6 +702,7 @@ def get_os_info():
output = temp_failure_retry(child_stdout.readline).strip()
child_stdout.close()
child_stdin.close()
os.wait()
# some distros put n/a in places, so remove those
output = output.replace('n/a', '').replace('N/A', '')
return output
@ -726,23 +755,21 @@ def sanitize_filename(filename):
return filename
def allow_showing_notification(account, type = None, advanced_notif_num = None,
first = True):
def allow_showing_notification(account, type = 'notify_on_new_message',
advanced_notif_num = None, is_first_message = True):
'''is it allowed to show nofication?
check OUR status and if we allow notifications for that status
type is the option that need to be True ex: notify_on_signing
first: set it to false when it's not the first message'''
if advanced_notif_num != None:
type is the option that need to be True e.g.: notify_on_signing
is_first_message: set it to false when it's not the first message'''
if advanced_notif_num is not None:
popup = gajim.config.get_per('notifications', str(advanced_notif_num),
'popup')
if popup == 'yes':
return True
if popup == 'no':
return False
if type and (not gajim.config.get(type) or not first):
if type and (not gajim.config.get(type) or not is_first_message):
return False
if type and gajim.config.get(type) and first:
return True
if gajim.config.get('autopopupaway'): # always show notification
return True
if gajim.connections[account].connected in (2, 3): # we're online or chat
@ -766,7 +793,7 @@ def allow_popup_window(account, advanced_notif_num = None):
return False
def allow_sound_notification(sound_event, advanced_notif_num = None):
if advanced_notif_num != None:
if advanced_notif_num is not None:
sound = gajim.config.get_per('notifications', str(advanced_notif_num),
'sound')
if sound == 'yes':
@ -796,3 +823,110 @@ def get_chat_control(account, contact):
highest_contact.resource:
return None
return gajim.interface.msg_win_mgr.get_control(contact.jid, account)
def reduce_chars_newlines(text, max_chars = 0, max_lines = 0):
'''Cut the chars after 'max_chars' on each line
and show only the first 'max_lines'.
If any of the params is not present (None or 0) the action
on it is not performed'''
def _cut_if_long(string):
if len(string) > max_chars:
string = string[:max_chars - 3] + '...'
return string
if isinstance(text, str):
text = text.decode('utf-8')
if max_lines == 0:
lines = text.split('\n')
else:
lines = text.split('\n', max_lines)[:max_lines]
if max_chars > 0:
if lines:
lines = map(lambda e: _cut_if_long(e), lines)
if lines:
reduced_text = reduce(lambda e, e1: e + '\n' + e1, lines)
else:
reduced_text = ''
return reduced_text
def get_notification_icon_tooltip_text():
text = None
unread_chat = gajim.events.get_nb_events(types = ['printed_chat',
'chat'])
unread_single_chat = gajim.events.get_nb_events(types = ['normal'])
unread_gc = gajim.events.get_nb_events(types = ['printed_gc_msg',
'gc_msg'])
unread_pm = gajim.events.get_nb_events(types = ['printed_pm', 'pm'])
accounts = get_accounts_info()
if unread_chat or unread_single_chat or unread_gc or unread_pm:
text = 'Gajim '
awaiting_events = unread_chat + unread_single_chat + unread_gc + unread_pm
if awaiting_events == unread_chat or awaiting_events == unread_single_chat \
or awaiting_events == unread_gc or awaiting_events == unread_pm:
# This condition is like previous if but with xor...
# Print in one line
text += '-'
else:
# Print in multiple lines
text += '\n '
if unread_chat:
text += ngettext(
' %d unread message',
' %d unread messages',
unread_chat, unread_chat, unread_chat)
text += '\n '
if unread_single_chat:
text += ngettext(
' %d unread single message',
' %d unread single messages',
unread_single_chat, unread_single_chat, unread_single_chat)
text += '\n '
if unread_gc:
text += ngettext(
' %d unread group chat message',
' %d unread group chat messages',
unread_gc, unread_gc, unread_gc)
text += '\n '
if unread_pm:
text += ngettext(
' %d unread private message',
' %d unread private messages',
unread_pm, unread_pm, unread_pm)
text += '\n '
text = text[:-4] # remove latest '\n '
elif len(accounts) > 1:
text = _('Gajim')
elif len(accounts) == 1:
message = accounts[0]['status_line']
message = reduce_chars_newlines(message, 100, 1)
text = _('Gajim - %s') % message
else:
text = _('Gajim - %s') % get_uf_show('offline')
return text
def get_accounts_info():
'''helper for notification icon tooltip'''
accounts = []
accounts_list = gajim.contacts.get_accounts()
accounts_list.sort()
for account in accounts_list:
status_idx = gajim.connections[account].connected
# uncomment the following to hide offline accounts
# if status_idx == 0: continue
status = gajim.SHOW_LIST[status_idx]
message = gajim.connections[account].status
single_line = get_uf_show(status)
if message is None:
message = ''
else:
message = message.strip()
if message != '':
single_line += ': ' + message
accounts.append({'name': account, 'status_line': single_line,
'show': status, 'message': message})
return accounts

View file

@ -8,7 +8,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>

View file

@ -1,17 +1,7 @@
## logger.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.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
@ -32,24 +22,15 @@ import exceptions
import gajim
try:
from pysqlite2 import dbapi2 as sqlite
import sqlite3 as sqlite # python 2.5
except ImportError:
raise exceptions.PysqliteNotAvailable
if os.name == 'nt':
try:
# Documents and Settings\[User Name]\Application Data\Gajim\logs.db
LOG_DB_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'logs.db')
except KeyError:
# win9x, ./logs.db
LOG_DB_PATH = 'logs.db'
else: # Unices
LOG_DB_PATH = os.path.expanduser('~/.gajim/logs.db')
from pysqlite2 import dbapi2 as sqlite
except ImportError:
raise exceptions.PysqliteNotAvailable
try:
LOG_DB_PATH = LOG_DB_PATH.decode(sys.getfilesystemencoding())
except:
pass
import configpaths
LOG_DB_PATH = configpaths.gajimpaths['LOG_DB']
class Constants:
def __init__(self):
@ -107,15 +88,33 @@ class Logger:
return
self.init_vars()
def init_vars(self):
# if locked, wait up to 20 sec to unlock
# before raise (hopefully should be enough)
def close_db(self):
if self.con:
self.con.close()
self.con = None
self.cur = None
def open_db(self):
self.close_db()
# if locked, wait up to 20 sec to unlock
# before raise (hopefully should be enough)
self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0,
isolation_level = 'IMMEDIATE')
self.cur = self.con.cursor()
self.set_synchronous(False)
def set_synchronous(self, sync):
try:
if sync:
self.cur.execute("PRAGMA synchronous = NORMAL")
else:
self.cur.execute("PRAGMA synchronous = OFF")
except sqlite.Error, e:
gajim.log.debug("Failed to set_synchronous(%s): %s" % (sync, str(e)))
def init_vars(self):
self.open_db()
self.get_jids_already_in_db()
def get_jids_already_in_db(self):
@ -136,9 +135,11 @@ class Logger:
and after that all okay'''
possible_room_jid, possible_nick = jid.split('/', 1)
return self.jid_is_room_jid(possible_room_jid)
def jid_is_room_jid(self, jid):
self.cur.execute('SELECT jid_id FROM jids WHERE jid=? AND type=?',
(possible_room_jid, constants.JID_ROOM_TYPE))
(jid, constants.JID_ROOM_TYPE))
row = self.cur.fetchone()
if row is None:
return False
@ -344,10 +345,8 @@ class Logger:
ROOM_JID/nick if pm-related.'''
if self.jids_already_in == []: # only happens if we just created the db
self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0,
isolation_level = 'IMMEDIATE')
self.cur = self.con.cursor()
self.open_db()
jid = jid.lower()
contact_name_col = None # holds nickname for kinds gcstatus, gc_msg
# message holds the message unless kind is status or gcstatus,

View file

@ -1,16 +1,6 @@
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -27,11 +17,21 @@ import sys
import locale
from common import gajim
import exceptions
try:
import sqlite3 as sqlite # python 2.5
except ImportError:
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
raise exceptions.PysqliteNotAvailable
import logger
class OptionsParser:
def __init__(self, filename):
self.__filename = filename
self.old_values = {} # values that are saved in the file and maybe
# no longer valid
self.old_values = {} # values that are saved in the file and maybe
# no longer valid
def read_line(self, line):
index = line.find(' = ')
@ -126,8 +126,7 @@ class OptionsParser:
os.chmod(self.__filename, 0600)
def update_config(self, old_version, new_version):
# Convert '0.x.y' to (0, x, y)
old_version_list = old_version.split('.')
old_version_list = old_version.split('.') # convert '0.x.y' to (0, x, y)
old = []
while len(old_version_list):
old.append(int(old_version_list.pop(0)))
@ -146,7 +145,15 @@ class OptionsParser:
self.update_config_to_01012()
if old < [0, 10, 1, 3] and new >= [0, 10, 1, 3]:
self.update_config_to_01013()
if old < [0, 10, 1, 4] and new >= [0, 10, 1, 4]:
self.update_config_to_01014()
if old < [0, 10, 1, 5] and new >= [0, 10, 1, 5]:
self.update_config_to_01015()
if old < [0, 10, 1, 6] and new >= [0, 10, 1, 6]:
self.update_config_to_01016()
if old < [0, 10, 1, 7] and new >= [0, 10, 1, 7]:
self.update_config_to_01017()
gajim.logger.init_vars()
gajim.config.set('version', new_version)
@ -202,13 +209,6 @@ class OptionsParser:
def assert_unread_msgs_table_exists(self):
'''create table unread_messages if there is no such table'''
import exceptions
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
raise exceptions.PysqliteNotAvailable
import logger
con = sqlite.connect(logger.LOG_DB_PATH)
cur = con.cursor()
try:
@ -278,13 +278,6 @@ class OptionsParser:
def update_config_to_01013(self):
'''create table transports_cache if there is no such table'''
import exceptions
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
raise exceptions.PysqliteNotAvailable
import logger
con = sqlite.connect(logger.LOG_DB_PATH)
cur = con.cursor()
try:
@ -301,3 +294,56 @@ class OptionsParser:
pass
con.close()
gajim.config.set('version', '0.10.1.3')
def update_config_to_01014(self):
'''apply indeces to the logs database'''
print _('migrating logs database to indeces')
con = sqlite.connect(logger.LOG_DB_PATH)
cur = con.cursor()
# apply indeces
try:
cur.executescript(
'''
CREATE INDEX idx_logs_jid_id_kind ON logs (jid_id, kind);
CREATE INDEX idx_unread_messages_jid_id ON unread_messages (jid_id);
'''
)
con.commit()
except:
pass
con.close()
gajim.config.set('version', '0.10.1.4')
def update_config_to_01015(self):
'''clean show values in logs database'''
con = sqlite.connect(logger.LOG_DB_PATH)
cur = con.cursor()
status = dict((i[5:].lower(), logger.constants.__dict__[i]) for i in \
logger.constants.__dict__.keys() if i.startswith('SHOW_'))
for show in status:
cur.execute('update logs set show = ? where show = ?;', (status[show],
show))
cur.execute('update logs set show = NULL where show not in (0, 1, 2, 3, 4, 5);')
con.commit()
cur.close() # remove this in 2007 [pysqlite old versions need this]
con.close()
gajim.config.set('version', '0.10.1.5')
def update_config_to_01016(self):
'''#2494 : Now we play gc_received_message sound even if
notify_on_all_muc_messages is false. Keep precedent behaviour.'''
if self.old_values.has_key('notify_on_all_muc_messages') and \
self.old_values['notify_on_all_muc_messages'] == 'False' and \
gajim.config.get_per('soundevents', 'muc_message_received', 'enabled'):
gajim.config.set_per('soundevents',\
'muc_message_received', 'enabled', False)
gajim.config.set('version', '0.10.1.6')
def update_config_to_01017(self):
'''trayicon_notification_on_new_messages ->
trayicon_notification_on_events '''
if self.old_values.has_key('trayicon_notification_on_new_messages'):
gajim.config.set('trayicon_notification_on_events',
self.old_values['trayicon_notification_on_new_messages'])
gajim.config.set('version', '0.10.1.7')

117
src/common/passwords.py Normal file
View file

@ -0,0 +1,117 @@
##
## Copyright (C) 2006 Gustavo J. A. M. Carneiro <gjcarneiro@gmail.com>
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
##
## 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; version 2 only.
##
## 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.
##
__all__ = ['get_password', 'save_password']
import gobject
from common import gajim
try:
import gnomekeyring
except ImportError:
USER_USES_GNOMEKEYRING = False
else:
if gnomekeyring.is_available():
USER_USES_GNOMEKEYRING = True
else:
USER_USES_GNOMEKEYRING = False
class PasswordStorage(object):
def get_password(self, account_name):
raise NotImplementedError
def save_password(self, account_name, password):
raise NotImplementedError
class SimplePasswordStorage(PasswordStorage):
def get_password(self, account_name):
return gajim.config.get_per('accounts', account_name, 'password')
def save_password(self, account_name, password):
gajim.config.set_per('accounts', account_name, 'password', password)
gajim.connections[account_name].password = password
class GnomePasswordStorage(PasswordStorage):
def __init__(self):
# self.keyring = gnomekeyring.get_default_keyring_sync()
## above line commented and code below inserted as workaround
## for the bug http://bugzilla.gnome.org/show_bug.cgi?id=363019
self.keyring = "default"
try:
gnomekeyring.create_sync(self.keyring, None)
except gnomekeyring.AlreadyExistsError:
pass
def get_password(self, account_name):
conf = gajim.config.get_per('accounts', account_name, 'password')
if conf is None:
return None
try:
unused, auth_token = conf.split('gnomekeyring:')
auth_token = int(auth_token)
except ValueError:
password = conf
## migrate the password over to keyring
try:
self.save_password(account_name, password, update=False)
except gnomekeyring.NoKeyringDaemonError:
## no keyring daemon: in the future, stop using it
set_storage(SimplePasswordStorage())
return password
try:
return gnomekeyring.item_get_info_sync(self.keyring,
auth_token).get_secret()
except gnomekeyring.DeniedError:
return None
except gnomekeyring.NoKeyringDaemonError:
## no keyring daemon: in the future, stop using it
set_storage(SimplePasswordStorage())
return None
def save_password(self, account_name, password, update=True):
display_name = _('Gajim account %s') % account_name
attributes = dict(account_name=str(account_name), gajim=1)
auth_token = gnomekeyring.item_create_sync(
self.keyring, gnomekeyring.ITEM_GENERIC_SECRET,
display_name, attributes, password, update)
token = 'gnomekeyring:%i' % auth_token
gajim.config.set_per('accounts', account_name, 'password', token)
storage = None
def get_storage():
global storage
if storage is None: # None is only in first time get_storage is called
if USER_USES_GNOMEKEYRING:
try:
storage = GnomePasswordStorage()
except gnomekeyring.NoKeyringDaemonError:
storage = SimplePasswordStorage()
else:
storage = SimplePasswordStorage()
return storage
def set_storage(storage_):
global storage
storage = storage_
def get_password(account_name):
return get_storage().get_password(account_name)
def save_password(account_name, password):
return get_storage().save_password(account_name, password)

View file

@ -186,6 +186,9 @@ class HostTester(Socks5, IdleObject):
def connect(self):
''' create the socket and plug it to the idlequeue '''
if self.host is None:
self.on_failure()
return None
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.setblocking(False)
self.fd = self._sock.fileno()

View file

@ -0,0 +1,126 @@
## rst_xhtml_generator.py
##
## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2006 Santiago Gala
##
## 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; version 2 only.
##
## 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.
##
try:
from docutils import io
from docutils.core import Publisher
from docutils.parsers.rst import roles
from docutils import nodes,utils
from docutils.parsers.rst.roles import set_classes
except:
def create_xhtml(text):
return None
else:
def jep_reference_role(role, rawtext, text, lineno, inliner,
options={}, content=[]):
'''Role to make handy references to Jabber Enhancement Proposals (JEP).
Use as :JEP:`71` (or jep, or jep-reference).
Modeled after the sample in docutils documentation.
'''
jep_base_url = 'http://www.jabber.org/jeps/'
jep_url = 'jep-%04d.html'
try:
jepnum = int(text)
if jepnum <= 0:
raise ValueError
except ValueError:
msg = inliner.reporter.error(
'JEP number must be a number greater than or equal to 1; '
'"%s" is invalid.' % text, line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
ref = jep_base_url + jep_url % jepnum
set_classes(options)
node = nodes.reference(rawtext, 'JEP ' + utils.unescape(text), refuri=ref,
**options)
return [node], []
roles.register_canonical_role('jep-reference', jep_reference_role)
from docutils.parsers.rst.languages.en import roles
roles['jep-reference'] = 'jep-reference'
roles['jep'] = 'jep-reference'
class HTMLGenerator:
'''Really simple HTMLGenerator starting from publish_parts.
It reuses the docutils.core.Publisher class, which means it is *not*
threadsafe.
'''
def __init__(self,
settings_spec=None,
settings_overrides=dict(report_level=5, halt_level=5),
config_section='general'):
self.pub = Publisher(reader=None, parser=None, writer=None,
settings=None,
source_class=io.StringInput,
destination_class=io.StringOutput)
self.pub.set_components(reader_name='standalone',
parser_name='restructuredtext',
writer_name='html')
# hack: JEP-0071 does not allow HTML char entities, so we hack our way
# out of it.
# &mdash; == u"\u2014"
# a setting to only emit charater entities in the writer would be nice
# FIXME: several &nbsp; are emitted, and they are explicitly forbidden
# in the JEP
# &nbsp; == u"\u00a0"
self.pub.writer.translator_class.attribution_formats['dash'] = (
u'\u2014', '')
self.pub.process_programmatic_settings(settings_spec,
settings_overrides,
config_section)
def create_xhtml(self, text,
destination=None,
destination_path=None,
enable_exit_status=None):
''' Create xhtml for a fragment of IM dialog.
We can use the source_name to store info about
the message.'''
self.pub.set_source(text, None)
self.pub.set_destination(destination, destination_path)
output = self.pub.publish(enable_exit_status=enable_exit_status)
# kludge until we can get docutils to stop generating (rare) &nbsp;
# entities
return u'\u00a0'.join(self.pub.writer.parts['fragment'].strip().split(
'&nbsp;'))
Generator = HTMLGenerator()
def create_xhtml(text):
return Generator.create_xhtml(text)
if __name__ == '__main__':
print Generator.create_xhtml('''
test::
>>> print 1
1
*I* like it. It is for :JEP:`71`
this `` should trigger`` should trigger the &nbsp; problem.
''')
print Generator.create_xhtml('''
*test1
test2_
''')

View file

@ -8,7 +8,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -33,13 +33,10 @@ STATE_AWAKE = 'awake'
SUPPORTED = True
try:
import common.idle as idle # when we launch gajim from sources
import idle
except:
try:
import idle # when Gajim is installed
except:
gajim.log.debug('Unable to load idle module')
SUPPORTED = False
gajim.log.debug('Unable to load idle module')
SUPPORTED = False
class Sleepy:

View file

@ -3,14 +3,14 @@
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <nkour@jabber.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -27,11 +27,8 @@
import socket
import select
import os
import struct
import sha
import time
from dialogs import BindPortError
from errno import EWOULDBLOCK
@ -74,13 +71,13 @@ class SocksQueue:
self.on_success = None
self.on_failure = None
def start_listener(self, host, port, sha_str, sha_handler, sid):
def start_listener(self, port, sha_str, sha_handler, sid):
''' start waiting for incomming connections on (host, port)
and do a socks5 authentication using sid for generated sha
'''
self.sha_handlers[sha_str] = (sha_handler, sid)
if self.listener == None:
self.listener = Socks5Listener(self.idlequeue, host, port)
self.listener = Socks5Listener(self.idlequeue, port)
self.listener.queue = self
self.listener.bind()
if self.listener.started is False:
@ -213,7 +210,7 @@ class SocksQueue:
sender = self.senders[file_props['hash']]
sender.account = account
result = get_file_contents(0)
result = self.get_file_contents(0)
self.process_result(result, sender)
def result_sha(self, sha_str, idx):
@ -350,7 +347,10 @@ class SocksQueue:
class Socks5:
def __init__(self, idlequeue, host, port, initiator, target, sid):
if host is not None:
self.host = socket.gethostbyname(host)
try:
self.host = socket.gethostbyname(host)
except socket.gaierror:
self.host = None
self.idlequeue = idlequeue
self.fd = -1
self.port = port
@ -787,12 +787,12 @@ class Socks5Sender(Socks5, IdleObject):
self.queue.remove_sender(self.queue_idx, False)
class Socks5Listener(IdleObject):
def __init__(self, idlequeue, host, port):
''' handle all incomming connections on (host, port)
def __init__(self, idlequeue, port):
''' handle all incomming connections on (0.0.0.0, port)
This class implements IdleObject, but we will expect
only pollin events though
'''
self.host, self.port = host, port
self.port = port
self.queue_idx = -1
self.idlequeue = idlequeue
self.queue = None

View file

@ -74,10 +74,8 @@ class NBCommonClient(CommonClient):
''' Called on disconnection. Calls disconnect handlers and cleans things up. '''
self.connected=''
self.DEBUG(self.DBG,'Disconnect detected','stop')
self.disconnect_handlers.reverse()
for i in self.disconnect_handlers:
for i in reversed(self.disconnect_handlers):
i()
self.disconnect_handlers.reverse()
if self.__dict__.has_key('NonBlockingRoster'):
self.NonBlockingRoster.PlugOut()
if self.__dict__.has_key('NonBlockingBind'):
@ -125,6 +123,8 @@ class NBCommonClient(CommonClient):
self.on_connect_failure(retry)
def _on_connected(self):
# connect succeded, so no need of this callback anymore
self.on_connect_failure = None
self.connected = 'tcp'
if self._Ssl:
transports_nb.NonBlockingTLS().PlugIn(self, now=1)

View file

@ -74,7 +74,6 @@ class Dispatcher(PlugIn):
self.RegisterProtocol('presence', Presence)
self.RegisterProtocol('message', Message)
self.RegisterDefaultHandler(self.returnStanzaHandler)
# Register Gajim's event handler as soon as dispatcher begins
self.RegisterEventHandler(self._owner._caller._event_dispatcher)
self.on_responses = {}
@ -84,7 +83,10 @@ class Dispatcher(PlugIn):
self._owner.lastErrNode = None
self._owner.lastErr = None
self._owner.lastErrCode = None
self.StreamInit()
if hasattr(self._owner, 'StreamInit'):
self._owner.StreamInit()
else:
self.StreamInit()
def plugout(self):
''' Prepares instance to be destructed. '''
@ -129,17 +131,18 @@ class Dispatcher(PlugIn):
try:
self.Stream.Parse(data)
# end stream:stream tag received
if self.Stream and self.Stream._NodeBuilder__depth == 0:
if self.Stream and self.Stream.has_received_endtag():
self._owner.Connection.disconnect()
return 0
except ExpatError:
sys.exc_clear()
self.DEBUG('Invalid XML received from server. Forcing disconnect.')
self.DEBUG('Invalid XML received from server. Forcing disconnect.', 'error')
self._owner.Connection.pollend()
return 0
if len(self._pendingExceptions) > 0:
_pendingException = self._pendingExceptions.pop()
raise _pendingException[0], _pendingException[1], _pendingException[2]
if len(data) == 0: return '0'
return len(data)
def RegisterNamespace(self, xmlns, order='info'):
@ -396,7 +399,7 @@ class Dispatcher(PlugIn):
Additional callback arguments can be specified in args. '''
self.SendAndWaitForResponse(stanza, 0, func, args)
def send(self, stanza):
def send(self, stanza, is_message = False):
''' Serialise stanza and put it on the wire. Assign an unique ID to it before send.
Returns assigned ID.'''
if type(stanza) in [type(''), type(u'')]:
@ -423,7 +426,10 @@ class Dispatcher(PlugIn):
stanza=route
stanza.setNamespace(self._owner.Namespace)
stanza.setParent(self._metastream)
self._owner.Connection.send(stanza)
if is_message:
self._owner.Connection.send(stanza, True)
else:
self._owner.Connection.send(stanza)
return _ID
def disconnect(self):

View file

@ -53,7 +53,6 @@ class IdleQueue:
self.selector = select.poll()
def remove_timeout(self, fd):
''' self explanatory, remove the timeout from 'read_timeouts' dict '''
if self.read_timeouts.has_key(fd):
del(self.read_timeouts[fd])

View file

@ -19,7 +19,7 @@ Protocol module contains tools that is needed for processing of
xmpp-related data structures.
"""
from simplexml import Node,ustr
from simplexml import Node,NodeBuilder,ustr
import time
NS_ACTIVITY ='http://jabber.org/protocol/activity' # JEP-0108
NS_ADDRESS ='http://jabber.org/protocol/address' # JEP-0033
@ -94,6 +94,7 @@ NS_VCARD_UPDATE =NS_VCARD+':x:update'
NS_VERSION ='jabber:iq:version'
NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # JEP-0130
NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # JEP-0071
NS_XHTML = 'http://www.w3.org/1999/xhtml' # "
NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # JEP-0141
NS_DATA_VALIDATE='http://jabber.org/protocol/xdata-validate' # JEP-0122
NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams'
@ -348,11 +349,18 @@ class Protocol(Node):
for tag in errtag.getChildren():
if tag.getName()<>'text': return tag.getName()
return errtag.getData()
def getErrorMsg(self):
""" Return the textual description of the error (if present) or the error condition """
errtag=self.getTag('error')
if errtag:
for tag in errtag.getChildren():
if tag.getName()=='text': return tag.getData()
return self.getError()
def getErrorCode(self):
""" Return the error code. Obsolette. """
""" Return the error code. Obsolete. """
return self.getTagAttr('error','code')
def setError(self,error,code=None):
""" Set the error code. Obsolette. Use error-conditions instead. """
""" Set the error code. Obsolete. Use error-conditions instead. """
if code:
if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error)
else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error)
@ -378,16 +386,29 @@ class Protocol(Node):
class Message(Protocol):
""" XMPP Message stanza - "push" mechanism."""
def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
def __init__(self, to=None, body=None, xhtml=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
""" Create message object. You can specify recipient, text of message, type of message
any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """
Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
if body: self.setBody(body)
if xhtml: self.setXHTML(xhtml)
if subject: self.setSubject(subject)
def getBody(self):
""" Returns text of the message. """
return self.getTagData('body')
def getXHTML(self, xmllang=None):
""" Returns serialized xhtml-im element text of the message.
TODO: Returning a DOM could make rendering faster."""
xhtml = self.getTag('html')
if xhtml:
if xmllang:
body = xhtml.getTag('body', attrs={'xml:lang':xmllang})
else:
body = xhtml.getTag('body')
return str(body)
return None
def getSubject(self):
""" Returns subject of the message. """
return self.getTagData('subject')
@ -397,6 +418,22 @@ class Message(Protocol):
def setBody(self,val):
""" Sets the text of the message. """
self.setTagData('body',val)
def setXHTML(self,val,xmllang=None):
""" Sets the xhtml text of the message (JEP-0071).
The parameter is the "inner html" to the body."""
try:
if xmllang:
dom = NodeBuilder('<body xmlns="'+NS_XHTML+'" xml:lang="'+xmllang+'" >' + val + '</body>').getDom()
else:
dom = NodeBuilder('<body xmlns="'+NS_XHTML+'">'+val+'</body>',0).getDom()
if self.getTag('html'):
self.getTag('html').addChild(node=dom)
else:
self.setTag('html',namespace=NS_XHTML_IM).addChild(node=dom)
except Exception, e:
print "Error", e
pass #FIXME: log. we could not set xhtml (parse error, whatever)
def setSubject(self,val):
""" Sets the subject of the message. """
self.setTagData('subject',val)

View file

@ -183,7 +183,7 @@ class Session:
if self.sendbuffer:
try:
# LOCK_QUEUE
sent=self._send(self.sendbuffer) # âÌÏËÉÒÕÀÝÁÑ ÛÔÕÞËÁ!
sent=self._send(self.sendbuffer) # blocking socket
except:
# UNLOCK_QUEUE
self.set_socket_state(SOCKET_DEAD)

View file

@ -302,6 +302,8 @@ class NodeBuilder:
self.Parse = self._parser.Parse
self.__depth = 0
self.__last_depth = 0
self.__max_depth = 0
self._dispatch_depth = 1
self._document_attrs = None
self._mini_dom=initial_node
@ -338,7 +340,7 @@ class NodeBuilder:
ns=attr[:sp] #
attrs[self.namespaces[ns]+attr[sp+1:]]=attrs[attr]
del attrs[attr] #
self.__depth += 1
self._inc_depth()
self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`), 'down')
if self.__depth == self._dispatch_depth:
if not self._mini_dom :
@ -366,7 +368,7 @@ class NodeBuilder:
self._ptr = self._ptr.parent
else:
self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", 'stop')
self.__depth -= 1
self._dec_depth()
self.last_is_data = 0
if self.__depth == 0: self.stream_footer_received()
@ -374,7 +376,7 @@ class NodeBuilder:
if self.last_is_data:
if self.data_buffer:
self.data_buffer.append(data)
else:
elif self._ptr:
self.data_buffer = [data]
self.last_is_data = 1
@ -399,6 +401,19 @@ class NodeBuilder:
""" Method called when stream just closed. """
self.check_data_buffer()
def has_received_endtag(self, level=0):
""" Return True if at least one end tag was seen (at level) """
return self.__depth <= level and self.__max_depth > level
def _inc_depth(self):
self.__last_depth = self.__depth
self.__depth += 1
self.__max_depth = max(self.__depth, self.__max_depth)
def _dec_depth(self):
self.__last_depth = self.__depth
self.__depth -= 1
def XML2Node(xml):
""" Converts supplied textual string into XML node. Handy f.e. for reading configuration file.
Raises xml.parser.expat.parsererror if provided string is not well-formed XML. """

View file

@ -143,11 +143,11 @@ class NonBlockingTcp(PlugIn, IdleObject):
def pollin(self):
self._do_receive()
def pollend(self):
def pollend(self, retry = False):
conn_failure_cb = self.on_connect_failure
self.disconnect()
if conn_failure_cb:
conn_failure_cb()
conn_failure_cb(retry)
def disconnect(self):
if self.state == -2: # already disconnected
@ -216,15 +216,19 @@ class NonBlockingTcp(PlugIn, IdleObject):
# "received" will be empty anyhow
if errnum == socket.SSL_ERROR_WANT_READ:
pass
elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]:
elif errnum == errno.ECONNRESET:
self.pollend(True)
# don't proccess result, caus it will raise error
return
elif errnum in [errno.ENOTCONN, errno.ESHUTDOWN]:
self.pollend()
# don't proccess result, cas it will raise error
# don't proccess result, caus it will raise error
return
elif not received :
if errnum != socket.SSL_ERROR_EOF:
# 8 EOF occurred in violation of protocol
self.DEBUG('Socket error while receiving data', 'error')
self.pollend()
self.pollend(True)
if self.state >= 0:
self.disconnect()
return

View file

View file

@ -0,0 +1,647 @@
## common/zeroconf/client_zeroconf.py
##
## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
## 2006 Dimitur Kirov <dkirov@gmail.com>
##
## 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; version 2 only.
##
## 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.
##
from common import gajim
import common.xmpp
from common.xmpp.idlequeue import IdleObject
from common.xmpp import dispatcher_nb, simplexml
from common.xmpp.client import *
from common.xmpp.simplexml import ustr
from common.zeroconf import zeroconf
from common.xmpp.protocol import *
import socket
import errno
import sys
from common.zeroconf import roster_zeroconf
MAX_BUFF_LEN = 65536
DATA_RECEIVED='DATA RECEIVED'
DATA_SENT='DATA SENT'
TYPE_SERVER, TYPE_CLIENT = range(2)
# wait XX sec to establish a connection
CONNECT_TIMEOUT_SECONDS = 10
# after XX sec with no activity, close the stream
ACTIVITY_TIMEOUT_SECONDS = 30
class ZeroconfListener(IdleObject):
def __init__(self, port, conn_holder):
''' handle all incomming connections on ('0.0.0.0', port)'''
self.port = port
self.queue_idx = -1
#~ self.queue = None
self.started = False
self._sock = None
self.fd = -1
self.caller = conn_holder.caller
self.conn_holder = conn_holder
def bind(self):
self._serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# will fail when port is busy, or we don't have rights to bind
try:
self._serv.bind(('0.0.0.0', self.port))
except Exception, e:
# unable to bind, show error dialog
return None
self._serv.listen(socket.SOMAXCONN)
self._serv.setblocking(False)
self.fd = self._serv.fileno()
gajim.idlequeue.plug_idle(self, False, True)
self.started = True
def pollend(self):
''' called when we stop listening on (host, port) '''
self.disconnect()
def pollin(self):
''' accept a new incomming connection and notify queue'''
sock = self.accept_conn()
P2PClient(sock[0], sock[1][0], sock[1][1], self.conn_holder)
def disconnect(self):
''' free all resources, we are not listening anymore '''
gajim.idlequeue.remove_timeout(self.fd)
gajim.idlequeue.unplug_idle(self.fd)
self.fd = -1
self.started = False
try:
self._serv.close()
except:
pass
self.conn_holder.kill_all_connections()
def accept_conn(self):
''' accepts a new incoming connection '''
_sock = self._serv.accept()
_sock[0].setblocking(False)
return _sock
class P2PClient(IdleObject):
def __init__(self, _sock, host, port, conn_holder, stanzaqueue = [], to = None):
self._owner = self
self.Namespace = 'jabber:client'
self.defaultNamespace = self.Namespace
self._component = 0
self._registered_name = None
self._caller = conn_holder.caller
self.conn_holder = conn_holder
self.stanzaqueue = stanzaqueue
self.to = to
self.Server = host
self.DBG = 'client'
self.Connection = None
if gajim.verbose:
debug = ['always', 'nodebuilder']
else:
debug = []
self._DEBUG = Debug.Debug(debug)
self.DEBUG = self._DEBUG.Show
self.debug_flags = self._DEBUG.debug_flags
self.debug_flags.append(self.DBG)
self.sock_hash = None
if _sock:
self.sock_type = TYPE_SERVER
else:
self.sock_type = TYPE_CLIENT
conn = P2PConnection('', _sock, host, port, self._caller, self.on_connect, self)
self.sock_hash = conn._sock.__hash__
self.fd = conn.fd
self.conn_holder.add_connection(self, self.Server, port, self.to)
# count messages in queue
for val in self.stanzaqueue:
stanza, is_message = val
if is_message:
if self.conn_holder.number_of_awaiting_messages.has_key(self.fd):
self.conn_holder.number_of_awaiting_messages[self.fd]+=1
else:
self.conn_holder.number_of_awaiting_messages[self.fd]=1
def add_stanza(self, stanza, is_message = False):
if self.Connection:
if self.Connection.state == -1:
return False
self.send(stanza, is_message)
else:
self.stanzaqueue.append((stanza, is_message))
if is_message:
if self.conn_holder.number_of_awaiting_messages.has_key(self.fd):
self.conn_holder.number_of_awaiting_messages[self.fd]+=1
else:
self.conn_holder.number_of_awaiting_messages[self.fd] = 1
return True
def on_message_sent(self, connection_id):
self.conn_holder.number_of_awaiting_messages[connection_id]-=1
def on_connect(self, conn):
self.Connection = conn
self.Connection.PlugIn(self)
dispatcher_nb.Dispatcher().PlugIn(self)
self._register_handlers()
if self.sock_type == TYPE_CLIENT:
while self.stanzaqueue:
stanza, is_message = self.stanzaqueue.pop(0)
self.send(stanza, is_message)
def StreamInit(self):
''' Send an initial stream header. '''
self.Dispatcher.Stream = simplexml.NodeBuilder()
self.Dispatcher.Stream._dispatch_depth = 2
self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch
self.Dispatcher.Stream.stream_header_received = self._check_stream_start
self.debug_flags.append(simplexml.DBG_NODEBUILDER)
self.Dispatcher.Stream.DEBUG = self.DEBUG
self.Dispatcher.Stream.features = None
if self.sock_type == TYPE_CLIENT:
self.send_stream_header()
def send_stream_header(self):
self.Dispatcher._metastream = Node('stream:stream')
self.Dispatcher._metastream.setNamespace(self.Namespace)
# XXX TLS support
#~ self._metastream.setAttr('version', '1.0')
self.Dispatcher._metastream.setAttr('xmlns:stream', NS_STREAMS)
self.Dispatcher.send("<?xml version='1.0'?>%s>" % str(self.Dispatcher._metastream)[:-2])
def _check_stream_start(self, ns, tag, attrs):
if ns<>NS_STREAMS or tag<>'stream':
self._caller.dispatch('MSGERROR',[unicode(self.to), -1, \
_('Connection to host could not be established: Incorrect answer from server.'), None, None])
self.Connection.DEBUG('Incorrect stream start: (%s,%s).Terminating! ' % (tag, ns), 'error')
self.Connection.disconnect()
return
if self.sock_type == TYPE_SERVER:
self.send_stream_header()
while self.stanzaqueue:
stanza, is_message = self.stanzaqueue.pop(0)
self.send(stanza, is_message)
def on_disconnect(self):
if self.conn_holder:
if self.conn_holder.number_of_awaiting_messages.has_key(self.fd):
if self.conn_holder.number_of_awaiting_messages[self.fd] > 0:
self._caller.dispatch('MSGERROR',[unicode(self.to), -1, \
_('Connection to host could not be established'), None, None])
del self.conn_holder.number_of_awaiting_messages[self.fd]
self.conn_holder.remove_connection(self.sock_hash)
if self.__dict__.has_key('Dispatcher'):
self.Dispatcher.PlugOut()
if self.__dict__.has_key('P2PConnection'):
self.P2PConnection.PlugOut()
self.Connection = None
self._caller = None
self.conn_holder = None
def force_disconnect(self):
if self.Connection:
self.disconnect()
else:
self.on_disconnect()
def _on_receive_document_attrs(self, data):
if data:
self.Dispatcher.ProcessNonBlocking(data)
if not hasattr(self, 'Dispatcher') or \
self.Dispatcher.Stream._document_attrs is None:
return
self.onreceive(None)
if self.Dispatcher.Stream._document_attrs.has_key('version') and \
self.Dispatcher.Stream._document_attrs['version'] == '1.0':
#~ self.onreceive(self._on_receive_stream_features)
#XXX continue with TLS
return
self.onreceive(None)
return True
def _register_handlers(self):
self.RegisterHandler('message', lambda conn, data:self._caller._messageCB(self.Server, conn, data))
self.RegisterHandler('iq', self._caller._siSetCB, 'set',
common.xmpp.NS_SI)
self.RegisterHandler('iq', self._caller._siErrorCB, 'error',
common.xmpp.NS_SI)
self.RegisterHandler('iq', self._caller._siResultCB, 'result',
common.xmpp.NS_SI)
self.RegisterHandler('iq', self._caller._bytestreamSetCB, 'set',
common.xmpp.NS_BYTESTREAM)
self.RegisterHandler('iq', self._caller._bytestreamResultCB, 'result',
common.xmpp.NS_BYTESTREAM)
self.RegisterHandler('iq', self._caller._bytestreamErrorCB, 'error',
common.xmpp.NS_BYTESTREAM)
class P2PConnection(IdleObject, PlugIn):
''' class for sending file to socket over socks5 '''
def __init__(self, sock_hash, _sock, host = None, port = None, caller = None, on_connect = None, client = None):
IdleObject.__init__(self)
self._owner = client
PlugIn.__init__(self)
self.DBG_LINE='socket'
self.sendqueue = []
self.sendbuff = None
self.buff_is_message = False
self._sock = _sock
self.sock_hash = None
self.host, self.port = host, port
self.on_connect = on_connect
self.client = client
self.writable = False
self.readable = False
self._exported_methods=[self.send, self.disconnect, self.onreceive]
self.on_receive = None
if _sock:
self._sock = _sock
self.state = 1
self._sock.setblocking(False)
self.fd = self._sock.fileno()
self.on_connect(self)
else:
self.state = 0
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.setblocking(False)
self.fd = self._sock.fileno()
gajim.idlequeue.plug_idle(self, True, False)
self.set_timeout(CONNECT_TIMEOUT_SECONDS)
self.do_connect()
def set_timeout(self, timeout):
gajim.idlequeue.remove_timeout(self.fd)
if self.state >= 0:
gajim.idlequeue.set_read_timeout(self.fd, timeout)
def plugin(self, owner):
self.onreceive(owner._on_receive_document_attrs)
self._plug_idle()
return True
def plugout(self):
''' Disconnect from the remote server and unregister self.disconnected method from
the owner's dispatcher. '''
self.disconnect()
self._owner = None
def onreceive(self, recv_handler):
if not recv_handler:
if hasattr(self._owner, 'Dispatcher'):
self.on_receive = self._owner.Dispatcher.ProcessNonBlocking
else:
self.on_receive = None
return
_tmp = self.on_receive
# make sure this cb is not overriden by recursive calls
if not recv_handler(None) and _tmp == self.on_receive:
self.on_receive = recv_handler
def send(self, packet, is_message = False):
'''Append stanza to the queue of messages to be send.
If supplied data is unicode string, encode it to utf-8.
'''
if self.state <= 0:
return
r = packet
if isinstance(r, unicode):
r = r.encode('utf-8')
elif not isinstance(r, str):
r = ustr(r).encode('utf-8')
self.sendqueue.append((r, is_message))
self._plug_idle()
def read_timeout(self):
if self.client.conn_holder.number_of_awaiting_messages.has_key(self.fd) \
and self.client.conn_holder.number_of_awaiting_messages[self.fd] > 0:
self.client._caller.dispatch('MSGERROR',[unicode(self.client.to), -1, \
_('Connection to host could not be established: Timeout while sending data.'), None, None])
self.client.conn_holder.number_of_awaiting_messages[self.fd] = 0
self.pollend()
def do_connect(self):
errnum = 0
try:
self._sock.connect((self.host, self.port))
self._sock.setblocking(False)
except Exception, ee:
(errnum, errstr) = ee
if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK):
return
# win32 needs this
elif errnum not in (0, 10056, errno.EISCONN) or self.state != 0:
self.disconnect()
return None
else: # socket is already connected
self._sock.setblocking(False)
self.state = 1 # connected
self.on_connect(self)
return 1 # we are connected
def pollout(self):
if self.state == 0:
self.do_connect()
return
gajim.idlequeue.remove_timeout(self.fd)
self._do_send()
def pollend(self):
self.state = -1
self.disconnect()
def pollin(self):
''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.'''
received = ''
errnum = 0
try:
# get as many bites, as possible, but not more than RECV_BUFSIZE
received = self._sock.recv(MAX_BUFF_LEN)
except Exception, e:
if len(e.args) > 0 and isinstance(e.args[0], int):
errnum = e[0]
sys.exc_clear()
# "received" will be empty anyhow
if errnum == socket.SSL_ERROR_WANT_READ:
pass
elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]:
self.pollend()
# don't proccess result, cas it will raise error
return
elif not received :
if errnum != socket.SSL_ERROR_EOF:
# 8 EOF occurred in violation of protocol
self.pollend()
if self.state >= 0:
self.disconnect()
return
if self.state < 0:
return
if self.on_receive:
if self._owner.sock_type == TYPE_CLIENT:
self.set_timeout(ACTIVITY_TIMEOUT_SECONDS)
if received.strip():
self.DEBUG(received, 'got')
if hasattr(self._owner, 'Dispatcher'):
self._owner.Dispatcher.Event('', DATA_RECEIVED, received)
self.on_receive(received)
else:
# This should never happed, so we need the debug
self.DEBUG('Unhandled data received: %s' % received,'error')
self.disconnect()
return True
def onreceive(self, recv_handler):
if not recv_handler:
if hasattr(self._owner, 'Dispatcher'):
self.on_receive = self._owner.Dispatcher.ProcessNonBlocking
else:
self.on_receive = None
return
_tmp = self.on_receive
# make sure this cb is not overriden by recursive calls
if not recv_handler(None) and _tmp == self.on_receive:
self.on_receive = recv_handler
def disconnect(self):
''' Closes the socket. '''
gajim.idlequeue.remove_timeout(self.fd)
gajim.idlequeue.unplug_idle(self.fd)
try:
self._sock.shutdown(socket.SHUT_RDWR)
self._sock.close()
except:
# socket is already closed
pass
self.fd = -1
self.state = -1
if self._owner:
self._owner.on_disconnect()
def _do_send(self):
if not self.sendbuff:
if not self.sendqueue:
return None # nothing to send
self.sendbuff, self.buff_is_message = self.sendqueue.pop(0)
self.sent_data = self.sendbuff
try:
send_count = self._sock.send(self.sendbuff)
if send_count:
self.sendbuff = self.sendbuff[send_count:]
if not self.sendbuff and not self.sendqueue:
if self.state < 0:
gajim.idlequeue.unplug_idle(self.fd)
self._on_send()
self.disconnect()
return
# we are not waiting for write
self._plug_idle()
self._on_send()
except socket.error, e:
sys.exc_clear()
if e[0] == socket.SSL_ERROR_WANT_WRITE:
return True
if self.state < 0:
self.disconnect()
return
self._on_send_failure()
return
if self._owner.sock_type == TYPE_CLIENT:
self.set_timeout(ACTIVITY_TIMEOUT_SECONDS)
return True
def _plug_idle(self):
readable = self.state != 0
if self.sendqueue or self.sendbuff:
writable = True
else:
writable = False
if self.writable != writable or self.readable != readable:
gajim.idlequeue.plug_idle(self, writable, readable)
def _on_send(self):
if self.sent_data and self.sent_data.strip():
self.DEBUG(self.sent_data,'sent')
if hasattr(self._owner, 'Dispatcher'):
self._owner.Dispatcher.Event('', DATA_SENT, self.sent_data)
self.sent_data = None
if self.buff_is_message:
self._owner.on_message_sent(self.fd)
self.buff_is_message = False
def _on_send_failure(self):
self.DEBUG("Socket error while sending data",'error')
self._owner.disconnected()
self.sent_data = None
class ClientZeroconf:
def __init__(self, caller):
self.caller = caller
self.zeroconf = None
self.roster = None
self.last_msg = ''
self.connections = {}
self.recipient_to_hash = {}
self.ip_to_hash = {}
self.hash_to_port = {}
self.listener = None
self.number_of_awaiting_messages = {}
def test_avahi(self):
try:
import avahi
except ImportError:
return False
return True
def connect(self, show, msg):
self.port = self.start_listener(self.caller.port)
if not self.port:
return False
self.zeroconf_init(show, msg)
if not self.zeroconf.connect():
self.disconnect()
return None
self.roster = roster_zeroconf.Roster(self.zeroconf)
return True
def remove_announce(self):
if self.zeroconf:
return self.zeroconf.remove_announce()
def announce(self):
if self.zeroconf:
return self.zeroconf.announce()
def set_show_msg(self, show, msg):
if self.zeroconf:
self.zeroconf.txt['msg'] = msg
self.last_msg = msg
return self.zeroconf.update_txt(show)
def resolve_all(self):
if self.zeroconf:
self.zeroconf.resolve_all()
def reannounce(self, txt):
self.remove_announce()
self.zeroconf.txt = txt
self.zeroconf.port = self.port
self.zeroconf.username = self.caller.username
return self.announce()
def zeroconf_init(self, show, msg):
self.zeroconf = zeroconf.Zeroconf(self.caller._on_new_service,
self.caller._on_remove_service, self.caller._on_name_conflictCB,
self.caller._on_disconnected, self.caller._on_error, self.caller.username, self.caller.host, self.port)
self.zeroconf.txt['msg'] = msg
self.zeroconf.txt['status'] = show
self.zeroconf.txt['1st'] = self.caller.first
self.zeroconf.txt['last'] = self.caller.last
self.zeroconf.txt['jid'] = self.caller.jabber_id
self.zeroconf.txt['email'] = self.caller.email
self.zeroconf.username = self.caller.username
self.zeroconf.host = self.caller.host
self.zeroconf.port = self.port
self.last_msg = msg
def disconnect(self):
if self.listener:
self.listener.disconnect()
self.listener = None
if self.zeroconf:
self.zeroconf.disconnect()
self.zeroconf = None
if self.roster:
self.roster.zeroconf = None
self.roster._data = None
self.roster = None
def kill_all_connections(self):
for connection in self.connections.values():
connection.force_disconnect()
def add_connection(self, connection, ip, port, recipient):
sock_hash = connection.sock_hash
if sock_hash not in self.connections:
self.connections[sock_hash] = connection
self.ip_to_hash[ip] = sock_hash
self.hash_to_port[sock_hash] = port
if recipient:
self.recipient_to_hash[recipient] = sock_hash
def remove_connection(self, sock_hash):
if sock_hash in self.connections:
del self.connections[sock_hash]
for i in self.recipient_to_hash:
if self.recipient_to_hash[i] == sock_hash:
del self.recipient_to_hash[i]
break
for i in self.ip_to_hash:
if self.ip_to_hash[i] == sock_hash:
del self.ip_to_hash[i]
break
if self.hash_to_port.has_key(sock_hash):
del self.hash_to_port[sock_hash]
def start_listener(self, port):
for p in range(port, port + 5):
self.listener = ZeroconfListener(p, self)
self.listener.bind()
if self.listener.started:
return p
self.listener = None
return False
def getRoster(self):
if self.roster:
return self.roster.getRoster()
return {}
def send(self, stanza, is_message = False):
stanza.setFrom(self.roster.zeroconf.name)
to = stanza.getTo()
try:
item = self.roster[to]
except KeyError:
self.caller.dispatch('MSGERROR', [unicode(to), '-1', _('Contact is offline. Your message could not be sent.'), None, None])
return False
# look for hashed connections
if to in self.recipient_to_hash:
conn = self.connections[self.recipient_to_hash[to]]
if conn.add_stanza(stanza, is_message):
return
if item['address'] in self.ip_to_hash:
hash = self.ip_to_hash[item['address']]
if self.hash_to_port[hash] == item['port']:
conn = self.connections[hash]
if conn.add_stanza(stanza, is_message):
return
# otherwise open new connection
P2PClient(None, item['address'], item['port'], self, [(stanza, is_message)], to)

View file

@ -0,0 +1,906 @@
##
## Copyright (C) 2006 Gajim Team
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <nkour@jabber.org>
## - Dimitur Kirov <dkirov@gmail.com>
## - Travis Shirk <travis@pobox.com>
## - Stefan Bethge <stefan@lanpartei.de>
##
## 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; version 2 only.
##
## 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.
##
import os
import time
import base64
import sha
import socket
import sys
from calendar import timegm
#import socks5
import common.xmpp
from common import GnuPG
from common import helpers
from common import gajim
from common.zeroconf import zeroconf
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible']
# kind of events we can wait for an answer
VCARD_PUBLISHED = 'vcard_published'
VCARD_ARRIVED = 'vcard_arrived'
AGENT_REMOVED = 'agent_removed'
HAS_IDLE = True
try:
import idle
except:
gajim.log.debug(_('Unable to load idle module'))
HAS_IDLE = False
class ConnectionBytestream:
def __init__(self):
self.files_props = {}
def is_transfer_stoped(self, file_props):
if file_props.has_key('error') and file_props['error'] != 0:
return True
if file_props.has_key('completed') and file_props['completed']:
return True
if file_props.has_key('connected') and file_props['connected'] == False:
return True
if not file_props.has_key('stopped') or not file_props['stopped']:
return False
return True
def send_success_connect_reply(self, streamhost):
''' send reply to the initiator of FT that we
made a connection
'''
if streamhost is None:
return None
iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result',
frm = streamhost['target'])
iq.setAttr('id', streamhost['id'])
query = iq.setTag('query')
query.setNamespace(common.xmpp.NS_BYTESTREAM)
stream_tag = query.setTag('streamhost-used')
stream_tag.setAttr('jid', streamhost['jid'])
self.connection.send(iq)
def remove_transfers_for_contact(self, contact):
''' stop all active transfer for contact '''
for file_props in self.files_props.values():
if self.is_transfer_stoped(file_props):
continue
receiver_jid = unicode(file_props['receiver']).split('/')[0]
if contact.jid == receiver_jid:
file_props['error'] = -5
self.remove_transfer(file_props)
self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, ''))
sender_jid = unicode(file_props['sender'])
if contact.jid == sender_jid:
file_props['error'] = -3
self.remove_transfer(file_props)
def remove_all_transfers(self):
''' stops and removes all active connections from the socks5 pool '''
for file_props in self.files_props.values():
self.remove_transfer(file_props, remove_from_list = False)
del(self.files_props)
self.files_props = {}
def remove_transfer(self, file_props, remove_from_list = True):
if file_props is None:
return
self.disconnect_transfer(file_props)
sid = file_props['sid']
gajim.socks5queue.remove_file_props(self.name, sid)
if remove_from_list:
if self.files_props.has_key('sid'):
del(self.files_props['sid'])
def disconnect_transfer(self, file_props):
if file_props is None:
return
if file_props.has_key('hash'):
gajim.socks5queue.remove_sender(file_props['hash'])
if file_props.has_key('streamhosts'):
for host in file_props['streamhosts']:
if host.has_key('idx') and host['idx'] > 0:
gajim.socks5queue.remove_receiver(host['idx'])
gajim.socks5queue.remove_sender(host['idx'])
def send_socks5_info(self, file_props, fast = True, receiver = None,
sender = None):
''' send iq for the present streamhosts and proxies '''
if type(self.peerhost) != tuple:
return
port = gajim.config.get('file_transfers_port')
ft_override_host_to_send = gajim.config.get('ft_override_host_to_send')
if receiver is None:
receiver = file_props['receiver']
if sender is None:
sender = file_props['sender']
proxyhosts = []
sha_str = helpers.get_auth_sha(file_props['sid'], sender,
receiver)
file_props['sha_str'] = sha_str
if not ft_override_host_to_send:
ft_override_host_to_send = self.peerhost[0]
try:
ft_override_host_to_send = socket.gethostbyname(
ft_override_host_to_send)
except socket.gaierror:
self.dispatch('ERROR', (_('Wrong host'), _('The host you configured as the ft_override_host_to_send advanced option is not valid, so ignored.')))
ft_override_host_to_send = self.peerhost[0]
listener = gajim.socks5queue.start_listener(port,
sha_str, self._result_socks5_sid, file_props['sid'])
if listener == None:
file_props['error'] = -5
self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props,
''))
self._connect_error(unicode(receiver), file_props['sid'],
file_props['sid'], code = 406)
return
iq = common.xmpp.Protocol(name = 'iq', to = unicode(receiver),
typ = 'set')
file_props['request-id'] = 'id_' + file_props['sid']
iq.setID(file_props['request-id'])
query = iq.setTag('query')
query.setNamespace(common.xmpp.NS_BYTESTREAM)
query.setAttr('mode', 'tcp')
query.setAttr('sid', file_props['sid'])
streamhost = query.setTag('streamhost')
streamhost.setAttr('port', unicode(port))
streamhost.setAttr('host', ft_override_host_to_send)
streamhost.setAttr('jid', sender)
self.connection.send(iq)
def send_file_rejection(self, file_props):
''' informs sender that we refuse to download the file '''
# user response to ConfirmationDialog may come after we've disconneted
if not self.connection or self.connected < 2:
return
iq = common.xmpp.Protocol(name = 'iq',
to = unicode(file_props['sender']), typ = 'error')
iq.setAttr('id', file_props['request-id'])
err = common.xmpp.ErrorNode(code = '403', typ = 'cancel', name =
'forbidden', text = 'Offer Declined')
iq.addChild(node=err)
self.connection.send(iq)
def send_file_approval(self, file_props):
''' send iq, confirming that we want to download the file '''
# user response to ConfirmationDialog may come after we've disconneted
if not self.connection or self.connected < 2:
return
iq = common.xmpp.Protocol(name = 'iq',
to = unicode(file_props['sender']), typ = 'result')
iq.setAttr('id', file_props['request-id'])
si = iq.setTag('si')
si.setNamespace(common.xmpp.NS_SI)
if file_props.has_key('offset') and file_props['offset']:
file_tag = si.setTag('file')
file_tag.setNamespace(common.xmpp.NS_FILE)
range_tag = file_tag.setTag('range')
range_tag.setAttr('offset', file_props['offset'])
feature = si.setTag('feature')
feature.setNamespace(common.xmpp.NS_FEATURE)
_feature = common.xmpp.DataForm(typ='submit')
feature.addChild(node=_feature)
field = _feature.setField('stream-method')
field.delAttr('type')
field.setValue(common.xmpp.NS_BYTESTREAM)
self.connection.send(iq)
def send_file_request(self, file_props):
''' send iq for new FT request '''
if not self.connection or self.connected < 2:
return
our_jid = gajim.get_jid_from_account(self.name)
frm = our_jid
file_props['sender'] = frm
fjid = file_props['receiver'].jid
iq = common.xmpp.Protocol(name = 'iq', to = fjid,
typ = 'set')
iq.setID(file_props['sid'])
self.files_props[file_props['sid']] = file_props
si = iq.setTag('si')
si.setNamespace(common.xmpp.NS_SI)
si.setAttr('profile', common.xmpp.NS_FILE)
si.setAttr('id', file_props['sid'])
file_tag = si.setTag('file')
file_tag.setNamespace(common.xmpp.NS_FILE)
file_tag.setAttr('name', file_props['name'])
file_tag.setAttr('size', file_props['size'])
desc = file_tag.setTag('desc')
if file_props.has_key('desc'):
desc.setData(file_props['desc'])
file_tag.setTag('range')
feature = si.setTag('feature')
feature.setNamespace(common.xmpp.NS_FEATURE)
_feature = common.xmpp.DataForm(typ='form')
feature.addChild(node=_feature)
field = _feature.setField('stream-method')
field.setAttr('type', 'list-single')
field.addOption(common.xmpp.NS_BYTESTREAM)
self.connection.send(iq)
def _result_socks5_sid(self, sid, hash_id):
''' store the result of sha message from auth. '''
if not self.files_props.has_key(sid):
return
file_props = self.files_props[sid]
file_props['hash'] = hash_id
return
def _connect_error(self, to, _id, sid, code = 404):
''' cb, when there is an error establishing BS connection, or
when connection is rejected'''
msg_dict = {
404: 'Could not connect to given hosts',
405: 'Cancel',
406: 'Not acceptable',
}
msg = msg_dict[code]
iq = None
iq = common.xmpp.Protocol(name = 'iq', to = to,
typ = 'error')
iq.setAttr('id', _id)
err = iq.setTag('error')
err.setAttr('code', unicode(code))
err.setData(msg)
self.connection.send(iq)
if code == 404:
file_props = gajim.socks5queue.get_file_props(self.name, sid)
if file_props is not None:
self.disconnect_transfer(file_props)
file_props['error'] = -3
self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg))
def _proxy_auth_ok(self, proxy):
'''cb, called after authentication to proxy server '''
file_props = self.files_props[proxy['sid']]
iq = common.xmpp.Protocol(name = 'iq', to = proxy['initiator'],
typ = 'set')
auth_id = "au_" + proxy['sid']
iq.setID(auth_id)
query = iq.setTag('query')
query.setNamespace(common.xmpp.NS_BYTESTREAM)
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(file_props['proxy_receiver'])
iq.setID(auth_id)
self.connection.send(iq)
# register xmpppy handlers for bytestream and FT stanzas
def _bytestreamErrorCB(self, con, iq_obj):
gajim.log.debug('_bytestreamErrorCB')
id = unicode(iq_obj.getAttr('id'))
frm = unicode(iq_obj.getFrom())
query = iq_obj.getTag('query')
gajim.proxy65_manager.error_cb(frm, query)
jid = unicode(iq_obj.getFrom())
id = id[3:]
if not self.files_props.has_key(id):
return
file_props = self.files_props[id]
file_props['error'] = -4
self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
raise common.xmpp.NodeProcessed
def _bytestreamSetCB(self, con, iq_obj):
gajim.log.debug('_bytestreamSetCB')
target = unicode(iq_obj.getAttr('to'))
id = unicode(iq_obj.getAttr('id'))
query = iq_obj.getTag('query')
sid = unicode(query.getAttr('sid'))
file_props = gajim.socks5queue.get_file_props(
self.name, sid)
streamhosts=[]
for item in query.getChildren():
if item.getName() == 'streamhost':
host_dict={
'state': 0,
'target': target,
'id': id,
'sid': sid,
'initiator': unicode(iq_obj.getFrom())
}
for attr in item.getAttrs():
host_dict[attr] = item.getAttr(attr)
streamhosts.append(host_dict)
if file_props is None:
if self.files_props.has_key(sid):
file_props = self.files_props[sid]
file_props['fast'] = streamhosts
if file_props['type'] == 's':
if file_props.has_key('streamhosts'):
file_props['streamhosts'].extend(streamhosts)
else:
file_props['streamhosts'] = streamhosts
if not gajim.socks5queue.get_file_props(self.name, sid):
gajim.socks5queue.add_file_props(self.name, file_props)
gajim.socks5queue.connect_to_hosts(self.name, sid,
self.send_success_connect_reply, None)
raise common.xmpp.NodeProcessed
file_props['streamhosts'] = streamhosts
if file_props['type'] == 'r':
gajim.socks5queue.connect_to_hosts(self.name, sid,
self.send_success_connect_reply, self._connect_error)
raise common.xmpp.NodeProcessed
def _ResultCB(self, con, iq_obj):
gajim.log.debug('_ResultCB')
# if we want to respect jep-0065 we have to check for proxy
# activation result in any result iq
real_id = unicode(iq_obj.getAttr('id'))
if real_id[:3] != 'au_':
return
frm = unicode(iq_obj.getFrom())
id = real_id[3:]
if self.files_props.has_key(id):
file_props = self.files_props[id]
if file_props['streamhost-used']:
for host in file_props['proxyhosts']:
if host['initiator'] == frm and host.has_key('idx'):
gajim.socks5queue.activate_proxy(host['idx'])
raise common.xmpp.NodeProcessed
def _bytestreamResultCB(self, con, iq_obj):
gajim.log.debug('_bytestreamResultCB')
frm = unicode(iq_obj.getFrom())
real_id = unicode(iq_obj.getAttr('id'))
query = iq_obj.getTag('query')
gajim.proxy65_manager.resolve_result(frm, query)
try:
streamhost = query.getTag('streamhost-used')
except: # this bytestream result is not what we need
pass
id = real_id[3:]
if self.files_props.has_key(id):
file_props = self.files_props[id]
else:
raise common.xmpp.NodeProcessed
if streamhost is None:
# proxy approves the activate query
if real_id[:3] == 'au_':
id = real_id[3:]
if not file_props.has_key('streamhost-used') or \
file_props['streamhost-used'] is False:
raise common.xmpp.NodeProcessed
if not file_props.has_key('proxyhosts'):
raise common.xmpp.NodeProcessed
for host in file_props['proxyhosts']:
if host['initiator'] == frm and \
unicode(query.getAttr('sid')) == file_props['sid']:
gajim.socks5queue.activate_proxy(host['idx'])
break
raise common.xmpp.NodeProcessed
jid = streamhost.getAttr('jid')
if file_props.has_key('streamhost-used') and \
file_props['streamhost-used'] is True:
raise common.xmpp.NodeProcessed
if real_id[:3] == 'au_':
gajim.socks5queue.send_file(file_props, self.name)
raise common.xmpp.NodeProcessed
proxy = None
if file_props.has_key('proxyhosts'):
for proxyhost in file_props['proxyhosts']:
if proxyhost['jid'] == jid:
proxy = proxyhost
if proxy != None:
file_props['streamhost-used'] = True
if not file_props.has_key('streamhosts'):
file_props['streamhosts'] = []
file_props['streamhosts'].append(proxy)
file_props['is_a_proxy'] = True
receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy, file_props['sid'], file_props)
gajim.socks5queue.add_receiver(self.name, receiver)
proxy['idx'] = receiver.queue_idx
gajim.socks5queue.on_success = self._proxy_auth_ok
raise common.xmpp.NodeProcessed
else:
gajim.socks5queue.send_file(file_props, self.name)
if file_props.has_key('fast'):
fasts = file_props['fast']
if len(fasts) > 0:
self._connect_error(frm, fasts[0]['id'], file_props['sid'],
code = 406)
raise common.xmpp.NodeProcessed
def _siResultCB(self, con, iq_obj):
gajim.log.debug('_siResultCB')
self.peerhost = con._owner.Connection._sock.getsockname()
id = iq_obj.getAttr('id')
if not self.files_props.has_key(id):
# no such jid
return
file_props = self.files_props[id]
if file_props is None:
# file properties for jid is none
return
if file_props.has_key('request-id'):
# we have already sent streamhosts info
return
file_props['receiver'] = unicode(iq_obj.getFrom())
si = iq_obj.getTag('si')
file_tag = si.getTag('file')
range_tag = None
if file_tag:
range_tag = file_tag.getTag('range')
if range_tag:
offset = range_tag.getAttr('offset')
if offset:
file_props['offset'] = int(offset)
length = range_tag.getAttr('length')
if length:
file_props['length'] = int(length)
feature = si.setTag('feature')
if feature.getNamespace() != common.xmpp.NS_FEATURE:
return
form_tag = feature.getTag('x')
form = common.xmpp.DataForm(node=form_tag)
field = form.getField('stream-method')
if field.getValue() != common.xmpp.NS_BYTESTREAM:
return
self.send_socks5_info(file_props, fast = True)
raise common.xmpp.NodeProcessed
def _siSetCB(self, con, iq_obj):
gajim.log.debug('_siSetCB')
jid = unicode(iq_obj.getFrom())
si = iq_obj.getTag('si')
profile = si.getAttr('profile')
mime_type = si.getAttr('mime-type')
if profile != common.xmpp.NS_FILE:
return
file_tag = si.getTag('file')
file_props = {'type': 'r'}
for attribute in file_tag.getAttrs():
if attribute in ('name', 'size', 'hash', 'date'):
val = file_tag.getAttr(attribute)
if val is None:
continue
file_props[attribute] = val
file_desc_tag = file_tag.getTag('desc')
if file_desc_tag is not None:
file_props['desc'] = file_desc_tag.getData()
if mime_type is not None:
file_props['mime-type'] = mime_type
our_jid = gajim.get_jid_from_account(self.name)
file_props['receiver'] = our_jid
file_props['sender'] = unicode(iq_obj.getFrom())
file_props['request-id'] = unicode(iq_obj.getAttr('id'))
file_props['sid'] = unicode(si.getAttr('id'))
gajim.socks5queue.add_file_props(self.name, file_props)
self.dispatch('FILE_REQUEST', (jid, file_props))
raise common.xmpp.NodeProcessed
def _siErrorCB(self, con, iq_obj):
gajim.log.debug('_siErrorCB')
si = iq_obj.getTag('si')
profile = si.getAttr('profile')
if profile != common.xmpp.NS_FILE:
return
id = iq_obj.getAttr('id')
if not self.files_props.has_key(id):
# no such jid
return
file_props = self.files_props[id]
if file_props is None:
# file properties for jid is none
return
jid = unicode(iq_obj.getFrom())
file_props['error'] = -3
self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
raise common.xmpp.NodeProcessed
class ConnectionVcard:
def __init__(self):
self.vcard_sha = None
self.vcard_shas = {} # sha of contacts
self.room_jids = [] # list of gc jids so that vcard are saved in a folder
def add_sha(self, p, send_caps = True):
'''
c = p.setTag('x', namespace = common.xmpp.NS_VCARD_UPDATE)
if self.vcard_sha is not None:
c.setTagData('photo', self.vcard_sha)
if send_caps:
return self.add_caps(p)
return p
'''
pass
def add_caps(self, p):
'''
# advertise our capabilities in presence stanza (jep-0115)
c = p.setTag('c', namespace = common.xmpp.NS_CAPS)
c.setAttr('node', 'http://gajim.org/caps')
c.setAttr('ext', 'ftrans')
c.setAttr('ver', gajim.version)
return p
'''
pass
def node_to_dict(self, node):
dict = {}
for info in node.getChildren():
name = info.getName()
if name in ('ADR', 'TEL', 'EMAIL'): # we can have several
if not dict.has_key(name):
dict[name] = []
entry = {}
for c in info.getChildren():
entry[c.getName()] = c.getData()
dict[name].append(entry)
elif info.getChildren() == []:
dict[name] = info.getData()
else:
dict[name] = {}
for c in info.getChildren():
dict[name][c.getName()] = c.getData()
return dict
def save_vcard_to_hd(self, full_jid, card):
jid, nick = gajim.get_room_and_nick_from_fjid(full_jid)
puny_jid = helpers.sanitize_filename(jid)
path = os.path.join(gajim.VCARD_PATH, puny_jid)
if jid in self.room_jids or os.path.isdir(path):
# remove room_jid file if needed
if os.path.isfile(path):
os.remove(path)
# create folder if needed
if not os.path.isdir(path):
os.mkdir(path, 0700)
puny_nick = helpers.sanitize_filename(nick)
path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick)
else:
path_to_file = path
fil = open(path_to_file, 'w')
fil.write(str(card))
fil.close()
def get_cached_vcard(self, fjid, is_fake_jid = False):
'''return the vcard as a dict
return {} if vcard was too old
return None if we don't have cached vcard'''
jid, nick = gajim.get_room_and_nick_from_fjid(fjid)
puny_jid = helpers.sanitize_filename(jid)
if is_fake_jid:
puny_nick = helpers.sanitize_filename(nick)
path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick)
else:
path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid)
if not os.path.isfile(path_to_file):
return None
# We have the vcard cached
f = open(path_to_file)
c = f.read()
f.close()
card = common.xmpp.Node(node = c)
vcard = self.node_to_dict(card)
if vcard.has_key('PHOTO'):
if not isinstance(vcard['PHOTO'], dict):
del vcard['PHOTO']
elif vcard['PHOTO'].has_key('SHA'):
cached_sha = vcard['PHOTO']['SHA']
if self.vcard_shas.has_key(jid) and self.vcard_shas[jid] != \
cached_sha:
# user change his vcard so don't use the cached one
return {}
vcard['jid'] = jid
vcard['resource'] = gajim.get_resource_from_jid(fjid)
return vcard
def request_vcard(self, jid = None, is_fake_jid = False):
'''request the VCARD. If is_fake_jid is True, it means we request a vcard
to a fake jid, like in private messages in groupchat'''
if not self.connection:
return
'''
iq = common.xmpp.Iq(typ = 'get')
if jid:
iq.setTo(jid)
iq.setTag(common.xmpp.NS_VCARD + ' vCard')
id = self.connection.getAnID()
iq.setID(id)
self.awaiting_answers[id] = (VCARD_ARRIVED, jid)
if is_fake_jid:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
if not room_jid in self.room_jids:
self.room_jids.append(room_jid)
self.connection.send(iq)
#('VCARD', {entry1: data, entry2: {entry21: data, ...}, ...})
'''
pass
def send_vcard(self, vcard):
if not self.connection:
return
'''
iq = common.xmpp.Iq(typ = 'set')
iq2 = iq.setTag(common.xmpp.NS_VCARD + ' vCard')
for i in vcard:
if i == 'jid':
continue
if isinstance(vcard[i], dict):
iq3 = iq2.addChild(i)
for j in vcard[i]:
iq3.addChild(j).setData(vcard[i][j])
elif type(vcard[i]) == type([]):
for j in vcard[i]:
iq3 = iq2.addChild(i)
for k in j:
iq3.addChild(k).setData(j[k])
else:
iq2.addChild(i).setData(vcard[i])
id = self.connection.getAnID()
iq.setID(id)
self.connection.send(iq)
# Add the sha of the avatar
if vcard.has_key('PHOTO') and isinstance(vcard['PHOTO'], dict) and \
vcard['PHOTO'].has_key('BINVAL'):
photo = vcard['PHOTO']['BINVAL']
photo_decoded = base64.decodestring(photo)
our_jid = gajim.get_jid_from_account(self.name)
gajim.interface.save_avatar_files(our_jid, photo_decoded)
avatar_sha = sha.sha(photo_decoded).hexdigest()
iq2.getTag('PHOTO').setTagData('SHA', avatar_sha)
self.awaiting_answers[id] = (VCARD_PUBLISHED, iq2)
'''
pass
class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream):
def __init__(self):
ConnectionVcard.__init__(self)
ConnectionBytestream.__init__(self)
# List of IDs we are waiting answers for {id: (type_of_request, data), }
self.awaiting_answers = {}
# List of IDs that will produce a timeout is answer doesn't arrive
# {time_of_the_timeout: (id, message to send to gui), }
self.awaiting_timeouts = {}
# keep the jids we auto added (transports contacts) to not send the
# SUBSCRIBED event to gui
self.automatically_added = []
try:
idle.init()
except:
HAS_IDLE = False
def _messageCB(self, ip, con, msg):
'''Called when we receive a message'''
msgtxt = msg.getBody()
msghtml = msg.getXHTML()
mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None
tim = msg.getTimestamp()
tim = time.strptime(tim, '%Y%m%dT%H:%M:%S')
tim = time.localtime(timegm(tim))
frm = msg.getFrom()
if frm == None:
for key in self.connection.zeroconf.contacts:
if ip == self.connection.zeroconf.contacts[key][zeroconf.C_ADDRESS]:
frm = key
frm = unicode(frm)
jid = frm
no_log_for = gajim.config.get_per('accounts', self.name,
'no_log_for').split()
encrypted = False
chatstate = None
encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED)
decmsg = ''
# invitations
invite = None
if not encTag:
invite = msg.getTag('x', namespace = common.xmpp.NS_MUC_USER)
if invite and not invite.getTag('invite'):
invite = None
delayed = msg.getTag('x', namespace = common.xmpp.NS_DELAY) != None
msg_id = None
composing_jep = None
xtags = msg.getTags('x')
# chatstates - look for chatstate tags in a message if not delayed
if not delayed:
composing_jep = False
children = msg.getChildren()
for child in children:
if child.getNamespace() == 'http://jabber.org/protocol/chatstates':
chatstate = child.getName()
composing_jep = 'JEP-0085'
break
# No JEP-0085 support, fallback to JEP-0022
if not chatstate:
chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT)
if chatstate_child:
chatstate = 'active'
composing_jep = 'JEP-0022'
if not msgtxt and chatstate_child.getTag('composing'):
chatstate = 'composing'
# JEP-0172 User Nickname
user_nick = msg.getTagData('nick')
if not user_nick:
user_nick = ''
if encTag and GnuPG.USE_GPG:
#decrypt
encmsg = encTag.getData()
keyID = gajim.config.get_per('accounts', self.name, 'keyid')
if keyID:
decmsg = self.gpg.decrypt(encmsg, keyID)
if decmsg:
msgtxt = decmsg
encrypted = True
if mtype == 'error':
error_msg = msg.getError()
if not error_msg:
error_msg = msgtxt
msgtxt = None
if self.name not in no_log_for:
gajim.logger.write('error', frm, error_msg, tim = tim,
subject = subject)
self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt,
tim))
elif mtype == 'chat': # it's type 'chat'
if not msg.getTag('body') and chatstate is None: #no <body>
return
if msg.getTag('body') and self.name not in no_log_for and jid not in\
no_log_for and msgtxt:
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
chatstate, msg_id, composing_jep, user_nick, msghtml))
elif mtype == 'normal': # it's single message
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
if invite:
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
subject, chatstate, msg_id, composing_jep, user_nick))
# END messageCB
'''
def build_http_auth_answer(self, iq_obj, answer):
if answer == 'yes':
iq = iq_obj.buildReply('result')
elif answer == 'no':
iq = iq_obj.buildReply('error')
iq.setError('not-authorized', 401)
self.connection.send(iq)
'''
def parse_data_form(self, node):
dic = {}
tag = node.getTag('title')
if tag:
dic['title'] = tag.getData()
tag = node.getTag('instructions')
if tag:
dic['instructions'] = tag.getData()
i = 0
for child in node.getChildren():
if child.getName() != 'field':
continue
var = child.getAttr('var')
ctype = child.getAttr('type')
label = child.getAttr('label')
if not var and ctype != 'fixed': # We must have var if type != fixed
continue
dic[i] = {}
if var:
dic[i]['var'] = var
if ctype:
dic[i]['type'] = ctype
if label:
dic[i]['label'] = label
tags = child.getTags('value')
if len(tags):
dic[i]['values'] = []
for tag in tags:
data = tag.getData()
if ctype == 'boolean':
if data in ('yes', 'true', 'assent', '1'):
data = True
else:
data = False
dic[i]['values'].append(data)
tag = child.getTag('desc')
if tag:
dic[i]['desc'] = tag.getData()
option_tags = child.getTags('option')
if len(option_tags):
dic[i]['options'] = {}
j = 0
for option_tag in option_tags:
dic[i]['options'][j] = {}
label = option_tag.getAttr('label')
tags = option_tag.getTags('value')
dic[i]['options'][j]['values'] = []
for tag in tags:
dic[i]['options'][j]['values'].append(tag.getData())
if not label:
label = dic[i]['options'][j]['values'][0]
dic[i]['options'][j]['label'] = label
j += 1
if not dic[i].has_key('values'):
dic[i]['values'] = [dic[i]['options'][0]['values'][0]]
i += 1
return dic
def store_metacontacts(self, tags):
''' fake empty method '''
# serverside metacontacts are not supported with zeroconf
# (there is no server)
pass
def remove_transfers_for_contact(self, contact):
''' stop all active transfer for contact '''
'''for file_props in self.files_props.values():
if self.is_transfer_stoped(file_props):
continue
receiver_jid = unicode(file_props['receiver']).split('/')[0]
if contact.jid == receiver_jid:
file_props['error'] = -5
self.remove_transfer(file_props)
self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props))
sender_jid = unicode(file_props['sender']).split('/')[0]
if contact.jid == sender_jid:
file_props['error'] = -3
self.remove_transfer(file_props)
'''
pass
def remove_all_transfers(self):
''' stops and removes all active connections from the socks5 pool '''
'''
for file_props in self.files_props.values():
self.remove_transfer(file_props, remove_from_list = False)
del(self.files_props)
self.files_props = {}
'''
pass
def remove_transfer(self, file_props, remove_from_list = True):
'''
if file_props is None:
return
self.disconnect_transfer(file_props)
sid = file_props['sid']
gajim.socks5queue.remove_file_props(self.name, sid)
if remove_from_list:
if self.files_props.has_key('sid'):
del(self.files_props['sid'])
'''
pass

View file

@ -0,0 +1,500 @@
## common/zeroconf/connection_zeroconf.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <nkour@jabber.org>
## - Dimitur Kirov <dkirov@gmail.com>
## - Travis Shirk <travis@pobox.com>
## - Stefan Bethge <stefan@lanpartei.de>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2006 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Stefan Bethge <stefan@lanpartei.de>
##
## 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; version 2 only.
##
## 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.
##
import os
import random
random.seed()
import signal
if os.name != 'nt':
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
import getpass
import gobject
import notify
from common import helpers
from common import gajim
from common import GnuPG
from common.zeroconf import connection_handlers_zeroconf
from common.zeroconf import client_zeroconf
from connection_handlers_zeroconf import *
USE_GPG = GnuPG.USE_GPG
class ConnectionZeroconf(ConnectionHandlersZeroconf):
'''Connection class'''
def __init__(self, name):
ConnectionHandlersZeroconf.__init__(self)
# system username
self.username = None
self.name = name
self.connected = 0 # offline
self.connection = None
self.gpg = None
self.is_zeroconf = True
self.privacy_rules_supported = False
self.status = ''
self.old_show = ''
self.priority = 0
self.call_resolve_timeout = False
#self.time_to_reconnect = None
#self.new_account_info = None
self.bookmarks = []
#we don't need a password, but must be non-empty
self.password = 'zeroconf'
self.autoconnect = False
self.sync_with_global_status = True
self.no_log_for = False
# Do we continue connection when we get roster (send presence,get vcard...)
self.continue_connect_info = None
if USE_GPG:
self.gpg = GnuPG.GnuPG()
gajim.config.set('usegpg', True)
else:
gajim.config.set('usegpg', False)
self.get_config_values_or_default()
self.muc_jid = {} # jid of muc server for each transport type
self.vcard_supported = False
def get_config_values_or_default(self):
''' get name, host, port from config, or
create zeroconf account with default values'''
if not gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name'):
gajim.log.debug('Creating zeroconf account')
gajim.config.add_per('accounts', gajim.ZEROCONF_ACC_NAME)
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect', True)
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for', '')
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'password', 'zeroconf')
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status', True)
#XXX make sure host is US-ASCII
self.host = unicode(socket.gethostname())
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', self.host)
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port', 5298)
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'is_zeroconf', True)
self.host = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname')
self.port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port')
self.autoconnect = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect')
self.sync_with_global_status = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status')
self.no_log_for = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for')
self.first = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name')
self.last = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name')
self.jabber_id = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id')
self.email = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email')
if not self.username:
self.username = unicode(getpass.getuser())
gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name', self.username)
else:
self.username = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name')
# END __init__
def dispatch(self, event, data):
if gajim.handlers.has_key(event):
gajim.handlers[event](self.name, data)
def _reconnect(self):
gajim.log.debug('reconnect')
signed = self.get_signed_msg(self.status)
self.reconnect()
def quit(self, kill_core):
if kill_core and self.connected > 1:
self.disconnect()
def disable_account(self):
self.disconnect()
def test_gpg_passphrase(self, password):
self.gpg.passphrase = password
keyID = gajim.config.get_per('accounts', self.name, 'keyid')
signed = self.gpg.sign('test', keyID)
self.gpg.password = None
return signed != 'BAD_PASSPHRASE'
def get_signed_msg(self, msg):
signed = ''
keyID = gajim.config.get_per('accounts', self.name, 'keyid')
if keyID and USE_GPG:
use_gpg_agent = gajim.config.get('use_gpg_agent')
if self.connected < 2 and self.gpg.passphrase is None and \
not use_gpg_agent:
# We didn't set a passphrase
self.dispatch('ERROR', (_('OpenPGP passphrase was not given'),
#%s is the account name here
_('You will be connected to %s without OpenPGP.') % self.name))
elif self.gpg.passphrase is not None or use_gpg_agent:
signed = self.gpg.sign(msg, keyID)
if signed == 'BAD_PASSPHRASE':
signed = ''
if self.connected < 2:
self.dispatch('BAD_PASSPHRASE', ())
return signed
def _on_resolve_timeout(self):
if self.connected:
self.connection.resolve_all()
diffs = self.roster.getDiffs()
for key in diffs:
self.roster.setItem(key)
self.dispatch('ROSTER_INFO', (key, self.roster.getName(key),
'both', 'no', self.roster.getGroups(key)))
self.dispatch('NOTIFY', (key, self.roster.getStatus(key),
self.roster.getMessage(key), 'local', 0, None, 0))
#XXX open chat windows don't get refreshed (full name), add that
return self.call_resolve_timeout
# callbacks called from zeroconf
def _on_new_service(self,jid):
self.roster.setItem(jid)
self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid)))
self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0))
def _on_remove_service(self, jid):
self.roster.delItem(jid)
# 'NOTIFY' (account, (jid, status, status message, resource, priority,
# keyID, timestamp))
self.dispatch('NOTIFY', (jid, 'offline', '', 'local', 0, None, 0))
def _on_disconnected(self):
self.disconnect()
self.dispatch('STATUS', 'offline')
self.dispatch('CONNECTION_LOST',
(_('Connection with account "%s" has been lost') % self.name,
_('To continue sending and receiving messages, you will need to reconnect.')))
self.status = 'offline'
self.disconnect()
def _on_name_conflictCB(self, alt_name):
self.disconnect()
self.dispatch('STATUS', 'offline')
self.dispatch('ZC_NAME_CONFLICT', alt_name)
def _on_error(self, message):
self.dispatch('ERROR', (_('Avahi error'), _("%s\nLink-local messaging might not work properly.") % message))
def connect(self, show = 'online', msg = ''):
self.get_config_values_or_default()
if not self.connection:
self.connection = client_zeroconf.ClientZeroconf(self)
if not self.connection.test_avahi():
self.dispatch('STATUS', 'offline')
self.status = 'offline'
self.dispatch('CONNECTION_LOST',
(_('Could not connect to "%s"') % self.name,
_('Please check if Avahi is installed.')))
self.disconnect()
return
result = self.connection.connect(show, msg)
if not result:
self.dispatch('STATUS', 'offline')
self.status = 'offline'
if result is False:
self.dispatch('CONNECTION_LOST',
(_('Could not start local service'),
_('Unable to bind to port %d.' % self.port)))
else: # result is None
self.dispatch('CONNECTION_LOST',
(_('Could not start local service'),
_('Please check if avahi-daemon is running.')))
self.disconnect()
return
else:
self.connection.announce()
self.roster = self.connection.getRoster()
self.dispatch('ROSTER', self.roster)
#display contacts already detected and resolved
for jid in self.roster.keys():
self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid)))
self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0))
self.connected = STATUS_LIST.index(show)
# refresh all contacts data every five seconds
self.call_resolve_timeout = True
gobject.timeout_add(5000, self._on_resolve_timeout)
return True
def disconnect(self, on_purpose = False):
self.connected = 0
self.time_to_reconnect = None
if self.connection:
self.connection.disconnect()
self.connection = None
# stop calling the timeout
self.call_resolve_timeout = False
def reannounce(self):
if self.connected:
txt = {}
txt['1st'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name')
txt['last'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name')
txt['jid'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id')
txt['email'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email')
self.connection.reannounce(txt)
def update_details(self):
if self.connection:
port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port')
if port != self.port:
self.port = port
last_msg = self.connection.last_msg
self.disconnect()
if not self.connect(self.status, last_msg):
return
if self.status != 'invisible':
self.connection.announce()
else:
self.reannounce()
def change_status(self, show, msg, sync = False, auto = False):
if not show in STATUS_LIST:
return -1
self.status = show
check = True #to check for errors from zeroconf
# 'connect'
if show != 'offline' and not self.connected:
if not self.connect(show, msg):
return
if show != 'invisible':
check = self.connection.announce()
else:
self.connected = STATUS_LIST.index(show)
# 'disconnect'
elif show == 'offline' and self.connected:
self.disconnect()
# update status
elif show != 'offline' and self.connected:
was_invisible = self.connected == STATUS_LIST.index('invisible')
self.connected = STATUS_LIST.index(show)
if show == 'invisible':
check = check and self.connection.remove_announce()
elif was_invisible:
if not self.connected:
check = check and self.connect(show, msg)
check = check and self.connection.announce()
if self.connection and not show == 'invisible':
check = check and self.connection.set_show_msg(show, msg)
#stay offline when zeroconf does something wrong
if check:
self.dispatch('STATUS', show)
else:
# show notification that avahi or system bus is down
self.dispatch('STATUS', 'offline')
self.status = 'offline'
self.dispatch('CONNECTION_LOST',
(_('Could not change status of account "%s"') % self.name,
_('Please check if avahi-daemon is running.')))
def get_status(self):
return STATUS_LIST[self.connected]
def send_message(self, jid, msg, keyID, type = 'chat', subject='',
chatstate = None, msg_id = None, composing_jep = None, resource = None,
user_nick = None):
fjid = jid
if not self.connection:
return
if not msg and chatstate is None:
return
if self.status in ('invisible', 'offline'):
self.dispatch('MSGERROR', [unicode(jid), '-1', _('You are not connected or not visible to others. Your message could not be sent.'), None, None])
return
msgtxt = msg
msgenc = ''
if keyID and USE_GPG:
#encrypt
msgenc = self.gpg.encrypt(msg, [keyID])
if msgenc:
msgtxt = '[This message is encrypted]'
lang = os.getenv('LANG')
if lang is not None or lang != 'en': # we're not english
msgtxt = _('[This message is encrypted]') +\
' ([This message is encrypted])' # one in locale and one en
if type == 'chat':
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type)
else:
if subject:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
typ = 'normal', subject = subject)
else:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
typ = 'normal')
if msgenc:
msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc)
# chatstates - if peer supports jep85 or jep22, send chatstates
# please note that the only valid tag inside a message containing a <body>
# tag is the active event
if chatstate is not None:
if composing_jep == 'JEP-0085' or not composing_jep:
# JEP-0085
msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES)
if composing_jep == 'JEP-0022' or not composing_jep:
# JEP-0022
chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT)
if not msgtxt: # when no <body>, add <id>
if not msg_id: # avoid putting 'None' in <id> tag
msg_id = ''
chatstate_node.setTagData('id', msg_id)
# when msgtxt, requests JEP-0022 composing notification
if chatstate is 'composing' or msgtxt:
chatstate_node.addChild(name = 'composing')
if not self.connection.send(msg_iq, msg != None):
return
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
ji = gajim.get_jid_without_resource(jid)
if self.name not in no_log_for and ji not in no_log_for:
log_msg = msg
if subject:
log_msg = _('Subject: %s\n%s') % (subject, msg)
if log_msg:
if type == 'chat':
kind = 'chat_msg_sent'
else:
kind = 'single_msg_sent'
gajim.logger.write(kind, jid, log_msg)
self.dispatch('MSGSENT', (jid, msg, keyID))
def send_stanza(self, stanza):
# send a stanza untouched
if not self.connection:
return
self.connection.send(stanza)
def ack_subscribed(self, jid):
gajim.log.debug('This should not happen (ack_subscribed)')
def ack_unsubscribed(self, jid):
gajim.log.debug('This should not happen (ack_unsubscribed)')
def request_subscription(self, jid, msg = '', name = '', groups = [],
auto_auth = False):
gajim.log.debug('This should not happen (request_subscription)')
def send_authorization(self, jid):
gajim.log.debug('This should not happen (send_authorization)')
def refuse_authorization(self, jid):
gajim.log.debug('This should not happen (refuse_authorization)')
def unsubscribe(self, jid, remove_auth = True):
gajim.log.debug('This should not happen (unsubscribe)')
def unsubscribe_agent(self, agent):
gajim.log.debug('This should not happen (unsubscribe_agent)')
def update_contact(self, jid, name, groups):
if self.connection:
self.connection.getRoster().setItem(jid = jid, name = name,
groups = groups)
def new_account(self, name, config, sync = False):
gajim.log.debug('This should not happen (new_account)')
def _on_new_account(self, con = None, con_type = None):
gajim.log.debug('This should not happen (_on_new_account)')
def account_changed(self, new_name):
self.name = new_name
def request_last_status_time(self, jid, resource):
gajim.log.debug('This should not happen (request_last_status_time)')
def request_os_info(self, jid, resource):
gajim.log.debug('This should not happen (request_os_info)')
def get_settings(self):
gajim.log.debug('This should not happen (get_settings)')
def get_bookmarks(self):
gajim.log.debug('This should not happen (get_bookmarks)')
def store_bookmarks(self):
gajim.log.debug('This should not happen (store_bookmarks)')
def get_metacontacts(self):
gajim.log.debug('This should not happen (get_metacontacts)')
def send_agent_status(self, agent, ptype):
gajim.log.debug('This should not happen (send_agent_status)')
def gpg_passphrase(self, passphrase):
if USE_GPG:
use_gpg_agent = gajim.config.get('use_gpg_agent')
if use_gpg_agent:
self.gpg.passphrase = None
else:
self.gpg.passphrase = passphrase
def ask_gpg_keys(self):
if USE_GPG:
keys = self.gpg.get_keys()
return keys
return None
def ask_gpg_secrete_keys(self):
if USE_GPG:
keys = self.gpg.get_secret_keys()
return keys
return None
def _event_dispatcher(self, realm, event, data):
if realm == '':
if event == common.xmpp.transports.DATA_RECEIVED:
self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore'))
elif event == common.xmpp.transports.DATA_SENT:
self.dispatch('STANZA_SENT', unicode(data))
# END ConnectionZeroconf

View file

@ -0,0 +1,156 @@
## common/zeroconf/roster_zeroconf.py
##
## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
##
## 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; version 2 only.
##
## 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.
##
from common.zeroconf import zeroconf
class Roster:
def __init__(self, zeroconf):
self._data = None
self.zeroconf = zeroconf # our zeroconf instance
def update_roster(self):
for val in self.zeroconf.contacts.values():
self.setItem(val[zeroconf.C_NAME])
def getRoster(self):
#print 'roster_zeroconf.py: getRoster'
if self._data is None:
self._data = {}
self.update_roster()
return self
def getDiffs(self):
''' update the roster with new data and return dict with
jid -> new status pairs to do notifications and stuff '''
diffs = {}
old_data = self._data.copy()
self.update_roster()
for key in old_data.keys():
if self._data.has_key(key):
if old_data[key] != self._data[key]:
diffs[key] = self._data[key]['status']
#print 'roster_zeroconf.py: diffs:' + str(diffs)
return diffs
def setItem(self, jid, name = '', groups = ''):
#print 'roster_zeroconf.py: setItem %s' % jid
contact = self.zeroconf.get_contact(jid)
if not contact:
return
(service_jid, domain, interface, protocol, host, address, port, bare_jid, txt) \
= contact
self._data[jid]={}
self._data[jid]['ask'] = 'no' #?
self._data[jid]['subscription'] = 'both'
self._data[jid]['groups'] = []
self._data[jid]['resources'] = {}
self._data[jid]['address'] = address
self._data[jid]['host'] = host
self._data[jid]['port'] = port
txt_dict = self.zeroconf.txt_array_to_dict(txt)
if txt_dict.has_key('status'):
status = txt_dict['status']
else:
status = ''
nm = ''
if txt_dict.has_key('1st'):
nm = txt_dict['1st']
if txt_dict.has_key('last'):
if nm != '':
nm += ' '
nm += txt_dict['last']
if nm:
self._data[jid]['name'] = nm
else:
self._data[jid]['name'] = jid
if status == 'avail':
status = 'online'
self._data[jid]['txt_dict'] = txt_dict
if not self._data[jid]['txt_dict'].has_key('msg'):
self._data[jid]['txt_dict']['msg'] = ''
self._data[jid]['status'] = status
self._data[jid]['show'] = status
def delItem(self, jid):
#print 'roster_zeroconf.py: delItem %s' % jid
if self._data.has_key(jid):
del self._data[jid]
def getItem(self, jid):
#print 'roster_zeroconf.py: getItem: %s' % jid
if self._data.has_key(jid):
return self._data[jid]
def __getitem__(self,jid):
#print 'roster_zeroconf.py: __getitem__'
return self._data[jid]
def getItems(self):
#print 'roster_zeroconf.py: getItems'
# Return list of all [bare] JIDs that the roster currently tracks.
return self._data.keys()
def keys(self):
#print 'roster_zeroconf.py: keys'
return self._data.keys()
def getRaw(self):
#print 'roster_zeroconf.py: getRaw'
return self._data
def getResources(self, jid):
#print 'roster_zeroconf.py: getResources(%s)' % jid
return {}
def getGroups(self, jid):
return self._data[jid]['groups']
def getName(self, jid):
if self._data.has_key(jid):
return self._data[jid]['name']
def getStatus(self, jid):
if self._data.has_key(jid):
return self._data[jid]['status']
def getMessage(self, jid):
if self._data.has_key(jid):
return self._data[jid]['txt_dict']['msg']
def getShow(self, jid):
#print 'roster_zeroconf.py: getShow'
return getStatus(jid)
def getPriority(jid):
return 5
def getSubscription(self,jid):
#print 'roster_zeroconf.py: getSubscription'
return 'both'
def Subscribe(self,jid):
pass
def Unsubscribe(self,jid):
pass
def Authorize(self,jid):
pass
def Unauthorize(self,jid):
pass

421
src/common/zeroconf/zeroconf.py Executable file
View file

@ -0,0 +1,421 @@
## common/zeroconf/zeroconf.py
##
## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
##
## 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; version 2 only.
##
## 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.
##
import os
import sys
import socket
from common import gajim
from common import xmpp
try:
import dbus.glib
except ImportError, e:
pass
C_NAME, C_DOMAIN, C_INTERFACE, C_PROTOCOL, C_HOST, \
C_ADDRESS, C_PORT, C_BARE_NAME, C_TXT = range(9)
class Zeroconf:
def __init__(self, new_serviceCB, remove_serviceCB, name_conflictCB,
disconnected_CB, error_CB, name, host, port):
self.avahi = None
self.domain = None # specific domain to browse
self.stype = '_presence._tcp'
self.port = port # listening port that gets announced
self.username = name
self.host = host
self.txt = {} # service data
#XXX these CBs should be set to None when we destroy the object
# (go offline), because they create a circular reference
self.new_serviceCB = new_serviceCB
self.remove_serviceCB = remove_serviceCB
self.name_conflictCB = name_conflictCB
self.disconnected_CB = disconnected_CB
self.error_CB = error_CB
self.service_browser = None
self.domain_browser = None
self.bus = None
self.server = None
self.contacts = {} # all current local contacts with data
self.entrygroup = None
self.connected = False
self.announced = False
self.invalid_self_contact = {}
## handlers for dbus callbacks
def entrygroup_commit_error_CB(self, err):
# left blank for possible later usage
pass
def error_callback1(self, err):
gajim.log.debug('Error while resolving: ' + str(err))
def error_callback(self, err):
gajim.log.debug(str(err))
# timeouts are non-critical
if str(err) != 'Timeout reached':
self.disconnect()
self.disconnected_CB()
def new_service_callback(self, interface, protocol, name, stype, domain, flags):
gajim.log.debug('Found service %s in domain %s on %i.%i.' % (name, domain, interface, protocol))
if not self.connected:
return
# synchronous resolving
self.server.ResolveService( int(interface), int(protocol), name, stype, \
domain, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), \
reply_handler=self.service_resolved_callback, error_handler=self.error_callback1)
def remove_service_callback(self, interface, protocol, name, stype, domain, flags):
gajim.log.debug('Service %s in domain %s on %i.%i disappeared.' % (name, domain, interface, protocol))
if not self.connected:
return
if name != self.name:
for key in self.contacts.keys():
if self.contacts[key][C_BARE_NAME] == name:
del self.contacts[key]
self.remove_serviceCB(key)
return
def new_service_type(self, interface, protocol, stype, domain, flags):
# Are we already browsing this domain for this type?
if self.service_browser:
return
object_path = self.server.ServiceBrowserNew(interface, protocol, \
stype, domain, dbus.UInt32(0))
self.service_browser = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, \
object_path) , self.avahi.DBUS_INTERFACE_SERVICE_BROWSER)
self.service_browser.connect_to_signal('ItemNew', self.new_service_callback)
self.service_browser.connect_to_signal('ItemRemove', self.remove_service_callback)
self.service_browser.connect_to_signal('Failure', self.error_callback)
def new_domain_callback(self,interface, protocol, domain, flags):
if domain != "local":
self.browse_domain(interface, protocol, domain)
def txt_array_to_dict(self, txt_array):
txt_dict = {}
for els in txt_array:
key, val = '', None
for c in els:
#FIXME: remove when outdated, this is for avahi < 0.6.14
if c < 0 or c > 255:
c = '.'
else:
c = chr(c)
if val is None:
if c == '=':
val = ''
else:
key += c
else:
val += c
if val is None: # missing '='
val = ''
txt_dict[key] = val.decode('utf-8')
return txt_dict
def service_resolved_callback(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags):
gajim.log.debug('Service data for service %s in domain %s on %i.%i:'
% (name, domain, interface, protocol))
gajim.log.debug('Host %s (%s), port %i, TXT data: %s' % (host, address, port,
self.txt_array_to_dict(txt)))
if not self.connected:
return
bare_name = name
if name.find('@') == -1:
name = name + '@' + name
# we don't want to see ourselves in the list
if name != self.name:
self.contacts[name] = (name, domain, interface, protocol, host, address, port,
bare_name, txt)
self.new_serviceCB(name)
else:
# remember data
# In case this is not our own record but of another
# gajim instance on the same machine,
# it will be used when we get a new name.
self.invalid_self_contact[name] = (name, domain, interface, protocol, host, address, port, bare_name, txt)
# different handler when resolving all contacts
def service_resolved_all_callback(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags):
if not self.connected:
return
bare_name = name
if name.find('@') == -1:
name = name + '@' + name
self.contacts[name] = (name, domain, interface, protocol, host, address, port, bare_name, txt)
def service_added_callback(self):
gajim.log.debug('Service successfully added')
def service_committed_callback(self):
gajim.log.debug('Service successfully committed')
def service_updated_callback(self):
gajim.log.debug('Service successfully updated')
def service_add_fail_callback(self, err):
gajim.log.debug('Error while adding service. %s' % str(err))
if str(err) == 'Local name collision':
alternative_name = self.server.GetAlternativeServiceName(self.username)
self.name_conflictCB(alternative_name)
return
self.error_CB(_('Error while adding service. %s') % str(err))
self.disconnect()
def server_state_changed_callback(self, state, error):
if state == self.avahi.SERVER_RUNNING:
self.create_service()
elif state == self.avahi.SERVER_COLLISION:
self.entrygroup.Reset()
elif state == self.avahi.CLIENT_FAILURE:
# does it ever go here?
gajim.log.debug('CLIENT FAILURE')
def entrygroup_state_changed_callback(self, state, error):
# the name is already present, so recreate
if state == self.avahi.ENTRY_GROUP_COLLISION:
self.service_add_fail_callback('Local name collision')
elif state == self.avahi.ENTRY_GROUP_FAILURE:
gajim.log.debug('zeroconf.py: ENTRY_GROUP_FAILURE reached(that'
' should not happen)')
# make zeroconf-valid names
def replace_show(self, show):
if show in ['chat', 'online', '']:
return 'avail'
elif show == 'xa':
return 'away'
return show
def avahi_txt(self):
utf8_dict = {}
for key in self.txt:
val = self.txt[key]
if isinstance(val, unicode):
utf8_dict[key] = val.encode('utf-8')
else:
utf8_dict[key] = val
return self.avahi.dict_to_txt_array(utf8_dict)
def create_service(self):
try:
if not self.entrygroup:
# create an EntryGroup for publishing
self.entrygroup = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, self.server.EntryGroupNew()), self.avahi.DBUS_INTERFACE_ENTRY_GROUP)
self.entrygroup.connect_to_signal('StateChanged', self.entrygroup_state_changed_callback)
txt = {}
#remove empty keys
for key,val in self.txt.iteritems():
if val:
txt[key] = val
txt['port.p2pj'] = self.port
txt['version'] = 1
txt['txtvers'] = 1
# replace gajim's show messages with compatible ones
if self.txt.has_key('status'):
txt['status'] = self.replace_show(self.txt['status'])
else:
txt['status'] = 'avail'
self.txt = txt
gajim.log.debug('Publishing service %s of type %s' % (self.name, self.stype))
self.entrygroup.AddService(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype, '', '', self.port, self.avahi_txt(), reply_handler=self.service_added_callback, error_handler=self.service_add_fail_callback)
self.entrygroup.Commit(reply_handler=self.service_committed_callback,
error_handler=self.entrygroup_commit_error_CB)
return True
except dbus.dbus_bindings.DBusException, e:
gajim.log.debug(str(e))
return False
def announce(self):
if not self.connected:
return False
state = self.server.GetState()
if state == self.avahi.SERVER_RUNNING:
self.create_service()
self.announced = True
return True
def remove_announce(self):
if self.announced == False:
return False
try:
if self.entrygroup.GetState() != self.avahi.ENTRY_GROUP_FAILURE:
self.entrygroup.Reset()
self.entrygroup.Free()
# .Free() has mem leaks
obj = self.entrygroup._obj
obj._bus = None
self.entrygroup._obj = None
self.entrygroup = None
self.announced = False
return True
else:
return False
except dbus.dbus_bindings.DBusException, e:
gajim.log.debug("Can't remove service. That should not happen")
def browse_domain(self, interface, protocol, domain):
self.new_service_type(interface, protocol, self.stype, domain, '')
def avahi_dbus_connect_cb(self, a, connect, disconnect):
if connect != "":
gajim.log.debug('Lost connection to avahi-daemon')
self.disconnect()
if self.disconnected_CB:
self.disconnected_CB()
else:
gajim.log.debug('We are connected to avahi-daemon')
# connect to dbus
def connect_dbus(self):
try:
import dbus
except ImportError:
gajim.log.debug('Error: python-dbus needs to be installed. No zeroconf support.')
return False
if self.bus:
return True
try:
self.bus = dbus.SystemBus()
self.bus.add_signal_receiver(self.avahi_dbus_connect_cb,
"NameOwnerChanged", "org.freedesktop.DBus",
arg0="org.freedesktop.Avahi")
except Exception, e:
# System bus is not present
self.bus = None
gajim.log.debug(str(e))
return False
else:
return True
# connect to avahi
def connect_avahi(self):
if not self.connect_dbus():
return False
try:
import avahi
self.avahi = avahi
except ImportError:
gajim.log.debug('Error: python-avahi needs to be installed. No zeroconf support.')
return False
if self.server:
return True
try:
self.server = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, \
self.avahi.DBUS_PATH_SERVER), self.avahi.DBUS_INTERFACE_SERVER)
self.server.connect_to_signal('StateChanged',
self.server_state_changed_callback)
except Exception, e:
# Avahi service is not present
self.server = None
gajim.log.debug(str(e))
return False
else:
return True
def connect(self):
self.name = self.username + '@' + self.host # service name
if not self.connect_avahi():
return False
self.connected = True
# start browsing
if self.domain is None:
# Explicitly browse .local
self.browse_domain(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, "local")
# Browse for other browsable domains
self.domain_browser = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, \
self.server.DomainBrowserNew(self.avahi.IF_UNSPEC, \
self.avahi.PROTO_UNSPEC, '', self.avahi.DOMAIN_BROWSER_BROWSE,\
dbus.UInt32(0))), self.avahi.DBUS_INTERFACE_DOMAIN_BROWSER)
self.domain_browser.connect_to_signal('ItemNew', self.new_domain_callback)
self.domain_browser.connect_to_signal('Failure', self.error_callback)
else:
self.browse_domain(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, domain)
return True
def disconnect(self):
if self.connected:
self.connected = False
if self.service_browser:
self.service_browser.Free()
self.service_browser._obj._bus = None
self.service_browser._obj = None
if self.domain_browser:
self.domain_browser.Free()
self.domain_browser._obj._bus = None
self.domain_browser._obj = None
self.remove_announce()
self.server._obj._bus = None
self.server._obj = None
self.server = None
self.service_browser = None
self.domain_browser = None
# refresh txt data of all contacts manually (no callback available)
def resolve_all(self):
if not self.connected:
return
for val in self.contacts.values():
self.server.ResolveService(int(val[C_INTERFACE]), int(val[C_PROTOCOL]), val[C_BARE_NAME], \
self.stype, val[C_DOMAIN], self.avahi.PROTO_UNSPEC, dbus.UInt32(0),\
reply_handler=self.service_resolved_all_callback, error_handler=self.error_callback)
def get_contacts(self):
if not jid in self.contacts:
return None
return self.contacts
def get_contact(self, jid):
if not jid in self.contacts:
return None
return self.contacts[jid]
def update_txt(self, show = None):
if show:
self.txt['status'] = self.replace_show(show)
txt = self.avahi_txt()
if self.connected and self.entrygroup:
self.entrygroup.UpdateServiceTxt(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype,'', txt, reply_handler=self.service_updated_callback, error_handler=self.error_callback)
return True
else:
return False
# END Zeroconf

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,8 @@
## conversation_textview.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -27,7 +18,6 @@ import gtk
import pango
import gobject
import time
import sys
import os
import tooltips
import dialogs
@ -39,12 +29,20 @@ from common import helpers
from calendar import timegm
from common.fuzzyclock import FuzzyClock
from htmltextview import HtmlTextView
from common.exceptions import GajimGeneralException
class ConversationTextview:
'''Class for the conversation textview (where user reads already said messages)
for chat/groupchat windows'''
def __init__(self, account):
# no need to inherit TextView, use it as property is safer
self.tv = gtk.TextView()
def __init__(self, account, used_in_history_window = False):
'''if used_in_history_window is True, then we do not show
Clear menuitem in context menu'''
self.used_in_history_window = used_in_history_window
# no need to inherit TextView, use it as atrribute is safer
self.tv = HtmlTextView()
self.tv.html_hyperlink_handler = self.html_hyperlink_handler
# set properties
self.tv.set_border_width(1)
@ -57,11 +55,13 @@ class ConversationTextview:
self.handlers = {}
# connect signals
id = self.tv.connect('motion_notify_event', self.on_textview_motion_notify_event)
id = self.tv.connect('motion_notify_event',
self.on_textview_motion_notify_event)
self.handlers[id] = self.tv
id = self.tv.connect('populate_popup', self.on_textview_populate_popup)
self.handlers[id] = self.tv
id = self.tv.connect('button_press_event', self.on_textview_button_press_event)
id = self.tv.connect('button_press_event',
self.on_textview_button_press_event)
self.handlers[id] = self.tv
self.account = account
@ -98,7 +98,7 @@ class ConversationTextview:
tag.set_property('weight', pango.WEIGHT_BOLD)
tag = buffer.create_tag('time_sometimes')
tag.set_property('foreground', 'grey')
tag.set_property('foreground', 'darkgrey')
tag.set_property('scale', pango.SCALE_SMALL)
tag.set_property('justification', gtk.JUSTIFY_CENTER)
@ -138,6 +138,11 @@ class ConversationTextview:
self.focus_out_end_iter_offset = None
self.line_tooltip = tooltips.BaseTooltip()
path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')
self.focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file)
# use it for hr too
self.tv.focus_out_line_pixbuf = self.focus_out_line_pixbuf
def del_handlers(self):
for i in self.handlers.keys():
@ -145,7 +150,7 @@ class ConversationTextview:
self.handlers[i].disconnect(i)
del self.handlers
self.tv.destroy()
#TODO
#FIXME:
# self.line_tooltip.destroy()
def update_tags(self):
@ -230,19 +235,14 @@ class ConversationTextview:
end_iter_for_previous_line)
# add the new focus out line
# FIXME: Why is this loaded from disk everytime
path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')
focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file)
end_iter = buffer.get_end_iter()
buffer.insert(end_iter, '\n')
buffer.insert_pixbuf(end_iter, focus_out_line_pixbuf)
buffer.insert_pixbuf(end_iter, self.focus_out_line_pixbuf)
end_iter = buffer.get_end_iter()
before_img_iter = end_iter.copy()
before_img_iter.backward_char() # one char back (an image also takes one char)
buffer.apply_tag_by_name('focus-out-line', before_img_iter, end_iter)
#FIXME: remove this workaround when bug is fixed
# c http://bugzilla.gnome.org/show_bug.cgi?id=318569
self.allow_focus_out_line = False
@ -316,19 +316,29 @@ class ConversationTextview:
def on_textview_populate_popup(self, textview, menu):
'''we override the default context menu and we prepend Clear
(only if used_in_history_window is False)
and if we have sth selected we show a submenu with actions on the phrase
(see on_conversation_textview_button_press_event)'''
item = gtk.SeparatorMenuItem()
menu.prepend(item)
item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
menu.prepend(item)
id = item.connect('activate', self.clear)
self.handlers[id] = item
separator_menuitem_was_added = False
if not self.used_in_history_window:
item = gtk.SeparatorMenuItem()
menu.prepend(item)
separator_menuitem_was_added = True
item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
menu.prepend(item)
id = item.connect('activate', self.clear)
self.handlers[id] = item
if self.selected_phrase:
s = self.selected_phrase
if len(s) > 25:
s = s[:21] + '...'
item = gtk.MenuItem(_('Actions for "%s"') % s)
if not separator_menuitem_was_added:
item = gtk.SeparatorMenuItem()
menu.prepend(item)
self.selected_phrase = helpers.reduce_chars_newlines(
self.selected_phrase, 25, 2)
item = gtk.MenuItem(_('_Actions for "%s"') % self.selected_phrase)
menu.prepend(item)
submenu = gtk.Menu()
item.set_submenu(submenu)
@ -360,19 +370,20 @@ class ConversationTextview:
self.handlers[id] = item
else:
if dict_link.find('%s') == -1:
#we must have %s in the url if not WIKTIONARY
# we must have %s in the url if not WIKTIONARY
item = gtk.MenuItem(_('Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
item.set_property('sensitive', False)
else:
link = dict_link % self.selected_phrase
id = item.connect('activate', self.visit_url_from_menuitem, link)
id = item.connect('activate', self.visit_url_from_menuitem,
link)
self.handlers[id] = item
submenu.append(item)
search_link = gajim.config.get('search_engine')
if search_link.find('%s') == -1:
#we must have %s in the url
# we must have %s in the url
item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
item.set_property('sensitive', False)
else:
@ -381,14 +392,18 @@ class ConversationTextview:
id = item.connect('activate', self.visit_url_from_menuitem, link)
self.handlers[id] = item
submenu.append(item)
item = gtk.MenuItem(_('Open as _Link'))
id = item.connect('activate', self.visit_url_from_menuitem, link)
self.handlers[id] = item
submenu.append(item)
menu.show_all()
def on_textview_button_press_event(self, widget, event):
# If we clicked on a taged text do NOT open the standard popup menu
# if normal text check if we have sth selected
self.selected_phrase = ''
self.selected_phrase = '' # do not move belove event button check!
if event.button != 3: # if not right click
return False
@ -426,18 +441,16 @@ class ConversationTextview:
def on_start_chat_activate(self, widget, jid):
gajim.interface.roster.new_chat_from_jid(self.account, jid)
def on_join_group_chat_menuitem_activate(self, widget, jid):
room, server = jid.split('@')
if gajim.interface.instances[self.account].has_key('join_gc'):
def on_join_group_chat_menuitem_activate(self, widget, room_jid):
if 'join_gc' in gajim.interface.instances[self.account]:
instance = gajim.interface.instances[self.account]['join_gc']
instance.xml.get_widget('server_entry').set_text(server)
instance.xml.get_widget('room_entry').set_text(room)
instance.xml.get_widget('room_jid_entry').set_text(room_jid)
gajim.interface.instances[self.account]['join_gc'].window.present()
else:
try:
gajim.interface.instances[self.account]['join_gc'] = \
dialogs.JoinGroupchatWindow(self.account, server, room)
except RuntimeError:
dialogs.JoinGroupchatWindow(self.account, room_jid)
except GajimGeneralException:
pass
def on_add_to_roster_activate(self, widget, jid):
@ -459,6 +472,7 @@ class ConversationTextview:
childs[6].hide() # join group chat
childs[7].hide() # add to roster
else: # It's a mail or a JID
text = text.lower()
id = childs[2].connect('activate', self.on_copy_link_activate, text)
self.handlers[id] = childs[2]
id = childs[3].connect('activate', self.on_open_link_activate, kind, text)
@ -506,6 +520,15 @@ class ConversationTextview:
# we launch the correct application
helpers.launch_browser_mailer(kind, word)
def html_hyperlink_handler(self, texttag, widget, event, iter, kind, href):
if event.type == gtk.gdk.BUTTON_PRESS:
if event.button == 3: # right click
self.make_link_menu(event, kind, href)
else:
# we launch the correct application
helpers.launch_browser_mailer(kind, href)
def detect_and_print_special_text(self, otext, other_tags):
'''detects special text (emots & links & formatting)
prints normal text before any special text it founts,
@ -562,6 +585,7 @@ class ConversationTextview:
img.show()
#add with possible animation
self.tv.add_child_at_anchor(img, anchor)
#FIXME: one day, somehow sync with regexp in gajim.py
elif special_text.startswith('http://') or \
special_text.startswith('www.') or \
special_text.startswith('ftp://') or \
@ -638,11 +662,11 @@ class ConversationTextview:
def print_empty_line(self):
buffer = self.tv.get_buffer()
end_iter = buffer.get_end_iter()
buffer.insert(end_iter, '\n')
buffer.insert_with_tags_by_name(end_iter, '\n', 'eol')
def print_conversation_line(self, text, jid, kind, name, tim,
other_tags_for_name = [], other_tags_for_time = [],
other_tags_for_text = [], subject = None, old_kind = None):
other_tags_for_name = [], other_tags_for_time = [], other_tags_for_text = [],
subject = None, old_kind = None, xhtml = None):
'''prints 'chat' type messages'''
buffer = self.tv.get_buffer()
buffer.begin_user_action()
@ -652,7 +676,7 @@ class ConversationTextview:
at_the_end = True
if buffer.get_char_count() > 0:
buffer.insert(end_iter, '\n')
buffer.insert_with_tags_by_name(end_iter, '\n', 'eol')
if kind == 'incoming_queue':
kind = 'incoming'
if old_kind == 'incoming_queue':
@ -664,7 +688,9 @@ class ConversationTextview:
current_print_time = gajim.config.get('print_time')
if current_print_time == 'always' and kind != 'info':
before_str = gajim.config.get('before_time')
before_str = helpers.from_one_line(before_str)
after_str = gajim.config.get('after_time')
after_str = helpers.from_one_line(after_str)
# get difference in days since epoch (86400 = 24*3600)
# number of days since epoch for current time (in GMT) -
# number of days since epoch for message (in GMT)
@ -682,10 +708,10 @@ class ConversationTextview:
format += day_str + ' '
format += '%X' + after_str
tim_format = time.strftime(format, tim)
# if tim_format comes as unicode because of day_str.
# we convert it to the encoding that we want (and that is utf-8)
tim_format = helpers.ensure_utf8_string(tim_format)
tim_format = tim_format.encode('utf-8')
if locale.getpreferredencoding() == 'UTF-8':
# if tim_format comes as unicode because of day_str.
# we convert it to the encoding that we want (and that is utf-8)
tim_format = helpers.ensure_utf8_string(tim_format)
buffer.insert_with_tags_by_name(end_iter, tim_format + ' ',
*other_tags_for_time)
elif current_print_time == 'sometimes' and kind != 'info':
@ -725,7 +751,7 @@ class ConversationTextview:
else:
self.print_name(name, kind, other_tags_for_name)
self.print_subject(subject)
self.print_real_text(text, text_tags, name)
self.print_real_text(text, text_tags, name, xhtml)
# scroll to the end of the textview
if at_the_end or kind == 'outgoing':
@ -748,7 +774,9 @@ class ConversationTextview:
name_tags = other_tags_for_name[:] # create a new list
name_tags.append(kind)
before_str = gajim.config.get('before_nickname')
before_str = helpers.from_one_line(before_str)
after_str = gajim.config.get('after_nickname')
after_str = helpers.from_one_line(after_str)
format = before_str + name + after_str + ' '
buffer.insert_with_tags_by_name(end_iter, format, *name_tags)
@ -760,8 +788,18 @@ class ConversationTextview:
buffer.insert(end_iter, subject)
self.print_empty_line()
def print_real_text(self, text, text_tags = [], name = None):
def print_real_text(self, text, text_tags = [], name = None, xhtml = None):
'''this adds normal and special text. call this to add text'''
if xhtml:
try:
if name and (text.startswith('/me ') or text.startswith('/me\n')):
xhtml = xhtml.replace('/me', '<dfn>%s</dfn>'% (name,), 1)
self.tv.display_html(xhtml.encode('utf-8'))
return
except Exception, e:
gajim.log.debug(str("Error processing xhtml")+str(e))
gajim.log.debug(str("with |"+xhtml+"|"))
buffer = self.tv.get_buffer()
# /me is replaced by name if name is given
if name and (text.startswith('/me ') or text.startswith('/me\n')):

View file

@ -3,7 +3,7 @@
##
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <nkour@jabber.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za>
@ -25,6 +25,7 @@ import os
import gtkgui_helpers
import vcard
import conversation_textview
import message_control
try:
import gtkspell
@ -40,6 +41,7 @@ from advanced import AdvancedConfigurationWindow
from common import gajim
from common import helpers
from common.exceptions import GajimGeneralException
class EditGroupsDialog:
'''Class for the edit group dialog window'''
@ -139,6 +141,9 @@ class EditGroupsDialog:
group = self.xml.get_widget('group_entry').get_text().decode('utf-8')
if not group:
return
# Do not allow special groups
if group in helpers.special_groups:
return
# check if it already exists
model = self.list.get_model()
iter = model.get_iter_root()
@ -180,14 +185,16 @@ class EditGroupsDialog:
if account not in accounts:
accounts.append(account)
for g in gajim.groups[account].keys():
if g in helpers.special_groups:
continue
if g in groups:
continue
groups[g] = 0
for g in contact.groups:
groups[g] += 1
group_list = groups.keys()
group_list = []
# Remove special groups if they are empty
for group in groups:
if group not in helpers.special_groups or groups[group] > 0:
group_list.append(group)
group_list.sort()
for group in group_list:
iter = store.append()
@ -264,6 +271,7 @@ class ChooseGPGKeyDialog:
renderer = gtk.CellRendererText()
self.keys_treeview.insert_column_with_attributes(-1, _('Contact name'),
renderer, text = 1)
self.keys_treeview.set_search_column(1)
self.fill_tree(secret_keys, selected)
self.window.show_all()
@ -405,12 +413,12 @@ class ChangeStatusMessageDialog:
class AddNewContactWindow:
'''Class for AddNewContactWindow'''
uid_labels = {'jabber': _('Jabber ID'),
'aim': _('AIM Address'),
'gadu-gadu': _('GG Number'),
'icq': _('ICQ Number'),
'msn': _('MSN Address'),
'yahoo': _('Yahoo! Address')}
uid_labels = {'jabber': _('Jabber ID:'),
'aim': _('AIM Address:'),
'gadu-gadu': _('GG Number:'),
'icq': _('ICQ Number:'),
'msn': _('MSN Address:'),
'yahoo': _('Yahoo! Address:')}
def __init__(self, account = None, jid = None, user_nick = None,
group = None):
self.account = account
@ -441,7 +449,8 @@ class AddNewContactWindow:
'uid_label', 'uid_entry', 'protocol_combobox', 'protocol_jid_combobox',
'protocol_hbox', 'nickname_entry', 'message_scrolledwindow',
'register_hbox', 'subscription_table', 'add_button',
'message_textview', 'connected_label', 'group_comboboxentry'):
'message_textview', 'connected_label', 'group_comboboxentry',
'auto_authorize_checkbutton'):
self.__dict__[w] = self.xml.get_widget(w)
if account and len(gajim.connections) >= 2:
prompt_text =\
@ -486,8 +495,10 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
liststore.append([type_, type_])
self.protocol_combobox.set_model(liststore)
self.protocol_combobox.set_active(0)
self.protocol_jid_combobox.set_sensitive(False)
self.protocol_jid_combobox.set_no_show_all(True)
self.protocol_jid_combobox.hide()
self.subscription_table.set_no_show_all(True)
self.auto_authorize_checkbutton.show()
self.message_scrolledwindow.set_no_show_all(True)
self.register_hbox.set_no_show_all(True)
self.register_hbox.hide()
@ -497,11 +508,13 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
self.protocol_jid_combobox.set_model(liststore)
self.xml.signal_autoconnect(self)
if jid:
type_ = gajim.get_transport_name_from_jid(jid) or 'jabber'
type_ = gajim.get_transport_name_from_jid(jid)
if not type_:
type_ = 'jabber'
if type_ == 'jabber':
self.uid_entry.set_text(jid)
else:
uid, transport = gajim.get_room_name_and_server_from_room_jid(jid)
uid, transport = gajim.get_name_and_server_from_jid(jid)
self.uid_entry.set_text(uid.replace('%', '@', 1))
#set protocol_combobox
model = self.protocol_combobox.get_model()
@ -515,13 +528,13 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
i += 1
# set protocol_jid_combobox
self.protocol_combobox.set_active(0)
self.protocol_jid_combobox.set_active(0)
model = self.protocol_jid_combobox.get_model()
iter = model.get_iter_first()
i = 0
while iter:
if model[iter][0] == transport:
self.protocol_combobox.set_active(i)
self.protocol_jid_combobox.set_active(i)
break
iter = model.iter_next(iter)
i += 1
@ -626,7 +639,7 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
else:
message= ''
group = self.group_comboboxentry.child.get_text().decode('utf-8')
auto_auth = self.xml.get_widget('auto_authorize_checkbutton').get_active()
auto_auth = self.auto_authorize_checkbutton.get_active()
gajim.interface.roster.req_sub(self, jid, message, self.account,
group = group, pseudo = nickname, auto_auth = auto_auth)
self.window.destroy()
@ -641,13 +654,15 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
for jid_ in self.agents[type_]:
model.append([jid_])
self.protocol_jid_combobox.set_active(0)
self.protocol_jid_combobox.set_sensitive(True)
if len(self.agents[type_]) > 1:
self.protocol_jid_combobox.set_no_show_all(False)
self.protocol_jid_combobox.show_all()
else:
self.protocol_jid_combobox.set_sensitive(False)
self.protocol_jid_combobox.hide()
if type_ in self.uid_labels:
self.uid_label.set_text(self.uid_labels[type_])
else:
self.uid_label.set_text(_('User ID'))
self.uid_label.set_text(_('User ID:'))
if type_ == 'jabber':
self.message_scrolledwindow.show()
else:
@ -655,6 +670,7 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
if type_ in self.available_types:
self.register_hbox.set_no_show_all(False)
self.register_hbox.show_all()
self.auto_authorize_checkbutton.hide()
self.connected_label.hide()
self.subscription_table.hide()
self.add_button.set_sensitive(False)
@ -668,9 +684,11 @@ _('Please fill in the data of the contact you want to add in account %s') %accou
self.subscription_table.hide()
self.connected_label.show()
self.add_button.set_sensitive(False)
self.auto_authorize_checkbutton.hide()
return
self.subscription_table.set_no_show_all(False)
self.subscription_table.show_all()
self.auto_authorize_checkbutton.show()
self.connected_label.hide()
self.add_button.set_sensitive(True)
@ -697,8 +715,14 @@ class AboutDialog:
dlg.set_version(gajim.version)
s = u'Copyright © 2003-2006 Gajim Team'
dlg.set_copyright(s)
text = open('../COPYING').read()
dlg.set_license(text)
copying_file_path = None
if os.path.isfile(os.path.join(gajim.defs.docdir, 'COPYING')):
copying_file_path = os.path.join(gajim.defs.docdir, 'COPYING')
elif os.path.isfile('../COPYING'):
copying_file_path = '../COPYING'
if copying_file_path:
text = open(copying_file_path).read()
dlg.set_license(text)
dlg.set_comments('%s\n%s %s\n%s %s'
% (_('A GTK+ jabber client'), \
@ -706,28 +730,40 @@ class AboutDialog:
_('PyGTK Version:'), self.tuple2str(gtk.pygtk_version)))
dlg.set_website('http://www.gajim.org/')
authors = []
authors_file = open('../AUTHORS').read()
authors_file = authors_file.split('\n')
for author in authors_file:
if author == 'CURRENT DEVELOPERS:':
authors.append(_('Current Developers:'))
elif author == 'PAST DEVELOPERS:':
authors.append('\n' + _('Past Developers:'))
elif author != '': # Real author line
authors.append(author)
authors_file_path = None
if os.path.isfile(os.path.join(gajim.defs.docdir, 'AUTHORS')):
authors_file_path = os.path.join(gajim.defs.docdir, 'AUTHORS')
elif os.path.isfile('../AUTHORS'):
authors_file_path = '../AUTHORS'
if authors_file_path:
authors = []
authors_file = open(authors_file_path).read()
authors_file = authors_file.split('\n')
for author in authors_file:
if author == 'CURRENT DEVELOPERS:':
authors.append(_('Current Developers:'))
elif author == 'PAST DEVELOPERS:':
authors.append('\n' + _('Past Developers:'))
elif author != '': # Real author line
authors.append(author)
thanks_file_path = None
if os.path.isfile(os.path.join(gajim.defs.docdir, 'THANKS')):
thanks_file_path = os.path.join(gajim.defs.docdir, 'THANKS')
elif os.path.isfile('../THANKS'):
thanks_file_path = '../THANKS'
if thanks_file_path:
authors.append('\n' + _('THANKS:'))
authors.append('\n' + _('THANKS:'))
text = open('../THANKS').read()
text_splitted = text.split('\n')
text = '\n'.join(text_splitted[:-2]) # remove one english sentence
# and add it manually as translatable
text += '\n%s\n' % _('Last but not least, we would like to thank all '
'the package maintainers.')
authors.append(text)
text = open(thanks_file_path).read()
text_splitted = text.split('\n')
text = '\n'.join(text_splitted[:-2]) # remove one english sentence
# and add it manually as translatable
text += '\n%s\n' % _('Last but not least, we would like to thank all '
'the package maintainers.')
authors.append(text)
dlg.set_authors(authors)
dlg.set_authors(authors)
if gtk.pygtk_version >= (2, 8, 0) and gtk.gtk_version >= (2, 8, 0):
dlg.props.wrap_license = True
@ -837,27 +873,31 @@ class FileChooserDialog(gtk.FileChooserDialog):
self.set_current_folder(current_folder)
else:
self.set_current_folder(helpers.get_documents_path())
buttons = self.action_area.get_children()
possible_responses = {gtk.STOCK_OPEN: on_response_ok,
gtk.STOCK_SAVE: on_response_ok,
gtk.STOCK_CANCEL: on_response_cancel}
for b in buttons:
for response in possible_responses:
if b.get_label() == response:
if not possible_responses[response]:
b.connect('clicked', self.just_destroy)
elif isinstance(possible_responses[response], tuple):
if len(possible_responses[response]) == 1:
b.connect('clicked', possible_responses[response][0])
else:
b.connect('clicked', *possible_responses[response])
else:
b.connect('clicked', possible_responses[response])
break
self.response_ok, self.response_cancel = \
on_response_ok, on_response_cancel
# in gtk+-2.10 clicked signal on some of the buttons in a dialog
# is emitted twice, so we cannot rely on 'clicked' signal
self.connect('response', self.on_dialog_response)
self.show_all()
def on_dialog_response(self, dialog, response):
if response in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_CLOSE):
if self.response_cancel:
if isinstance(self.response_cancel, tuple):
self.response_cancel[0](dialog, *self.response_cancel[1:])
else:
self.response_cancel(dialog)
else:
self.just_destroy(dialog)
elif response == gtk.RESPONSE_OK:
if self.response_ok:
if isinstance(self.response_ok, tuple):
self.response_ok[0](dialog, *self.response_ok[1:])
else:
self.response_ok(dialog)
else:
self.just_destroy(dialog)
def just_destroy(self, widget):
self.destroy()
@ -1015,6 +1055,12 @@ class SubscriptionRequestWindow:
xml.signal_autoconnect(self)
self.window.show_all()
def prepare_popup_menu(self):
xml = gtkgui_helpers.get_glade('subscription_request_popup_menu.glade')
menu = xml.get_widget('subscription_request_popup_menu')
xml.signal_autoconnect(self)
return menu
def on_close_button_clicked(self, widget):
self.window.destroy()
@ -1025,7 +1071,7 @@ class SubscriptionRequestWindow:
if self.jid not in gajim.contacts.get_jid_list(self.account):
AddNewContactWindow(self.account, self.jid, self.user_nick)
def on_contact_info_button_clicked(self, widget):
def on_contact_info_activate(self, widget):
'''ask vcard'''
if gajim.interface.instances[self.account]['infos'].has_key(self.jid):
gajim.interface.instances[self.account]['infos'][self.jid].window.present()
@ -1039,38 +1085,50 @@ class SubscriptionRequestWindow:
gajim.interface.instances[self.account]['infos'][self.jid].xml.\
get_widget('information_notebook').remove_page(0)
def on_start_chat_activate(self, widget):
'''open chat'''
gajim.interface.roster.new_chat_from_jid(self.account, self.jid)
def on_deny_button_clicked(self, widget):
'''refuse the request'''
gajim.connections[self.account].refuse_authorization(self.jid)
self.window.destroy()
def on_actions_button_clicked(self, widget):
'''popup action menu'''
menu = self.prepare_popup_menu()
menu.show_all()
gtkgui_helpers.popup_emoticons_under_button(menu, widget, self.window.window)
class JoinGroupchatWindow:
def __init__(self, account, server = '', room = '', nick = '',
automatic = False):
def __init__(self, account, room_jid = '', nick = '', automatic = False):
'''automatic is a dict like {'invities': []}
If automatic is not empty, this means room must be automaticaly configured
and when done, invities must be automatically invited'''
if server and room:
jid = room + '@' + server
if jid in gajim.gc_connected[account] and gajim.gc_connected[account][jid]:
ErrorDialog(_('You are already in room %s') % jid)
raise RuntimeError, 'You are already in this room'
if room_jid != '':
if room_jid in gajim.gc_connected[account] and\
gajim.gc_connected[account][room_jid]:
ErrorDialog(_('You are already in group chat %s') % room_jid)
raise GajimGeneralException, 'You are already in this group chat'
self.account = account
self.automatic = automatic
if nick == '':
nick = gajim.nicks[self.account]
if gajim.connections[account].connected < 2:
ErrorDialog(_('You are not connected to the server'),
_('You can not join a group chat unless you are connected.'))
raise RuntimeError, 'You must be connected to join a groupchat'
_('You can not join a group chat unless you are connected.'))
raise GajimGeneralException, 'You must be connected to join a groupchat'
self._empty_required_widgets = []
self.xml = gtkgui_helpers.get_glade('join_groupchat_window.glade')
self.window = self.xml.get_widget('join_groupchat_window')
self.xml.get_widget('server_entry').set_text(server)
self.xml.get_widget('room_entry').set_text(room)
self.xml.get_widget('nickname_entry').set_text(nick)
self._room_jid_entry = self.xml.get_widget('room_jid_entry')
self._nickname_entry = self.xml.get_widget('nickname_entry')
self._room_jid_entry.set_text(room_jid)
self._nickname_entry.set_text(nick)
self.xml.signal_autoconnect(self)
gajim.interface.instances[account]['join_gc'] = self #now add us to open windows
if len(gajim.connections) > 1:
@ -1090,19 +1148,14 @@ _('You can not join a group chat unless you are connected.'))
self.recently_combobox.append_text(g)
if len(self.recently_groupchat) == 0:
self.recently_combobox.set_sensitive(False)
elif server == '' and room == '':
elif room_jid == '':
self.recently_combobox.set_active(0)
self.xml.get_widget('room_entry').select_region(0, -1)
elif room and server:
self._room_jid_entry.select_region(0, -1)
elif room_jid != '':
self.xml.get_widget('join_button').grab_focus()
self._server_entry = self.xml.get_widget('server_entry')
self._room_entry = self.xml.get_widget('room_entry')
self._nickname_entry = self.xml.get_widget('nickname_entry')
if not self._server_entry.get_text():
self._empty_required_widgets.append(self._server_entry)
if not self._room_entry.get_text():
self._empty_required_widgets.append(self._room_entry)
if not self._room_jid_entry.get_text():
self._empty_required_widgets.append(self._room_jid_entry)
if not self._nickname_entry.get_text():
self._empty_required_widgets.append(self._nickname_entry)
if len(self._empty_required_widgets):
@ -1129,27 +1182,11 @@ _('You can not join a group chat unless you are connected.'))
if len(self._empty_required_widgets) == 0:
self.xml.get_widget('join_button').set_sensitive(True)
def on_room_entry_key_press_event(self, widget, event):
# Check for pressed @ and jump to server_entry if found
if event.keyval == gtk.keysyms.at:
self.xml.get_widget('server_entry').grab_focus()
return True
def on_server_entry_key_press_event(self, widget, event):
# If backspace is pressed in empty server_entry, return to the room entry
backspace = event.keyval == gtk.keysyms.BackSpace
server_entry = self.xml.get_widget('server_entry')
empty = len(server_entry.get_text()) == 0
if backspace and empty:
self.xml.get_widget('room_entry').grab_focus()
return True
def on_recently_combobox_changed(self, widget):
model = widget.get_model()
iter = widget.get_active_iter()
gid = model[iter][0].decode('utf-8')
self.xml.get_widget('room_entry').set_text(gid.split('@')[0])
self.xml.get_widget('server_entry').set_text(gid.split('@')[1])
iter_ = widget.get_active_iter()
room_jid = model[iter_][0].decode('utf-8')
self._room_jid_entry.set_text(room_jid)
def on_cancel_button_clicked(self, widget):
'''When Cancel button is clicked'''
@ -1157,30 +1194,35 @@ _('You can not join a group chat unless you are connected.'))
def on_join_button_clicked(self, widget):
'''When Join button is clicked'''
nickname = self.xml.get_widget('nickname_entry').get_text().decode(
'utf-8')
room = self.xml.get_widget('room_entry').get_text().decode('utf-8')
server = self.xml.get_widget('server_entry').get_text().decode('utf-8')
nickname = self._nickname_entry.get_text().decode('utf-8')
room_jid = self._room_jid_entry.get_text().decode('utf-8')
password = self.xml.get_widget('password_entry').get_text().decode(
'utf-8')
jid = '%s@%s' % (room, server)
try:
jid = helpers.parse_jid(jid)
room_jid = helpers.parse_jid(room_jid)
except:
ErrorDialog(_('Invalid room or server name'),
_('The room name or server name has not allowed characters.'))
ErrorDialog(_('Invalid group chat Jabber ID'),
_('The group chat Jabber ID has not allowed characters.'))
return
if jid in self.recently_groupchat:
self.recently_groupchat.remove(jid)
self.recently_groupchat.insert(0, jid)
if gajim.interface.msg_win_mgr.has_window(room_jid, self.account):
ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, self.account)
if ctrl.type_id != message_control.TYPE_GC:
ErrorDialog(_('This is not a group chat'),
_('%s is not the name of a group chat.') % room_jid)
return
if room_jid in self.recently_groupchat:
self.recently_groupchat.remove(room_jid)
self.recently_groupchat.insert(0, room_jid)
if len(self.recently_groupchat) > 10:
self.recently_groupchat = self.recently_groupchat[0:10]
gajim.config.set('recently_groupchat', ' '.join(self.recently_groupchat))
gajim.config.set('recently_groupchat',
' '.join(self.recently_groupchat))
if self.automatic:
gajim.automatic_rooms[self.account][jid] = self.automatic
gajim.interface.roster.join_gc_room(self.account, jid, nickname, password)
gajim.automatic_rooms[self.account][room_jid] = self.automatic
gajim.interface.roster.join_gc_room(self.account, room_jid, nickname,
password)
self.window.destroy()
@ -1192,7 +1234,7 @@ class NewChatDialog(InputDialog):
title = _('Start Chat with account %s') % account
else:
title = _('Start Chat')
prompt_text = _('Fill in the jid, or nick of the contact you would like\nto send a chat message to:')
prompt_text = _('Fill in the nickname or the Jabber ID of the contact you would like\nto send a chat message to:')
InputDialog.__init__(self, title, prompt_text, is_modal = False)
self.completion_dict = {}
@ -1240,7 +1282,7 @@ class ChangePasswordDialog:
if not account or gajim.connections[account].connected < 2:
ErrorDialog(_('You are not connected to the server'),
_('Without a connection, you can not change your password.'))
raise RuntimeError, 'You are not connected to the server'
raise GajimGeneralException, 'You are not connected to the server'
self.account = account
self.xml = gtkgui_helpers.get_glade('change_password_dialog.glade')
self.dialog = self.xml.get_widget('change_password_dialog')
@ -1302,7 +1344,8 @@ class PopupNotificationWindow:
# default image
if not path_to_image:
path_to_image = os.path.abspath(
os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'chat_msg_recv.png')) # img to display
os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
'chat_msg_recv.png')) # img to display
if event_type == _('Contact Signed In'):
bg_color = 'limegreen'
@ -1322,7 +1365,7 @@ class PopupNotificationWindow:
bg_color = 'tan1'
elif event_type == _('Contact Changed Status'):
bg_color = 'thistle2'
else: # Unknown event ! Shouldn't happen but deal with it
else: # Unknown event! Shouldn't happen but deal with it
bg_color = 'white'
popup_bg_color = gtk.gdk.color_parse(bg_color)
close_button.modify_bg(gtk.STATE_NORMAL, popup_bg_color)
@ -1421,8 +1464,12 @@ class SingleMessageWindow:
self.cancel_button = self.xml.get_widget('cancel_button')
self.close_button = self.xml.get_widget('close_button')
self.message_tv_buffer.connect('changed', self.update_char_counter)
self.to_entry.set_text(to)
if type(to) == type([]):
jid = ', '.join( [i[0].jid + '/' + i[0].resource for i in to])
self.to_entry.set_text(jid)
self.to_entry.set_sensitive(False)
else:
self.to_entry.set_text(to)
if gajim.config.get('use_speller') and HAS_GTK_SPELL and action == 'send':
try:
@ -1574,22 +1621,27 @@ class SingleMessageWindow:
ErrorDialog(_('Connection not available'),
_('Please make sure you are connected with "%s".') % self.account)
return
to_whom_jid = self.to_entry.get_text().decode('utf-8')
if self.completion_dict.has_key(to_whom_jid):
to_whom_jid = self.completion_dict[to_whom_jid].jid
subject = self.subject_entry.get_text().decode('utf-8')
begin, end = self.message_tv_buffer.get_bounds()
message = self.message_tv_buffer.get_text(begin, end).decode('utf-8')
if type(self.to) == type([]):
sender_list = [i[0].jid + '/' + i[0].resource for i in self.to]
else:
sender_list = [self.to_entry.get_text().decode('utf-8')]
for to_whom_jid in sender_list:
if self.completion_dict.has_key(to_whom_jid):
to_whom_jid = self.completion_dict[to_whom_jid].jid
subject = self.subject_entry.get_text().decode('utf-8')
begin, end = self.message_tv_buffer.get_bounds()
message = self.message_tv_buffer.get_text(begin, end).decode('utf-8')
if to_whom_jid.find('/announce/') != -1:
gajim.connections[self.account].send_motd(to_whom_jid, subject,
message)
return
if to_whom_jid.find('/announce/') != -1:
gajim.connections[self.account].send_motd(to_whom_jid, subject,
message)
return
# FIXME: allow GPG message some day
gajim.connections[self.account].send_message(to_whom_jid, message,
keyID = None, type = 'normal', subject=subject)
# FIXME: allow GPG message some day
gajim.connections[self.account].send_message(to_whom_jid, message,
keyID = None, type = 'normal', subject=subject)
self.subject_entry.set_text('') # we sent ok, clear the subject
self.message_tv_buffer.set_text('') # we sent ok, clear the textview
@ -1725,10 +1777,13 @@ class XMLConsoleWindow:
self.input_textview.grab_focus()
class PrivacyListWindow:
def __init__(self, account, privacy_list, list_type):
'''list_type can be 0 if list is created or 1 if it id edited'''
'''Window that is used for creating NEW or EDITING already there privacy
lists'''
def __init__(self, account, privacy_list_name, action):
'''action is 'EDIT' or 'NEW' depending on if we create a new priv list
or edit an already existing one'''
self.account = account
self.privacy_list = privacy_list
self.privacy_list_name = privacy_list_name
# Dicts and Default Values
self.active_rule = ''
@ -1740,7 +1795,7 @@ class PrivacyListWindow:
self.allow_deny = 'allow'
# Connect to glade
self.xml = gtkgui_helpers.get_glade('privacy_list_edit_window.glade')
self.xml = gtkgui_helpers.get_glade('privacy_list_window.glade')
self.window = self.xml.get_widget('privacy_list_edit_window')
# Add Widgets
@ -1762,10 +1817,9 @@ class PrivacyListWindow:
'privacy_list_default_checkbutton']:
self.__dict__[widget_to_add] = self.xml.get_widget(widget_to_add)
# Send translations
self.privacy_lists_title_label.set_label(
_('Privacy List <b><i>%s</i></b>') % \
gtkgui_helpers.escape_for_pango_markup(self.privacy_list))
gtkgui_helpers.escape_for_pango_markup(self.privacy_list_name))
if len(gajim.connections) > 1:
title = _('Privacy List for %s') % self.account
@ -1776,9 +1830,9 @@ class PrivacyListWindow:
self.open_rule_button.set_sensitive(False)
self.privacy_list_active_checkbutton.set_sensitive(False)
self.privacy_list_default_checkbutton.set_sensitive(False)
self.list_of_rules_combobox.set_sensitive(False)
# Check if list is created (0) or edited (1)
if list_type == 1:
if action == 'EDIT':
self.refresh_rules()
count = 0
@ -1793,22 +1847,20 @@ class PrivacyListWindow:
self.add_edit_vbox.set_no_show_all(True)
self.window.show_all()
self.add_edit_vbox.hide()
self.xml.signal_autoconnect(self)
def on_privacy_list_edit_window_destroy(self, widget):
'''close window'''
if gajim.interface.instances[self.account].has_key('privacy_list_%s' % \
self.privacy_list):
del gajim.interface.instances[self.account]['privacy_list_%s' % \
self.privacy_list]
key_name = 'privacy_list_%s' % self.privacy_list_name
if key_name in gajim.interface.instances[self.account]:
del gajim.interface.instances[self.account][key_name]
def check_active_default(self, a_d_dict):
if a_d_dict['active'] == self.privacy_list:
if a_d_dict['active'] == self.privacy_list_name:
self.privacy_list_active_checkbutton.set_active(True)
else:
self.privacy_list_active_checkbutton.set_active(False)
if a_d_dict['default'] == self.privacy_list:
if a_d_dict['default'] == self.privacy_list_name:
self.privacy_list_default_checkbutton.set_active(True)
else:
self.privacy_list_default_checkbutton.set_active(False)
@ -1818,11 +1870,10 @@ class PrivacyListWindow:
self.global_rules = {}
for rule in rules:
if rule.has_key('type'):
text_item = 'Order: %s, action: %s, type: %s, value: %s' % \
(rule['order'], rule['action'], rule['type'],
rule['value'])
text_item = _('Order: %s, action: %s, type: %s, value: %s') % \
(rule['order'], rule['action'], rule['type'], rule['value'])
else:
text_item = 'Order: %s, action: %s' % (rule['order'],
text_item = _('Order: %s, action: %s') % (rule['order'],
rule['action'])
self.global_rules[text_item] = rule
self.list_of_rules_combobox.append_text(text_item)
@ -1845,22 +1896,26 @@ class PrivacyListWindow:
gajim.connections[self.account].get_active_default_lists()
def refresh_rules(self):
gajim.connections[self.account].get_privacy_list(self.privacy_list)
gajim.connections[self.account].get_privacy_list(self.privacy_list_name)
def on_delete_rule_button_clicked(self, widget):
tags = []
for rule in self.global_rules:
if rule != \
self.list_of_rules_combobox.get_active_text().decode('utf-8'):
if rule != self.list_of_rules_combobox.get_active_text():
tags.append(self.global_rules[rule])
gajim.connections[self.account].set_privacy_list(
self.privacy_list, tags)
self.privacy_list_name, tags)
self.privacy_list_received(tags)
self.add_edit_vbox.hide()
if not tags: # we removed latest rule
if 'privacy_lists' in gajim.interface.instances[self.account]:
win = gajim.interface.instances[self.account]['privacy_lists']
win.remove_privacy_list_from_combobox(self.privacy_list_name)
win.draw_widgets()
def on_open_rule_button_clicked(self, widget):
self.add_edit_rule_label.set_label(
_('<b>Edit a rule</b>'))
_('<b>Edit a rule</b>'))
active_num = self.list_of_rules_combobox.get_active()
if active_num == -1:
self.active_rule = ''
@ -1909,29 +1964,31 @@ class PrivacyListWindow:
self.edit_queries_send_checkbutton.set_active(True)
elif child == 'message':
self.edit_send_messages_checkbutton.set_active(True)
if rule_info['action'] == 'allow':
self.edit_allow_radiobutton.set_active(True)
self.edit_allow_radiobutton.set_active(True)
else:
self.edit_deny_radiobutton.set_active(True)
self.edit_deny_radiobutton.set_active(True)
self.add_edit_vbox.show()
def on_privacy_list_active_checkbutton_toggled(self, widget):
if widget.get_active():
gajim.connections[self.account].set_active_list(self.privacy_list)
gajim.connections[self.account].set_active_list(
self.privacy_list_name)
else:
gajim.connections[self.account].set_active_list(None)
def on_privacy_list_default_checkbutton_toggled(self, widget):
if widget.get_active():
gajim.connections[self.account].set_default_list(self.privacy_list)
gajim.connections[self.account].set_default_list(
self.privacy_list_name)
else:
gajim.connections[self.account].set_default_list(None)
def on_new_rule_button_clicked(self, widget):
self.reset_fields()
self.add_edit_vbox.show()
def reset_fields(self):
self.edit_type_jabberid_entry.set_text('')
self.edit_allow_radiobutton.set_active(True)
@ -1950,12 +2007,10 @@ class PrivacyListWindow:
def get_current_tags(self):
if self.edit_type_jabberid_radiobutton.get_active():
edit_type = 'jid'
edit_value = \
self.edit_type_jabberid_entry.get_text().decode('utf-8')
edit_value = self.edit_type_jabberid_entry.get_text()
elif self.edit_type_group_radiobutton.get_active():
edit_type = 'group'
edit_value = \
self.edit_type_group_combobox.get_active_text().decode('utf-8')
edit_value = self.edit_type_group_combobox.get_active_text()
elif self.edit_type_subscription_radiobutton.get_active():
edit_type = 'subscription'
subs = ['none', 'both', 'from', 'to']
@ -1994,9 +2049,14 @@ class PrivacyListWindow:
else:
tags.append(current_tags)
gajim.connections[self.account].set_privacy_list(self.privacy_list, tags)
gajim.connections[self.account].set_privacy_list(
self.privacy_list_name, tags)
self.privacy_list_received(tags)
self.add_edit_vbox.hide()
if 'privacy_lists' in gajim.interface.instances[self.account]:
win = gajim.interface.instances[self.account]['privacy_lists']
win.add_privacy_list_to_combobox(self.privacy_list_name)
win.draw_widgets()
def on_list_of_rules_combobox_changed(self, widget):
self.add_edit_vbox.hide()
@ -2011,32 +2071,28 @@ class PrivacyListWindow:
if active_bool:
self.allow_deny = radiobutton
def on_privacy_list_close_button_clicked(self, widget):
def on_close_button_clicked(self, widget):
self.window.destroy()
def on_privacy_list_refresh_button_clicked(self, widget):
self.refresh_rules()
self.add_edit_vbox.hide()
class PrivacyListsWindow:
# To do: UTF-8 ???????
'''Window that is the main window for Privacy Lists;
we can list there the privacy lists and ask to create a new one
or edit an already there one'''
def __init__(self, account):
self.account = account
self.privacy_lists = []
self.privacy_lists_save = []
self.xml = gtkgui_helpers.get_glade('privacy_lists_first_window.glade')
self.xml = gtkgui_helpers.get_glade('privacy_lists_window.glade')
self.window = self.xml.get_widget('privacy_lists_first_window')
for widget_to_add in ['list_of_privacy_lists_combobox',
'delete_privacy_list_button', 'open_privacy_list_button',
'new_privacy_list_button', 'new_privacy_list_entry', 'buttons_hbox',
'privacy_lists_refresh_button', 'close_privacy_lists_window_button']:
self.__dict__[widget_to_add] = self.xml.get_widget(widget_to_add)
'delete_privacy_list_button', 'open_privacy_list_button',
'new_privacy_list_button', 'new_privacy_list_entry',
'privacy_lists_refresh_button', 'close_privacy_lists_window_button']:
self.__dict__[widget_to_add] = self.xml.get_widget(
widget_to_add)
self.draw_privacy_lists_in_combobox()
self.draw_privacy_lists_in_combobox([])
self.privacy_lists_refresh()
self.enabled = True
@ -2053,31 +2109,40 @@ class PrivacyListsWindow:
self.xml.signal_autoconnect(self)
def on_privacy_lists_first_window_destroy(self, widget):
'''close window'''
if gajim.interface.instances[self.account].has_key('privacy_lists'):
if 'privacy_lists' in gajim.interface.instances[self.account]:
del gajim.interface.instances[self.account]['privacy_lists']
def draw_privacy_lists_in_combobox(self):
def remove_privacy_list_from_combobox(self, privacy_list):
if privacy_list not in self.privacy_lists_save:
return
privacy_list_index = self.privacy_lists_save.index(privacy_list)
self.list_of_privacy_lists_combobox.remove_text(privacy_list_index)
self.privacy_lists_save.remove(privacy_list)
def add_privacy_list_to_combobox(self, privacy_list):
if privacy_list in self.privacy_lists_save:
return
self.list_of_privacy_lists_combobox.append_text(privacy_list)
self.privacy_lists_save.append(privacy_list)
def draw_privacy_lists_in_combobox(self, privacy_lists):
self.list_of_privacy_lists_combobox.set_active(-1)
self.list_of_privacy_lists_combobox.get_model().clear()
self.privacy_lists_save = self.privacy_lists
for add_item in self.privacy_lists:
self.list_of_privacy_lists_combobox.append_text(add_item)
if len(self.privacy_lists) == 0:
self.privacy_lists_save = []
for add_item in privacy_lists:
self.add_privacy_list_to_combobox(add_item)
self.draw_widgets()
def draw_widgets(self):
if len(self.privacy_lists_save) == 0:
self.list_of_privacy_lists_combobox.set_sensitive(False)
self.buttons_hbox.set_sensitive(False)
elif len(self.privacy_lists) == 1:
self.list_of_privacy_lists_combobox.set_active(0)
self.list_of_privacy_lists_combobox.set_sensitive(False)
self.buttons_hbox.set_sensitive(True)
self.open_privacy_list_button.set_sensitive(False)
self.delete_privacy_list_button.set_sensitive(False)
else:
self.list_of_privacy_lists_combobox.set_sensitive(True)
self.buttons_hbox.set_sensitive(True)
self.list_of_privacy_lists_combobox.set_active(0)
self.privacy_lists = []
def on_privacy_lists_refresh_button_clicked(self, widget):
self.privacy_lists_refresh()
self.open_privacy_list_button.set_sensitive(True)
self.delete_privacy_list_button.set_sensitive(True)
def on_close_button_clicked(self, widget):
self.window.destroy()
@ -2087,27 +2152,31 @@ class PrivacyListsWindow:
self.list_of_privacy_lists_combobox.get_active()]
gajim.connections[self.account].del_privacy_list(active_list)
self.privacy_lists_save.remove(active_list)
self.privacy_lists_received({'lists':self.privacy_lists_save})
self.privacy_lists_received({'lists': self.privacy_lists_save})
def privacy_lists_received(self, lists):
if not lists:
return
privacy_lists = []
for privacy_list in lists['lists']:
self.privacy_lists += [privacy_list]
self.draw_privacy_lists_in_combobox()
privacy_lists.append(privacy_list)
self.draw_privacy_lists_in_combobox(privacy_lists)
def privacy_lists_refresh(self):
gajim.connections[self.account].get_privacy_lists()
def on_new_privacy_list_button_clicked(self, widget):
name = self.new_privacy_list_entry.get_text().decode('utf-8')
if gajim.interface.instances[self.account].has_key(
'privacy_list_%s' % name):
gajim.interface.instances[self.account]['privacy_list_%s' % name].\
window.present()
name = self.new_privacy_list_entry.get_text()
if not name:
ErrorDialog(_('Invalid List Name'),
_('You must enter a name to create a privacy list.'))
return
key_name = 'privacy_list_%s' % name
if gajim.interface.instances[self.account].has_key(key_name):
gajim.interface.instances[self.account][key_name].window.present()
else:
gajim.interface.instances[self.account]['privacy_list_%s' % name] = \
PrivacyListWindow(self.account, name, 0)
gajim.interface.instances[self.account][key_name] = \
PrivacyListWindow(self.account, name, 'NEW')
self.new_privacy_list_entry.set_text('')
def on_privacy_lists_refresh_button_clicked(self, widget):
@ -2116,16 +2185,17 @@ class PrivacyListsWindow:
def on_open_privacy_list_button_clicked(self, widget):
name = self.privacy_lists_save[
self.list_of_privacy_lists_combobox.get_active()]
key_name = 'privacy_list_%s' % name
if gajim.interface.instances[self.account].has_key(
'privacy_list_%s' % name):
gajim.interface.instances[self.account]['privacy_list_%s' % name].\
window.present()
key_name):
gajim.interface.instances[self.account][key_name].window.present()
else:
gajim.interface.instances[self.account]['privacy_list_%s' % name] = \
PrivacyListWindow(self.account, name, 1)
gajim.interface.instances[self.account][key_name] = \
PrivacyListWindow(self.account, name, 'EDIT')
class InvitationReceivedDialog:
def __init__(self, account, room_jid, contact_jid, password = None, comment = None):
def __init__(self, account, room_jid, contact_jid, password = None,
comment = None):
self.room_jid = room_jid
self.account = account
@ -2133,8 +2203,8 @@ class InvitationReceivedDialog:
self.dialog = xml.get_widget('invitation_received_dialog')
#FIXME: use nickname instead of contact_jid
pritext = _('%(contact_jid)s has invited you to %(room_jid)s room') % {
'room_jid': room_jid, 'contact_jid': contact_jid }
pritext = _('%(contact_jid)s has invited you to group chat %(room_jid)s')\
% {'room_jid': room_jid, 'contact_jid': contact_jid }
label_text = '<big><b>%s</b></big>' % pritext
@ -2155,10 +2225,9 @@ class InvitationReceivedDialog:
def on_accept_button_clicked(self, widget):
self.dialog.destroy()
room, server = gajim.get_room_name_and_server_from_room_jid(self.room_jid)
try:
JoinGroupchatWindow(self.account, server = server, room = room)
except RuntimeError:
JoinGroupchatWindow(self.account, self.room_jid)
except GajimGeneralException:
pass
class ProgressDialog:
@ -2291,6 +2360,18 @@ class ImageChooserDialog(FileChooserDialog):
return
widget.get_preview_widget().set_from_pixbuf(pixbuf)
class AvatarChooserDialog(ImageChooserDialog):
def __init__(self, path_to_file = '', on_response_ok = None,
on_response_cancel = None, on_response_clear = None):
ImageChooserDialog.__init__(self, path_to_file, on_response_ok,
on_response_cancel)
button = gtk.Button(None, gtk.STOCK_CLEAR)
if on_response_clear:
button.connect('clicked', on_response_clear)
button.show_all()
self.action_area.pack_start(button)
self.action_area.reorder_child(button, 0)
class AddSpecialNotificationDialog:
def __init__(self, jid):
'''jid is the jid for which we want to add special notification

View file

@ -1,19 +1,9 @@
# -*- coding: utf-8 -*-
## config.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Stéphan Kochen <stephan@kochen.nl>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Stéphan Kochen <stephan@kochen.nl>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -36,10 +26,10 @@
# - def update_actions(self)
# - def default_action(self)
# - def _find_item(self, jid, node)
# - def _add_item(self, model, jid, node, item, force)
# - def _update_item(self, model, iter, jid, node, item)
# - def _update_info(self, model, iter, jid, node, identities, features, data)
# - def _update_error(self, model, iter, jid, node)
# - def _add_item(self, jid, node, item, force)
# - def _update_item(self, iter, jid, node, item)
# - def _update_info(self, iter, jid, node, identities, features, data)
# - def _update_error(self, iter, jid, node)
#
# * Should call the super class for this method.
# All others do not have to call back to the super class. (but can if they want
@ -60,6 +50,7 @@ import groups
from common import gajim
from common import xmpp
from common.exceptions import GajimGeneralException
# Dictionary mapping category, type pairs to browser class, image pairs.
# This is a function, so we can call it after the classes are declared.
@ -133,6 +124,13 @@ class CacheDictionary:
def __call__(self):
return self.value
def cleanup(self):
for key in self.cache.keys():
item = self.cache[key]
if item.source:
gobject.source_remove(item.source)
del self.cache[key]
def _expire_timeout(self, key):
'''The timeout has expired, remove the object.'''
if key in self.cache:
@ -144,8 +142,9 @@ class CacheDictionary:
item = self.cache[key]
if item.source:
gobject.source_remove(item.source)
source = gobject.timeout_add(self.lifetime, self._expire_timeout, key)
item.source = source
if self.lifetime:
source = gobject.timeout_add(self.lifetime, self._expire_timeout, key)
item.source = source
def __getitem__(self, key):
item = self.cache[key]
@ -217,11 +216,15 @@ class ServicesCache:
ServiceCache instance.'''
def __init__(self, account):
self.account = account
self._items = CacheDictionary(15, getrefresh = False)
self._info = CacheDictionary(15, getrefresh = False)
self._items = CacheDictionary(0, getrefresh = False)
self._info = CacheDictionary(0, getrefresh = False)
self._subscriptions = CacheDictionary(5, getrefresh=False)
self._cbs = {}
def cleanup(self):
self._items.cleanup()
self._info.cleanup()
def _clean_closure(self, cb, type, addr):
# A closure died, clean up
cbkey = (type, addr)
@ -393,12 +396,12 @@ class ServicesCache:
if self._cbs.has_key(cbkey):
del self._cbs[cbkey]
# object is needed so that property() works
# object is needed so that @property works
class ServiceDiscoveryWindow(object):
'''Class that represents the Services Discovery window.'''
def __init__(self, account, jid = '', node = '',
address_entry = False, parent = None):
self._account = account
self.account = account
self.parent = parent
if not jid:
jid = gajim.config.get_per('accounts', account, 'hostname')
@ -425,6 +428,7 @@ _('Without a connection, you can not browse available services'))
self.xml = gtkgui_helpers.get_glade('service_discovery_window.glade')
self.window = self.xml.get_widget('service_discovery_window')
self.services_treeview = self.xml.get_widget('services_treeview')
self.model = None
# This is more reliable than the cursor-changed signal.
selection = self.services_treeview.get_selection()
selection.connect_after('changed',
@ -455,7 +459,6 @@ _('Without a connection, you can not browse available services'))
liststore = gtk.ListStore(str)
self.address_comboboxentry.set_model(liststore)
self.address_comboboxentry.set_text_column(0)
self.latest_addresses = gajim.config.get(
'latest_disco_addresses').split()
if jid in self.latest_addresses:
@ -476,30 +479,35 @@ _('Without a connection, you can not browse available services'))
self.travel(jid, node)
self.window.show_all()
@property
def _get_account(self):
return self._account
return self.account
@property
def _set_account(self, value):
self._account = value
self.account = value
self.cache.account = value
if self.browser:
self.browser.account = value
account = property(_get_account, _set_account)
def _initial_state(self):
'''Set some initial state on the window. Separated in a method because
it's handy to use within browser's cleanup method.'''
self.progressbar.hide()
self.window.set_title(_('Service Discovery using account %s') % self.account)
title_text = _('Service Discovery using account %s') % self.account
self.window.set_title(title_text)
self._set_window_banner_text(_('Service Discovery'))
# FIXME: use self.banner_icon.clear() when we switch to GTK 2.8
self.banner_icon.set_from_file(None)
self.banner_icon.hide() # Just clearing it doesn't work
if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0):
self.banner_icon.clear()
else:
self.banner_icon.set_from_file(None)
self.banner_icon.hide() # Just clearing it doesn't work
def _set_window_banner_text(self, text, text_after = None):
theme = gajim.config.get('roster_theme')
bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs')
bannerfontattrs = gajim.config.get_per('themes', theme,
'bannerfontattrs')
if bannerfont:
font = pango.FontDescription(bannerfont)
@ -592,6 +600,7 @@ _('Without a connection, you can not browse available services'))
self.browser = None
self.window.destroy()
self.cache.cleanup()
for child in self.children[:]:
child.parent = None
if chain:
@ -724,9 +733,9 @@ class AgentBrowser:
note that the first two columns should ALWAYS be of type string and
contain the JID and node of the item respectively.'''
# JID, node, name, address
model = gtk.ListStore(str, str, str, str)
model.set_sort_column_id(3, gtk.SORT_ASCENDING)
self.window.services_treeview.set_model(model)
self.model = gtk.ListStore(str, str, str, str)
self.model.set_sort_column_id(3, gtk.SORT_ASCENDING)
self.window.services_treeview.set_model(self.model)
# Name column
col = gtk.TreeViewColumn(_('Name'))
renderer = gtk.CellRendererText()
@ -744,7 +753,7 @@ class AgentBrowser:
self.window.services_treeview.set_headers_visible(True)
def _clean_treemodel(self):
self.window.services_treeview.get_model().clear()
self.model.clear()
for col in self.window.services_treeview.get_columns():
self.window.services_treeview.remove_column(col)
self.window.services_treeview.set_headers_visible(False)
@ -876,8 +885,7 @@ class AgentBrowser:
def browse(self, force = False):
'''Fill the treeview with agents, fetching the info if necessary.'''
model = self.window.services_treeview.get_model()
model.clear()
self.model.clear()
self._total_items = self._progress = 0
self.window.progressbar.show()
self._pulse_timeout = gobject.timeout_add(250, self._pulse_timeout_cb)
@ -894,21 +902,21 @@ class AgentBrowser:
def _find_item(self, jid, node):
'''Check if an item is already in the treeview. Return an iter to it
if so, None otherwise.'''
model = self.window.services_treeview.get_model()
iter = model.get_iter_root()
iter = self.model.get_iter_root()
while iter:
cjid = model.get_value(iter, 0).decode('utf-8')
cnode = model.get_value(iter, 1).decode('utf-8')
cjid = self.model.get_value(iter, 0).decode('utf-8')
cnode = self.model.get_value(iter, 1).decode('utf-8')
if jid == cjid and node == cnode:
break
iter = model.iter_next(iter)
iter = self.model.iter_next(iter)
if iter:
return iter
return None
def _agent_items(self, jid, node, items, force):
'''Callback for when we receive a list of agent items.'''
model = self.window.services_treeview.get_model()
self.model.clear()
self._total_items = 0
gobject.source_remove(self._pulse_timeout)
self.window.progressbar.hide()
# The server returned an error
@ -920,53 +928,48 @@ class AgentBrowser:
_('This service does not contain any items to browse.'))
return
# We got a list of items
self.window.services_treeview.set_model(None)
for item in items:
jid = item['jid']
node = item.get('node', '')
iter = self._find_item(jid, node)
if iter:
# Already in the treeview
self._update_item(model, iter, jid, node, item)
else:
# Not in the treeview
self._total_items += 1
self._add_item(model, jid, node, item, force)
self._total_items += 1
self._add_item(jid, node, item, force)
self.window.services_treeview.set_model(self.model)
def _agent_info(self, jid, node, identities, features, data):
'''Callback for when we receive info about an agent's item.'''
addr = get_agent_address(jid, node)
model = self.window.services_treeview.get_model()
iter = self._find_item(jid, node)
if not iter:
# Not in the treeview, stop
return
if identities == 0:
# The server returned an error
self._update_error(model, iter, jid, node)
self._update_error(iter, jid, node)
else:
# We got our info
self._update_info(model, iter, jid, node,
self._update_info(iter, jid, node,
identities, features, data)
self.update_actions()
def _add_item(self, model, jid, node, item, force):
def _add_item(self, jid, node, item, force):
'''Called when an item should be added to the model. The result of a
disco#items query.'''
model.append((jid, node, item.get('name', ''),
self.model.append((jid, node, item.get('name', ''),
get_agent_address(jid, node)))
def _update_item(self, model, iter, jid, node, item):
def _update_item(self, iter, jid, node, item):
'''Called when an item should be updated in the model. The result of a
disco#items query. (seldom)'''
if item.has_key('name'):
model[iter][2] = item['name']
self.model[iter][2] = item['name']
def _update_info(self, model, iter, jid, node, identities, features, data):
def _update_info(self, iter, jid, node, identities, features, data):
'''Called when an item should be updated in the model with further info.
The result of a disco#info query.'''
model[iter][2] = identities[0].get('name', '')
self.model[iter][2] = identities[0].get('name', '')
def _update_error(self, model, iter, jid, node):
def _update_error(self, iter, jid, node):
'''Called when a disco#info query failed for an item.'''
pass
@ -1050,14 +1053,12 @@ class ToplevelAgentBrowser(AgentBrowser):
# These are all callbacks to make tooltips work
def on_treeview_leave_notify_event(self, widget, event):
model = widget.get_model()
props = widget.get_path_at_pos(int(event.x), int(event.y))
if self.tooltip.timeout > 0:
if not props or self.tooltip.id == props[0]:
self.tooltip.hide_tooltip()
def on_treeview_motion_notify_event(self, widget, event):
model = widget.get_model()
props = widget.get_path_at_pos(int(event.x), int(event.y))
if self.tooltip.timeout > 0:
if not props or self.tooltip.id != props[0]:
@ -1066,12 +1067,12 @@ class ToplevelAgentBrowser(AgentBrowser):
[row, col, x, y] = props
iter = None
try:
iter = model.get_iter(row)
iter = self.model.get_iter(row)
except:
self.tooltip.hide_tooltip()
return
jid = model[iter][0]
state = model[iter][4]
jid = self.model[iter][0]
state = self.model[iter][4]
# Not a category, and we have something to say about state
if jid and state > 0 and \
(self.tooltip.timeout == 0 or self.tooltip.id != props[0]):
@ -1088,10 +1089,10 @@ class ToplevelAgentBrowser(AgentBrowser):
# JID, node, icon, description, state
# State means 2 when error, 1 when fetching, 0 when succes.
view = self.window.services_treeview
model = gtk.TreeStore(str, str, gtk.gdk.Pixbuf, str, int)
model.set_sort_func(4, self._treemodel_sort_func)
model.set_sort_column_id(4, gtk.SORT_ASCENDING)
view.set_model(model)
self.model = gtk.TreeStore(str, str, gtk.gdk.Pixbuf, str, int)
self.model.set_sort_func(4, self._treemodel_sort_func)
self.model.set_sort_column_id(4, gtk.SORT_ASCENDING)
view.set_model(self.model)
col = gtk.TreeViewColumn()
# Icon Renderer
@ -1195,16 +1196,10 @@ class ToplevelAgentBrowser(AgentBrowser):
if not iter:
return
service = model[iter][0].decode('utf-8')
if service.find('@') != -1:
services = service.split('@', 1)
room = services[0]
service = services[1]
else:
room = ''
if not gajim.interface.instances[self.account].has_key('join_gc'):
try:
dialogs.JoinGroupchatWindow(self.account, service, room)
except RuntimeError:
dialogs.JoinGroupchatWindow(self.account, service)
except GajimGeneralException:
pass
else:
gajim.interface.instances[self.account]['join_gc'].window.present()
@ -1251,10 +1246,11 @@ class ToplevelAgentBrowser(AgentBrowser):
# We can register this agent
registered_transports = []
jid_list = gajim.contacts.get_jid_list(self.account)
for j in jid_list:
contact = gajim.contacts.get_first_contact_from_jid(self.account, j)
for jid in jid_list:
contact = gajim.contacts.get_first_contact_from_jid(
self.account, jid)
if _('Transports') in contact.groups:
registered_transports.append(j)
registered_transports.append(jid)
if jid in registered_transports:
self.register_button.set_label(_('_Edit'))
else:
@ -1333,41 +1329,38 @@ class ToplevelAgentBrowser(AgentBrowser):
def _create_category(self, cat, type=None):
'''Creates a category row.'''
model = self.window.services_treeview.get_model()
cat, prio = self._friendly_category(cat, type)
return model.append(None, ('', '', None, cat, prio))
return self.model.append(None, ('', '', None, cat, prio))
def _find_category(self, cat, type=None):
'''Looks up a category row and returns the iterator to it, or None.'''
model = self.window.services_treeview.get_model()
cat, prio = self._friendly_category(cat, type)
iter = model.get_iter_root()
iter = self.model.get_iter_root()
while iter:
if model.get_value(iter, 3).decode('utf-8') == cat:
if self.model.get_value(iter, 3).decode('utf-8') == cat:
break
iter = model.iter_next(iter)
iter = self.model.iter_next(iter)
if iter:
return iter
return None
def _find_item(self, jid, node):
model = self.window.services_treeview.get_model()
iter = None
cat_iter = model.get_iter_root()
cat_iter = self.model.get_iter_root()
while cat_iter and not iter:
iter = model.iter_children(cat_iter)
iter = self.model.iter_children(cat_iter)
while iter:
cjid = model.get_value(iter, 0).decode('utf-8')
cnode = model.get_value(iter, 1).decode('utf-8')
cjid = self.model.get_value(iter, 0).decode('utf-8')
cnode = self.model.get_value(iter, 1).decode('utf-8')
if jid == cjid and node == cnode:
break
iter = model.iter_next(iter)
cat_iter = model.iter_next(cat_iter)
iter = self.model.iter_next(iter)
cat_iter = self.model.iter_next(cat_iter)
if iter:
return iter
return None
def _add_item(self, model, jid, node, item, force):
def _add_item(self, jid, node, item, force):
# Row text
addr = get_agent_address(jid, node)
if item.has_key('name'):
@ -1391,21 +1384,21 @@ class ToplevelAgentBrowser(AgentBrowser):
cat = self._find_category(*cat_args)
if not cat:
cat = self._create_category(*cat_args)
model.append(cat, (item['jid'], item.get('node', ''), pix, descr, 1))
self.model.append(cat, (item['jid'], item.get('node', ''), pix, descr, 1))
self._expand_all()
# Grab info on the service
self.cache.get_info(jid, node, self._agent_info, force = force)
self._update_progressbar()
def _update_item(self, model, iter, jid, node, item):
def _update_item(self, iter, jid, node, item):
addr = get_agent_address(jid, node)
if item.has_key('name'):
descr = "<b>%s</b>\n%s" % (item['name'], addr)
else:
descr = "<b>%s</b>" % addr
model[iter][3] = descr
self.model[iter][3] = descr
def _update_info(self, model, iter, jid, node, identities, features, data):
def _update_info(self, iter, jid, node, identities, features, data):
addr = get_agent_address(jid, node)
name = identities[0].get('name', '')
if name:
@ -1427,32 +1420,32 @@ class ToplevelAgentBrowser(AgentBrowser):
break
# Check if we have to move categories
old_cat_iter = model.iter_parent(iter)
old_cat = model.get_value(old_cat_iter, 3).decode('utf-8')
if model.get_value(old_cat_iter, 3) == cat:
old_cat_iter = self.model.iter_parent(iter)
old_cat = self.model.get_value(old_cat_iter, 3).decode('utf-8')
if self.model.get_value(old_cat_iter, 3) == cat:
# Already in the right category, just update
model[iter][2] = pix
model[iter][3] = descr
model[iter][4] = 0
self.model[iter][2] = pix
self.model[iter][3] = descr
self.model[iter][4] = 0
return
# Not in the right category, move it.
model.remove(iter)
self.model.remove(iter)
# Check if the old category is empty
if not model.iter_is_valid(old_cat_iter):
if not self.model.iter_is_valid(old_cat_iter):
old_cat_iter = self._find_category(old_cat)
if not model.iter_children(old_cat_iter):
model.remove(old_cat_iter)
if not self.model.iter_children(old_cat_iter):
self.model.remove(old_cat_iter)
cat_iter = self._find_category(cat, type)
if not cat_iter:
cat_iter = self._create_category(cat, type)
model.append(cat_iter, (jid, node, pix, descr, 0))
self.model.append(cat_iter, (jid, node, pix, descr, 0))
self._expand_all()
def _update_error(self, model, iter, jid, node):
def _update_error(self, iter, jid, node):
addr = get_agent_address(jid, node)
model[iter][4] = 2
self.model[iter][4] = 2
self._progress += 1
self._update_progressbar()
@ -1466,11 +1459,13 @@ class MucBrowser(AgentBrowser):
# JID, node, name, users, description, fetched
# This is rather long, I'd rather not use a data_func here though.
# Users is a string, because want to be able to leave it empty.
model = gtk.ListStore(str, str, str, str, str, bool)
model.set_sort_column_id(2, gtk.SORT_ASCENDING)
self.window.services_treeview.set_model(model)
self.model = gtk.ListStore(str, str, str, str, str, bool)
self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
self.window.services_treeview.set_model(self.model)
# Name column
col = gtk.TreeViewColumn(_('Name'))
col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
col.set_fixed_width(100)
renderer = gtk.CellRendererText()
col.pack_start(renderer)
col.set_attributes(renderer, text = 2)
@ -1490,6 +1485,13 @@ class MucBrowser(AgentBrowser):
col.set_attributes(renderer, text = 4)
self.window.services_treeview.insert_column(col, -1)
col.set_resizable(True)
# Id column
col = gtk.TreeViewColumn(_('Id'))
renderer = gtk.CellRendererText()
col.pack_start(renderer)
col.set_attributes(renderer, text = 0)
self.window.services_treeview.insert_column(col, -1)
col.set_resizable(True)
self.window.services_treeview.set_headers_visible(True)
# Source id for idle callback used to start disco#info queries.
self._fetch_source = None
@ -1499,7 +1501,8 @@ class MucBrowser(AgentBrowser):
self.vadj = self.window.services_scrollwin.get_property('vadjustment')
self.vadj_cbid = self.vadj.connect('value-changed', self.on_scroll)
# And to size changes
self.size_cbid = self.window.services_scrollwin.connect('size-allocate', self.on_scroll)
self.size_cbid = self.window.services_scrollwin.connect(
'size-allocate', self.on_scroll)
def _clean_treemodel(self):
if self.size_cbid:
@ -1528,16 +1531,12 @@ class MucBrowser(AgentBrowser):
if not iter:
return
service = model[iter][0].decode('utf-8')
if service.find('@') != -1:
services = service.split('@', 1)
room = services[0]
service = services[1]
else:
room = model[iter][1].decode('utf-8')
if not gajim.interface.instances[self.account].has_key('join_gc'):
room = model[iter][1].decode('utf-8')
if 'join_gc' not in gajim.interface.instances[self.account]:
try:
dialogs.JoinGroupchatWindow(self.account, service, room)
except RuntimeError:
room_jid = '%s@%s' % (service, room)
dialogs.JoinGroupchatWindow(self.account, service)
except GajimGeneralException:
pass
else:
gajim.interface.instances[self.account]['join_gc'].window.present()
@ -1572,7 +1571,6 @@ class MucBrowser(AgentBrowser):
# Prevent a silly warning, try again in a bit.
self._fetch_source = gobject.timeout_add(100, self._start_info_query)
return
model = view.get_model()
# We have to do this in a pygtk <2.8 compatible way :/
#start, end = self.window.services_treeview.get_visible_range()
rect = view.get_visible_rect()
@ -1581,7 +1579,7 @@ class MucBrowser(AgentBrowser):
try:
sx, sy = view.tree_to_widget_coords(rect.x, rect.y)
spath = view.get_path_at_pos(sx, sy)[0]
iter = model.get_iter(spath)
iter = self.model.get_iter(spath)
except TypeError:
self._fetch_source = None
return
@ -1595,14 +1593,14 @@ class MucBrowser(AgentBrowser):
except TypeError:
# We're at the end of the model, we can leave end=None though.
pass
while iter and model.get_path(iter) != end:
if not model.get_value(iter, 5):
jid = model.get_value(iter, 0).decode('utf-8')
node = model.get_value(iter, 1).decode('utf-8')
while iter and self.model.get_path(iter) != end:
if not self.model.get_value(iter, 5):
jid = self.model.get_value(iter, 0).decode('utf-8')
node = self.model.get_value(iter, 1).decode('utf-8')
self.cache.get_info(jid, node, self._agent_info)
self._fetch_source = True
return
iter = model.iter_next(iter)
iter = self.model.iter_next(iter)
self._fetch_source = None
def _channel_altinfo(self, jid, node, items, name = None):
@ -1623,22 +1621,21 @@ class MucBrowser(AgentBrowser):
self._fetch_source = None
return
else:
model = self.window.services_treeview.get_model()
iter = self._find_item(jid, node)
if iter:
if name:
model[iter][2] = name
model[iter][3] = len(items) # The number of users
model[iter][5] = True
self.model[iter][2] = name
self.model[iter][3] = len(items) # The number of users
self.model[iter][5] = True
self._fetch_source = None
self._query_visible()
def _add_item(self, model, jid, node, item, force):
model.append((jid, node, item.get('name', ''), '', '', False))
def _add_item(self, jid, node, item, force):
self.model.append((jid, node, item.get('name', ''), '', '', False))
if not self._fetch_source:
self._fetch_source = gobject.idle_add(self._start_info_query)
def _update_info(self, model, iter, jid, node, identities, features, data):
def _update_info(self, iter, jid, node, identities, features, data):
name = identities[0].get('name', '')
for form in data:
typefield = form.getField('FORM_TYPE')
@ -1648,14 +1645,14 @@ class MucBrowser(AgentBrowser):
users = form.getField('muc#roominfo_occupants')
descr = form.getField('muc#roominfo_description')
if users:
model[iter][3] = users.getValue()
self.model[iter][3] = users.getValue()
if descr:
model[iter][4] = descr.getValue()
self.model[iter][4] = descr.getValue()
# Only set these when we find a form with additional info
# Some servers don't support forms and put extra info in
# the name attribute, so we preserve it in that case.
model[iter][2] = name
model[iter][5] = True
self.model[iter][2] = name
self.model[iter][5] = True
break
else:
# We didn't find a form, switch to alternate query mode
@ -1665,7 +1662,7 @@ class MucBrowser(AgentBrowser):
self._fetch_source = None
self._query_visible()
def _update_error(self, model, iter, jid, node):
def _update_error(self, iter, jid, node):
# switch to alternate query mode
self.cache.get_items(jid, node, self._channel_altinfo)

View file

@ -213,7 +213,7 @@ class FileTransfersWindow:
_('Connection with peer cannot be established.'))
self.tree.get_selection().unselect_all()
def show_stopped(self, jid, file_props):
def show_stopped(self, jid, file_props, error_msg = ''):
self.window.present()
self.window.window.focus()
if file_props['type'] == 'r':
@ -222,6 +222,8 @@ _('Connection with peer cannot be established.'))
file_name = file_props['name']
sectext = '\t' + _('Filename: %s') % file_name
sectext += '\n\t' + _('Recipient: %s') % jid
if error_msg:
sectext += '\n\t' + _('Error message: %s') % error_msg
dialogs.ErrorDialog(_('File transfer stopped by the contact of the other side'), \
sectext)
self.tree.get_selection().unselect_all()
@ -244,11 +246,16 @@ _('Connection with peer cannot be established.'))
gtk.RESPONSE_OK,
True, # select multiple true as we can select many files to send
gajim.config.get('last_send_dir'),
on_response_ok = on_ok,
on_response_cancel = lambda e:dialog.destroy()
)
btn = dialog.add_button(_('_Send'), gtk.RESPONSE_OK)
btn.set_use_stock(True) # FIXME: add send icon to this button (JUMP_TO)
btn.connect('clicked', on_ok)
btn = gtk.Button(_('_Send'))
btn.set_property('can-default', True)
# FIXME: add send icon to this button (JUMP_TO)
dialog.add_action_widget(btn, gtk.RESPONSE_OK)
dialog.set_default_response(gtk.RESPONSE_OK)
btn.show()
def send_file(self, account, contact, file_path):
''' start the real transfer(upload) of the file '''
@ -448,8 +455,10 @@ _('Connection with peer cannot be established.'))
for ev_type in ('file-error', 'file-completed', 'file-request-error',
'file-send-error', 'file-stopped'):
for event in gajim.events.get_events(account, jid, [ev_type]):
if event.parameters[1]['sid'] == file_props['sid']:
if event.parameters['sid'] == file_props['sid']:
gajim.events.remove_events(account, jid, event)
gajim.interface.roster.draw_contact(jid, account)
gajim.interface.roster.show_title()
del(self.files_props[sid[0]][sid[1:]])
del(file_props)
@ -558,7 +567,7 @@ _('Connection with peer cannot be established.'))
(file_path, file_name) = os.path.split(file_props['file-name'])
else:
file_name = file_props['name']
text_props = file_name + '\n'
text_props = gtkgui_helpers.escape_for_pango_markup(file_name) + '\n'
text_props += contact.get_shown_name()
self.model.set(iter, 1, text_labels, 2, text_props, C_SID,
file_props['type'] + file_props['sid'])

View file

@ -1,22 +1,8 @@
#!/bin/sh
''':'
exec python -OOt "$0" ${1+"$@"}
' '''
## scripts/gajim-remote.py
#!/usr/bin/env python
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -28,7 +14,7 @@ exec python -OOt "$0" ${1+"$@"}
## GNU General Public License for more details.
##
# gajim-remote help will show you the DBUS API of Gajim
# gajim-remote help will show you the D-BUS API of Gajim
import sys
import locale
@ -51,13 +37,11 @@ def send_error(error_message):
try:
import dbus
except:
raise exceptions.DbusNotSupported
_version = getattr(dbus, 'version', (0, 20, 0))
if _version[1] >= 41:
import dbus.service
import dbus.glib
except:
print str(exceptions.DbusNotSupported())
sys.exit(1)
OBJ_PATH = '/org/gajim/dbus/RemoteObject'
INTERFACE = 'org.gajim.dbus.RemoteInterface'
@ -90,8 +74,8 @@ class GajimRemote:
_('Shows or hides the roster window'),
[]
],
'show_next_unread': [
_('Popups a window with the next unread message'),
'show_next_pending_event': [
_('Popups a window with the next pending event'),
[]
],
'list_contacts': [
@ -320,14 +304,8 @@ class GajimRemote:
except:
raise exceptions.SessionBusNotPresent
if _version[1] >= 30:
obj = self.sbus.get_object(SERVICE, OBJ_PATH)
interface = dbus.Interface(obj, INTERFACE)
elif _version[1] < 30:
self.service = self.sbus.get_service(SERVICE)
interface = self.service.get_object(OBJ_PATH, INTERFACE)
else:
send_error(_('Unknown D-Bus version: %s') % _version[1])
obj = self.sbus.get_object(SERVICE, OBJ_PATH)
interface = dbus.Interface(obj, INTERFACE)
# get the function asked
self.method = interface.__getattr__(self.command)
@ -447,10 +425,7 @@ class GajimRemote:
''' calls self.method with arguments from sys.argv[2:] '''
args = sys.argv[2:]
args = [i.decode(PREFERRED_ENCODING) for i in sys.argv[2:]]
if _version[1] >= 41:
args = [dbus.String(i) for i in args]
else:
args = [i.encode('UTF-8') for i in sys.argv[2:]]
args = [dbus.String(i) for i in args]
try:
res = self.method(*args)
return res

View file

@ -1,23 +1,11 @@
#!/bin/sh
''':'
exec python -OOt "$0" ${1+"$@"}
' '''
#!/usr/bin/env python
## gajim.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
## - Travis Shirk <travis@pobox.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005 Travis Shirk <travis@pobox.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -41,6 +29,8 @@ from chat_control import ChatControlBase
from atom_window import AtomWindow
from common import exceptions
from common.zeroconf import connection_zeroconf
from common import dbus_support
if os.name == 'posix': # dl module is Unix Only
try: # rename the process name to gajim
@ -82,16 +72,14 @@ except exceptions.PysqliteNotAvailable, e:
if os.name == 'nt':
try:
import winsound # windows-only built-in module for playing wav
import win32api
import win32con
except:
pritext = _('Gajim needs pywin32 to run')
sectext = _('Please make sure that Pywin32 is installed on your system. You can get it at %s') % 'http://sourceforge.net/project/showfiles.php?group_id=78018'
if pritext:
dlg = gtk.MessageDialog(None,
gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
dlg.format_secondary_text(sectext)
dlg.run()
@ -144,23 +132,16 @@ for o, a in opts:
elif o in ('-p', '--profile'): # gajim --profile name
profile = a
pid_filename = os.path.expanduser('~/.gajim/gajim')
config_filename = os.path.expanduser('~/.gajim/config')
if os.name == 'nt':
try:
# Documents and Settings\[User Name]\Application Data\Gajim\logs
config_filename = os.environ['appdata'] + '/Gajim/config'
pid_filename = os.environ['appdata'] + '/Gajim/gajim'
except KeyError:
# win9x so ./config
config_filename = 'config'
pid_filename = 'gajim'
import locale
profile = unicode(profile, locale.getpreferredencoding())
if profile:
config_filename += '.%s' % profile
pid_filename += '.%s' % profile
import common.configpaths
common.configpaths.init_profile(profile)
gajimpaths = common.configpaths.gajimpaths
pid_filename = gajimpaths['PID_FILE']
config_filename = gajimpaths['CONFIG_FILE']
pid_filename += '.pid'
import dialogs
if os.path.exists(pid_filename):
path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
@ -194,7 +175,6 @@ atexit.register(on_exit)
parser = optparser.OptionsParser(config_filename)
import roster_window
import systray
import profile_window
import config
@ -217,7 +197,7 @@ class GlibIdleQueue(idlequeue.IdleQueue):
Start listening for events from fd
'''
res = gobject.io_add_watch(fd, flags, self.process_events,
priority=gobject.PRIORITY_LOW)
priority=gobject.PRIORITY_LOW)
# store the id of the watch, so that we can remove it on unplug
self.events[fd] = res
@ -274,7 +254,7 @@ class Interface:
on_response_no = (response, account, data[3], 'no'))
def handle_event_error_answer(self, account, array):
#('ERROR_ANSWER', account, (id, jid_from. errmsg, errcode))
#('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode))
id, jid_from, errmsg, errcode = array
if unicode(errcode) in ('403', '406') and id:
# show the error dialog
@ -286,7 +266,7 @@ class Interface:
file_props = ft.files_props['s'][sid]
file_props['error'] = -4
self.handle_event_file_request_error(account,
(jid_from, file_props))
(jid_from, file_props, errmsg))
conn = gajim.connections[account]
conn.disconnect_transfer(file_props)
return
@ -341,7 +321,8 @@ class Interface:
# Inform all controls for this account of the connection state change
for ctrl in self.msg_win_mgr.get_controls():
if ctrl.account == account:
if status == 'offline':
if status == 'offline' or (status == 'invisible' and \
gajim.connections[account].is_zeroconf):
ctrl.got_disconnected()
else:
# Other code rejoins all GCs, so we don't do it here
@ -422,12 +403,13 @@ class Interface:
elif contact1.show in statuss:
old_show = statuss.index(contact1.show)
if (resources != [''] and (len(lcontact) != 1 or
lcontact[0].show != 'offline')) and jid.find('@') > 0:
lcontact[0].show != 'offline')) and jid.find('@') > 0:
old_show = 0
contact1 = gajim.contacts.copy_contact(contact1)
lcontact.append(contact1)
contact1.resource = resource
if contact1.jid.find('@') > 0 and len(lcontact) == 1: # It's not an agent
if contact1.jid.find('@') > 0 and len(lcontact) == 1:
# It's not an agent
if old_show == 0 and new_show > 1:
if not contact1.jid in gajim.newly_added[account]:
gajim.newly_added[account].append(contact1.jid)
@ -459,6 +441,7 @@ class Interface:
if ji in jid_list:
# Update existing iter
self.roster.draw_contact(ji, account)
self.roster.draw_group(_('Transports'), account)
# transport just signed in/out, don't show popup notifications
# for 30s
account_ji = account + '/' + ji
@ -509,18 +492,23 @@ class Interface:
def handle_event_msg(self, account, array):
# 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
# chatstate, msg_id, composing_jep, user_nick)) user_nick is JEP-0172
# chatstate, msg_id, composing_jep, user_nick, xhtml))
# user_nick is JEP-0172
full_jid_with_resource = array[0]
jid = gajim.get_jid_without_resource(full_jid_with_resource)
resource = gajim.get_resource_from_jid(full_jid_with_resource)
message = array[1]
encrypted = array[3]
msg_type = array[4]
subject = array[5]
chatstate = array[6]
msg_id = array[7]
composing_jep = array[8]
xhtml = array[10]
if gajim.config.get('ignore_incoming_xhtml'):
xhtml = None
if gajim.jid_is_transport(jid):
jid = jid.replace('@', '')
@ -530,6 +518,7 @@ class Interface:
message_control.TYPE_GC:
# It's a Private message
pm = True
msg_type = 'pm'
chat_control = None
jid_of_control = full_jid_with_resource
@ -544,7 +533,8 @@ class Interface:
# unknow contact or offline message
jid_of_control = jid
chat_control = self.msg_win_mgr.get_control(jid, account)
elif highest_contact and resource != highest_contact.resource:
elif highest_contact and resource != highest_contact.resource and \
highest_contact.show != 'offline':
jid_of_control = full_jid_with_resource
chat_control = None
elif not pm:
@ -575,39 +565,51 @@ class Interface:
contact.msg_id = msg_id
# THIS MUST BE AFTER chatstates handling
# AND BEFORE playsound (else we here sounding on chatstates!)
# AND BEFORE playsound (else we ear sounding on chatstates!)
if not message: # empty message text
return
if gajim.config.get('ignore_unknown_contacts') and \
not gajim.contacts.get_contact(account, jid) and not pm:
return
if not contact:
# contact is not in the roster, create a fake one to display
# notification
contact = common.contacts.Contact(jid = jid, resource = resource)
advanced_notif_num = notify.get_advanced_notification('message_received',
account, contact)
# Is it a first or next message received ?
first = False
if not chat_control and not gajim.events.get_events(account,
jid_of_control, ['chat']):
# It's a first message and not a Private Message
if msg_type == 'normal':
if not gajim.events.get_events(account, jid, ['normal']):
first = True
elif not chat_control and not gajim.events.get_events(account,
jid_of_control, [msg_type]): # msg_type can be chat or pm
first = True
if pm:
nickname = resource
msg_type = 'pm'
groupchat_control.on_private_message(nickname, message, array[2])
groupchat_control.on_private_message(nickname, message, array[2],
xhtml)
else:
# array: (jid, msg, time, encrypted, msg_type, subject)
self.roster.on_message(jid, message, array[2], account, array[3],
msg_type, subject, resource, msg_id, array[9], advanced_notif_num)
if encrypted:
self.roster.on_message(jid, message, array[2], account, array[3],
msg_type, subject, resource, msg_id, array[9],
advanced_notif_num)
else:
# xhtml in last element
self.roster.on_message(jid, message, array[2], account, array[3],
msg_type, subject, resource, msg_id, array[9],
advanced_notif_num, xhtml = xhtml)
nickname = gajim.get_name_from_jid(account, jid)
# Check and do wanted notifications
msg = message
if subject:
msg = _('Subject: %s') % subject + '\n' + msg
notify.notify('new_message', jid, account, [msg_type, first, nickname,
msg], advanced_notif_num)
notify.notify('new_message', jid_of_control, account, [msg_type,
first, nickname, msg], advanced_notif_num)
if self.remote_ctrl:
self.remote_ctrl.raise_signal('NewMessage', (account, array))
@ -617,33 +619,35 @@ class Interface:
full_jid_with_resource = array[0]
jids = full_jid_with_resource.split('/', 1)
jid = jids[0]
gcs = self.msg_win_mgr.get_controls(message_control.TYPE_GC)
for gc_control in gcs:
if jid == gc_control.contact.jid:
if len(jids) > 1: # it's a pm
nick = jids[1]
if not self.msg_win_mgr.get_control(full_jid_with_resource, account):
tv = gc_control.list_treeview
model = tv.get_model()
i = gc_control.get_contact_iter(nick)
if i:
show = model[i][3]
else:
show = 'offline'
gc_c = gajim.contacts.create_gc_contact(room_jid = jid,
name = nick, show = show)
c = gajim.contacts.contact_from_gc_contact(gc_c)
self.roster.new_chat(c, account, private_chat = True)
ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account)
ctrl.print_conversation('Error %s: %s' % (array[1], array[2]),
'status')
return
gc_control.print_conversation('Error %s: %s' % (array[1], array[2]))
if gc_control.parent_win.get_active_jid() == jid:
gc_control.set_subject(gc_control.subject)
gc_control = self.msg_win_mgr.get_control(jid, account)
if gc_control and gc_control.type_id != message_control.TYPE_GC:
gc_control = None
if gc_control:
if len(jids) > 1: # it's a pm
nick = jids[1]
if not self.msg_win_mgr.get_control(full_jid_with_resource,
account):
tv = gc_control.list_treeview
model = tv.get_model()
iter = gc_control.get_contact_iter(nick)
if iter:
show = model[iter][3]
else:
show = 'offline'
gc_c = gajim.contacts.create_gc_contact(room_jid = jid,
name = nick, show = show)
c = gajim.contacts.contact_from_gc_contact(gc_c)
self.roster.new_chat(c, account, private_chat = True)
ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account)
ctrl.print_conversation('Error %s: %s' % (array[1], array[2]),
'status')
return
gc_control.print_conversation('Error %s: %s' % (array[1], array[2]))
if gc_control.parent_win.get_active_jid() == jid:
gc_control.set_subject(gc_control.subject)
return
if gajim.jid_is_transport(jid):
jid = jid.replace('@', '')
msg = array[2]
@ -697,8 +701,8 @@ class Interface:
self.remote_ctrl.raise_signal('Subscribed', (account, array))
def handle_event_unsubscribed(self, account, jid):
dialogs.InformationDialog(_('Contact "%s" removed subscription from you') % jid,
_('You will always see him or her as offline.'))
dialogs.InformationDialog(_('Contact "%s" removed subscription from you')\
% jid, _('You will always see him or her as offline.'))
# FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does not show deny
gajim.connections[account].ack_unsubscribed(jid)
if self.remote_ctrl:
@ -741,8 +745,8 @@ class Interface:
config.ServiceRegistrationWindow(array[0], array[1], account,
array[2])
else:
dialogs.ErrorDialog(_('Contact with "%s" cannot be established')\
% array[0], _('Check your connection or try again later.'))
dialogs.ErrorDialog(_('Contact with "%s" cannot be established') \
% array[0], _('Check your connection or try again later.'))
def handle_event_agent_info_items(self, account, array):
#('AGENT_INFO_ITEMS', account, (agent, node, items))
@ -851,6 +855,7 @@ class Interface:
self.remote_ctrl.raise_signal('LastStatusTime', (account, array))
def handle_event_os_info(self, account, array):
#'OS_INFO' (account, (jid, resource, client_info, os_info))
win = None
if self.instances[account]['infos'].has_key(array[0]):
win = self.instances[account]['infos'][array[0]]
@ -875,8 +880,8 @@ class Interface:
# Get the window and control for the updated status, this may be a PrivateChatControl
control = self.msg_win_mgr.get_control(room_jid, account)
if control:
control.chg_contact_status(nick, show, status, array[4], array[5], array[6],
array[7], array[8], array[9], array[10])
control.chg_contact_status(nick, show, status, array[4], array[5],
array[6], array[7], array[8], array[9], array[10])
# print status in chat window and update status/GPG image
if self.msg_win_mgr.has_window(fjid, account):
@ -892,38 +897,48 @@ class Interface:
if self.remote_ctrl:
self.remote_ctrl.raise_signal('GCPresence', (account, array))
def handle_event_gc_msg(self, account, array):
# ('GC_MSG', account, (jid, msg, time))
# ('GC_MSG', account, (jid, msg, time, has_timestamp, htmlmsg))
jids = array[0].split('/', 1)
room_jid = jids[0]
gc_control = self.msg_win_mgr.get_control(room_jid, account)
if not gc_control:
return
xhtml = array[4]
if gajim.config.get('ignore_incoming_xhtml'):
xhtml = None
if len(jids) == 1:
# message from server
nick = ''
else:
# message from someone
nick = jids[1]
gc_control.on_message(nick, array[1], array[2])
gc_control.on_message(nick, array[1], array[2], array[3], xhtml)
if self.remote_ctrl:
self.remote_ctrl.raise_signal('GCMessage', (account, array))
def handle_event_gc_subject(self, account, array):
#('GC_SUBJECT', account, (jid, subject, body))
#('GC_SUBJECT', account, (jid, subject, body, has_timestamp))
jids = array[0].split('/', 1)
jid = jids[0]
gc_control = self.msg_win_mgr.get_control(jid, account)
if not gc_control:
return
gc_control.set_subject(array[1])
# We can receive a subject with a body that contains "X has set the subject to Y" ...
if array[2]:
gc_control.print_conversation(array[2])
# ... Or the message comes from the occupant who set the subject
elif len(jids) > 1:
gc_control.print_conversation('%s has set the subject to %s' % (jids[1], array[1]))
# Standard way, the message comes from the occupant who set the subject
text = None
if len(jids) > 1:
text = '%s has set the subject to %s' % (jids[1], array[1])
# Workaround for psi bug http://flyspray.psi-im.org/task/595 , to be
# deleted one day. We can receive a subject with a body that contains
# "X has set the subject to Y" ...
elif array[2]:
text = array[2]
if text is not None:
if array[3]:
gc_control.print_old_conversation(text)
else:
gc_control.print_conversation(text)
def handle_event_gc_config(self, account, array):
#('GC_CONFIG', account, (jid, config)) config is a dict
@ -959,7 +974,7 @@ class Interface:
self.add_event(account, jid, 'gc-invitation', (room_jid, array[2],
array[3]))
if helpers.allow_showing_notification(account, 'notify_on_new_message'):
if helpers.allow_showing_notification(account):
path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
'gc_invitation.png')
path = gtkgui_helpers.get_path_to_generic_or_avatar(path)
@ -1010,7 +1025,7 @@ class Interface:
return
# Add it to roster
contact = gajim.contacts.create_contact(jid = jid, name = name,
groups = groups, show = 'offline', sub = sub, ask = ask)
groups = groups, show = 'offline', sub = sub, ask = ask)
gajim.contacts.add_contact(account, contact)
self.roster.add_contact_to_roster(jid, account)
else:
@ -1075,13 +1090,17 @@ class Interface:
gmail_messages_list = array[2]
if gajim.config.get('notify_on_new_gmail_email'):
img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
'single_msg_recv.png') #FIXME: find a better image
title = _('New E-mail on %(gmail_mail_address)s') % \
'new_email_recv.png')
title = _('New mail on %(gmail_mail_address)s') % \
{'gmail_mail_address': jid}
text = i18n.ngettext('You have %d new E-mail message', 'You have %d new E-mail messages', gmail_new_messages, gmail_new_messages, gmail_new_messages)
text = i18n.ngettext('You have %d new mail conversation',
'You have %d new mail conversations', gmail_new_messages,
gmail_new_messages, gmail_new_messages)
if gajim.config.get('notify_on_new_gmail_email_extra'):
for gmessage in gmail_messages_list:
#FIXME: emulate Gtalk client popups. find out what they parse and how
#they decide what to show
# each message has a 'From', 'Subject' and 'Snippet' field
text += _('\nFrom: %(from_address)s') % \
{'from_address': gmessage['From']}
@ -1149,8 +1168,15 @@ class Interface:
self.roster.show_title()
if no_queue: # We didn't have a queue: we change icons
if not gajim.contacts.get_contact_with_highest_priority(account, jid):
# add contact to roster ("Not In The Roster") if he is not
self.roster.add_to_not_in_the_roster(account, jid)
self.roster.draw_contact(jid, account)
# Show contact in roster (if he is invisible for example) and select line
path = self.roster.get_path(jid, account)
self.roster.show_and_select_path(path, jid, account)
def remove_first_event(self, account, jid, type_ = None):
event = gajim.events.get_first_event(account, jid, type_)
self.remove_event(account, jid, event)
@ -1161,24 +1187,26 @@ class Interface:
return
# no other event?
if not len(gajim.events.get_events(account, jid)):
if not gajim.config.get('showoffline'):
contact = gajim.contacts.get_contact_with_highest_priority(account,
jid)
if contact:
self.roster.really_remove_contact(contact, account)
contact = gajim.contacts.get_contact_with_highest_priority(account,
jid)
show_transport = gajim.config.get('show_transports_group')
if contact and (contact.show in ('error', 'offline') and \
not gajim.config.get('showoffline') or (
gajim.jid_is_transport(jid) and not show_transport)):
self.roster.really_remove_contact(contact, account)
self.roster.show_title()
self.roster.draw_contact(jid, account)
def handle_event_file_request_error(self, account, array):
jid = array[0]
file_props = array[1]
# ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
jid, file_props, errmsg = array
ft = self.instances['file_transfers']
ft.set_status(file_props['type'], file_props['sid'], 'stop')
errno = file_props['error']
if helpers.allow_popup_window(account):
if errno in (-4, -5):
ft.show_stopped(jid, file_props)
ft.show_stopped(jid, file_props, errmsg)
else:
ft.show_request_error(file_props)
return
@ -1224,8 +1252,11 @@ class Interface:
path_to_image = path, title = event_type, text = txt)
def handle_event_file_progress(self, account, file_props):
self.instances['file_transfers'].set_progress(file_props['type'],
file_props['sid'], file_props['received-len'])
if time.time() - self.last_ftwindow_update > 0.5:
# update ft window every 500ms
self.last_ftwindow_update = time.time()
self.instances['file_transfers'].set_progress(file_props['type'],
file_props['sid'], file_props['received-len'])
def handle_event_file_rcv_completed(self, account, file_props):
ft = self.instances['file_transfers']
@ -1328,7 +1359,9 @@ class Interface:
self.instances[account]['xml_console'].print_stanza(stanza, 'outgoing')
def handle_event_vcard_published(self, account, array):
dialogs.InformationDialog(_('vCard publication succeeded'), _('Your personal information has been published successfully.'))
if self.instances[account].has_key('profile'):
win = self.instances[account]['profile']
win.vcard_published()
for gc_control in self.msg_win_mgr.get_controls(message_control.TYPE_GC):
if gc_control.account == account:
show = gajim.SHOW_LIST[gajim.connections[account].connected]
@ -1337,7 +1370,9 @@ class Interface:
gc_control.room_jid, show, status)
def handle_event_vcard_not_published(self, account, array):
dialogs.InformationDialog(_('vCard publication failed'), _('There was an error while publishing your personal information, try again later.'))
if self.instances[account].has_key('profile'):
win = self.instances[account]['profile']
win.vcard_not_published()
def handle_event_signed_in(self, account, empty):
'''SIGNED_IN event is emitted when we sign in, so handle it'''
@ -1362,12 +1397,11 @@ class Interface:
if gajim.gc_connected[account].has_key(room_jid) and\
gajim.gc_connected[account][room_jid]:
continue
room, server = gajim.get_room_name_and_server_from_room_jid(room_jid)
nick = gc_control.nick
password = ''
if gajim.gc_passwords.has_key(room_jid):
password = gajim.gc_passwords[room_jid]
gajim.connections[account].join_gc(nick, room, server, password)
gajim.connections[account].join_gc(nick, room_jid, password)
def handle_event_metacontacts(self, account, tags_list):
gajim.contacts.define_metacontacts(account, tags_list)
@ -1401,6 +1435,21 @@ class Interface:
if win.startswith('privacy_list_'):
self.instances[account][win].check_active_default(data)
def handle_event_zc_name_conflict(self, account, data):
dlg = dialogs.InputDialog(_('Username Conflict'),
_('Please type a new username for your local account'),
is_modal = True)
dlg.input_entry.set_text(data)
response = dlg.get_response()
if response == gtk.RESPONSE_OK:
new_name = dlg.input_entry.get_text()
gajim.config.set_per('accounts', account, 'name', new_name)
status = gajim.connections[account].status
gajim.connections[account].username = new_name
gajim.connections[account].change_status(status, '')
else:
gajim.connections[account].change_status('offline','')
def read_sleepy(self):
'''Check idle status and change that status if needed'''
if not self.sleeper.poll():
@ -1614,25 +1663,28 @@ class Interface:
menu.show_all()
return menu
def init_emoticons(self):
if not gajim.config.get('emoticons_theme'):
def init_emoticons(self, need_reload = False):
emot_theme = gajim.config.get('emoticons_theme')
if not emot_theme:
return
#initialize emoticons dictionary and unique images list
self.emoticons_images = list()
self.emoticons = dict()
emot_theme = gajim.config.get('emoticons_theme')
if not emot_theme:
return
path = os.path.join(gajim.DATA_DIR, 'emoticons', emot_theme)
if not os.path.exists(path):
# It's maybe a user theme
path = os.path.join(gajim.MY_EMOTS_PATH, emot_theme)
if not os.path.exists(path): # theme doesn't exist
if not os.path.exists(path): # theme doesn't exist, disable emoticons
gajim.config.set('emoticons_theme', '')
return
sys.path.append(path)
from emoticons import emoticons as emots
import emoticons
if need_reload:
# we need to reload else that doesn't work when changing emoticon set
reload(emoticons)
emots = emoticons.emoticons
for emot in emots:
emot_file = os.path.join(path, emots[emot])
if not self.image_is_ok(emot_file):
@ -1646,7 +1698,7 @@ class Interface:
self.emoticons_images.append((emot, pix))
self.emoticons[emot.upper()] = emot_file
sys.path.remove(path)
del emots
del emoticons
if self.emoticons_menu:
self.emoticons_menu.destroy()
self.emoticons_menu = self.prepare_emoticons_menu()
@ -1707,6 +1759,7 @@ class Interface:
'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received,
'PRIVACY_LISTS_ACTIVE_DEFAULT': \
self.handle_event_privacy_lists_active_default,
'ZC_NAME_CONFLICT': self.handle_event_zc_name_conflict,
}
gajim.handlers = self.handlers
@ -1726,20 +1779,28 @@ class Interface:
err_str)
sys.exit()
def handle_event(self, account, jid, type_):
def handle_event(self, account, fjid, type_):
w = None
fjid = jid
resource = gajim.get_resource_from_jid(jid)
jid = gajim.get_jid_without_resource(jid)
resource = gajim.get_resource_from_jid(fjid)
jid = gajim.get_jid_without_resource(fjid)
if type_ in ('printed_gc_msg', 'gc_msg'):
w = self.msg_win_mgr.get_window(jid, account)
elif type_ in ('printed_chat', 'chat'):
elif type_ in ('printed_chat', 'chat', ''):
# '' is for log in/out notifications
if self.msg_win_mgr.has_window(fjid, account):
w = self.msg_win_mgr.get_window(fjid, account)
else:
highest_contact = gajim.contacts.get_contact_with_highest_priority(
account, jid)
# jid can have a window if this resource was lower when he sent
# message and is now higher because the other one is offline
if resource and highest_contact.resource == resource and \
not self.msg_win_mgr.has_window(jid, account):
resource = None
fjid = jid
contact = gajim.contacts.get_contact(account, jid, resource)
if isinstance(contact, list):
contact = gajim.contacts.get_first_contact_from_jid(account, jid)
if not contact or isinstance(contact, list):
contact = highest_contact
self.roster.new_chat(contact, account, resource = resource)
w = self.msg_win_mgr.get_window(fjid, account)
gajim.last_message_time[account][jid] = 0 # long time ago
@ -1763,16 +1824,18 @@ class Interface:
elif type_ in ('normal', 'file-request', 'file-request-error',
'file-send-error', 'file-error', 'file-stopped', 'file-completed'):
# Get the first single message event
event = gajim.events.get_first_event(account, jid, type_)
# Open the window
self.roster.open_event(account, jid, event)
elif type_ == 'gmail':
if gajim.config.get_per('accounts', account, 'savepass'):
url = ('http://www.google.com/accounts/ServiceLoginAuth?service=mail&Email=%s&Passwd=%s&continue=https://mail.google.com/mail') %\
(urllib.quote(gajim.config.get_per('accounts', account, 'name')),
urllib.quote(gajim.config.get_per('accounts', account, 'password')))
event = gajim.events.get_first_event(account, fjid, type_)
if not event:
# default to jid without resource
event = gajim.events.get_first_event(account, jid, type_)
# Open the window
self.roster.open_event(account, jid, event)
else:
url = ('http://mail.google.com/')
# Open the window
self.roster.open_event(account, fjid, event)
elif type_ == 'gmail':
url = 'http://mail.google.com/mail?account_id=%s' % urllib.quote(
gajim.config.get_per('accounts', account, 'name'))
helpers.launch_browser_mailer('url', url)
elif type_ == 'gc-invitation':
event = gajim.events.get_first_event(account, jid, type_)
@ -1780,6 +1843,7 @@ class Interface:
dialogs.InvitationReceivedDialog(account, data[0], jid, data[2],
data[1])
gajim.events.remove_events(account, jid, event)
self.roster.draw_contact(jid, account)
if w:
w.set_active_tab(fjid, account)
w.window.present()
@ -1857,11 +1921,17 @@ class Interface:
self.handle_event_file_progress)
gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue)
self.register_handlers()
if gajim.config.get('enable_zeroconf'):
gajim.connections[gajim.ZEROCONF_ACC_NAME] = common.zeroconf.connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME)
for account in gajim.config.get_per('accounts'):
gajim.connections[account] = common.connection.Connection(account)
if not gajim.config.get_per('accounts', account, 'is_zeroconf'):
gajim.connections[account] = common.connection.Connection(account)
# gtk hooks
gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail')
gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url')
if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0):
gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url')
self.instances = {'logs': {}}
@ -1891,6 +1961,12 @@ class Interface:
else:
self.remote_ctrl = None
if gajim.config.get('networkmanager_support') and dbus_support.supported:
try:
import network_manager_listener
except:
print >> sys.stderr, _('Network Manager support not available')
self.show_vcard_when_connect = []
path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
@ -1904,18 +1980,18 @@ class Interface:
self.systray_enabled = False
self.systray_capabilities = False
if os.name == 'nt':
try:
import systraywin32
except: # user doesn't have trayicon capabilities
pass
else:
self.systray_capabilities = True
self.systray = systraywin32.SystrayWin32()
else:
if os.name == 'nt' and gtk.pygtk_version >= (2, 10, 0) and\
gtk.gtk_version >= (2, 10, 0):
import statusicon
self.systray = statusicon.StatusIcon()
self.systray_capabilities = True
else: # use ours, not GTK+ one
# [FIXME: remove this when we migrate to 2.10 and we can do
# cool tooltips somehow and (not dying to keep) animation]
import systray
self.systray_capabilities = systray.HAS_SYSTRAY_CAPABILITIES
if self.systray_capabilities:
self.systray = systray.Systray()
self.systray = systray.Systray()
if self.systray_capabilities and gajim.config.get('trayicon'):
self.show_systray()
@ -1945,6 +2021,9 @@ class Interface:
'choose another language by setting the speller_language option.'
) % lang)
gajim.config.set('use_speller', False)
self.last_ftwindow_update = 0
gobject.timeout_add(100, self.autoconnect)
gobject.timeout_add(200, self.process_connections)
gobject.timeout_add(500, self.read_sleepy)
@ -1968,7 +2047,8 @@ if __name__ == '__main__':
cli = gnome.ui.master_client()
cli.connect('die', die_cb)
path_to_gajim_script = gtkgui_helpers.get_abspath_for_script('gajim')
path_to_gajim_script = gtkgui_helpers.get_abspath_for_script(
'gajim')
if path_to_gajim_script:
argv = [path_to_gajim_script]
@ -1983,5 +2063,6 @@ if __name__ == '__main__':
gtkgui_helpers.possibly_set_gajim_as_xmpp_handler()
check_paths.check_and_possibly_create_paths()
Interface()
gtk.main()

View file

@ -9,7 +9,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -51,7 +51,7 @@ class GajimThemesWindow:
self.themes_tree = self.xml.get_widget('themes_treeview')
self.theme_options_vbox = self.xml.get_widget('theme_options_vbox')
self.colorbuttons = {}
for chatstate in ('active', 'inactive', 'composing', 'paused', 'gone',
for chatstate in ('inactive', 'composing', 'paused', 'gone',
'muc_msg', 'muc_directed_msg'):
self.colorbuttons[chatstate] = self.xml.get_widget(chatstate + \
'_colorbutton')
@ -198,7 +198,7 @@ class GajimThemesWindow:
self.no_update = False
gajim.interface.roster.change_roster_style(None)
for chatstate in ('active', 'inactive', 'composing', 'paused', 'gone',
for chatstate in ('inactive', 'composing', 'paused', 'gone',
'muc_msg', 'muc_directed_msg'):
color = gajim.config.get_per('themes', theme, 'state_' + chatstate + \
'_color')
@ -333,11 +333,6 @@ class GajimThemesWindow:
font_props[1] = True
return font_props
def on_active_colorbutton_color_set(self, widget):
self.no_update = True
self._set_color(True, widget, 'state_active_color')
self.no_update = False
def on_inactive_colorbutton_color_set(self, widget):
self.no_update = True
self._set_color(True, widget, 'state_inactive_color')

View file

@ -4,7 +4,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -39,12 +39,13 @@ from common import helpers
from chat_control import ChatControl
from chat_control import ChatControlBase
from conversation_textview import ConversationTextview
from common.exceptions import GajimGeneralException
#(status_image, type, nick, shown_nick)
(
C_IMG, # image to show state (online, new message etc)
C_TYPE, # type of the row ('contact' or 'group')
C_NICK, # contact nickame or group name
C_NICK, # contact nickame or ROLE name
C_TYPE, # type of the row ('contact' or 'role')
C_TEXT, # text shown in the cellrenderer
C_AVATAR, # avatar of the contact
) = range(5)
@ -94,6 +95,9 @@ class PrivateChatControl(ChatControl):
TYPE_ID = message_control.TYPE_PM
def __init__(self, parent_win, contact, acct):
room_jid = contact.jid.split('/')[0]
room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, acct)
self.room_name = room_ctrl.name
ChatControl.__init__(self, parent_win, contact, acct)
self.TYPE_ID = 'pm'
self.display_names = (_('Private Chat'), _('Private Chats'))
@ -103,9 +107,10 @@ class PrivateChatControl(ChatControl):
if not message:
return
# We need to make sure that we can still send through the room and that the
# recipient did not go away
contact = gajim.contacts.get_first_contact_from_jid(self.account, self.contact.jid)
# We need to make sure that we can still send through the room and that
# the recipient did not go away
contact = gajim.contacts.get_first_contact_from_jid(self.account,
self.contact.jid)
if contact is None:
# contact was from pm in MUC
room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid)
@ -114,7 +119,7 @@ class PrivateChatControl(ChatControl):
dialogs.ErrorDialog(
_('Sending private message failed'),
#in second %s code replaces with nickname
_('You are no longer in room "%s" or "%s" has left.') % \
_('You are no longer in group chat "%s" or "%s" has left.') % \
(room, nick))
return
@ -172,17 +177,20 @@ class GroupchatControl(ChatControlBase):
self.nick = contact.name
self.name = self.room_jid.split('@')[0]
self.hide_chat_buttons_always = gajim.config.get('always_hide_groupchat_buttons')
self.hide_chat_buttons_always = gajim.config.get(
'always_hide_groupchat_buttons')
self.chat_buttons_set_visible(self.hide_chat_buttons_always)
self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_groupchat_banner'))
self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'), gajim.config.get('hide_groupchat_occupants_list'))
self.widget_set_visible(self.xml.get_widget('banner_eventbox'),
gajim.config.get('hide_groupchat_banner'))
self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'),
gajim.config.get('hide_groupchat_occupants_list'))
self.gc_refer_to_nick_char = gajim.config.get('gc_refer_to_nick_char')
self._last_selected_contact = None # None or holds jid, account tuple
# alphanum sorted
self.muc_cmds = ['ban', 'chat', 'query', 'clear', 'close', 'compact',
'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick', 'part',
'names', 'say', 'topic']
'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick',
'part', 'names', 'say', 'topic']
# muc attention flag (when we are mentioned in a muc)
# if True, the room has mentioned us
self.attention_flag = False
@ -200,7 +208,8 @@ class GroupchatControl(ChatControlBase):
xm = gtkgui_helpers.get_glade('gc_control_popup_menu.glade')
widget = xm.get_widget('bookmark_room_menuitem')
id = widget.connect('activate', self._on_bookmark_room_menuitem_activate)
id = widget.connect('activate',
self._on_bookmark_room_menuitem_activate)
self.handlers[id] = widget
widget = xm.get_widget('change_nick_menuitem')
@ -208,11 +217,13 @@ class GroupchatControl(ChatControlBase):
self.handlers[id] = widget
widget = xm.get_widget('configure_room_menuitem')
id = widget.connect('activate', self._on_configure_room_menuitem_activate)
id = widget.connect('activate',
self._on_configure_room_menuitem_activate)
self.handlers[id] = widget
widget = xm.get_widget('change_subject_menuitem')
id = widget.connect('activate', self._on_change_subject_menuitem_activate)
id = widget.connect('activate',
self._on_change_subject_menuitem_activate)
self.handlers[id] = widget
widget = xm.get_widget('compact_view_menuitem')
@ -226,29 +237,27 @@ class GroupchatControl(ChatControlBase):
self.gc_popup_menu = xm.get_widget('gc_control_popup_menu')
self.name_label = self.xml.get_widget('banner_name_label')
id = self.parent_win.window.connect('focus-in-event',
self._on_window_focus_in_event)
self.handlers[id] = self.parent_win.window
# set the position of the current hpaned
self.hpaned_position = gajim.config.get('gc-hpaned-position')
self.hpaned = self.xml.get_widget('hpaned')
self.hpaned.set_position(self.hpaned_position)
list_treeview = self.list_treeview = self.xml.get_widget('list_treeview')
selection = list_treeview.get_selection()
self.list_treeview = self.xml.get_widget('list_treeview')
selection = self.list_treeview.get_selection()
id = selection.connect('changed',
self.on_list_treeview_selection_changed)
self.handlers[id] = selection
id = list_treeview.connect('style-set', self.on_list_treeview_style_set)
self.handlers[id] = list_treeview
id = self.list_treeview.connect('style-set',
self.on_list_treeview_style_set)
self.handlers[id] = self.list_treeview
# we want to know when the the widget resizes, because that is
# an indication that the hpaned has moved...
# FIXME: Find a better indicator that the hpaned has moved.
id = self.list_treeview.connect('size-allocate',
self.on_treeview_size_allocate)
self.handlers[id] = self.list_treeview
#status_image, type, nickname, shown_nick
#status_image, shown_nick, type, nickname, avatar
store = gtk.TreeStore(gtk.Image, str, str, str, gtk.gdk.Pixbuf)
store.set_sort_column_id(C_TEXT, gtk.SORT_ASCENDING)
self.list_treeview.set_model(store)
@ -275,7 +284,8 @@ class GroupchatControl(ChatControlBase):
renderer_text = gtk.CellRendererText() # nickname
column.pack_start(renderer_text, expand = True)
column.add_attribute(renderer_text, 'markup', C_TEXT)
column.set_cell_data_func(renderer_text, tree_cell_data_func, self.list_treeview)
column.set_cell_data_func(renderer_text, tree_cell_data_func,
self.list_treeview)
self.list_treeview.append_column(column)
@ -316,15 +326,6 @@ class GroupchatControl(ChatControlBase):
menu.show_all()
def notify_on_new_messages(self):
return gajim.config.get('notify_on_all_muc_messages') or \
self.attention_flag
def _on_window_focus_in_event(self, widget, event):
'''When window gets focus'''
if self.parent_win.get_active_jid() == self.room_jid:
self.conv_textview.allow_focus_out_line = True
def on_treeview_size_allocate(self, widget, allocation):
'''The MUC treeview has resized. Move the hpaned in all tabs to match'''
self.hpaned_position = self.hpaned.get_position()
@ -371,39 +372,44 @@ class GroupchatControl(ChatControlBase):
has_focus = self.parent_win.window.get_property('has-toplevel-focus')
current_tab = self.parent_win.get_active_control() == self
color_name = None
color = None
theme = gajim.config.get('roster_theme')
if chatstate == 'attention' and (not has_focus or not current_tab):
self.attention_flag = True
color = gajim.config.get_per('themes', theme,
color_name = gajim.config.get_per('themes', theme,
'state_muc_directed_msg_color')
elif chatstate:
if chatstate == 'active' or (current_tab and has_focus):
self.attention_flag = False
color = gajim.config.get_per('themes', theme,
'state_active_color')
# get active color from gtk
color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
not self.attention_flag:
color = gajim.config.get_per('themes', theme, 'state_muc_msg_color')
if color:
color = gtk.gdk.colormap_get_system().alloc_color(color)
color_name = gajim.config.get_per('themes', theme,
'state_muc_msg_color')
if color_name:
color = gtk.gdk.colormap_get_system().alloc_color(color_name)
label_str = self.name
# count waiting highlighted messages
unread = ''
num_unread = self.get_nb_unread()
if num_unread == 1:
unread = '*'
elif num_unread > 1:
unread = '[' + unicode(num_unread) + ']'
label_str = unread + label_str
return (label_str, color)
def get_tab_image(self):
# Set tab image (always 16x16); unread messages show the 'message' image
img_16 = gajim.interface.roster.get_appropriate_state_images(
self.room_jid, icon_name = 'message')
# Set tab image (always 16x16)
tab_image = None
if self.attention_flag and gajim.config.get('show_unread_tab_icon'):
tab_image = img_16['message']
if gajim.gc_connected[self.account][self.room_jid]:
tab_image = gajim.interface.roster.load_icon('muc_active')
else:
if gajim.gc_connected[self.account][self.room_jid]:
tab_image = img_16['muc_active']
else:
tab_image = img_16['muc_inactive']
tab_image = gajim.interface.roster.load_icon('muc_inactive')
return tab_image
def update_ui(self):
@ -430,15 +436,18 @@ class GroupchatControl(ChatControlBase):
childs[3].set_sensitive(False)
return menu
def on_message(self, nick, msg, tim):
def on_message(self, nick, msg, tim, has_timestamp = False, xhtml = None):
if not nick:
# message from server
self.print_conversation(msg, tim = tim)
self.print_conversation(msg, tim = tim, xhtml = xhtml)
else:
# message from someone
self.print_conversation(msg, nick, tim)
if has_timestamp:
self.print_old_conversation(msg, nick, tim, xhtml)
else:
self.print_conversation(msg, nick, tim, xhtml)
def on_private_message(self, nick, msg, tim):
def on_private_message(self, nick, msg, tim, xhtml):
# Do we have a queue?
fjid = self.room_jid + '/' + nick
no_queue = len(gajim.events.get_events(self.account, fjid)) == 0
@ -446,11 +455,11 @@ class GroupchatControl(ChatControlBase):
# We print if window is opened
pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account)
if pm_control:
pm_control.print_conversation(msg, tim = tim)
pm_control.print_conversation(msg, tim = tim, xhtml = xhtml)
return
event = gajim.events.create_event('pm', (msg, '', 'incoming', tim,
False, '', None))
False, '', None, xhtml))
gajim.events.add_event(self.account, fjid, event)
autopopup = gajim.config.get('autopopup')
@ -458,7 +467,7 @@ class GroupchatControl(ChatControlBase):
iter = self.get_contact_iter(nick)
path = self.list_treeview.get_model().get_path(iter)
if not autopopup or (not autopopupaway and \
gajim.connections[self.account].connected > 2):
gajim.connections[self.account].connected > 2):
if no_queue: # We didn't have a queue: we change icons
model = self.list_treeview.get_model()
state_images =\
@ -467,6 +476,7 @@ class GroupchatControl(ChatControlBase):
image = state_images['message']
model[iter][C_IMG] = image
self.parent_win.show_title()
self.parent_win.redraw_tab(self)
else:
self._start_private_message(nick)
# Scroll to line
@ -499,7 +509,26 @@ class GroupchatControl(ChatControlBase):
gc_count_nicknames_colors = 0
gc_custom_colors = {}
def print_conversation(self, text, contact = '', tim = None):
def print_old_conversation(self, text, contact = '', tim = None,
xhtml = None):
if isinstance(text, str):
text = unicode(text, 'utf-8')
if contact:
if contact == self.nick: # it's us
kind = 'outgoing'
else:
kind = 'incoming'
else:
kind = 'status'
if gajim.config.get('restored_messages_small'):
small_attr = ['small']
else:
small_attr = []
ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
small_attr, small_attr + ['restored_message'],
small_attr + ['restored_message'], xhtml = xhtml)
def print_conversation(self, text, contact = '', tim = None, xhtml = None):
'''Print a line in the conversation:
if contact is set: it's a message from someone or an info message (contact
= 'info' in such a case)
@ -553,11 +582,16 @@ class GroupchatControl(ChatControlBase):
self.check_and_possibly_add_focus_out_line()
ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
other_tags_for_name, [], other_tags_for_text)
other_tags_for_name, [], other_tags_for_text, xhtml = xhtml)
def get_nb_unread(self):
nb = len(gajim.events.get_events(self.account, self.room_jid,
['printed_gc_msg']))
nb += self.get_nb_unread_pm()
return nb
def get_nb_unread_pm(self):
nb = 0
for nick in gajim.contacts.get_nick_list(self.account, self.room_jid):
nb += len(gajim.events.get_events(self.account, self.room_jid + '/' + \
nick, ['pm']))
@ -570,8 +604,7 @@ class GroupchatControl(ChatControlBase):
# Do we play a sound on every muc message?
if gajim.config.get_per('soundevents', 'muc_message_received', 'enabled'):
if gajim.config.get('notify_on_all_muc_messages'):
sound = 'received'
sound = 'received'
# Are any of the defined highlighting words in the text?
if self.needs_visual_notification(text):
@ -622,13 +655,16 @@ class GroupchatControl(ChatControlBase):
word[char_position:char_position+1]
if (refer_to_nick_char != ''):
refer_to_nick_char_code = ord(refer_to_nick_char)
if ((refer_to_nick_char_code < 65 or refer_to_nick_char_code > 123)\
or (refer_to_nick_char_code < 97 and refer_to_nick_char_code > 90)):
if ((refer_to_nick_char_code < 65 or \
refer_to_nick_char_code > 123) or \
(refer_to_nick_char_code < 97 and \
refer_to_nick_char_code > 90)):
return True
else:
# This is A->Z or a->z, we can be sure our nick is the beginning
# of a real word, do not highlight. Note that we can probably
# do a better detection of non-punctuation characters
# This is A->Z or a->z, we can be sure our nick is the
# beginning of a real word, do not highlight. Note that we
# can probably do a better detection of non-punctuation
# characters
return False
else: # Special word == word, no char after in word
return True
@ -638,7 +674,7 @@ class GroupchatControl(ChatControlBase):
self.subject = subject
self.name_label.set_ellipsize(pango.ELLIPSIZE_END)
subject = gtkgui_helpers.reduce_chars_newlines(subject, max_lines = 2)
subject = helpers.reduce_chars_newlines(subject, max_lines = 2)
subject = gtkgui_helpers.escape_for_pango_markup(subject)
font_attrs, font_attrs_small = self.get_font_attrs()
text = '<span %s>%s</span>' % (font_attrs, self.room_jid)
@ -646,11 +682,10 @@ class GroupchatControl(ChatControlBase):
text += '\n<span %s>%s</span>' % (font_attrs_small, subject)
self.name_label.set_markup(text)
event_box = self.name_label.get_parent()
if subject == '':
self.subject = _('This room has no subject')
# tooltip must always hold ALL the subject
self.subject_tooltip.set_tip(event_box, self.subject)
if self.subject:
# tooltip must always hold ALL the subject
self.subject_tooltip.set_tip(event_box, self.subject)
def got_connected(self):
gajim.gc_connected[self.account][self.room_jid] = True
@ -677,7 +712,8 @@ class GroupchatControl(ChatControlBase):
gc_contact.affiliation, gc_contact.status,
gc_contact.jid)
def on_send_pm(self, widget=None, model=None, iter=None, nick=None, msg=None):
def on_send_pm(self, widget = None, model = None, iter = None, nick = None,
msg = None):
'''opens a chat window and msg is not None sends private message to a
contact in a room'''
if nick is None:
@ -686,14 +722,16 @@ class GroupchatControl(ChatControlBase):
self._start_private_message(nick)
if msg:
gajim.interface.msg_win_mgr.get_control(fjid, self.account).send_message(msg)
gajim.interface.msg_win_mgr.get_control(fjid, self.account).\
send_message(msg)
def draw_contact(self, nick, selected=False, focus=False):
iter = self.get_contact_iter(nick)
if not iter:
return
model = self.list_treeview.get_model()
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
nick)
state_images = gajim.interface.roster.jabber_state_images['16']
if len(gajim.events.get_events(self.account, self.room_jid + '/' + nick)):
image = state_images['message']
@ -706,7 +744,7 @@ class GroupchatControl(ChatControlBase):
if status and gajim.config.get('show_status_msgs_in_roster'):
status = status.strip()
if status != '':
status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 1)
status = helpers.reduce_chars_newlines(status, max_lines = 1)
# escape markup entities and make them small italic and fg color
color = gtkgui_helpers._get_fade_color(self.list_treeview,
selected, focus)
@ -720,6 +758,8 @@ class GroupchatControl(ChatControlBase):
def draw_avatar(self, nick):
model = self.list_treeview.get_model()
iter = self.get_contact_iter(nick)
if not iter:
return
if gajim.config.get('show_avatars_in_roster'):
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(self.room_jid + \
'/' + nick, True)
@ -731,8 +771,8 @@ class GroupchatControl(ChatControlBase):
scaled_pixbuf = None
model[iter][C_AVATAR] = scaled_pixbuf
def chg_contact_status(self, nick, show, status, role, affiliation, jid, reason, actor,
statusCode, new_nick):
def chg_contact_status(self, nick, show, status, role, affiliation, jid,
reason, actor, statusCode, new_nick):
'''When an occupant changes his or her status'''
if show == 'invisible':
return
@ -824,7 +864,8 @@ class GroupchatControl(ChatControlBase):
self.add_contact_to_roster(nick, show, role,
affiliation, status, jid)
else:
c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
c = gajim.contacts.get_gc_contact(self.account, self.room_jid,
nick)
if c.show == show and c.status == status and \
c.affiliation == affiliation: #no change
return
@ -841,20 +882,22 @@ class GroupchatControl(ChatControlBase):
print_status = None
for bookmark in gajim.connections[self.account].bookmarks:
if bookmark['jid'] == self.room_jid:
print_status = bookmark['print_status']
print_status = bookmark.get('print_status', None)
break
if print_status is None:
if not print_status:
print_status = gajim.config.get('print_status_in_muc')
nick_jid = nick
if jid:
nick_jid += ' (%s)' % jid
# delete ressource
simple_jid = gajim.get_jid_without_resource(jid)
nick_jid += ' (%s)' % simple_jid
if show == 'offline' and print_status in ('all', 'in_and_out'):
st = _('%s has left') % nick_jid
if reason:
st += ' [%s]' % reason
else:
if newly_created and print_status in ('all', 'in_and_out'):
st = _('%s has joined the room') % nick_jid
st = _('%s has joined the group chat') % nick_jid
elif print_status == 'all':
st = _('%s is now %s') % (nick_jid, helpers.get_uf_show(show))
if st:
@ -881,9 +924,9 @@ class GroupchatControl(ChatControlBase):
role_iter = self.get_role_iter(role)
if not role_iter:
role_iter = model.append(None,
(gajim.interface.roster.jabber_state_images['16']['closed'], 'role',
role, '<b>%s</b>' % role_name, None))
iter = model.append(role_iter, (None, 'contact', nick, name, None))
(gajim.interface.roster.jabber_state_images['16']['closed'], role,
'role', '<b>%s</b>' % role_name, None))
iter = model.append(role_iter, (None, nick, 'contact', name, None))
if not nick in gajim.contacts.get_nick_list(self.account, self.room_jid):
gc_contact = gajim.contacts.create_gc_contact(room_jid = self.room_jid,
name = nick, show = show, status = status, role = role,
@ -892,7 +935,7 @@ class GroupchatControl(ChatControlBase):
self.draw_contact(nick)
self.draw_avatar(nick)
# Do not ask avatar to irc rooms as irc transports reply with messages
r, server = gajim.get_room_name_and_server_from_room_jid(self.room_jid)
server = gajim.get_server_from_jid(self.room_jid)
if gajim.config.get('ask_avatars_on_startup') and \
not server.startswith('irc'):
fjid = self.room_jid + '/' + nick
@ -925,7 +968,8 @@ class GroupchatControl(ChatControlBase):
iter = self.get_contact_iter(nick)
if not iter:
return
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
nick)
if gc_contact:
gajim.contacts.remove_gc_contact(self.account, gc_contact)
parent_iter = model.iter_parent(iter)
@ -1005,8 +1049,10 @@ class GroupchatControl(ChatControlBase):
new_topic = message_array.pop(0)
gajim.connections[self.account].send_gc_subject(self.room_jid,
new_topic)
else:
elif self.subject is not '':
self.print_conversation(self.subject, 'info')
else:
self.print_conversation(_('This group chat has no subject'), 'info')
self.clear(self.msg_textview)
return True
elif command == 'invite':
@ -1034,15 +1080,13 @@ class GroupchatControl(ChatControlBase):
elif command == 'join':
# example: /join room@conference.example.com/nick
if len(message_array):
message_array = message_array[0]
if message_array.find('@') >= 0:
room, servernick = message_array.split('@')
if servernick.find('/') >= 0:
server, nick = servernick.split('/', 1)
room_jid = message_array[0]
if room_jid.find('@') >= 0:
if room_jid.find('/') >= 0:
room_jid, nick = room_jid.split('/', 1)
else:
server = servernick
nick = ''
#join_gc window is needed in order to provide for password entry.
# join_gc window is needed in order to provide for password entry.
if gajim.interface.instances[self.account].has_key('join_gc'):
gajim.interface.instances[self.account]['join_gc'].\
window.present()
@ -1050,13 +1094,13 @@ class GroupchatControl(ChatControlBase):
try:
gajim.interface.instances[self.account]['join_gc'] =\
dialogs.JoinGroupchatWindow(self.account,
server = server, room = room, nick = nick)
except RuntimeError:
room_jid = room_jid, nick = nick)
except GajimGeneralException:
pass
self.clear(self.msg_textview)
else:
#%s is something the user wrote but it is not a jid so we inform
s = _('%s does not appear to be a valid JID') % message_array
s = _('%s does not appear to be a valid JID') % message_array[0]
self.print_conversation(s, 'info')
else:
self.get_command_help(command)
@ -1066,21 +1110,21 @@ class GroupchatControl(ChatControlBase):
reason = 'offline'
if len(message_array):
reason = message_array.pop(0)
self.parent_win.remove_tab(self,reason)
self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason)
self.clear(self.msg_textview)
return True
elif command == 'ban':
if len(message_array):
message_array = message_array[0].split()
nick = message_array.pop(0)
room_nicks = gajim.contacts.get_nick_list(self.account, self.room_jid)
room_nicks = gajim.contacts.get_nick_list(self.account,
self.room_jid)
reason = ' '.join(message_array)
if nick in room_nicks:
ban_jid = gajim.construct_fjid(self.room_jid, nick)
gajim.connections[self.account].gc_set_affiliation(self.room_jid,
ban_jid, 'outcast', reason)
self.clear(self.msg_textview)
elif nick.find('@') >= 0:
gc_contact = gajim.contacts.get_gc_contact(self.account,
self.room_jid, nick)
nick = gc_contact.jid
if nick.find('@') >= 0:
gajim.connections[self.account].gc_set_affiliation(self.room_jid,
nick, 'outcast', reason)
self.clear(self.msg_textview)
@ -1094,7 +1138,8 @@ class GroupchatControl(ChatControlBase):
if len(message_array):
message_array = message_array[0].split()
nick = message_array.pop(0)
room_nicks = gajim.contacts.get_nick_list(self.account, self.room_jid)
room_nicks = gajim.contacts.get_nick_list(self.account,
self.room_jid)
reason = ' '.join(message_array)
if nick in room_nicks:
gajim.connections[self.account].gc_set_role(self.room_jid, nick,
@ -1154,7 +1199,8 @@ class GroupchatControl(ChatControlBase):
if not self._process_command(message):
# Send the message
gajim.connections[self.account].send_gc_message(self.room_jid, message)
gajim.connections[self.account].send_gc_message(self.room_jid,
message)
self.msg_textview.get_buffer().set_text('')
self.msg_textview.grab_focus()
@ -1162,10 +1208,11 @@ class GroupchatControl(ChatControlBase):
if command == 'help':
self.print_conversation(_('Commands: %s') % self.muc_cmds, 'info')
elif command == 'ban':
s = _('Usage: /%s <nickname|JID> [reason], bans the JID from the room.'
s = _('Usage: /%s <nickname|JID> [reason], bans the JID from the group chat.'
' The nickname of an occupant may be substituted, but not if it '
'contains "@". If the JID is currently in the room, he/she/it will '
'also be kicked. Does NOT support spaces in nickname.') % command
'contains "@". If the JID is currently in the group chat, '
'he/she/it will also be kicked. Does NOT support spaces in '
'nickname.') % command
self.print_conversation(s, 'info')
elif command == 'chat' or command == 'query':
self.print_conversation(_('Usage: /%s <nickname>, opens a private chat'
@ -1177,10 +1224,11 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(_('Usage: /%s [reason], closes the current '
'window or tab, displaying reason if specified.') % command, 'info')
elif command == 'compact':
self.print_conversation(_('Usage: /%s, hide the chat buttons.') % command, 'info')
self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \
command, 'info')
elif command == 'invite':
self.print_conversation(_('Usage: /%s <JID> [reason], invites JID to '
'the current room, optionally providing a reason.') % command,
'the current group chat, optionally providing a reason.') % command,
'info')
elif command == 'join':
self.print_conversation(_('Usage: /%s <room>@<server>[/nickname], '
@ -1188,12 +1236,12 @@ class GroupchatControl(ChatControlBase):
% command, 'info')
elif command == 'kick':
self.print_conversation(_('Usage: /%s <nickname> [reason], removes '
'the occupant specified by nickname from the room and optionally '
'displays a reason. Does NOT support spaces in nickname.') % \
command, 'info')
'the occupant specified by nickname from the group chat and '
'optionally displays a reason. Does NOT support spaces in '
'nickname.') % command, 'info')
elif command == 'me':
self.print_conversation(_('Usage: /%s <action>, sends action to the '
'current room. Use third person. (e.g. /%s explodes.)') % \
'current group chat. Use third person. (e.g. /%s explodes.)') % \
(command, command), 'info')
elif command == 'msg':
s = _('Usage: /%s <nickname> [message], opens a private message window'
@ -1201,16 +1249,16 @@ class GroupchatControl(ChatControlBase):
command
self.print_conversation(s, 'info')
elif command == 'nick':
s = _('Usage: /%s <nickname>, changes your nickname in current room.')\
% command
s = _('Usage: /%s <nickname>, changes your nickname in current group '
'chat.') % command
self.print_conversation(s, 'info')
elif command == 'names':
s = _('Usage: /%s , display the names of room occupants.')\
s = _('Usage: /%s , display the names of group chat occupants.')\
% command
self.print_conversation(s, 'info')
elif command == 'topic':
self.print_conversation(_('Usage: /%s [topic], displays or updates the'
' current room topic.') % command, 'info')
' current group chat topic.') % command, 'info')
elif command == 'say':
self.print_conversation(_('Usage: /%s <message>, sends a message '
'without looking for other commands.') % command, 'info')
@ -1218,7 +1266,8 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(_('No help info for /%s') % command, 'info')
def get_role(self, nick):
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
nick)
if gc_contact:
return gc_contact.role
else:
@ -1249,7 +1298,13 @@ class GroupchatControl(ChatControlBase):
self.handlers[i].disconnect(i)
del self.handlers[i]
def allow_shutdown(self):
def allow_shutdown(self, method):
'''If check_selection is True, '''
if method == self.parent_win.CLOSE_ESC:
model, iter = self.list_treeview.get_selection().get_selected()
if iter:
self.list_treeview.get_selection().unselect_all()
return False
retval = True
includes = gajim.config.get('confirm_close_muc_rooms').split(' ')
excludes = gajim.config.get('noconfirm_close_muc_rooms').split(' ')
@ -1257,9 +1312,10 @@ class GroupchatControl(ChatControlBase):
if (gajim.config.get('confirm_close_muc') or self.room_jid in includes) \
and gajim.gc_connected[self.account][self.room_jid] and self.room_jid not\
in excludes:
pritext = _('Are you sure you want to leave room "%s"?') % self.name
pritext = _('Are you sure you want to leave group chat "%s"?')\
% self.name
sectext = _('If you close this window, you will be disconnected '
'from this room.')
'from this group chat.')
dialog = dialogs.ConfirmationDialogCheck(pritext, sectext,
_('Do _not ask me again'))
@ -1275,6 +1331,7 @@ class GroupchatControl(ChatControlBase):
return retval
def set_control_active(self, state):
self.conv_textview.allow_focus_out_line = True
self.attention_flag = False
ChatControlBase.set_control_active(self, state)
if not state:
@ -1299,7 +1356,8 @@ class GroupchatControl(ChatControlBase):
_('Please specify the new subject:'), self.subject)
response = instance.get_response()
if response == gtk.RESPONSE_OK:
# Note, we don't update self.subject since we don't know whether it will work yet
# Note, we don't update self.subject since we don't know whether it
# will work yet
subject = instance.input_entry.get_text().decode('utf-8')
gajim.connections[self.account].send_gc_subject(self.room_jid, subject)
@ -1324,15 +1382,14 @@ class GroupchatControl(ChatControlBase):
'jid': self.room_jid,
'autojoin': '0',
'password': '',
'nick': self.nick,
'print_status' : gajim.config.get('print_status_in_muc')
'nick': self.nick
}
for bookmark in gajim.connections[self.account].bookmarks:
if bookmark['jid'] == bm['jid']:
dialogs.ErrorDialog(
_('Bookmark already set'),
_('Room "%s" is already in your bookmarks.') % bm['jid'])
_('Group Chat "%s" is already in your bookmarks.') % bm['jid'])
return
gajim.connections[self.account].bookmarks.append(bm)
@ -1344,7 +1401,8 @@ class GroupchatControl(ChatControlBase):
_('Bookmark has been added successfully'),
_('You can manage your bookmarks via Actions menu in your roster.'))
def handle_message_textview_mykey_press(self, widget, event_keyval, event_keymod):
def handle_message_textview_mykey_press(self, widget, event_keyval,
event_keymod):
# NOTE: handles mykeypress which is custom signal connected to this
# CB in new_room(). for this singal see message_textview.py
@ -1356,17 +1414,30 @@ class GroupchatControl(ChatControlBase):
message_buffer = widget.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8')
message = message_buffer.get_text(start_iter, end_iter, False).decode(
'utf-8')
if event.keyval == gtk.keysyms.Tab: # TAB
cursor_position = message_buffer.get_insert()
end_iter = message_buffer.get_iter_at_mark(cursor_position)
text = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8')
if text.endswith(' '):
if not self.last_key_tabs:
return False
text = message_buffer.get_text(start_iter, end_iter, False).decode(
'utf-8')
splitted_text = text.split()
# topic completion
splitted_text2 = text.split(None, 1)
if text.startswith('/topic '):
if len(splitted_text2) == 2 and \
self.subject.startswith(splitted_text2[1]) and\
len(self.subject) > len(splitted_text2[1]):
message_buffer.insert_at_cursor(
self.subject[len(splitted_text2[1]):])
return True
elif len(splitted_text2) == 1 and text.startswith('/topic '):
message_buffer.delete(start_iter, end_iter)
message_buffer.insert_at_cursor('/topic '+self.subject)
return True
# command completion
if text.startswith('/') and len(splitted_text) == 1:
text = splitted_text[0]
@ -1435,7 +1506,11 @@ class GroupchatControl(ChatControlBase):
def on_list_treeview_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.Escape:
widget.get_selection().unselect_all()
selection = widget.get_selection()
model, iter = selection.get_selected()
if iter:
widget.get_selection().unselect_all()
return True
def on_list_treeview_row_expanded(self, widget, iter, path):
'''When a row is expanded: change the icon of the arrow'''
@ -1473,8 +1548,8 @@ class GroupchatControl(ChatControlBase):
# looking for user's affiliation and role
user_nick = self.nick
user_affiliation = gajim.contacts.get_gc_contact(self.account, self.room_jid,
user_nick).affiliation
user_affiliation = gajim.contacts.get_gc_contact(self.account,
self.room_jid, user_nick).affiliation
user_role = self.get_role(user_nick)
# making menu from glade
@ -1483,9 +1558,10 @@ class GroupchatControl(ChatControlBase):
# these conditions were taken from JEP 0045
item = xml.get_widget('kick_menuitem')
if user_role != 'moderator' or \
(user_affiliation == 'admin' and target_affiliation == 'owner') or \
(user_affiliation == 'member' and target_affiliation in ('admin', 'owner')) or \
(user_affiliation == 'none' and target_affiliation != 'none'):
(user_affiliation == 'admin' and target_affiliation == 'owner') or \
(user_affiliation == 'member' and target_affiliation in ('admin',
'owner')) or (user_affiliation == 'none' and target_affiliation != \
'none'):
item.set_sensitive(False)
id = item.connect('activate', self.kick, nick)
self.handlers[id] = item
@ -1493,18 +1569,18 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('voice_checkmenuitem')
item.set_active(target_role != 'visitor')
if user_role != 'moderator' or \
user_affiliation == 'none' or \
(user_affiliation=='member' and target_affiliation!='none') or \
target_affiliation in ('admin', 'owner'):
user_affiliation == 'none' or \
(user_affiliation=='member' and target_affiliation!='none') or \
target_affiliation in ('admin', 'owner'):
item.set_sensitive(False)
id = item.connect('activate', self.on_voice_checkmenuitem_activate,
nick)
nick)
self.handlers[id] = item
item = xml.get_widget('moderator_checkmenuitem')
item.set_active(target_role == 'moderator')
if not user_affiliation in ('admin', 'owner') or \
target_affiliation in ('admin', 'owner'):
target_affiliation in ('admin', 'owner'):
item.set_sensitive(False)
id = item.connect('activate', self.on_moderator_checkmenuitem_activate,
nick)
@ -1512,8 +1588,8 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('ban_menuitem')
if not user_affiliation in ('admin', 'owner') or \
(target_affiliation in ('admin', 'owner') and\
user_affiliation != 'owner'):
(target_affiliation in ('admin', 'owner') and\
user_affiliation != 'owner'):
item.set_sensitive(False)
id = item.connect('activate', self.ban, jid)
self.handlers[id] = item
@ -1521,7 +1597,7 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('member_checkmenuitem')
item.set_active(target_affiliation != 'none')
if not user_affiliation in ('admin', 'owner') or \
(user_affiliation != 'owner' and target_affiliation in ('admin','owner')):
(user_affiliation != 'owner' and target_affiliation in ('admin','owner')):
item.set_sensitive(False)
id = item.connect('activate', self.on_member_checkmenuitem_activate,
jid)
@ -1711,19 +1787,23 @@ class GroupchatControl(ChatControlBase):
def grant_voice(self, widget, nick):
'''grant voice privilege to a user'''
gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant')
gajim.connections[self.account].gc_set_role(self.room_jid, nick,
'participant')
def revoke_voice(self, widget, nick):
'''revoke voice privilege to a user'''
gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'visitor')
gajim.connections[self.account].gc_set_role(self.room_jid, nick,
'visitor')
def grant_moderator(self, widget, nick):
'''grant moderator privilege to a user'''
gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'moderator')
gajim.connections[self.account].gc_set_role(self.room_jid, nick,
'moderator')
def revoke_moderator(self, widget, nick):
'''revoke moderator privilege to a user'''
gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant')
gajim.connections[self.account].gc_set_role(self.room_jid, nick,
'participant')
def ban(self, widget, jid):
'''ban a user'''
@ -1738,17 +1818,17 @@ class GroupchatControl(ChatControlBase):
else:
return # stop banning procedure
gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid,
'outcast', reason)
'outcast', reason)
def grant_membership(self, widget, jid):
'''grant membership privilege to a user'''
gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid,
'member')
'member')
def revoke_membership(self, widget, jid):
'''revoke membership privilege to a user'''
gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid,
'none')
'none')
def grant_admin(self, widget, jid):
'''grant administrative privilege to a user'''
@ -1772,10 +1852,11 @@ class GroupchatControl(ChatControlBase):
c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
c2 = gajim.contacts.contact_from_gc_contact(c)
if gajim.interface.instances[self.account]['infos'].has_key(c2.jid):
gajim.interface.instances[self.account]['infos'][c2.jid].window.present()
gajim.interface.instances[self.account]['infos'][c2.jid].window.\
present()
else:
gajim.interface.instances[self.account]['infos'][c2.jid] = \
vcard.VcardWindow(c2, self.account, is_fake = True)
vcard.VcardWindow(c2, self.account, c)
def on_history(self, widget, nick):
jid = gajim.construct_fjid(self.room_jid, nick)

View file

@ -1,18 +1,7 @@
## gtkexcepthook.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
##
## Initially written and submitted by Gustavo J. A. M. Carneiro
##
@ -33,6 +22,7 @@ import threading
import gtk
import pango
from common import i18n
import dialogs
from cStringIO import StringIO

View file

@ -2,7 +2,7 @@
##
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2004-2005 Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <nkour@jabber.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005 Travis Shirk <travis@pobox.com>
## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za>
@ -169,30 +169,6 @@ def get_default_font():
return None
def reduce_chars_newlines(text, max_chars = 0, max_lines = 0):
'''Cut the chars after 'max_chars' on each line
and show only the first 'max_lines'.
If any of the params is not present (None or 0) the action
on it is not performed'''
def _cut_if_long(str):
if len(str) > max_chars:
str = str[:max_chars - 3] + '...'
return str
if max_lines == 0:
lines = text.split('\n')
else:
lines = text.split('\n', max_lines)[:max_lines]
if max_chars > 0:
if lines:
lines = map(lambda e: _cut_if_long(e), lines)
if lines:
reduced_text = reduce(lambda e, e1: e + '\n' + e1, lines)
else:
reduced_text = ''
return reduced_text
def escape_for_pango_markup(string):
# escapes < > & ' "
# for pango markup not to break
@ -207,7 +183,30 @@ def escape_for_pango_markup(string):
return escaped_str
def autodetect_browser_mailer():
# recognize the environment for appropriate browser/mailer
# recognize the environment and set appropriate browser/mailer
if user_runs_gnome():
gajim.config.set('openwith', 'gnome-open')
elif user_runs_kde():
gajim.config.set('openwith', 'kfmclient exec')
elif user_runs_xfce():
gajim.config.set('openwith', 'exo-open')
else:
gajim.config.set('openwith', 'custom')
def user_runs_gnome():
return 'gnome-session' in get_running_processes()
def user_runs_kde():
return 'startkde' in get_running_processes()
def user_runs_xfce():
procs = get_running_processes()
if 'startxfce4' in procs or 'xfce4-session' in procs:
return True
return False
def get_running_processes():
'''returns running processes or None (if not /proc exists)'''
if os.path.isdir('/proc'):
# under Linux: checking if 'gnome-session' or
# 'startkde' programs were run before gajim, by
@ -237,12 +236,9 @@ def autodetect_browser_mailer():
# list of processes
processes = [os.path.basename(os.readlink('/proc/' + f +'/exe')) for f in files]
if 'gnome-session' in processes:
gajim.config.set('openwith', 'gnome-open')
elif 'startkde' in processes:
gajim.config.set('openwith', 'kfmclient exec')
else:
gajim.config.set('openwith', 'custom')
return processes
return []
def move_window(window, x, y):
'''moves the window but also checks if out of screen'''
@ -390,7 +386,7 @@ def possibly_move_window_in_current_desktop(window):
current virtual desktop
window is GTK window'''
if os.name == 'nt':
return
return False
root_window = gtk.gdk.screen_get_default().get_root_window()
# current user's vd
@ -406,6 +402,8 @@ def possibly_move_window_in_current_desktop(window):
# we are in another VD that the window was
# so show it in current VD
window.present()
return True
return False
def file_is_locked(path_to_file):
'''returns True if file is locked (WINDOWS ONLY)'''
@ -680,6 +678,14 @@ default_name = ''):
file_path = dialog.get_filename()
file_path = decode_filechooser_file_paths((file_path,))[0]
if os.path.exists(file_path):
# check if we have write permissions
if not os.access(file_path, os.W_OK):
file_name = os.path.basename(file_path)
dialogs.ErrorDialog(_('Cannot overwrite existing file "%s"' %
file_name),
_('A file with this name already exists and you do not have '
'permission to overwrite it.'))
return
dialog2 = dialogs.FTOverwriteConfirmationDialog(
_('This file already exists'), _('What do you want to do?'),
False)
@ -688,6 +694,13 @@ default_name = ''):
response = dialog2.get_response()
if response < 0:
return
else:
dirname = os.path.dirname(file_path)
if not os.access(dirname, os.W_OK):
dialogs.ErrorDialog(_('Directory "%s" is not writable') % \
dirname, _('You do not have permission to create files in this'
' directory.'))
return
# Get pixbuf
pixbuf = None
@ -710,8 +723,8 @@ default_name = ''):
try:
pixbuf.save(file_path, type_)
except:
#XXX Check for permissions
os.remove(file_path)
if os.path.exists(file_path):
os.remove(file_path)
new_file_path = '.'.join(file_path.split('.')[:-1]) + '.jpeg'
dialog2 = dialogs.ConfirmationDialog(_('Extension not supported'),
_('Image cannot be saved in %(type)s format. Save as %(new_filename)s?') % {'type': type_, 'new_filename': new_file_path},
@ -735,3 +748,6 @@ default_name = ''):
dialog.set_current_name(default_name)
dialog.connect('delete-event', lambda widget, event:
on_cancel(widget))
def on_bm_header_changed_state(widget, event):
widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state

View file

@ -1,7 +1,4 @@
#!/bin/sh
''':'
exec python -OOt "$0" ${1+"$@"}
' '''
#!/usr/bin/env python
## history_manager.py
##
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
@ -33,6 +30,9 @@ import dialogs
import gtkgui_helpers
from common.logger import LOG_DB_PATH, constants
#FIXME: constants should implement 2 way mappings
status = dict((constants.__dict__[i], i[5:].lower()) for i in \
constants.__dict__.keys() if i.startswith('SHOW_'))
from common import gajim
from common import helpers
@ -44,14 +44,17 @@ C_SUBJECT,
C_NICKNAME
) = range(2, 6)
try:
from pysqlite2 import dbapi2 as sqlite
import sqlite3 as sqlite # python 2.5
except ImportError:
raise exceptions.PysqliteNotAvailable
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
raise exceptions.PysqliteNotAvailable
class HistoryManager:
def __init__(self):
path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
pix = gtk.gdk.pixbuf_new_from_file(path_to_file)
@ -193,7 +196,9 @@ class HistoryManager:
gtk.main_quit()
def _fill_jids_listview(self):
self.cur.execute('SELECT jid, jid_id FROM jids ORDER BY jid')
# get those jids that have at least one entry in logs
self.cur.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN (SELECT '
'distinct logs.jid_id FROM logs) ORDER BY jid')
rows = self.cur.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
for row in rows:
self.jids_already_in.append(row[0]) # jid
@ -310,6 +315,7 @@ class HistoryManager:
except ValueError:
pass
else:
color = None
if kind in (constants.KIND_SINGLE_MSG_RECV,
constants.KIND_CHAT_MSG_RECV, constants.KIND_GC_MSG):
# it is the other side
@ -325,11 +331,14 @@ class HistoryManager:
message = ''
else:
message = ' : ' + message
message = helpers.get_uf_show(show) + message
message = '<span foreground="%s">%s</span>' % (color,
gtkgui_helpers.escape_for_pango_markup(message))
self.logs_liststore.append((log_line_id, jid_id, time_, message,
message = helpers.get_uf_show(gajim.SHOW_LIST[show]) + message
message_ = '<span'
if color:
message_ += ' foreground="%s"' % color
message_ += '>%s</span>' % \
gtkgui_helpers.escape_for_pango_markup(message)
self.logs_liststore.append((log_line_id, jid_id, time_, message_,
subject, nickname))
def _fill_search_results_listview(self, text):

View file

@ -8,7 +8,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -59,7 +59,8 @@ class HistoryWindow:
self.calendar = xml.get_widget('calendar')
scrolledwindow = xml.get_widget('scrolledwindow')
self.history_textview = conversation_textview.ConversationTextview(account)
self.history_textview = conversation_textview.ConversationTextview(
account, used_in_history_window = True)
scrolledwindow.add(self.history_textview.tv)
self.history_buffer = self.history_textview.tv.get_buffer()
self.history_buffer.create_tag('highlight', background = 'yellow')
@ -209,7 +210,9 @@ class HistoryWindow:
if gajim.config.get('print_time') == 'always':
before_str = gajim.config.get('before_time')
before_str = helpers.from_one_line(before_str)
after_str = gajim.config.get('after_time')
after_str = helpers.from_one_line(after_str)
format = before_str + '%X' + after_str + ' '
tim = time.strftime(format, time.localtime(float(tim)))
buf.insert(end_iter, tim) # add time
@ -277,7 +280,9 @@ class HistoryWindow:
if contact_name and kind != constants.KIND_GCSTATUS:
# add stuff before and after contact name
before_str = gajim.config.get('before_nickname')
before_str = helpers.from_one_line(before_str)
after_str = gajim.config.get('after_nickname')
after_str = helpers.from_one_line(after_str)
format = before_str + contact_name + after_str + ' '
buf.insert_with_tags_by_name(end_iter, format, tag_name)

963
src/htmltextview.py Normal file
View file

@ -0,0 +1,963 @@
### Copyright (C) 2005 Gustavo J. A. M. Carneiro
### Copyright (C) 2006 Santiago Gala
###
### This library is free software; you can redistribute it and/or
### modify it under the terms of the GNU Lesser General Public
### License as published by the Free Software Foundation; either
### version 2 of the License, or (at your option) any later version.
###
### This library 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
### Lesser General Public License for more details.
###
### You should have received a copy of the GNU Lesser General Public
### License along with this library; if not, write to the
### Free Software Foundation, Inc., 59 Temple Place - Suite 330,
### Boston, MA 02111-1307, USA.
"""
A gtk.TextView-based renderer for XHTML-IM, as described in:
http://www.jabber.org/jeps/jep-0071.html
Starting with the version posted by Gustavo Carneiro,
I (Santiago Gala) am trying to make it more compatible
with the markup that docutils generate, and also more
modular.
"""
import gobject
import pango
import gtk
import xml.sax, xml.sax.handler
import re
import warnings
from cStringIO import StringIO
import urllib2
import operator
from common import gajim
#from common import i18n
import tooltips
__all__ = ['HtmlTextView']
whitespace_rx = re.compile("\\s+")
allwhitespace_rx = re.compile("^\\s*$")
## pixels = points * display_resolution
display_resolution = 0.3514598*(gtk.gdk.screen_height() /
float(gtk.gdk.screen_height_mm()))
#embryo of CSS classes
classes = {
#'system-message':';display: none',
'problematic':';color: red',
}
#styles for elemens
element_styles = {
'u' : ';text-decoration: underline',
'em' : ';font-style: oblique',
'cite' : '; background-color:rgb(170,190,250); font-style: oblique',
'li' : '; margin-left: 1em; margin-right: 10%',
'strong' : ';font-weight: bold',
'pre' : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%',
'kbd' : ';background-color:rgb(210,210,210);font-family: monospace',
'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%',
'dt' : ';font-weight: bold; font-style: oblique',
'dd' : ';margin-left: 2em; font-style: oblique'
}
# no difference for the moment
element_styles['dfn'] = element_styles['em']
element_styles['var'] = element_styles['em']
# deprecated, legacy, presentational
element_styles['tt'] = element_styles['kbd']
element_styles['i'] = element_styles['em']
element_styles['b'] = element_styles['strong']
class_styles = {
}
"""
==========
JEP-0071
==========
This Integration Set includes a subset of the modules defined for
XHTML 1.0 but does not redefine any existing modules, nor
does it define any new modules. Specifically, it includes the
following modules only:
- Structure
- Text
* Block
phrasal
addr, blockquote, pre
Struc
div,p
Heading
h1, h2, h3, h4, h5, h6
* Inline
phrasal
abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
structural
br, span
- Hypertext (a)
- List (ul, ol, dl)
- Image (img)
- Style Attribute
Therefore XHTML-IM uses the following content models:
Block.mix
Block-like elements, e.g., paragraphs
Flow.mix
Any block or inline elements
Inline.mix
Character-level elements
InlineNoAnchor.class
Anchor element
InlinePre.mix
Pre element
XHTML-IM also uses the following Attribute Groups:
Core.extra.attrib
TBD
I18n.extra.attrib
TBD
Common.extra
style
...
#block level:
#Heading h
# ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
#Block ( phrasal = address | blockquote | pre )
#NOT ( presentational = hr )
# ( structural = div | p )
#other: section
#Inline ( phrasal = abbr | acronym | cite | code | dfn | em | kbd | q | samp | strong | var )
#NOT ( presentational = b | big | i | small | sub | sup | tt )
# ( structural = br | span )
#Param/Legacy param, font, basefont, center, s, strike, u, dir, menu, isindex
#
"""
BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ))
BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', ))
BLOCK_PRES = set(( 'hr', )) #not in xhtml-im
BLOCK_STRUCT = set(( 'div', 'p', ))
BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;)
BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS)
INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', '))
INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im
INLINE_STRUCT = set('br, span'.split(', '))
INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT)
LIST_ELEMS = set( 'dl, ol, ul'.split(', '))
for name in BLOCK_HEAD:
num = eval(name[1])
size = (num-1) // 2
weigth = (num - 1) % 2
element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size],
('font-weight: bold', 'font-style: oblique')[weigth],
)
def build_patterns(view, config, interface):
#extra, rst does not mark _underline_ or /it/ up
#actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild
basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\
r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
view.basic_pattern_re = re.compile(basic_pattern)
#TODO: emoticons
emoticons_pattern = ''
if config.get('emoticons_theme'):
# When an emoticon is bordered by an alpha-numeric character it is NOT
# expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc.
# We still allow multiple emoticons side-by-side like :P:P:P
# sort keys by length so :qwe emot is checked before :q
keys = interface.emoticons.keys()
keys.sort(interface.on_emoticon_sort)
emoticons_pattern_prematch = ''
emoticons_pattern_postmatch = ''
emoticon_length = 0
for emoticon in keys: # travel thru emoticons list
emoticon_escaped = re.escape(emoticon) # espace regexp metachars
emoticons_pattern += emoticon_escaped + '|'# | means or in regexp
if (emoticon_length != len(emoticon)):
# Build up expressions to match emoticons next to other emoticons
emoticons_pattern_prematch = emoticons_pattern_prematch[:-1] + ')|(?<='
emoticons_pattern_postmatch = emoticons_pattern_postmatch[:-1] + ')|(?='
emoticon_length = len(emoticon)
emoticons_pattern_prematch += emoticon_escaped + '|'
emoticons_pattern_postmatch += emoticon_escaped + '|'
# We match from our list of emoticons, but they must either have
# whitespace, or another emoticon next to it to match successfully
# [\w.] alphanumeric and dot (for not matching 8) in (2.8))
emoticons_pattern = '|' + \
'(?:(?<![\w.]' + emoticons_pattern_prematch[:-1] + '))' + \
'(?:' + emoticons_pattern[:-1] + ')' + \
'(?:(?![\w.]' + emoticons_pattern_postmatch[:-1] + '))'
# because emoticons match later (in the string) they need to be after
# basic matches that may occur earlier
emot_and_basic_pattern = basic_pattern + emoticons_pattern
view.emot_and_basic_re = re.compile(emot_and_basic_pattern, re.IGNORECASE)
def _parse_css_color(color):
'''_parse_css_color(css_color) -> gtk.gdk.Color'''
if color.startswith("rgb(") and color.endswith(')'):
r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
return gtk.gdk.Color(r, g, b)
else:
return gtk.gdk.color_parse(color)
class HtmlHandler(xml.sax.handler.ContentHandler):
def __init__(self, textview, startiter):
xml.sax.handler.ContentHandler.__init__(self)
self.textbuf = textview.get_buffer()
self.textview = textview
self.iter = startiter
self.text = ''
self.starting=True
self.preserve = False
self.styles = [] # a gtk.TextTag or None, for each span level
self.list_counters = [] # stack (top at head) of list
# counters, or None for unordered list
def _parse_style_color(self, tag, value):
color = _parse_css_color(value)
tag.set_property("foreground-gdk", color)
def _parse_style_background_color(self, tag, value):
color = _parse_css_color(value)
tag.set_property("background-gdk", color)
if gtk.gtk_version >= (2, 8):
tag.set_property("paragraph-background-gdk", color)
if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):
def _get_current_attributes(self):
attrs = self.textview.get_default_attributes()
self.iter.backward_char()
self.iter.get_attributes(attrs)
self.iter.forward_char()
return attrs
else:
## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
def _get_current_style_attr(self, propname, comb_oper=None):
tags = [tag for tag in self.styles if tag is not None]
tags.reverse()
is_set_name = propname + "-set"
value = None
for tag in tags:
if tag.get_property(is_set_name):
if value is None:
value = tag.get_property(propname)
if comb_oper is None:
return value
else:
value = comb_oper(value, tag.get_property(propname))
return value
class _FakeAttrs(object):
__slots__ = ("font", "font_scale")
def _get_current_attributes(self):
attrs = self._FakeAttrs()
attrs.font_scale = self._get_current_style_attr("scale",
operator.mul)
if attrs.font_scale is None:
attrs.font_scale = 1.0
attrs.font = self._get_current_style_attr("font-desc")
if attrs.font is None:
attrs.font = self.textview.style.font_desc
return attrs
def __parse_length_frac_size_allocate(self, textview, allocation,
frac, callback, args):
callback(allocation.width*frac, *args)
def _parse_length(self, value, font_relative, callback, *args):
'''Parse/calc length, converting to pixels, calls callback(length, *args)
when the length is first computed or changes'''
if value.endswith('%'):
frac = float(value[:-1])/100
if font_relative:
attrs = self._get_current_attributes()
font_size = attrs.font.get_size() / pango.SCALE
callback(frac*display_resolution*font_size, *args)
else:
## CSS says "Percentage values: refer to width of the closest
## block-level ancestor"
## This is difficult/impossible to implement, so we use
## textview width instead; a reasonable approximation..
alloc = self.textview.get_allocation()
self.__parse_length_frac_size_allocate(self.textview, alloc,
frac, callback, args)
self.textview.connect("size-allocate",
self.__parse_length_frac_size_allocate,
frac, callback, args)
elif value.endswith('pt'): # points
callback(float(value[:-2])*display_resolution, *args)
elif value.endswith('em'): # ems, the height of the element's font
attrs = self._get_current_attributes()
font_size = attrs.font.get_size() / pango.SCALE
callback(float(value[:-2])*display_resolution*font_size, *args)
elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
## FIXME: figure out how to calculate this correctly
## for now 'em' size is used as approximation
attrs = self._get_current_attributes()
font_size = attrs.font.get_size() / pango.SCALE
callback(float(value[:-2])*display_resolution*font_size, *args)
elif value.endswith('px'): # pixels
callback(int(value[:-2]), *args)
else:
warnings.warn("Unable to parse length value '%s'" % value)
def __parse_font_size_cb(length, tag):
tag.set_property("size-points", length/display_resolution)
__parse_font_size_cb = staticmethod(__parse_font_size_cb)
def _parse_style_display(self, tag, value):
if value == 'none':
tag.set_property('invisible','true')
#Fixme: display: block, inline
def _parse_style_font_size(self, tag, value):
try:
scale = {
"xx-small": pango.SCALE_XX_SMALL,
"x-small": pango.SCALE_X_SMALL,
"small": pango.SCALE_SMALL,
"medium": pango.SCALE_MEDIUM,
"large": pango.SCALE_LARGE,
"x-large": pango.SCALE_X_LARGE,
"xx-large": pango.SCALE_XX_LARGE,
} [value]
except KeyError:
pass
else:
attrs = self._get_current_attributes()
tag.set_property("scale", scale / attrs.font_scale)
return
if value == 'smaller':
tag.set_property("scale", pango.SCALE_SMALL)
return
if value == 'larger':
tag.set_property("scale", pango.SCALE_LARGE)
return
self._parse_length(value, True, self.__parse_font_size_cb, tag)
def _parse_style_font_style(self, tag, value):
try:
style = {
"normal": pango.STYLE_NORMAL,
"italic": pango.STYLE_ITALIC,
"oblique": pango.STYLE_OBLIQUE,
} [value]
except KeyError:
warnings.warn("unknown font-style %s" % value)
else:
tag.set_property("style", style)
def __frac_length_tag_cb(self,length, tag, propname):
styles = self._get_style_tags()
if styles:
length += styles[-1].get_property(propname)
tag.set_property(propname, length)
#__frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
def _parse_style_margin_left(self, tag, value):
self._parse_length(value, False, self.__frac_length_tag_cb,
tag, "left-margin")
def _parse_style_margin_right(self, tag, value):
self._parse_length(value, False, self.__frac_length_tag_cb,
tag, "right-margin")
def _parse_style_font_weight(self, tag, value):
## TODO: missing 'bolder' and 'lighter'
try:
weight = {
'100': pango.WEIGHT_ULTRALIGHT,
'200': pango.WEIGHT_ULTRALIGHT,
'300': pango.WEIGHT_LIGHT,
'400': pango.WEIGHT_NORMAL,
'500': pango.WEIGHT_NORMAL,
'600': pango.WEIGHT_BOLD,
'700': pango.WEIGHT_BOLD,
'800': pango.WEIGHT_ULTRABOLD,
'900': pango.WEIGHT_HEAVY,
'normal': pango.WEIGHT_NORMAL,
'bold': pango.WEIGHT_BOLD,
} [value]
except KeyError:
warnings.warn("unknown font-style %s" % value)
else:
tag.set_property("weight", weight)
def _parse_style_font_family(self, tag, value):
tag.set_property("family", value)
def _parse_style_text_align(self, tag, value):
try:
align = {
'left': gtk.JUSTIFY_LEFT,
'right': gtk.JUSTIFY_RIGHT,
'center': gtk.JUSTIFY_CENTER,
'justify': gtk.JUSTIFY_FILL,
} [value]
except KeyError:
warnings.warn("Invalid text-align:%s requested" % value)
else:
tag.set_property("justification", align)
def _parse_style_text_decoration(self, tag, value):
if value == "none":
tag.set_property("underline", pango.UNDERLINE_NONE)
tag.set_property("strikethrough", False)
elif value == "underline":
tag.set_property("underline", pango.UNDERLINE_SINGLE)
tag.set_property("strikethrough", False)
elif value == "overline":
warnings.warn("text-decoration:overline not implemented")
tag.set_property("underline", pango.UNDERLINE_NONE)
tag.set_property("strikethrough", False)
elif value == "line-through":
tag.set_property("underline", pango.UNDERLINE_NONE)
tag.set_property("strikethrough", True)
elif value == "blink":
warnings.warn("text-decoration:blink not implemented")
else:
warnings.warn("text-decoration:%s not implemented" % value)
def _parse_style_white_space(self, tag, value):
if value == 'pre':
tag.set_property("wrap_mode", gtk.WRAP_NONE)
elif value == 'normal':
tag.set_property("wrap_mode", gtk.WRAP_WORD)
elif value == 'nowrap':
tag.set_property("wrap_mode", gtk.WRAP_NONE)
## build a dictionary mapping styles to methods, for greater speed
__style_methods = dict()
for style in ["background-color", "color", "font-family", "font-size",
"font-style", "font-weight", "margin-left", "margin-right",
"text-align", "text-decoration", "white-space", 'display' ]:
try:
method = locals()["_parse_style_%s" % style.replace('-', '_')]
except KeyError:
warnings.warn("Style attribute '%s' not yet implemented" % style)
else:
__style_methods[style] = method
del style
## --
def _get_style_tags(self):
return [tag for tag in self.styles if tag is not None]
def _create_url(self, href, title, type_, id_):
tag = self.textbuf.create_tag(id_)
if href and href[0] != '#':
tag.href = href
tag.type_ = type_ # to be used by the URL handler
tag.connect('event', self.textview.html_hyperlink_handler, 'url', href)
tag.set_property('foreground', '#0000ff')
tag.set_property('underline', pango.UNDERLINE_SINGLE)
tag.is_anchor = True
if title:
tag.title = title
return tag
def _begin_span(self, style, tag=None, id_=None):
if style is None:
self.styles.append(tag)
return None
if tag is None:
if id_:
tag = self.textbuf.create_tag(id_)
else:
tag = self.textbuf.create_tag()
for attr, val in [item.split(':', 1) for item in style.split(';') if len(item.strip())]:
attr = attr.strip().lower()
val = val.strip()
try:
method = self.__style_methods[attr]
except KeyError:
warnings.warn("Style attribute '%s' requested "
"but not yet implemented" % attr)
else:
method(self, tag, val)
self.styles.append(tag)
def _end_span(self):
self.styles.pop()
def _jump_line(self):
self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
self.starting = True
def _insert_text(self, text):
if self.starting and text != '\n':
self.starting = (text[-1] == '\n')
tags = self._get_style_tags()
if tags:
self.textbuf.insert_with_tags(self.iter, text, *tags)
else:
self.textbuf.insert(self.iter, text)
def _starts_line(self):
return self.starting or self.iter.starts_line()
def _flush_text(self):
if not self.text: return
text, self.text = self.text, ''
if not self.preserve:
text = text.replace('\n', ' ')
self.handle_specials(whitespace_rx.sub(' ', text))
else:
self._insert_text(text.strip("\n"))
def _anchor_event(self, tag, textview, event, iter, href, type_):
if event.type == gtk.gdk.BUTTON_PRESS:
self.textview.emit("url-clicked", href, type_)
return True
return False
def handle_specials(self, text):
index = 0
se = self.textview.config.get('show_ascii_formatting_chars')
if self.textview.config.get('emoticons_theme'):
iterator = self.textview.emot_and_basic_re.finditer(text)
else:
iterator = self.textview.basic_pattern_re.finditer(text)
for match in iterator:
start, end = match.span()
special_text = text[start:end]
if start != 0:
self._insert_text(text[index:start])
index = end # update index
#emoticons
possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
if self.textview.config.get('emoticons_theme') and \
possible_emot_ascii_caps in self.textview.interface.emoticons.keys():
#it's an emoticon
emot_ascii = possible_emot_ascii_caps
anchor = self.textbuf.create_child_anchor(self.iter)
img = gtk.Image()
img.set_from_file(self.textview.interface.emoticons[emot_ascii])
img.show()
# TODO: add alt/tooltip with the special_text (a11y)
self.textview.add_child_at_anchor(img, anchor)
else:
# now print it
if special_text.startswith('/'): # it's explicit italics
self.startElement('i', {})
elif special_text.startswith('_'): # it's explicit underline
self.startElement("u", {})
if se: self._insert_text(special_text[0])
self.handle_specials(special_text[1:-1])
if se: self._insert_text(special_text[0])
if special_text.startswith('_'): # it's explicit underline
self.endElement('u')
if special_text.startswith('/'): # it's explicit italics
self.endElement('i')
if index < len(text):
self._insert_text(text[index:])
def characters(self, content):
if self.preserve:
self.text += content
return
if allwhitespace_rx.match(content) is not None and self._starts_line():
return
self.text += content
self.starting = False
def startElement(self, name, attrs):
self._flush_text()
klass = [i for i in attrs.get('class',' ').split(' ') if i]
style = attrs.get('style','')
#Add styles defined for classes
#TODO: priority between class and style elements?
for k in klass:
if k in classes:
style += classes[k]
tag = None
#FIXME: if we want to use id, it needs to be unique across
# the whole textview, so we need to add something like the
# message-id to it.
#id_ = attrs.get('id',None)
id_ = None
if name == 'a':
#TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
href = attrs.get('href', None)
title = attrs.get('title', attrs.get('rel',href))
type_ = attrs.get('type', None)
tag = self._create_url(href, title, type_, id_)
elif name == 'blockquote':
cite = attrs.get('cite', None)
if cite:
tag = self.textbuf.create_tag(id_)
tag.title = title
tag.is_anchor = True
elif name in LIST_ELEMS:
style += ';margin-left: 2em'
if name in element_styles:
style += element_styles[name]
if style == '':
style = None
self._begin_span(style, tag, id_)
if name == 'br':
pass # handled in endElement
elif name == 'hr':
pass # handled in endElement
elif name in BLOCK:
if not self._starts_line():
self._jump_line()
if name == 'pre':
self.preserve = True
elif name == 'span':
pass
elif name in ('dl', 'ul'):
if not self._starts_line():
self._jump_line()
self.list_counters.append(None)
elif name == 'ol':
if not self._starts_line():
self._jump_line()
self.list_counters.append(0)
elif name == 'li':
if self.list_counters[-1] is None:
li_head = unichr(0x2022)
else:
self.list_counters[-1] += 1
li_head = "%i." % self.list_counters[-1]
self.text = ' '*len(self.list_counters)*4 + li_head + ' '
self._flush_text()
self.starting = True
elif name == 'dd':
self._jump_line()
elif name == 'dt':
if not self.starting:
self._jump_line()
elif name == 'img':
try:
## Max image size = 10 MB (to try to prevent DoS)
mem = urllib2.urlopen(attrs['src']).read(10*1024*1024)
## Caveat: GdkPixbuf is known not to be safe to load
## images from network... this program is now potentially
## hackable ;)
loader = gtk.gdk.PixbufLoader()
loader.write(mem); loader.close()
pixbuf = loader.get_pixbuf()
except Exception, ex:
gajim.log.debug(str('Error loading image'+ex))
pixbuf = None
alt = attrs.get('alt', "Broken image")
try:
loader.close()
except: pass
if pixbuf is not None:
tags = self._get_style_tags()
if tags:
tmpmark = self.textbuf.create_mark(None, self.iter, True)
self.textbuf.insert_pixbuf(self.iter, pixbuf)
if tags:
start = self.textbuf.get_iter_at_mark(tmpmark)
for tag in tags:
self.textbuf.apply_tag(tag, start, self.iter)
self.textbuf.delete_mark(tmpmark)
else:
self._insert_text("[IMG: %s]" % alt)
elif name == 'body' or name == 'html':
pass
elif name == 'a':
pass
elif name in INLINE:
pass
else:
warnings.warn("Unhandled element '%s'" % name)
def endElement(self, name):
endPreserving = False
newLine = False
if name == 'br':
newLine = True
elif name == 'hr':
#FIXME: plenty of unused attributes (width, height,...) :)
self._jump_line()
try:
self.textbuf.insert_pixbuf(self.iter, self.textview.focus_out_line_pixbuf)
#self._insert_text(u"\u2550"*40)
self._jump_line()
except Exception, e:
gajim.log.debug(str("Error in hr"+e))
elif name in LIST_ELEMS:
self.list_counters.pop()
elif name == 'li':
newLine = True
elif name == 'img':
pass
elif name == 'body' or name == 'html':
pass
elif name == 'a':
pass
elif name in INLINE:
pass
elif name in ('dd', 'dt', ):
pass
elif name in BLOCK:
if name == 'pre':
endPreserving = True
else:
warnings.warn("Unhandled element '%s'" % name)
self._flush_text()
if endPreserving:
self.preserve = False
if newLine:
self._jump_line()
self._end_span()
#if not self._starts_line():
# self.text = ' '
class HtmlTextView(gtk.TextView):
__gtype_name__ = 'HtmlTextView'
__gsignals__ = {
'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
}
def __init__(self):
gobject.GObject.__init__(self)
self.set_wrap_mode(gtk.WRAP_CHAR)
self.set_editable(False)
self._changed_cursor = False
self.connect("motion-notify-event", self.__motion_notify_event)
self.connect("leave-notify-event", self.__leave_event)
self.connect("enter-notify-event", self.__motion_notify_event)
self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL)
self.tooltip = tooltips.BaseTooltip()
self.config = gajim.config
self.interface = gajim.interface
# end big hack
build_patterns(self,gajim.config,gajim.interface)
def __leave_event(self, widget, event):
if self._changed_cursor:
window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
self._changed_cursor = False
def show_tooltip(self, tag):
if not self.tooltip.win:
# check if the current pointer is still over the line
text = getattr(tag, 'title', False)
if text:
pointer = self.get_pointer()
position = self.window.get_origin()
win = self.get_toplevel()
self.tooltip.show_tooltip(text, 8, position[1] + pointer[1])
def __motion_notify_event(self, widget, event):
x, y, _ = widget.window.get_pointer()
x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
tags = widget.get_iter_at_location(x, y).get_tags()
is_over_anchor = False
for tag in tags:
if getattr(tag, 'is_anchor', False):
is_over_anchor = True
break
if self.tooltip.timeout != 0:
# Check if we should hide the line tooltip
if not is_over_anchor:
self.tooltip.hide_tooltip()
if not self._changed_cursor and is_over_anchor:
window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
self._changed_cursor = True
gobject.timeout_add(500,
self.show_tooltip, tag)
elif self._changed_cursor and not is_over_anchor:
window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
self._changed_cursor = False
return False
def display_html(self, html):
buffer = self.get_buffer()
eob = buffer.get_end_iter()
## this works too if libxml2 is not available
# parser = xml.sax.make_parser(['drv_libxml2'])
# parser.setFeature(xml.sax.handler.feature_validation, True)
parser = xml.sax.make_parser()
parser.setContentHandler(HtmlHandler(self, eob))
parser.parse(StringIO(html))
#if not eob.starts_line():
# buffer.insert(eob, "\n")
if gobject.pygtk_version < (2, 8):
gobject.type_register(HtmlTextView)
change_cursor = None
if __name__ == '__main__':
htmlview = HtmlTextView()
tooltip = tooltips.BaseTooltip()
def on_textview_motion_notify_event(widget, event):
'''change the cursor to a hand when we are over a mail or an url'''
global change_cursor
pointer_x, pointer_y, spam = htmlview.window.get_pointer()
x, y = htmlview.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x,
pointer_y)
tags = htmlview.get_iter_at_location(x, y).get_tags()
if change_cursor:
htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
gtk.gdk.Cursor(gtk.gdk.XTERM))
change_cursor = None
tag_table = htmlview.get_buffer().get_tag_table()
over_line = False
for tag in tags:
try:
if tag.is_anchor:
htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
gtk.gdk.Cursor(gtk.gdk.HAND2))
change_cursor = tag
elif tag == tag_table.lookup('focus-out-line'):
over_line = True
except: pass
#if line_tooltip.timeout != 0:
# Check if we should hide the line tooltip
# if not over_line:
# line_tooltip.hide_tooltip()
#if over_line and not line_tooltip.win:
# line_tooltip.timeout = gobject.timeout_add(500,
# show_line_tooltip)
# htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
# gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
# change_cursor = tag
htmlview.connect('motion_notify_event', on_textview_motion_notify_event)
def handler(texttag, widget, event, iter, kind, href):
if event.type == gtk.gdk.BUTTON_PRESS:
print href
htmlview.html_hyperlink_handler = handler
htmlview.display_html('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
' <span style="font-size: 500%; font-family: serif">World</span>\n'
'</div>\n')
htmlview.display_html("<hr />")
htmlview.display_html("""
<p style='font-size:large'>
<span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
I&apos;m <span style='color:green'>green</span>
with <span style='font-weight: bold'>envy</span>!
</p>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
<p style='margin-left: 5px; margin-right: 2%'>
&quot;A foolish consistency is the hobgoblin of little minds.&quot;
</p>
</body>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
<p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
alt='A License to Jabber'
height='261'
width='537'/></p>
</body>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<ul style='background-color:rgb(120,140,100)'>
<li> One </li>
<li> Two </li>
<li> Three </li>
</ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
def faciter(n,acc):
if n==0: return acc
return faciter(n-1, acc*n)
if n&lt;0: raise ValueError("Must be non-negative")
return faciter(n,1)</pre>
</body>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<ol style='background-color:rgb(120,140,100)'>
<li> One </li>
<li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
<li> One </li>
<li> Two </li>
<li> Three </li>
</ul></li>
<li> Three </li></ol>
</body>
""")
htmlview.show()
sw = gtk.ScrolledWindow()
sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC)
sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC)
sw.set_property("border-width", 0)
sw.add(htmlview)
sw.show()
frame = gtk.Frame()
frame.set_shadow_type(gtk.SHADOW_IN)
frame.show()
frame.add(sw)
w = gtk.Window()
w.add(frame)
w.set_default_size(400, 300)
w.show_all()
w.connect("destroy", lambda w: gtk.main_quit())
gtk.main()

View file

@ -57,7 +57,7 @@ class MessageControl:
or inactive (state is False)'''
pass # Derived types MUST implement this method
def allow_shutdown(self):
def allow_shutdown(self, method):
'''Called to check is a control is allowed to shutdown.
If a control is not in a suitable shutdown state this method
should return False'''
@ -68,10 +68,6 @@ class MessageControl:
# NOTE: Derived classes MUST implement this
pass
def notify_on_new_messages(self):
# NOTE: Derived classes MUST implement this
return False
def repaint_themed_widgets(self, theme):
pass # NOTE: Derived classes SHOULD implement this

View file

@ -8,7 +8,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>

View file

@ -4,7 +4,7 @@
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Nikos Kouremenos <kourem@gmail.com>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
@ -40,6 +40,13 @@ class MessageWindow:
# DND_TARGETS is the targets needed by drag_source_set and drag_dest_set
DND_TARGETS = [('GAJIM_TAB', 0, 81)]
hid = 0 # drag_data_received handler id
(
CLOSE_TAB_MIDDLE_CLICK,
CLOSE_ESC,
CLOSE_CLOSE_BUTTON,
CLOSE_COMMAND,
CLOSE_CTRL_KEY
) = range(5)
def __init__(self, acct, type):
# A dictionary of dictionaries where _contacts[account][jid] == A MessageControl
@ -104,6 +111,16 @@ class MessageWindow:
self.notebook.drag_dest_set(gtk.DEST_DEFAULT_ALL, self.DND_TARGETS,
gtk.gdk.ACTION_MOVE)
def change_account_name(self, old_name, new_name):
if self._controls.has_key(old_name):
self._controls[new_name] = self._controls[old_name]
del self._controls[old_name]
for ctrl in self.controls():
if ctrl.account == old_name:
ctrl.account = new_name
if self.account == old_name:
self.account = new_name
def get_num_controls(self):
n = 0
for dict in self._controls.values():
@ -130,7 +147,7 @@ class MessageWindow:
def _on_window_delete(self, win, event):
# Make sure all controls are okay with being deleted
for ctrl in self.controls():
if not ctrl.allow_shutdown():
if not ctrl.allow_shutdown(self.CLOSE_CLOSE_BUTTON):
return True # halt the delete
return False
@ -189,7 +206,7 @@ class MessageWindow:
self.popup_menu(event)
elif event.button == 2: # middle click
ctrl = self._widget_to_control(child)
self.remove_tab(ctrl)
self.remove_tab(ctrl, self.CLOSE_TAB_MIDDLE_CLICK)
def _on_message_textview_mykeypress_event(self, widget, event_keyval,
event_keymod):
@ -214,15 +231,17 @@ class MessageWindow:
def _on_close_button_clicked(self, button, control):
'''When close button is pressed: close a tab'''
self.remove_tab(control)
self.remove_tab(control, self.CLOSE_CLOSE_BUTTON)
def show_title(self, urgent = True, control = None):
'''redraw the window's title'''
unread = 0
for ctrl in self.controls():
if ctrl.type_id == message_control.TYPE_GC and not \
gajim.config.get('notify_on_all_muc_messages') and not \
ctrl.attention_flag:
gajim.config.get('notify_on_all_muc_messages') and not \
ctrl.attention_flag:
# count only pm messages
unread += ctrl.get_nb_unread_pm()
continue
unread += ctrl.get_nb_unread()
@ -269,10 +288,11 @@ class MessageWindow:
ctrl_page = self.notebook.page_num(ctrl.widget)
self.notebook.set_current_page(ctrl_page)
def remove_tab(self, ctrl, reason = None):
'''reason is only for gc (offline status message)'''
def remove_tab(self, ctrl, mothod, reason = None, force = False):
'''reason is only for gc (offline status message)
if force is True, do not ask any confirmation'''
# Shutdown the MessageControl
if not ctrl.allow_shutdown():
if not force and not ctrl.allow_shutdown(mothod):
return
if reason is not None: # We are leaving gc with a status message
ctrl.shutdown(reason)
@ -292,28 +312,22 @@ class MessageWindow:
if len(self._controls[ctrl.account]) == 0:
del self._controls[ctrl.account]
# Notify a dupicate nick to update their banner and clear account display
for c in self.controls():
if c == self:
continue
if ctrl.contact.get_shown_name() == c.contact.get_shown_name():
c.draw_banner()
if self.get_num_controls() == 1: # we are going from two tabs to one
show_tabs_if_one_tab = gajim.config.get('tabs_always_visible')
self.notebook.set_show_tabs(show_tabs_if_one_tab)
if not show_tabs_if_one_tab:
self.alignment.set_property('top-padding', 0)
self.show_title()
elif self.get_num_controls() == 0:
if self.get_num_controls() == 0:
# These are not called when the window is destroyed like this, fake it
gajim.interface.msg_win_mgr._on_window_delete(self.window, None)
gajim.interface.msg_win_mgr._on_window_destroy(self.window)
# dnd clean up
self.notebook.disconnect(self.hid)
self.notebook.drag_dest_unset()
self.window.destroy()
return # don't show_title, we are dead
elif self.get_num_controls() == 1: # we are going from two tabs to one
show_tabs_if_one_tab = gajim.config.get('tabs_always_visible')
self.notebook.set_show_tabs(show_tabs_if_one_tab)
if not show_tabs_if_one_tab:
self.alignment.set_property('top-padding', 0)
self.show_title()
def redraw_tab(self, ctrl, chatstate = None):
hbox = self.notebook.get_tab_label(ctrl.widget).get_children()[0]
@ -488,9 +502,9 @@ class MessageWindow:
elif event.keyval == gtk.keysyms.Tab: # CTRL + TAB
self.move_to_next_unread_tab(True)
elif event.keyval == gtk.keysyms.F4: # CTRL + F4
self.remove_tab(ctrl)
self.remove_tab(ctrl, self.CLOSE_CTRL_KEY)
elif event.keyval == gtk.keysyms.w: # CTRL + W
self.remove_tab(ctrl)
self.remove_tab(ctrl, self.CLOSE_CTRL_KEY)
# MOD1 (ALT) mask
elif event.state & gtk.gdk.MOD1_MASK:
@ -513,7 +527,7 @@ class MessageWindow:
# Close tab bindings
elif event.keyval == gtk.keysyms.Escape and \
gajim.config.get('escape_key_closes'): # Escape
self.remove_tab(ctrl)
self.remove_tab(ctrl, self.CLOSE_ESC)
else:
# If the active control has a message_textview pass the event to it
active_ctrl = self.get_active_control()
@ -618,7 +632,11 @@ class MessageWindowMgr:
# Map the mode to a int constant for frequent compares
mode = gajim.config.get('one_message_window')
self.mode = common.config.opt_one_window_types.index(mode)
def change_account_name(self, old_name, new_name):
for win in self.windows():
win.change_account_name(old_name, new_name)
def _new_window(self, acct, type):
win = MessageWindow(acct, type)
# we track the lifetime of this window

159
src/music_track_listener.py Normal file
View file

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
## musictracklistener.py
##
## Copyright (C) 2006 Gustavo Carneiro <gjcarneiro@gmail.com>
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
##
## 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; version 2 only.
##
## 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.
##
import gobject
if __name__ == '__main__':
# install _() func before importing dbus_support
from common import i18n
from common import dbus_support
if dbus_support.supported:
import dbus
import dbus.glib
class MusicTrackInfo(object):
__slots__ = ['title', 'album', 'artist', 'duration', 'track_number']
class MusicTrackListener(gobject.GObject):
__gsignals__ = {
'music-track-changed': (gobject.SIGNAL_RUN_LAST, None, (object,)),
}
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
super(MusicTrackListener, self).__init__()
self._last_playing_music = None
bus = dbus.SessionBus()
## Muine
bus.add_signal_receiver(self._muine_music_track_change_cb, 'SongChanged',
'org.gnome.Muine.Player')
bus.add_signal_receiver(self._player_name_owner_changed,
'NameOwnerChanged', 'org.freedesktop.DBus', arg0='org.gnome.Muine')
bus.add_signal_receiver(self._player_playing_changed_cb, 'StateChanged',
'org.gnome.Muine.Player')
## Rhythmbox
bus.add_signal_receiver(self._rhythmbox_music_track_change_cb,
'playingUriChanged', 'org.gnome.Rhythmbox.Player')
bus.add_signal_receiver(self._player_name_owner_changed,
'NameOwnerChanged', 'org.freedesktop.DBus', arg0='org.gnome.Rhythmbox')
bus.add_signal_receiver(self._player_playing_changed_cb,
'playingChanged', 'org.gnome.Rhythmbox.Player')
def do_music_track_changed(self, info):
if info is not None:
self._last_playing_music = info
def _player_name_owner_changed(self, name, old, new):
if not new:
self.emit('music-track-changed', None)
def _player_playing_changed_cb(self, playing):
if playing:
self.emit('music-track-changed', self._last_playing_music)
else:
self.emit('music-track-changed', None)
def _muine_properties_extract(self, song_string):
d = dict((x.strip() for x in s1.split(':', 1)) for s1 in \
song_string.split('\n'))
info = MusicTrackInfo()
info.title = d['title']
info.album = d['album']
info.artist = d['artist']
info.duration = int(d['duration'])
info.track_number = int(d['track_number'])
return info
def _muine_music_track_change_cb(self, arg):
info = self._muine_properties_extract(arg)
self.emit('music-track-changed', info)
def _rhythmbox_properties_extract(self, props):
info = MusicTrackInfo()
info.title = props['title']
info.album = props['album']
info.artist = props['artist']
info.duration = int(props['duration'])
info.track_number = int(props['track-number'])
return info
def _rhythmbox_music_track_change_cb(self, uri):
bus = dbus.SessionBus()
rbshellobj = bus.get_object('org.gnome.Rhythmbox',
'/org/gnome/Rhythmbox/Shell')
rbshell = dbus.Interface(rbshellobj, 'org.gnome.Rhythmbox.Shell')
props = rbshell.getSongProperties(uri)
info = self._rhythmbox_properties_extract(props)
self.emit('music-track-changed', info)
def get_playing_track(self):
'''Return a MusicTrackInfo for the currently playing
song, or None if no song is playing'''
bus = dbus.SessionBus()
## Check Muine playing track
if dbus.dbus_bindings.bus_name_has_owner(bus.get_connection(),
'org.gnome.Muine'):
obj = bus.get_object('org.gnome.Muine', '/org/gnome/Muine/Player')
player = dbus.Interface(obj, 'org.gnome.Muine.Player')
if player.GetPlaying():
song_string = player.GetCurrentSong()
song = self._muine_properties_extract(song_string)
self._last_playing_music = song
return song
## Check Rhythmbox playing song
if dbus.dbus_bindings.bus_name_has_owner(bus.get_connection(),
'org.gnome.Rhythmbox'):
rbshellobj = bus.get_object('org.gnome.Rhythmbox',
'/org/gnome/Rhythmbox/Shell')
player = dbus.Interface(
bus.get_object('org.gnome.Rhythmbox',
'/org/gnome/Rhythmbox/Player'), 'org.gnome.Rhythmbox.Player')
rbshell = dbus.Interface(rbshellobj, 'org.gnome.Rhythmbox.Shell')
uri = player.getPlayingUri()
props = rbshell.getSongProperties(uri)
info = self._rhythmbox_properties_extract(props)
self._last_playing_music = info
return info
return None
# here we test :)
if __name__ == '__main__':
def music_track_change_cb(listener, music_track_info):
if music_track_info is None:
print "Stop!"
else:
print music_track_info.title
listener = MusicTrackListener.get()
listener.connect('music-track-changed', music_track_change_cb)
track = listener.get_playing_track()
if track is None:
print 'Now not playing anything'
else:
print 'Now playing: "%s" by %s' % (track.title, track.artist)
gobject.MainLoop().run()

View file

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
## network_manager_listener.py
## Copyright (C) 2006 Jeffrey C. Ollie <jeff at ocjtech.us>
## Copyright (C) 2006 Stefan Bethge <stefan at lanpartei.de>
##
## 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; version 2 only.
##
## 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.
##
from common import gajim
def device_now_active(self, *args):
for connection in gajim.connections.itervalues():
if gajim.config.get_per('accounts', connection.name, 'listen_to_network_manager') and gajim.config.get_per('accounts', connection.name, 'sync_with_global_status'):
connection.change_status('online', '')
def device_no_longer_active(self, *args):
for connection in gajim.connections.itervalues():
if gajim.config.get_per('accounts', connection.name, 'listen_to_network_manager') and gajim.config.get_per('accounts', connection.name, 'sync_with_global_status'):
connection.change_status('offline', '')
from common.dbus_support import system_bus
import dbus
import dbus.glib
bus = system_bus.SystemBus()
bus.add_signal_receiver(device_no_longer_active,
'DeviceNoLongerActive',
'org.freedesktop.NetworkManager',
'org.freedesktop.NetworkManager',
'/org/freedesktop/NetworkManager')
bus.add_signal_receiver(device_now_active,
'DeviceNowActive',
'org.freedesktop.NetworkManager',
'org.freedesktop.NetworkManager',
'/org/freedesktop/NetworkManager')

View file

@ -4,7 +4,7 @@
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com>
##
## DBUS/libnotify connection code:
## Notification daemon connection via D-Bus code:
## Copyright (C) 2005 by Sebastian Estienne
##
## This program is free software; you can redistribute it and/or modify
@ -25,12 +25,19 @@ import gtkgui_helpers
from common import gajim
from common import helpers
import dbus_support
from common import dbus_support
if dbus_support.supported:
import dbus
if dbus_support.version >= (0, 41, 0):
import dbus.glib
import dbus.service
import dbus.glib
import dbus.service
USER_HAS_PYNOTIFY = True # user has pynotify module
try:
import pynotify
pynotify.init('Gajim Notification')
except ImportError:
USER_HAS_PYNOTIFY = False
def get_show_in_roster(event, account, contact):
'''Return True if this event must be shown in roster, else False'''
@ -42,27 +49,23 @@ def get_show_in_roster(event, account, contact):
return False
if event == 'message_received':
chat_control = helpers.get_chat_control(account, contact)
if not chat_control:
return True
elif event == 'ft_request':
return True
return False
if chat_control:
return False
return True
def get_show_in_systray(event, account, contact):
'''Return True if this event must be shown in roster, else False'''
'''Return True if this event must be shown in systray, else False'''
num = get_advanced_notification(event, account, contact)
if num != None:
if gajim.config.get_per('notifications', str(num), 'systray') == 'yes':
return True
if gajim.config.get_per('notifications', str(num), 'systray') == 'no':
return False
if event in ('message_received', 'ft_request', 'gc_msg_highlight',
'ft_request'):
return True
return False
return gajim.config.get('trayicon_notification_on_events')
def get_advanced_notification(event, account, contact):
'''Returns the number of the first advanced notification or None'''
'''Returns the number of the first (top most)
advanced notification else None'''
num = 0
notif = gajim.config.get_per('notifications', str(num))
while notif:
@ -98,7 +101,7 @@ def get_advanced_notification(event, account, contact):
if tab_opened == 'both':
tab_opened_ok = True
else:
chat_control = helper.get_chat_control(account, contact)
chat_control = helpers.get_chat_control(account, contact)
if (chat_control and tab_opened == 'yes') or (not chat_control and \
tab_opened == 'no'):
tab_opened_ok = True
@ -109,17 +112,19 @@ def get_advanced_notification(event, account, contact):
notif = gajim.config.get_per('notifications', str(num))
def notify(event, jid, account, parameters, advanced_notif_num = None):
'''Check what type of notifications we want, depending on basic configuration
of notifications and advanced one and do these notifications'''
'''Check what type of notifications we want, depending on basic
and the advanced configuration of notifications and do these notifications;
advanced_notif_num holds the number of the first (top most) advanced
notification'''
# First, find what notifications we want
do_popup = False
do_sound = False
do_cmd = False
if (event == 'status_change'):
if event == 'status_change':
new_show = parameters[0]
status_message = parameters[1]
# Default : No popup for status change
elif (event == 'contact_connected'):
# Default: No popup for status change
elif event == 'contact_connected':
status_message = parameters
j = gajim.get_jid_without_resource(jid)
server = gajim.get_server_from_jid(j)
@ -135,43 +140,44 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
'enabled') and not gajim.block_signed_in_notifications[account] and \
not block_transport:
do_sound = True
elif (event == 'contact_disconnected'):
elif event == 'contact_disconnected':
status_message = parameters
if helpers.allow_showing_notification(account, 'notify_on_signout'):
do_popup = True
if gajim.config.get_per('soundevents', 'contact_disconnected',
'enabled'):
do_sound = True
elif (event == 'new_message'):
elif event == 'new_message':
message_type = parameters[0]
first = parameters[1]
is_first_message = parameters[1]
nickname = parameters[2]
message = parameters[3]
if helpers.allow_showing_notification(account, 'notify_on_new_message',
advanced_notif_num, first):
advanced_notif_num, is_first_message):
do_popup = True
if first and helpers.allow_sound_notification('first_message_received',
advanced_notif_num):
if is_first_message and helpers.allow_sound_notification(
'first_message_received', advanced_notif_num):
do_sound = True
elif not first and helpers.allow_sound_notification(
elif not is_first_message and helpers.allow_sound_notification(
'next_message_received', advanced_notif_num):
do_sound = True
else:
print '*Event not implemeted yet*'
if advanced_notif_num != None and gajim.config.get_per('notifications',
if advanced_notif_num is not None and gajim.config.get_per('notifications',
str(advanced_notif_num), 'run_command'):
do_cmd = True
# Do the wanted notifications
if (do_popup):
if (event == 'contact_connected' or event == 'contact_disconnected' or \
event == 'status_change'): # Common code for popup for these 3 events
if (event == 'contact_disconnected'):
if do_popup:
if event in ('contact_connected', 'contact_disconnected',
'status_change'): # Common code for popup for these three events
if event == 'contact_disconnected':
show_image = 'offline.png'
suffix = '_notif_size_bw.png'
else: #Status Change or Connected
# TODO : for status change, we don't always 'online.png', but we
# FIXME: for status change,
# we don't always 'online.png', but we
# first need 48x48 for all status
show_image = 'online.png'
suffix = '_notif_size_colored.png'
@ -186,7 +192,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
iconset, '48x48', show_image)
path = gtkgui_helpers.get_path_to_generic_or_avatar(img,
jid = jid, suffix = suffix)
if (event == 'status_change'):
if event == 'status_change':
title = _('%(nick)s Changed Status') % \
{'nick': gajim.get_name_from_jid(account, jid)}
text = _('%(nick)s is now %(status)s') % \
@ -196,7 +202,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
text = text + " : " + status_message
popup(_('Contact Changed Status'), jid, account,
path_to_image = path, title = title, text = text)
elif (event == 'contact_connected'):
elif event == 'contact_connected':
title = _('%(nickname)s Signed In') % \
{'nickname': gajim.get_name_from_jid(account, jid)}
text = ''
@ -204,7 +210,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
text = status_message
popup(_('Contact Signed In'), jid, account,
path_to_image = path, title = title, text = text)
elif (event == 'contact_disconnected'):
elif event == 'contact_disconnected':
title = _('%(nickname)s Signed Out') % \
{'nickname': gajim.get_name_from_jid(account, jid)}
text = ''
@ -212,7 +218,7 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
text = status_message
popup(_('Contact Signed Out'), jid, account,
path_to_image = path, title = title, text = text)
elif (event == 'new_message'):
elif event == 'new_message':
if message_type == 'normal': # single message
event_type = _('New Single Message')
img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
@ -222,10 +228,10 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
text = message
elif message_type == 'pm': # private message
event_type = _('New Private Message')
room_name, t = gajim.get_room_name_and_server_from_room_jid(jid)
room_name = gajim.get_nick_from_jid(jid)
img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
'priv_msg_recv.png')
title = _('New Private Message from room %s') % room_name
title = _('New Private Message from group chat %s') % room_name
text = _('%(nickname)s: %(message)s') % {'nickname': nickname,
'message': message}
else: # chat message
@ -239,18 +245,18 @@ def notify(event, jid, account, parameters, advanced_notif_num = None):
popup(event_type, jid, account, message_type,
path_to_image = path, title = title, text = text)
if (do_sound):
if do_sound:
snd_file = None
snd_event = None # If not snd_file, play the event
if (event == 'new_message'):
if advanced_notif_num != None and gajim.config.get_per('notifications',
str(advanced_notif_num), 'sound') == 'yes':
if event == 'new_message':
if advanced_notif_num is not None and gajim.config.get_per(
'notifications', str(advanced_notif_num), 'sound') == 'yes':
snd_file = gajim.config.get_per('notifications',
str(advanced_notif_num), 'sound_file')
elif advanced_notif_num != None and gajim.config.get_per(
elif advanced_notif_num is not None and gajim.config.get_per(
'notifications', str(advanced_notif_num), 'sound') == 'no':
pass # do not set snd_event
elif first:
elif is_first_message:
snd_event = 'first_message_received'
else:
snd_event = 'next_message_received'
@ -276,20 +282,61 @@ def popup(event_type, jid, account, msg_type = '', path_to_image = None,
the older style PopupNotificationWindow method.'''
text = gtkgui_helpers.escape_for_pango_markup(text)
title = gtkgui_helpers.escape_for_pango_markup(title)
if gajim.config.get('use_notif_daemon') and dbus_support.supported:
try:
DesktopNotification(event_type, jid, account, msg_type, path_to_image,
title, text)
return
DesktopNotification(event_type, jid, account, msg_type,
path_to_image, title, text)
return # sucessfully did D-Bus Notification procedure!
except dbus.dbus_bindings.DBusException, e:
# Connection to D-Bus failed, try popup
# Connection to D-Bus failed
gajim.log.debug(str(e))
except TypeError, e:
# This means that we sent the message incorrectly
gajim.log.debug(str(e))
instance = dialogs.PopupNotificationWindow(event_type, jid, account, msg_type, \
path_to_image, title, text)
gajim.interface.roster.popup_notification_windows.append(instance)
# we failed to speak to notification daemon via D-Bus
if USER_HAS_PYNOTIFY: # try via libnotify
if not text:
text = gajim.get_name_from_jid(account, jid) # default value of text
if not title:
title = event_type
# default image
if not path_to_image:
path_to_image = os.path.abspath(
os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
'chat_msg_recv.png')) # img to display
notification = pynotify.Notification(title, text)
timeout = gajim.config.get('notification_timeout') * 1000 # make it ms
notification.set_timeout(timeout)
notification.set_category(event_type)
notification.set_data('event_type', event_type)
notification.set_data('jid', jid)
notification.set_data('account', account)
notification.set_data('msg_type', event_type)
notification.set_data('path_to_image', path_to_image)
notification.add_action('default', 'Default Action',
on_pynotify_notification_clicked)
notification.show()
else: # go old style
instance = dialogs.PopupNotificationWindow(event_type, jid,
account, msg_type, path_to_image, title, text)
gajim.interface.roster.popup_notification_windows.append(
instance)
def on_pynotify_notification_clicked(notification, action):
event_type = notification.get_data('event_type')
jid = notification.get_data('jid')
account = notification.get_data('account')
msg_type = notification.get_data('msg_type')
path_to_image = notification.get_data('path_to_image')
notification.close()
gajim.interface.handle_event(account, jid, msg_type)
class NotificationResponseManager:
'''Collects references to pending DesktopNotifications and manages there
@ -342,7 +389,7 @@ class NotificationResponseManager:
notification_response_manager = NotificationResponseManager()
class DesktopNotification:
'''A DesktopNotification that interfaces with DBus via the Desktop
'''A DesktopNotification that interfaces with D-Bus via the Desktop
Notification specification'''
def __init__(self, event_type, jid, account, msg_type = '',
path_to_image = None, title = None, text = None):

View file

@ -13,18 +13,17 @@
## GNU General Public License for more details.
##
# THIS FILE IS FOR **OUR** PROFILE (when we edit our INFO)
import gtk
import gobject
import base64
import mimetypes
import os
import time
import locale
import gtkgui_helpers
import dialogs
from common import helpers
from common import gajim
from common.i18n import Q_
@ -60,17 +59,40 @@ class ProfileWindow:
def __init__(self, account):
self.xml = gtkgui_helpers.get_glade('profile_window.glade')
self.window = self.xml.get_widget('profile_window')
self.progressbar = self.xml.get_widget('progressbar')
self.statusbar = self.xml.get_widget('statusbar')
self.context_id = self.statusbar.get_context_id('profile')
self.account = account
self.jid = gajim.get_jid_from_account(account)
self.avatar_mime_type = None
self.avatar_encoded = None
self.message_id = self.statusbar.push(self.context_id,
_('Retrieving profile...'))
self.update_progressbar_timeout_id = gobject.timeout_add(100,
self.update_progressbar)
self.remove_statusbar_timeout_id = None
# Create Image for avatar button
image = gtk.Image()
self.xml.get_widget('PHOTO_button').set_image(image)
self.xml.signal_autoconnect(self)
self.window.show_all()
def update_progressbar(self):
self.progressbar.pulse()
return True # loop forever
def remove_statusbar(self, message_id):
self.statusbar.remove(self.context_id, message_id)
self.remove_statusbar_timeout_id = None
def on_profile_window_destroy(self, widget):
if self.update_progressbar_timeout_id is not None:
gobject.source_remove(self.update_progressbar_timeout_id)
if self.remove_statusbar_timeout_id is not None:
gobject.source_remove(self.remove_statusbar_timeout_id)
del gajim.interface.instances[self.account]['profile']
def on_profile_window_key_press_event(self, widget, event):
@ -79,8 +101,10 @@ class ProfileWindow:
def on_clear_button_clicked(self, widget):
# empty the image
self.xml.get_widget('PHOTO_image').set_from_icon_name('stock_person',
gtk.ICON_SIZE_DIALOG)
button = self.xml.get_widget('PHOTO_button')
image = button.get_image()
image.set_from_pixbuf(None)
button.set_label(_('Click to set your avatar'))
self.avatar_encoded = None
self.avatar_mime_type = None
@ -124,24 +148,36 @@ class ProfileWindow:
pixbuf = gtkgui_helpers.get_pixbuf_from_data(data)
# rescale it
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard')
image = self.xml.get_widget('PHOTO_image')
button = self.xml.get_widget('PHOTO_button')
image = button.get_image()
image.set_from_pixbuf(pixbuf)
button.set_label('')
self.avatar_encoded = base64.encodestring(data)
# returns None if unknown type
self.avatar_mime_type = mimetypes.guess_type(path_to_file)[0]
self.dialog = dialogs.ImageChooserDialog(on_response_ok = on_ok)
def on_clear(widget):
self.dialog.destroy()
self.on_clear_button_clicked(widget)
self.dialog = dialogs.AvatarChooserDialog(on_response_ok = on_ok,
on_response_clear = on_clear)
def on_PHOTO_button_press_event(self, widget, event):
'''If right-clicked, show popup'''
if event.button == 3 and self.avatar_encoded: # right click
menu = gtk.Menu()
nick = gajim.config.get_per('accounts', self.account, 'name')
menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.jid, None, nick + '.jpeg')
menu.append(menuitem)
# Try to get pixbuf
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(self.jid)
if pixbuf:
nick = gajim.config.get_per('accounts', self.account, 'name')
menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.jid, None, nick + '.jpeg')
menu.append(menuitem)
# show clear
menuitem = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
menuitem.connect('activate', self.on_clear_button_clicked)
@ -162,18 +198,23 @@ class ProfileWindow:
def set_values(self, vcard):
if not 'PHOTO' in vcard:
# set default image
image = self.xml.get_widget('PHOTO_image')
image.set_from_icon_name('stock_person', gtk.ICON_SIZE_DIALOG)
button = self.xml.get_widget('PHOTO_button')
image = button.get_image()
image.set_from_pixbuf(None)
button.set_label(_('Click to set your avatar'))
for i in vcard.keys():
if i == 'PHOTO':
pixbuf, self.avatar_encoded, self.avatar_mime_type = \
get_avatar_pixbuf_encoded_mime(vcard[i])
image = self.xml.get_widget('PHOTO_image')
button = self.xml.get_widget('PHOTO_button')
image = button.get_image()
if not pixbuf:
image.set_from_icon_name('stock_person', gtk.ICON_SIZE_DIALOG)
image.set_from_pixbuf(None)
button.set_label(_('Click to set your avatar'))
continue
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard')
image.set_from_pixbuf(pixbuf)
button.set_label('')
continue
if i == 'ADR' or i == 'TEL' or i == 'EMAIL':
for entry in vcard[i]:
@ -191,6 +232,18 @@ class ProfileWindow:
vcard[i], 0)
else:
self.set_value(i + '_entry', vcard[i])
if self.update_progressbar_timeout_id is not None:
if self.message_id:
self.statusbar.remove(self.context_id, self.message_id)
self.message_id = self.statusbar.push(self.context_id,
_('Information received'))
self.remove_statusbar_timeout_id = gobject.timeout_add(3000,
self.remove_statusbar, self.message_id)
gobject.source_remove(self.update_progressbar_timeout_id)
# redraw progressbar after avatar is set so that windows is already
# resized. Else progressbar is not correctly redrawn
gobject.idle_add(self.progressbar.set_fraction, 0)
self.update_progressbar_timeout_id = None
def add_to_vcard(self, vcard, entry, txt):
'''Add an information to the vCard dictionary'''
@ -248,6 +301,9 @@ class ProfileWindow:
return vcard
def on_publish_button_clicked(self, widget):
if self.update_progressbar_timeout_id:
# Operation in progress
return
if gajim.connections[self.account].connected < 2:
dialogs.ErrorDialog(_('You are not connected to the server'),
_('Without a connection you can not publish your contact '
@ -261,8 +317,42 @@ class ProfileWindow:
nick = gajim.config.get_per('accounts', self.account, 'name')
gajim.nicks[self.account] = nick
gajim.connections[self.account].send_vcard(vcard)
self.message_id = self.statusbar.push(self.context_id,
_('Sending profile...'))
self.update_progressbar_timeout_id = gobject.timeout_add(100,
self.update_progressbar)
def vcard_published(self):
if self.message_id:
self.statusbar.remove(self.context_id, self.message_id)
self.message_id = self.statusbar.push(self.context_id,
_('Information published'))
self.remove_statusbar_timeout_id = gobject.timeout_add(3000,
self.remove_statusbar, self.message_id)
if self.update_progressbar_timeout_id is not None:
gobject.source_remove(self.update_progressbar_timeout_id)
self.progressbar.set_fraction(0)
self.update_progressbar_timeout_id = None
def vcard_not_published(self):
if self.message_id:
self.statusbar.remove(self.context_id, self.message_id)
self.message_id = self.statusbar.push(self.context_id,
_('Information NOT published'))
self.remove_statusbar_timeout_id = gobject.timeout_add(3000,
self.remove_statusbar, self.message_id)
if self.update_progressbar_timeout_id is not None:
gobject.source_remove(self.update_progressbar_timeout_id)
self.progressbar.set_fraction(0)
self.update_progressbar_timeout_id = None
dialogs.InformationDialog(_('vCard publication failed'),
_('There was an error while publishing your personal information, '
'try again later.'))
def on_retrieve_button_clicked(self, widget):
if self.update_progressbar_timeout_id:
# Operation in progress
return
entries = ['FN', 'NICKNAME', 'BDAY', 'EMAIL_HOME_USERID', 'URL',
'TEL_HOME_NUMBER', 'N_FAMILY', 'N_GIVEN', 'N_MIDDLE', 'N_PREFIX',
'N_SUFFIX', 'ADR_HOME_STREET', 'ADR_HOME_EXTADR', 'ADR_HOME_LOCALITY',
@ -275,9 +365,18 @@ class ProfileWindow:
for e in entries:
self.xml.get_widget(e + '_entry').set_text('')
self.xml.get_widget('DESC_textview').get_buffer().set_text('')
self.xml.get_widget('PHOTO_image').set_from_icon_name('stock_person',
gtk.ICON_SIZE_DIALOG)
button = self.xml.get_widget('PHOTO_button')
image = button.get_image()
image.set_from_pixbuf(None)
button.set_label(_('Click to set your avatar'))
gajim.connections[self.account].request_vcard(self.jid)
else:
dialogs.ErrorDialog(_('You are not connected to the server'),
_('Without a connection, you can not get your contact information.'))
_('Without a connection, you can not get your contact information.'))
self.message_id = self.statusbar.push(self.context_id,
_('Retrieving profile...'))
self.update_progressbar_timeout_id = gobject.timeout_add(100,
self.update_progressbar)
def on_close_button_clicked(self, widget):
self.window.destroy()

View file

@ -1,19 +1,9 @@
## remote_control.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
## - Andrew Sayman <lorien420@myrealbox.com>
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -33,60 +23,33 @@ from common import helpers
from time import time
from dialogs import AddNewContactWindow, NewChatDialog
import dbus_support
from common import dbus_support
if dbus_support.supported:
import dbus
if dbus_support.version >= (0, 41, 0):
if dbus_support:
import dbus.service
import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it
DbusPrototype = dbus.service.Object
elif dbus_support.version >= (0, 20, 0):
DbusPrototype = dbus.Object
else: #dbus is not defined
DbusPrototype = str
import dbus.glib
INTERFACE = 'org.gajim.dbus.RemoteInterface'
OBJ_PATH = '/org/gajim/dbus/RemoteObject'
SERVICE = 'org.gajim.dbus'
# type mapping, it is different in each version
ident = lambda e: e
if dbus_support.version[1] >= 43:
# in most cases it is a utf-8 string
DBUS_STRING = dbus.String
# type mapping
# general type (for use in dicts,
# where all values should have the same type)
DBUS_VARIANT = dbus.Variant
DBUS_BOOLEAN = dbus.Boolean
DBUS_DOUBLE = dbus.Double
DBUS_INT32 = dbus.Int32
# dictionary with string key and binary value
DBUS_DICT_SV = lambda : dbus.Dictionary({}, signature="sv")
# dictionary with string key and value
DBUS_DICT_SS = lambda : dbus.Dictionary({}, signature="ss")
# empty type
DBUS_NONE = lambda : dbus.Variant(0)
# in most cases it is a utf-8 string
DBUS_STRING = dbus.String
else: # 33, 35, 36
DBUS_DICT_SV = lambda : {}
DBUS_DICT_SS = lambda : {}
DBUS_STRING = lambda e: unicode(e).encode('utf-8')
# this is the only way to return lists and dicts of mixed types
DBUS_VARIANT = lambda e: (isinstance(e, (str, unicode)) and \
DBUS_STRING(e)) or repr(e)
DBUS_NONE = lambda : ''
if dbus_support.version[1] >= 41: # 35, 36
DBUS_BOOLEAN = dbus.Boolean
DBUS_DOUBLE = dbus.Double
DBUS_INT32 = dbus.Int32
else: # 33
DBUS_BOOLEAN = ident
DBUS_INT32 = ident
DBUS_DOUBLE = ident
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible']
# general type (for use in dicts, where all values should have the same type)
DBUS_VARIANT = dbus.Variant
DBUS_BOOLEAN = dbus.Boolean
DBUS_DOUBLE = dbus.Double
DBUS_INT32 = dbus.Int32
# dictionary with string key and binary value
DBUS_DICT_SV = lambda : dbus.Dictionary({}, signature="sv")
# dictionary with string key and value
DBUS_DICT_SS = lambda : dbus.Dictionary({}, signature="ss")
# empty type
DBUS_NONE = lambda : dbus.Variant(0)
def get_dbus_struct(obj):
''' recursively go through all the items and replace
@ -123,65 +86,35 @@ class Remote:
self.signal_object = None
session_bus = dbus_support.session_bus.SessionBus()
if dbus_support.version[1] >= 41:
service = dbus.service.BusName(SERVICE, bus=session_bus)
self.signal_object = SignalObject(service)
elif dbus_support.version[1] <= 40 and dbus_support.version[1] >= 20:
service=dbus.Service(SERVICE, session_bus)
self.signal_object = SignalObject(service)
service = dbus.service.BusName(SERVICE, bus=session_bus)
self.signal_object = SignalObject(service)
def raise_signal(self, signal, arg):
if self.signal_object:
self.signal_object.raise_signal(signal,
get_dbus_struct(arg))
get_dbus_struct(arg))
class SignalObject(DbusPrototype):
''' Local object definition for /org/gajim/dbus/RemoteObject. This doc must
not be visible, because the clients can access only the remote object. '''
class SignalObject(dbus.service.Object):
''' Local object definition for /org/gajim/dbus/RemoteObject.
(This docstring is not be visible, because the clients can access only the remote object.)'''
def __init__(self, service):
self.first_show = True
self.vcard_account = None
# register our dbus API
if dbus_support.version[1] >= 41:
DbusPrototype.__init__(self, service, OBJ_PATH)
elif dbus_support.version[1] >= 30:
DbusPrototype.__init__(self, OBJ_PATH, service)
else:
DbusPrototype.__init__(self, OBJ_PATH, service,
[ self.toggle_roster_appearance,
self.show_next_unread,
self.list_contacts,
self.list_accounts,
self.account_info,
self.change_status,
self.open_chat,
self.send_message,
self.send_single_message,
self.contact_info,
self.send_file,
self.prefs_list,
self.prefs_store,
self.prefs_del,
self.prefs_put,
self.add_contact,
self.remove_contact,
self.get_status,
self.get_status_message,
self.start_chat,
self.send_xml,
])
dbus.service.Object.__init__(self, service, OBJ_PATH)
def raise_signal(self, signal, arg):
''' raise a signal, with a single string message '''
'''raise a signal, with a single string message'''
from dbus import dbus_bindings
message = dbus_bindings.Signal(OBJ_PATH, INTERFACE, signal)
i = message.get_iter(True)
i.append(arg)
self._connection.send(message)
@dbus.service.method(INTERFACE)
def get_status(self, *args):
'''get_status(account = None)
returns status (show to be exact) which is the global one
@ -193,8 +126,9 @@ class SignalObject(DbusPrototype):
return helpers.get_global_show()
# return show for the given account
index = gajim.connections[account].connected
return DBUS_STRING(STATUS_LIST[index])
return DBUS_STRING(gajim.SHOW_LIST[index])
@dbus.service.method(INTERFACE)
def get_status_message(self, *args):
'''get_status(account = None)
returns status which is the global one
@ -208,7 +142,7 @@ class SignalObject(DbusPrototype):
status = gajim.connections[account].status
return DBUS_STRING(status)
@dbus.service.method(INTERFACE)
def get_account_and_contact(self, account, jid):
''' get the account (if not given) and contact instance from jid'''
connected_account = None
@ -236,6 +170,7 @@ class SignalObject(DbusPrototype):
return connected_account, contact
@dbus.service.method(INTERFACE)
def send_file(self, *args):
'''send_file(file_path, jid, account=None)
send file, located at 'file_path' to 'jid', using account
@ -254,7 +189,7 @@ class SignalObject(DbusPrototype):
return False
def _send_message(self, jid, message, keyID, account, type = 'chat', subject = None):
''' can be called from send_chat_message (default when send_message)
'''can be called from send_chat_message (default when send_message)
or send_single_message'''
if not jid or not message:
return None # or raise error
@ -269,28 +204,31 @@ class SignalObject(DbusPrototype):
return True
return False
@dbus.service.method(INTERFACE)
def send_chat_message(self, *args):
''' send_message(jid, message, keyID=None, account=None)
'''send_message(jid, message, keyID=None, account=None)
send chat 'message' to 'jid', using account (optional) 'account'.
if keyID is specified, encrypt the message with the pgp key '''
jid, message, keyID, account = self._get_real_arguments(args, 4)
jid = self._get_real_jid(jid, account)
return self._send_message(jid, message, keyID, account)
@dbus.service.method(INTERFACE)
def send_single_message(self, *args):
''' send_single_message(jid, subject, message, keyID=None, account=None)
'''send_single_message(jid, subject, message, keyID=None, account=None)
send single 'message' to 'jid', using account (optional) 'account'.
if keyID is specified, encrypt the message with the pgp key '''
jid, subject, message, keyID, account = self._get_real_arguments(args, 5)
jid = self._get_real_jid(jid, account)
return self._send_message(jid, message, keyID, account, type, subject)
@dbus.service.method(INTERFACE)
def open_chat(self, *args):
''' start_chat(jid, account=None) -> shows the tabbed window for new
message to 'jid', using account(optional) 'account' '''
jid, account = self._get_real_arguments(args, 2)
if not jid:
# FIXME: raise exception for missing argument (dbus0.35+)
raise MissingArgument
return None
jid = self._get_real_jid(jid, account)
@ -332,13 +270,14 @@ class SignalObject(DbusPrototype):
return True
return False
@dbus.service.method(INTERFACE)
def change_status(self, *args, **keywords):
''' change_status(status, message, account). account is optional -
if not specified status is changed for all accounts. '''
status, message, account = self._get_real_arguments(args, 3)
if status not in ('offline', 'online', 'chat',
'away', 'xa', 'dnd', 'invisible'):
# FIXME: raise exception for bad status (dbus0.35)
raise InvalidArgument
return None
if account:
gobject.idle_add(gajim.interface.roster.send_status, account,
@ -346,25 +285,29 @@ class SignalObject(DbusPrototype):
else:
# account not specified, so change the status of all accounts
for acc in gajim.contacts.get_accounts():
if not gajim.config.get_per('accounts', acc, 'sync_with_global_status'):
continue
gobject.idle_add(gajim.interface.roster.send_status, acc,
status, message)
return None
def show_next_unread(self, *args):
''' Show the window(s) with next waiting messages in tabbed/group chats. '''
@dbus.service.method(INTERFACE)
def show_next_pending_event(self, *args):
'''Show the window(s) with next pending event in tabbed/group chats.'''
if gajim.events.get_nb_events():
gajim.interface.systray.handle_first_event()
@dbus.service.method(INTERFACE)
def contact_info(self, *args):
''' get vcard info for a contact. Return cached value of the vcard.
'''get vcard info for a contact. Return cached value of the vcard.
'''
[jid] = self._get_real_arguments(args, 1)
if not isinstance(jid, unicode):
jid = unicode(jid)
if not jid:
# FIXME: raise exception for missing argument (0.3+)
raise MissingArgument
return None
jid = self._get_real_jid(jid, account)
jid = self._get_real_jid(jid)
cached_vcard = gajim.connections.values()[0].get_cached_vcard(jid)
if cached_vcard:
@ -373,8 +316,9 @@ class SignalObject(DbusPrototype):
# return empty dict
return DBUS_DICT_SV()
@dbus.service.method(INTERFACE)
def list_accounts(self, *args):
''' list register accounts '''
'''list register accounts'''
result = gajim.contacts.get_accounts()
if result and len(result) > 0:
result_array = []
@ -383,8 +327,9 @@ class SignalObject(DbusPrototype):
return result_array
return None
@dbus.service.method(INTERFACE)
def account_info(self, *args):
''' show info on account: resource, jid, nick, prio, message '''
'''show info on account: resource, jid, nick, prio, message'''
[for_account] = self._get_real_arguments(args, 1)
if not gajim.connections.has_key(for_account):
# account is invalid
@ -392,19 +337,19 @@ class SignalObject(DbusPrototype):
account = gajim.connections[for_account]
result = DBUS_DICT_SS()
index = account.connected
result['status'] = DBUS_STRING(STATUS_LIST[index])
result['status'] = DBUS_STRING(gajim.SHOW_LIST[index])
result['name'] = DBUS_STRING(account.name)
result['jid'] = DBUS_STRING(gajim.get_jid_from_account(account.name))
result['message'] = DBUS_STRING(account.status)
result['priority'] = DBUS_STRING(unicode(gajim.config.get_per('accounts',
account.name, 'priority')))
result['priority'] = DBUS_STRING(unicode(account.priority))
result['resource'] = DBUS_STRING(unicode(gajim.config.get_per('accounts',
account.name, 'resource')))
account.name, 'resource')))
return result
@dbus.service.method(INTERFACE)
def list_contacts(self, *args):
''' list all contacts in the roster. If the first argument is specified,
then return the contacts for the specified account '''
'''list all contacts in the roster. If the first argument is specified,
then return the contacts for the specified account'''
[for_account] = self._get_real_arguments(args, 1)
result = []
accounts = gajim.contacts.get_accounts()
@ -426,6 +371,7 @@ class SignalObject(DbusPrototype):
return None
return result
@dbus.service.method(INTERFACE)
def toggle_roster_appearance(self, *args):
''' shows/hides the roster window '''
win = gajim.interface.roster.window
@ -439,6 +385,7 @@ class SignalObject(DbusPrototype):
else:
win.window.focus(long(time()))
@dbus.service.method(INTERFACE)
def prefs_list(self, *args):
prefs_dict = DBUS_DICT_SS()
def get_prefs(data, name, path, value):
@ -453,6 +400,7 @@ class SignalObject(DbusPrototype):
gajim.config.foreach(get_prefs)
return prefs_dict
@dbus.service.method(INTERFACE)
def prefs_store(self, *args):
try:
gajim.interface.save_config()
@ -460,6 +408,7 @@ class SignalObject(DbusPrototype):
return False
return True
@dbus.service.method(INTERFACE)
def prefs_del(self, *args):
[key] = self._get_real_arguments(args, 1)
if not key:
@ -473,6 +422,7 @@ class SignalObject(DbusPrototype):
gajim.config.del_per(key_path[0], key_path[1], key_path[2])
return True
@dbus.service.method(INTERFACE)
def prefs_put(self, *args):
[key] = self._get_real_arguments(args, 1)
if not key:
@ -486,6 +436,7 @@ class SignalObject(DbusPrototype):
gajim.config.set_per(key_path[0], key_path[1], subname, value)
return True
@dbus.service.method(INTERFACE)
def add_contact(self, *args):
[jid, account] = self._get_real_arguments(args, 2)
if account:
@ -501,6 +452,7 @@ class SignalObject(DbusPrototype):
AddNewContactWindow(account = None, jid = jid)
return True
@dbus.service.method(INTERFACE)
def remove_contact(self, *args):
[jid, account] = self._get_real_arguments(args, 2)
jid = self._get_real_jid(jid, account)
@ -595,9 +547,11 @@ class SignalObject(DbusPrototype):
contact_dict['resources'] = DBUS_VARIANT(contact_dict['resources'])
return contact_dict
@dbus.service.method(INTERFACE)
def get_unread_msgs_number(self, *args):
return str(gajim.events.get_nb_events)
return str(gajim.events.get_nb_events())
@dbus.service.method(INTERFACE)
def start_chat(self, *args):
[account] = self._get_real_arguments(args, 1)
if not account:
@ -606,6 +560,7 @@ class SignalObject(DbusPrototype):
NewChatDialog(account)
return True
@dbus.service.method(INTERFACE)
def send_xml(self, *args):
xml, account = self._get_real_arguments(args, 2)
if account:
@ -613,36 +568,3 @@ class SignalObject(DbusPrototype):
else:
for acc in gajim.contacts.get_accounts():
gajim.connections[acc].send_stanza(xml)
if dbus_support.version[1] >= 30 and dbus_support.version[1] <= 40:
method = dbus.method
signal = dbus.signal
elif dbus_support.version[1] >= 41:
method = dbus.service.method
signal = dbus.service.signal
# prevent using decorators, because they are not supported
# on python < 2.4
# FIXME: use decorators when python2.3 (and dbus 0.23) is OOOOOOLD
toggle_roster_appearance = method(INTERFACE)(toggle_roster_appearance)
list_contacts = method(INTERFACE)(list_contacts)
list_accounts = method(INTERFACE)(list_accounts)
show_next_unread = method(INTERFACE)(show_next_unread)
change_status = method(INTERFACE)(change_status)
open_chat = method(INTERFACE)(open_chat)
contact_info = method(INTERFACE)(contact_info)
send_message = method(INTERFACE)(send_chat_message)
send_single_message = method(INTERFACE)(send_single_message)
send_file = method(INTERFACE)(send_file)
prefs_list = method(INTERFACE)(prefs_list)
prefs_put = method(INTERFACE)(prefs_put)
prefs_del = method(INTERFACE)(prefs_del)
prefs_store = method(INTERFACE)(prefs_store)
remove_contact = method(INTERFACE)(remove_contact)
add_contact = method(INTERFACE)(add_contact)
get_status = method(INTERFACE)(get_status)
get_status_message = method(INTERFACE)(get_status_message)
account_info = method(INTERFACE)(account_info)
get_unread_msgs_number = method(INTERFACE)(get_unread_msgs_number)
start_chat = method(INTERFACE)(start_chat)
send_xml = method(INTERFACE)(send_xml)

File diff suppressed because it is too large Load diff

68
src/statusicon.py Normal file
View file

@ -0,0 +1,68 @@
## statusicon.py
##
## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
##
## 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 2
## 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.
##
import gtk
import systray
from common import gajim
from common import helpers
class StatusIcon(systray.Systray):
'''Class for the notification area icon'''
#FIXME: when we migrate to GTK 2.10 stick only to this class
# (move base stuff from systray.py and rm it)
#NOTE: gtk api does NOT allow:
# leave, enter motion notify
# and can't do cool tooltips we use
# and we could use blinking instead of unsupported animation
# or we could emulate animation by every foo ms chaning the image
def __init__(self):
systray.Systray.__init__(self)
self.status_icon = gtk.StatusIcon()
def show_icon(self):
self.status_icon.connect('activate', self.on_status_icon_left_clicked)
self.status_icon.connect('popup-menu', self.on_status_icon_right_clicked)
self.set_img()
self.status_icon.props.visible = True
def on_status_icon_right_clicked(self, widget, event_button, event_time):
self.make_menu(event_button, event_time)
def hide_icon(self):
self.status_icon.props.visible = False
def on_status_icon_left_clicked(self, widget):
self.on_left_click()
def set_img(self):
'''apart from image, we also update tooltip text here'''
if not gajim.interface.systray_enabled:
return
text = helpers.get_notification_icon_tooltip_text()
self.status_icon.set_tooltip(text)
if gajim.events.get_nb_systray_events():
state = 'message'
else:
state = self.status
#FIXME: do not always use 16x16 (ask actually used size and use that)
image = gajim.interface.roster.jabber_state_images['16'][state]
if image.get_storage_type() == gtk.IMAGE_PIXBUF:
self.status_icon.props.pixbuf = image.get_pixbuf()
#FIXME: oops they forgot to support GIF animation?
#or they were lazy to get it to work under Windows! WTF!
#elif image.get_storage_type() == gtk.IMAGE_ANIMATION:
# self.img_tray.set_from_animation(image.get_animation())

View file

@ -43,7 +43,7 @@ except:
class Systray:
'''Class for icon in the notification area
This class is both base class (for systraywin32.py) and normal class
This class is both base class (for statusicon.py) and normal class
for trayicon in GNU/Linux'''
def __init__(self):
@ -94,18 +94,17 @@ class Systray:
def on_new_chat(self, widget, account):
dialogs.NewChatDialog(account)
def make_menu(self, event = None):
'''create chat with and new message (sub) menus/menuitems
event is None when we're in Windows
'''
def make_menu(self, event_button, event_time):
'''create chat with and new message (sub) menus/menuitems'''
for m in self.popup_menus:
m.destroy()
chat_with_menuitem = self.xml.get_widget('chat_with_menuitem')
single_message_menuitem = self.xml.get_widget('single_message_menuitem')
single_message_menuitem = self.xml.get_widget(
'single_message_menuitem')
status_menuitem = self.xml.get_widget('status_menu')
join_gc_menuitem = self.xml.get_widget('join_gc_menuitem')
sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem')
if self.single_message_handler_id:
single_message_menuitem.handler_disconnect(
@ -124,11 +123,12 @@ class Systray:
# We need our own set of status icons, let's make 'em!
iconset = gajim.config.get('iconset')
if not iconset:
iconset = 'dcraven'
path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16')
state_images = gajim.interface.roster.load_iconset(path)
if state_images.has_key('muc_active'):
join_gc_menuitem.set_image(state_images['muc_active'])
for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
uf_show = helpers.get_uf_show(show, use_mnemonic = True)
item = gtk.ImageMenuItem(uf_show)
@ -159,7 +159,8 @@ class Systray:
sub_menu.append(item)
item.connect('activate', self.on_show_menuitem_activate, 'offline')
iskey = connected_accounts > 0
iskey = connected_accounts > 0 and not (connected_accounts == 1 and
gajim.connections[gajim.connections.keys()[0]].is_zeroconf)
chat_with_menuitem.set_sensitive(iskey)
single_message_menuitem.set_sensitive(iskey)
join_gc_menuitem.set_sensitive(iskey)
@ -170,12 +171,15 @@ class Systray:
self.popup_menus.append(account_menu_for_chat_with)
account_menu_for_single_message = gtk.Menu()
single_message_menuitem.set_submenu(account_menu_for_single_message)
single_message_menuitem.set_submenu(
account_menu_for_single_message)
self.popup_menus.append(account_menu_for_single_message)
accounts_list = gajim.contacts.get_accounts()
accounts_list.sort()
for account in accounts_list:
if gajim.connections[account].is_zeroconf:
continue
if gajim.connections[account].connected > 1:
#for chat_with
item = gtk.MenuItem(_('using account %s') % account)
@ -194,8 +198,11 @@ class Systray:
label.set_use_underline(False)
gc_item = gtk.MenuItem()
gc_item.add(label)
gc_item.connect('state-changed',
gtkgui_helpers.on_bm_header_changed_state)
gc_sub_menu.append(gc_item)
gajim.interface.roster.add_bookmarks_list(gc_sub_menu, account)
gajim.interface.roster.add_bookmarks_list(gc_sub_menu,
account)
elif connected_accounts == 1: # one account
# one account connected, no need to show 'as jid'
@ -205,24 +212,25 @@ class Systray:
'activate', self.on_new_chat, account)
# for single message
single_message_menuitem.remove_submenu()
self.single_message_handler_id = single_message_menuitem.connect(
'activate', self.on_single_message_menuitem_activate, account)
self.single_message_handler_id = single_message_menuitem.\
connect('activate',
self.on_single_message_menuitem_activate, account)
# join gc
gajim.interface.roster.add_bookmarks_list(gc_sub_menu, account)
gajim.interface.roster.add_bookmarks_list(gc_sub_menu,
account)
break # No other connected account
if event is None:
# None means windows (we explicitly popup in systraywin32.py)
if self.added_hide_menuitem is False:
self.systray_context_menu.prepend(gtk.SeparatorMenuItem())
item = gtk.MenuItem(_('Hide this menu'))
self.systray_context_menu.prepend(item)
self.added_hide_menuitem = True
sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on'))
if os.name == 'nt' and gtk.pygtk_version >= (2, 10, 0) and\
gtk.gtk_version >= (2, 10, 0):
self.systray_context_menu.popup(None, None,
gtk.status_icon_position_menu, event_button,
event_time, self.status_icon)
else: # GNU and Unices
self.systray_context_menu.popup(None, None, None, event.button,
event.time)
self.systray_context_menu.popup(None, None, None, event_button,
event_time)
self.systray_context_menu.show_all()
def on_show_all_events_menuitem_activate(self, widget):
@ -232,6 +240,10 @@ class Systray:
for event in events[account][jid]:
gajim.interface.handle_event(account, jid, event.type_)
def on_sounds_mute_menuitem_activate(self, widget):
gajim.config.set('sounds_on', not widget.get_active())
gajim.interface.save_config()
def on_show_roster_menuitem_activate(self, widget):
win = gajim.interface.roster.window
win.present()
@ -250,11 +262,11 @@ class Systray:
if len(gajim.events.get_systray_events()) == 0:
# no pending events, so toggle visible/hidden for roster window
if win.get_property('visible'): # visible in ANY virtual desktop?
win.hide() # we hide it from VD that was visible in
# but we could be in another VD right now. eg vd2
# and we want not only to hide it in vd1 but also show it in vd2
gtkgui_helpers.possibly_move_window_in_current_desktop(win)
# we could be in another VD right now. eg vd2
# and we want to show it in vd2
if not gtkgui_helpers.possibly_move_window_in_current_desktop(win):
win.hide() # else we hide it from VD that was visible in
else:
win.present()
else:
@ -275,12 +287,14 @@ class Systray:
def on_clicked(self, widget, event):
self.on_tray_leave_notify_event(widget, None)
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1: # Left click
if event.type != gtk.gdk.BUTTON_PRESS:
return
if event.button == 1: # Left click
self.on_left_click()
elif event.button == 2: # middle click
self.on_middle_click()
elif event.button == 3: # right click
self.make_menu(event)
self.make_menu(event.button, event.time)
def on_show_menuitem_activate(self, widget, show):
# we all add some fake (we cannot select those nor have them as show)
@ -295,6 +309,7 @@ class Systray:
active = gajim.interface.roster.status_combobox.get_active()
status = model[active][2].decode('utf-8')
dlg = dialogs.ChangeStatusMessageDialog(status)
dlg.window.present()
message = dlg.run()
if message is not None: # None if user press Cancel
accounts = gajim.connections.keys()

View file

@ -1,305 +0,0 @@
## src/systraywin32.py
##
## Contributors for this file:
## - Yann Le Boulanger <asterix@lagaule.org>
## - Nikos Kouremenos <kourem@gmail.com>
## - Dimitur Kirov <dkirov@gmail.com>
##
## code initially based on
## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/334779
## with some ideas/help from pysystray.sf.net
##
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
## Vincent Hanquez <tab@snarc.org>
## Nikos Kouremenos <nkour@jabber.org>
## Dimitur Kirov <dkirov@gmail.com>
## Travis Shirk <travis@pobox.com>
## Norman Rasmussen <norman@rasmussen.co.za>
##
## 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; version 2 only.
##
## 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.
##
import win32gui
import pywintypes
import win32con # winapi constants
import systray
import gtk
import os
WM_TASKBARCREATED = win32gui.RegisterWindowMessage('TaskbarCreated')
WM_TRAYMESSAGE = win32con.WM_USER + 20
import gtkgui_helpers
from common import gajim
from common import i18n
class SystrayWINAPI:
def __init__(self, gtk_window):
self._window = gtk_window
self._hwnd = gtk_window.window.handle
self._message_map = {}
self.notify_icon = None
# Sublass the window and inject a WNDPROC to process messages.
self._oldwndproc = win32gui.SetWindowLong(self._hwnd,
win32con.GWL_WNDPROC, self._wndproc)
def add_notify_icon(self, menu, hicon=None, tooltip=None):
""" Creates a notify icon for the gtk window. """
if not self.notify_icon:
if not hicon:
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
self.notify_icon = NotifyIcon(self._hwnd, hicon, tooltip)
# Makes redraw if the taskbar is restarted.
self.message_map({WM_TASKBARCREATED: self.notify_icon._redraw})
def message_map(self, msg_map={}):
""" Maps message processing to callback functions ala win32gui. """
if msg_map:
if self._message_map:
duplicatekeys = [key for key in msg_map.keys()
if self._message_map.has_key(key)]
for key in duplicatekeys:
new_value = msg_map[key]
if isinstance(new_value, list):
raise TypeError('Dict cannot have list values')
value = self._message_map[key]
if new_value != value:
new_value = [new_value]
if isinstance(value, list):
value += new_value
else:
value = [value] + new_value
msg_map[key] = value
self._message_map.update(msg_map)
def remove_notify_icon(self):
""" Removes the notify icon. """
if self.notify_icon:
self.notify_icon.remove()
self.notify_icon = None
def remove(self, *args):
""" Unloads the extensions. """
self._message_map = {}
self.remove_notify_icon()
self = None
def show_balloon_tooltip(self, title, text, timeout=10,
icon=win32gui.NIIF_NONE):
""" Shows a baloon tooltip. """
if not self.notify_icon:
self.add_notifyicon()
self.notify_icon.show_balloon(title, text, timeout, icon)
def _wndproc(self, hwnd, msg, wparam, lparam):
""" A WINDPROC to process window messages. """
if self._message_map.has_key(msg):
callback = self._message_map[msg]
if isinstance(callback, list):
for cb in callback:
cb(hwnd, msg, wparam, lparam)
else:
callback(hwnd, msg, wparam, lparam)
return win32gui.CallWindowProc(self._oldwndproc, hwnd, msg, wparam,
lparam)
class NotifyIcon:
def __init__(self, hwnd, hicon, tooltip=None):
self._hwnd = hwnd
self._id = 0
self._flags = win32gui.NIF_MESSAGE | win32gui.NIF_ICON
self._callbackmessage = WM_TRAYMESSAGE
self._hicon = hicon
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self._get_nid())
except pywintypes.error:
pass
if tooltip: self.set_tooltip(tooltip)
def _get_nid(self):
""" Function to initialise & retrieve the NOTIFYICONDATA Structure. """
nid = [self._hwnd, self._id, self._flags, self._callbackmessage, self._hicon]
if not hasattr(self, '_tip'): self._tip = ''
nid.append(self._tip)
if not hasattr(self, '_info'): self._info = ''
nid.append(self._info)
if not hasattr(self, '_timeout'): self._timeout = 0
nid.append(self._timeout)
if not hasattr(self, '_infotitle'): self._infotitle = ''
nid.append(self._infotitle)
if not hasattr(self, '_infoflags'):self._infoflags = win32gui.NIIF_NONE
nid.append(self._infoflags)
return tuple(nid)
def remove(self):
""" Removes the tray icon. """
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, self._get_nid())
except pywintypes.error:
pass
def set_tooltip(self, tooltip):
""" Sets the tray icon tooltip. """
self._flags = self._flags | win32gui.NIF_TIP
self._tip = tooltip
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self._get_nid())
except pywintypes.error:
pass
def show_balloon(self, title, text, timeout=10, icon=win32gui.NIIF_NONE):
""" Shows a balloon tooltip from the tray icon. """
self._flags = self._flags | win32gui.NIF_INFO
self._infotitle = title
self._info = text
self._timeout = timeout * 1000
self._infoflags = icon
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self._get_nid())
except pywintypes.error:
pass
def _redraw(self, *args):
""" Redraws the tray icon. """
self.remove()
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self._get_nid())
except pywintypes.error:
pass
class SystrayWin32(systray.Systray):
def __init__(self):
# Note: gtk window must be realized before installing extensions.
systray.Systray.__init__(self)
self.jids = []
self.status = 'offline'
self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade')
self.systray_context_menu = self.xml.get_widget('systray_context_menu')
self.added_hide_menuitem = False
# self.tray_ico_imgs = self.load_icos()
w = gtk.Window() # just a window to pass
w.realize() # realize it so gtk window exists
self.systray_winapi = SystrayWINAPI(w)
self.xml.signal_autoconnect(self)
# Set up the callback messages
self.systray_winapi.message_map({
WM_TRAYMESSAGE: self.on_clicked
})
def show_icon(self):
#self.systray_winapi.add_notify_icon(self.systray_context_menu, tooltip = 'Gajim')
#self.systray_winapi.notify_icon.menu = self.systray_context_menu
# do not remove set_img does both above.
# maybe I can only change img without readding
# the notify icon? HOW??
self.set_img()
def hide_icon(self):
self.systray_winapi.remove()
def on_clicked(self, hwnd, message, wparam, lparam):
if lparam == win32con.WM_RBUTTONUP: # Right click
self.make_menu()
self.systray_winapi.notify_icon.menu.popup(None, None, None, 0, 0)
elif lparam == win32con.WM_MBUTTONUP: # Middle click
self.on_middle_click()
elif lparam == win32con.WM_LBUTTONUP: # Left click
self.on_left_click()
def set_img(self):
self.tray_ico_imgs = self.load_icos() #FIXME: do not do this here
# see gajim.interface.roster.reload_jabber_state_images() to merge
if len(self.jids) > 0:
state = 'message'
else:
state = self.status
hicon = self.tray_ico_imgs[state]
if hicon is None:
return
self.systray_winapi.remove_notify_icon()
self.systray_winapi.add_notify_icon(self.systray_context_menu, hicon,
'Gajim')
self.systray_winapi.notify_icon.menu = self.systray_context_menu
nb = gajim.events.get_nb_systray_events()
if nb > 0:
text = i18n.ngettext(
'Gajim - %d unread message',
'Gajim - %d unread messages',
nb, nb, nb)
else:
text = 'Gajim'
self.systray_winapi.notify_icon.set_tooltip(text)
def load_icos(self):
'''load .ico files and return them to a dic of SHOW --> img_obj'''
iconset = str(gajim.config.get('iconset'))
if not iconset:
iconset = 'dcraven'
imgs = {}
path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16', 'icos')
# icon folder for missing icons
path_dcraven_iconset = os.path.join(gajim.DATA_DIR, 'iconsets', 'dcraven',
'16x16', 'icos')
states_list = gajim.SHOW_LIST
# trayicon apart from show holds message state too
states_list.append('message')
for state in states_list:
path_to_ico = os.path.join(path, state + '.ico')
if not os.path.isfile(path_to_ico): # fallback to dcraven iconset
path_to_ico = os.path.join(path_dcraven_iconset, state + '.ico')
if os.path.exists(path_to_ico):
hinst = win32gui.GetModuleHandle(None)
img_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
try:
image = win32gui.LoadImage(hinst, path_to_ico,
win32con.IMAGE_ICON, 0, 0, img_flags)
except pywintypes.error:
imgs[state] = None
else:
imgs[state] = image
return imgs

View file

@ -21,7 +21,6 @@ import time
import locale
import gtkgui_helpers
import message_control
from common import gajim
from common import helpers
@ -38,7 +37,7 @@ class BaseTooltip:
tooltip.hide_tooltip()
* data - the text to be displayed (extenders override this argument and
dislpay more complex contents)
display more complex contents)
* widget_height - the height of the widget on which we want to show tooltip
* widget_y_position - the vertical position of the widget on the screen
@ -135,7 +134,7 @@ class BaseTooltip:
preferred_y = widget_y_position + widget_height + 4
self.preferred_position = [pointer_x, preferred_y]
self.widget_height =widget_height
self.widget_height = widget_height
self.win.ensure_style()
self.win.show_all()
@ -177,10 +176,12 @@ class StatusTable:
# make sure 'status' is unicode before we send to to reduce_chars
if isinstance(status, str):
status = unicode(status, encoding='utf-8')
# reduce to 200 chars, 1 line
status = gtkgui_helpers.reduce_chars_newlines(status, 200, 1)
str_status += ' - ' + status
return gtkgui_helpers.escape_for_pango_markup(str_status)
# reduce to 100 chars, 1 line
status = helpers.reduce_chars_newlines(status, 100, 1)
str_status = gtkgui_helpers.escape_for_pango_markup(str_status)
status = gtkgui_helpers.escape_for_pango_markup(status)
str_status += ' - <i>' + status + '</i>'
return str_status
def add_status_row(self, file_path, show, str_status, status_time = None, show_lock = False):
''' appends a new row with status icon to the table '''
@ -213,13 +214,6 @@ class StatusTable:
gtk.ICON_SIZE_MENU)
self.table.attach(lock_image, 4, 5, self.current_row,
self.current_row + 1, 0, 0, 0, 0)
if status_time:
self.current_row += 1
# decode locale encoded string, the same way as below (10x nk)
local_time = time.strftime("%c", status_time)
local_time = local_time.decode(locale.getpreferredencoding())
status_time_label = gtk.Label(local_time)
status_time_label.set_alignment(0, 0)
class NotificationAreaTooltip(BaseTooltip, StatusTable):
''' Tooltip that is shown in the notification area '''
@ -227,27 +221,6 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable):
BaseTooltip.__init__(self)
StatusTable.__init__(self)
def get_accounts_info(self):
accounts = []
accounts_list = gajim.contacts.get_accounts()
accounts_list.sort()
for account in accounts_list:
status_idx = gajim.connections[account].connected
# uncomment the following to hide offline accounts
# if status_idx == 0: continue
status = gajim.SHOW_LIST[status_idx]
message = gajim.connections[account].status
single_line = helpers.get_uf_show(status)
if message is None:
message = ''
else:
message = message.strip()
if message != '':
single_line += ': ' + message
accounts.append({'name': account, 'status_line': single_line,
'show': status, 'message': message})
return accounts
def fill_table_with_accounts(self, accounts):
iconset = gajim.config.get('iconset')
if not iconset:
@ -259,7 +232,7 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable):
# there are possible pango TBs on 'set_markup'
if isinstance(message, str):
message = unicode(message, encoding = 'utf-8')
message = gtkgui_helpers.reduce_chars_newlines(message, 50, 1)
message = helpers.reduce_chars_newlines(message, 100, 1)
message = gtkgui_helpers.escape_for_pango_markup(message)
if gajim.con_types.has_key(acct['name']) and \
gajim.con_types[acct['name']] in ('tls', 'ssl'):
@ -278,67 +251,16 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable):
def populate(self, data):
self.create_window()
self.create_table()
self.hbox = gtk.HBox()
self.table.set_property('column-spacing', 1)
text, single_line = '', ''
unread_chat = gajim.events.get_nb_events(types = ['printed_chat', 'chat'])
unread_single_chat = gajim.events.get_nb_events(types = ['normal'])
unread_gc = gajim.events.get_nb_events(types = ['printed_gc_msg',
'gc_msg'])
unread_pm = gajim.events.get_nb_events(types = ['printed_pm', 'pm'])
accounts = self.get_accounts_info()
if unread_chat or unread_single_chat or unread_gc or unread_pm:
text = 'Gajim '
awaiting_events = unread_chat + unread_single_chat + unread_gc + unread_pm
if awaiting_events == unread_chat or awaiting_events == unread_single_chat \
or awaiting_events == unread_gc or awaiting_events == unread_pm:
# This condition is like previous if but with xor...
# Print in one line
text += '-'
else:
# Print in multiple lines
text += '\n '
if unread_chat:
text += i18n.ngettext(
' %d unread message',
' %d unread messages',
unread_chat, unread_chat, unread_chat)
text += '\n '
if unread_single_chat:
text += i18n.ngettext(
' %d unread single message',
' %d unread single messages',
unread_single_chat, unread_single_chat, unread_single_chat)
text += '\n '
if unread_gc:
text += i18n.ngettext(
' %d unread group chat message',
' %d unread group chat messages',
unread_gc, unread_gc, unread_gc)
text += '\n '
if unread_pm:
text += i18n.ngettext(
' %d unread private message',
' %d unread private messages',
unread_pm, unread_pm, unread_pm)
text += '\n '
text = text[:-4] # remove latest '\n '
elif len(accounts) > 1:
text = _('Gajim')
self.current_current_row = 1
accounts = helpers.get_accounts_info()
if len(accounts) > 1:
self.table.resize(2, 1)
self.fill_table_with_accounts(accounts)
self.hbox = gtk.HBox()
self.table.set_property('column-spacing', 1)
elif len(accounts) == 1:
message = accounts[0]['status_line']
message = gtkgui_helpers.reduce_chars_newlines(message, 50, 1)
message = gtkgui_helpers.escape_for_pango_markup(message)
text = _('Gajim - %s') % message
else:
text = _('Gajim - %s') % helpers.get_uf_show('offline')
text = helpers.get_notification_icon_tooltip_text()
text = gtkgui_helpers.escape_for_pango_markup(text)
self.add_text_row(text)
self.hbox.add(self.table)
self.win.add(self.hbox)
@ -364,27 +286,37 @@ class GCTooltip(BaseTooltip):
vcard_table.set_homogeneous(False)
vcard_current_row = 1
properties = []
if contact.jid.strip() != '':
jid_markup = '<span weight="bold">' + contact.jid + '</span>'
else:
jid_markup = '<span weight="bold">' + \
nick_markup = '<b>' + \
gtkgui_helpers.escape_for_pango_markup(contact.get_shown_name()) \
+ '</span>'
properties.append((jid_markup, None))
properties.append((_('Role: '), helpers.get_uf_role(contact.role)))
properties.append((_('Affiliation: '), contact.affiliation.capitalize()))
if hasattr(contact, 'resource') and contact.resource.strip() != '':
properties.append((_('Resource: '),
gtkgui_helpers.escape_for_pango_markup(contact.resource) ))
show = helpers.get_uf_show(contact.show)
if contact.status:
+ '</b>'
properties.append((nick_markup, None))
if contact.status: # status message
status = contact.status.strip()
if status != '':
# escape markup entities
status = gtkgui_helpers.reduce_chars_newlines(status, 200, 5)
show += ' - ' + gtkgui_helpers.escape_for_pango_markup(status)
properties.append((_('Status: '), show))
status = helpers.reduce_chars_newlines(status, 100, 5)
status = '<i>' +\
gtkgui_helpers.escape_for_pango_markup(status) + '</i>'
properties.append((status, None))
else: # no status message, show SHOW instead
show = helpers.get_uf_show(contact.show)
show = '<i>' + show + '</i>'
properties.append((show, None))
if contact.jid.strip() != '':
properties.append((_('JID: '), contact.jid))
if hasattr(contact, 'resource') and contact.resource.strip() != '':
properties.append((_('Resource: '),
gtkgui_helpers.escape_for_pango_markup(contact.resource) ))
if contact.affiliation != 'none':
uf_affiliation = helpers.get_uf_affiliation(contact.affiliation)
affiliation_str = \
_('%(owner_or_admin_or_member)s of this group chat') %\
{'owner_or_admin_or_member': uf_affiliation}
properties.append((affiliation_str, None))
# Add avatar
puny_name = helpers.sanitize_filename(contact.name)
@ -410,22 +342,23 @@ class GCTooltip(BaseTooltip):
label.set_alignment(0, 0)
if property[1]:
label.set_markup(property[0])
vcard_table.attach(label, 1, 2, vcard_current_row, vcard_current_row + 1,
gtk.FILL, vertical_fill, 0, 0)
vcard_table.attach(label, 1, 2, vcard_current_row,
vcard_current_row + 1, gtk.FILL, vertical_fill, 0, 0)
label = gtk.Label()
label.set_alignment(0, 0)
label.set_markup(property[1])
label.set_line_wrap(True)
vcard_table.attach(label, 2, 3, vcard_current_row, vcard_current_row + 1,
gtk.EXPAND | gtk.FILL, vertical_fill, 0, 0)
vcard_table.attach(label, 2, 3, vcard_current_row,
vcard_current_row + 1, gtk.EXPAND | gtk.FILL,
vertical_fill, 0, 0)
else:
label.set_markup(property[0])
vcard_table.attach(label, 1, 3, vcard_current_row, vcard_current_row + 1,
gtk.FILL, vertical_fill, 0)
vcard_table.attach(label, 1, 3, vcard_current_row,
vcard_current_row + 1, gtk.FILL, vertical_fill, 0)
self.avatar_image.set_alignment(0, 0)
vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row +1,
gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3)
vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row + 1,
gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3)
self.win.add(vcard_table)
class RosterTooltip(NotificationAreaTooltip):
@ -445,8 +378,7 @@ class RosterTooltip(NotificationAreaTooltip):
self.create_table()
if not contacts or len(contacts) == 0:
# Tooltip for merged accounts row
accounts = self.get_accounts_info()
self.current_current_row = 0
accounts = helpers.get_accounts_info()
self.table.resize(2, 1)
self.spacer_label = ''
self.fill_table_with_accounts(accounts)
@ -458,15 +390,6 @@ class RosterTooltip(NotificationAreaTooltip):
prim_contact = gajim.contacts.get_highest_prio_contact_from_contacts(
contacts)
transport = gajim.get_transport_name_from_jid(prim_contact.jid)
if transport:
file_path = os.path.join(gajim.DATA_DIR, 'iconsets', 'transports',
transport , '16x16')
else:
iconset = gajim.config.get('iconset')
if not iconset:
iconset = 'dcraven'
file_path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16')
puny_jid = helpers.sanitize_filename(prim_contact.jid)
table_size = 3
@ -486,23 +409,12 @@ class RosterTooltip(NotificationAreaTooltip):
vcard_table.set_homogeneous(False)
vcard_current_row = 1
properties = []
jid_markup = '<span weight="bold">' + prim_contact.jid + '</span>'
properties.append((jid_markup, None))
properties.append((_('Name: '), gtkgui_helpers.escape_for_pango_markup(
prim_contact.get_shown_name())))
if prim_contact.sub:
properties.append(( _('Subscription: '),
gtkgui_helpers.escape_for_pango_markup(helpers.get_uf_sub(prim_contact.sub))))
if prim_contact.keyID:
keyID = None
if len(prim_contact.keyID) == 8:
keyID = prim_contact.keyID
elif len(prim_contact.keyID) == 16:
keyID = prim_contact.keyID[8:]
if keyID:
properties.append((_('OpenPGP: '),
gtkgui_helpers.escape_for_pango_markup(keyID)))
name_markup = u'<span weight="bold">' + \
gtkgui_helpers.escape_for_pango_markup(prim_contact.get_shown_name())\
+ '</span>'
properties.append((name_markup, None))
num_resources = 0
# put contacts in dict, where key is priority
contacts_dict = {}
@ -514,12 +426,20 @@ class RosterTooltip(NotificationAreaTooltip):
else:
contacts_dict[contact.priority] = [contact]
if num_resources == 1 and contact.resource:
properties.append((_('Resource: '),
gtkgui_helpers.escape_for_pango_markup(contact.resource) + ' (' + \
unicode(contact.priority) + ')'))
if num_resources > 1:
properties.append((_('Status: '), ' '))
transport = gajim.get_transport_name_from_jid(
prim_contact.jid)
if transport:
file_path = os.path.join(gajim.DATA_DIR, 'iconsets',
'transports', transport , '16x16')
else:
iconset = gajim.config.get('iconset')
if not iconset:
iconset = 'dcraven'
file_path = os.path.join(gajim.DATA_DIR,
'iconsets', iconset, '16x16')
contact_keys = contacts_dict.keys()
contact_keys.sort()
contact_keys.reverse()
@ -535,29 +455,63 @@ class RosterTooltip(NotificationAreaTooltip):
else: # only one resource
if contact.show:
show = helpers.get_uf_show(contact.show)
if contact.last_status_time:
vcard_current_row += 1
if contact.show == 'offline':
text = ' - ' + _('Last status: %s')
else:
text = _(' since %s')
if time.strftime('%j', time.localtime())== \
time.strftime('%j', contact.last_status_time):
# it's today, show only the locale hour representation
local_time = time.strftime('%X',
contact.last_status_time)
else:
# time.strftime returns locale encoded string
local_time = time.strftime('%c',
contact.last_status_time)
local_time = local_time.decode(
locale.getpreferredencoding())
text = text % local_time
show += text
show = '<i>' + show + '</i>'
# we append show below
if contact.status:
status = contact.status.strip()
if status:
# reduce long status
# (no more than 200 chars on line and no more than 5 lines)
status = gtkgui_helpers.reduce_chars_newlines(status, 200, 5)
# (no more than 100 chars on line and no more than 5 lines)
status = helpers.reduce_chars_newlines(status, 100, 5)
# escape markup entities.
status = gtkgui_helpers.escape_for_pango_markup(status)
show += ' - ' + status
properties.append((_('Status: '), show))
if contact.last_status_time:
vcard_current_row += 1
if contact.show == 'offline':
text = _('Last status on %s')
else:
text = _('Since %s')
properties.append(('<i>%s</i>' % status, None))
properties.append((show, None))
properties.append((_('Jabber ID: '), prim_contact.jid ))
# time.strftime returns locale encoded string
local_time = time.strftime('%c', contact.last_status_time)
local_time = local_time.decode(locale.getpreferredencoding())
text = text % local_time
properties.append(('<span style="italic">%s</span>' % text, None))
# contact has only one ressource
if num_resources == 1 and contact.resource:
properties.append((_('Resource: '),
gtkgui_helpers.escape_for_pango_markup(contact.resource) +\
' (' + unicode(contact.priority) + ')'))
if prim_contact.sub and prim_contact.sub != 'both':
# ('both' is the normal sub so we don't show it)
properties.append(( _('Subscription: '),
gtkgui_helpers.escape_for_pango_markup(helpers.get_uf_sub(prim_contact.sub))))
if prim_contact.keyID:
keyID = None
if len(prim_contact.keyID) == 8:
keyID = prim_contact.keyID
elif len(prim_contact.keyID) == 16:
keyID = prim_contact.keyID[8:]
if keyID:
properties.append((_('OpenPGP: '),
gtkgui_helpers.escape_for_pango_markup(keyID)))
while properties:
property = properties.pop(0)
vcard_current_row += 1
@ -568,28 +522,26 @@ class RosterTooltip(NotificationAreaTooltip):
label.set_alignment(0, 0)
if property[1]:
label.set_markup(property[0])
vcard_table.attach(label, 1, 2, vcard_current_row, vcard_current_row + 1,
gtk.FILL, vertical_fill, 0, 0)
vcard_table.attach(label, 1, 2, vcard_current_row,
vcard_current_row + 1, gtk.FILL, vertical_fill, 0, 0)
label = gtk.Label()
if num_resources > 1 and not properties:
label.set_alignment(0, 1)
else:
label.set_alignment(0, 0)
label.set_alignment(0, 0)
label.set_markup(property[1])
label.set_line_wrap(True)
vcard_table.attach(label, 2, 3, vcard_current_row, vcard_current_row + 1,
gtk.EXPAND | gtk.FILL, vertical_fill, 0, 0)
vcard_table.attach(label, 2, 3, vcard_current_row,
vcard_current_row + 1, gtk.EXPAND | gtk.FILL,
vertical_fill, 0, 0)
else:
if isinstance(property[0], unicode):
if isinstance(property[0], (unicode, str)): #FIXME: rm unicode?
label.set_markup(property[0])
else:
label = property[0]
vcard_table.attach(label, 1, 3, vcard_current_row, vcard_current_row + 1,
gtk.FILL, vertical_fill, 0)
vcard_table.attach(label, 1, 3, vcard_current_row,
vcard_current_row + 1, gtk.FILL, vertical_fill, 0)
self.avatar_image.set_alignment(0, 0)
if table_size == 4:
vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row +1,
gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3)
vcard_table.attach(self.avatar_image, 3, 4, 2,
vcard_current_row + 1, gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3)
self.win.add(vcard_table)
class FileTransfersTooltip(BaseTooltip):

View file

@ -2,6 +2,7 @@
##
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
@ -13,16 +14,15 @@
## GNU General Public License for more details.
##
# THIS FILE IS FOR **OTHERS'** PROFILE (when we VIEW their INFO)
import gtk
import gobject
import base64
import mimetypes
import os
import time
import locale
import gtkgui_helpers
import dialogs
from common import helpers
from common import gajim
@ -57,25 +57,49 @@ def get_avatar_pixbuf_encoded_mime(photo):
class VcardWindow:
'''Class for contact's information window'''
def __init__(self, contact, account, is_fake = False):
def __init__(self, contact, account, gc_contact = None):
# the contact variable is the jid if vcard is true
self.xml = gtkgui_helpers.get_glade('vcard_information_window.glade')
self.window = self.xml.get_widget('vcard_information_window')
self.progressbar = self.xml.get_widget('progressbar')
self.contact = contact
self.account = account
self.is_fake = is_fake
self.gc_contact = gc_contact
self.avatar_mime_type = None
self.avatar_encoded = None
self.vcard_arrived = False
self.os_info_arrived = False
self.update_progressbar_timeout_id = gobject.timeout_add(100,
self.update_progressbar)
self.fill_jabber_page()
annotations = gajim.connections[self.account].annotations
if self.contact.jid in annotations:
buffer = self.xml.get_widget('textview_annotation').get_buffer()
buffer.set_text(annotations[self.contact.jid])
self.xml.signal_autoconnect(self)
self.window.show_all()
self.xml.get_widget('close_button').grab_focus()
def update_progressbar(self):
self.progressbar.pulse()
return True # loop forever
def on_vcard_information_window_destroy(self, widget):
if self.update_progressbar_timeout_id is not None:
gobject.source_remove(self.update_progressbar_timeout_id)
del gajim.interface.instances[self.account]['infos'][self.contact.jid]
buffer = self.xml.get_widget('textview_annotation').get_buffer()
annotation = buffer.get_text(buffer.get_start_iter(),
buffer.get_end_iter())
connection = gajim.connections[self.account]
if annotation != connection.annotations.get(self.contact.jid, ''):
connection.annotations[self.contact.jid] = annotation
connection.store_annotations()
def on_vcard_information_window_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.Escape:
@ -104,7 +128,8 @@ class VcardWindow:
menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.contact.jid, self.account, self.contact.name + '.jpeg')
self.contact.jid, self.account, self.contact.get_shown_name() +
'.jpeg')
menu.append(menuitem)
menu.connect('selection-done', lambda w:w.destroy())
# show the menu
@ -113,13 +138,24 @@ class VcardWindow:
def set_value(self, entry_name, value):
try:
self.xml.get_widget(entry_name).set_text(value)
if value and entry_name == 'URL_label':
if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0):
widget = gtk.LinkButton(value, value)
else:
widget = gtk.Label(value)
widget.set_selectable(True)
widget.show()
table = self.xml.get_widget('personal_info_table')
table.attach(widget, 1, 4, 3, 4, yoptions = 0)
else:
self.xml.get_widget(entry_name).set_text(value)
except AttributeError:
pass
def set_values(self, vcard):
for i in vcard.keys():
if i == 'PHOTO':
if i == 'PHOTO' and self.xml.get_widget('information_notebook').\
get_n_pages() > 4:
pixbuf, self.avatar_encoded, self.avatar_mime_type = \
get_avatar_pixbuf_encoded_mime(vcard[i])
image = self.xml.get_widget('PHOTO_image')
@ -130,7 +166,7 @@ class VcardWindow:
pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'vcard')
image.set_from_pixbuf(pixbuf)
continue
if i == 'ADR' or i == 'TEL' or i == 'EMAIL':
if i in ('ADR', 'TEL', 'EMAIL'):
for entry in vcard[i]:
add_on = '_HOME'
if 'WORK' in entry:
@ -144,14 +180,23 @@ class VcardWindow:
if i == 'DESC':
self.xml.get_widget('DESC_textview').get_buffer().set_text(
vcard[i], 0)
else:
elif i != 'jid': # Do not override jid_label
self.set_value(i + '_label', vcard[i])
self.vcard_arrived = True
self.test_remove_progressbar()
def test_remove_progressbar(self):
if self.update_progressbar_timeout_id is not None and \
self.vcard_arrived and self.os_info_arrived:
gobject.source_remove(self.update_progressbar_timeout_id)
self.progressbar.hide()
self.update_progressbar_timeout_id = None
def set_last_status_time(self):
self.fill_status_label()
def set_os_info(self, resource, client_info, os_info):
if self.xml.get_widget('information_notebook').get_n_pages() < 4:
if self.xml.get_widget('information_notebook').get_n_pages() < 5:
return
i = 0
client = ''
@ -174,16 +219,25 @@ class VcardWindow:
os = Q_('?OS:Unknown')
self.xml.get_widget('client_name_version_label').set_text(client)
self.xml.get_widget('os_label').set_text(os)
self.os_info_arrived = True
self.test_remove_progressbar()
def fill_status_label(self):
if self.xml.get_widget('information_notebook').get_n_pages() < 4:
if self.xml.get_widget('information_notebook').get_n_pages() < 5:
return
contact_list = gajim.contacts.get_contact(self.account, self.contact.jid)
connected_contact_list = []
for c in contact_list:
if c.show not in ('offline', 'error'):
connected_contact_list.append(c)
if not connected_contact_list:
# no connected contact, get the offline one
connected_contact_list = contact_list
# stats holds show and status message
stats = ''
one = True # Are we adding the first line ?
if contact_list:
for c in contact_list:
if connected_contact_list:
for c in connected_contact_list:
if not one:
stats += '\n'
stats += helpers.get_uf_show(c.show)
@ -212,26 +266,38 @@ class VcardWindow:
self.contact.get_shown_name() +
'</span></b>')
self.xml.get_widget('jid_label').set_text(self.contact.jid)
uf_sub = helpers.get_uf_sub(self.contact.sub)
self.xml.get_widget('subscription_label').set_text(uf_sub)
eb = self.xml.get_widget('subscription_label_eventbox')
if self.contact.sub == 'from':
tt_text = _("This contact is interested in your presence information, but you are not interested in his/her presence")
elif self.contact.sub == 'to':
tt_text = _("You are interested in the contact's presence information, but he/she is not interested in yours")
elif self.contact.sub == 'both':
tt_text = _("You and the contact are interested in each other's presence information")
else: # None
tt_text = _("You are not interested in the contact's presence, and neither he/she is interested in yours")
tooltips.set_tip(eb, tt_text)
subscription_label = self.xml.get_widget('subscription_label')
ask_label = self.xml.get_widget('ask_label')
if self.gc_contact:
self.xml.get_widget('subscription_title_label').set_text(_("Role:"))
uf_role = helpers.get_uf_role(self.gc_contact.role)
subscription_label.set_text(uf_role)
self.xml.get_widget('ask_title_label').set_text(_("Affiliation:"))
uf_affiliation = helpers.get_uf_affiliation(self.gc_contact.affiliation)
ask_label.set_text(uf_affiliation)
else:
uf_sub = helpers.get_uf_sub(self.contact.sub)
subscription_label.set_text(uf_sub)
eb = self.xml.get_widget('subscription_label_eventbox')
if self.contact.sub == 'from':
tt_text = _("This contact is interested in your presence information, but you are not interested in his/her presence")
elif self.contact.sub == 'to':
tt_text = _("You are interested in the contact's presence information, but he/she is not interested in yours")
elif self.contact.sub == 'both':
tt_text = _("You and the contact are interested in each other's presence information")
else: # None
tt_text = _("You are not interested in the contact's presence, and neither he/she is interested in yours")
tooltips.set_tip(eb, tt_text)
uf_ask = helpers.get_uf_ask(self.contact.ask)
ask_label.set_text(uf_ask)
eb = self.xml.get_widget('ask_label_eventbox')
if self.contact.ask == 'subscribe':
tooltips.set_tip(eb,
_("You are waiting contact's answer about your subscription request"))
label = self.xml.get_widget('ask_label')
uf_ask = helpers.get_uf_ask(self.contact.ask)
label.set_text(uf_ask)
eb = self.xml.get_widget('ask_label_eventbox')
if self.contact.ask == 'subscribe':
tooltips.set_tip(eb,
_("You are waiting contact's answer about your subscription request"))
log = True
if self.contact.jid in gajim.config.get_per('accounts', self.account,
'no_log_for').split(' '):
@ -251,8 +317,12 @@ class VcardWindow:
gajim.connections[self.account].request_last_status_time(self.contact.jid,
self.contact.resource)
# Request os info in contact is connected
if self.contact.show not in ('offline', 'error'):
# do not wait for os_info if contact is not connected or has error
# additional check for observer is needed, as show is offline for him
if self.contact.show in ('offline', 'error')\
and not self.contact.is_observer():
self.os_info_arrived = True
else: # Request os info if contact is connected
gobject.idle_add(gajim.connections[self.account].request_os_info,
self.contact.jid, self.contact.resource)
self.os_info = {0: {'resource': self.contact.resource, 'client': '',
@ -282,4 +352,160 @@ class VcardWindow:
self.fill_status_label()
gajim.connections[self.account].request_vcard(self.contact.jid, self.is_fake)
gajim.connections[self.account].request_vcard(self.contact.jid,
self.gc_contact is not None)
def on_close_button_clicked(self, widget):
self.window.destroy()
class ZeroconfVcardWindow:
def __init__(self, contact, account, is_fake = False):
# the contact variable is the jid if vcard is true
self.xml = gtkgui_helpers.get_glade('zeroconf_information_window.glade')
self.window = self.xml.get_widget('zeroconf_information_window')
self.contact = contact
self.account = account
self.is_fake = is_fake
# self.avatar_mime_type = None
# self.avatar_encoded = None
self.fill_contact_page()
self.fill_personal_page()
self.xml.signal_autoconnect(self)
self.window.show_all()
def on_zeroconf_information_window_destroy(self, widget):
del gajim.interface.instances[self.account]['infos'][self.contact.jid]
def on_zeroconf_information_window_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.Escape:
self.window.destroy()
def on_log_history_checkbutton_toggled(self, widget):
#log conversation history?
oldlog = True
no_log_for = gajim.config.get_per('accounts', self.account,
'no_log_for').split()
if self.contact.jid in no_log_for:
oldlog = False
log = widget.get_active()
if not log and not self.contact.jid in no_log_for:
no_log_for.append(self.contact.jid)
if log and self.contact.jid in no_log_for:
no_log_for.remove(self.contact.jid)
if oldlog != log:
gajim.config.set_per('accounts', self.account, 'no_log_for',
' '.join(no_log_for))
def on_PHOTO_eventbox_button_press_event(self, widget, event):
'''If right-clicked, show popup'''
if event.button == 3: # right click
menu = gtk.Menu()
menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.contact.jid, self.account, self.contact.get_shown_name() +
'.jpeg')
menu.append(menuitem)
menu.connect('selection-done', lambda w:w.destroy())
# show the menu
menu.show_all()
menu.popup(None, None, None, event.button, event.time)
def set_value(self, entry_name, value):
try:
if value and entry_name == 'URL_label':
if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0):
widget = gtk.LinkButton(value, value)
else:
widget = gtk.Label(value)
widget.set_selectable(True)
table = self.xml.get_widget('personal_info_table')
table.attach(widget, 1, 4, 3, 4, yoptions = 0)
else:
self.xml.get_widget(entry_name).set_text(value)
except AttributeError:
pass
def fill_status_label(self):
if self.xml.get_widget('information_notebook').get_n_pages() < 2:
return
contact_list = gajim.contacts.get_contact(self.account, self.contact.jid)
# stats holds show and status message
stats = ''
one = True # Are we adding the first line ?
if contact_list:
for c in contact_list:
if not one:
stats += '\n'
stats += helpers.get_uf_show(c.show)
if c.status:
stats += ': ' + c.status
if c.last_status_time:
stats += '\n' + _('since %s') % time.strftime('%c',
c.last_status_time).decode(locale.getpreferredencoding())
one = False
else: # Maybe gc_vcard ?
stats = helpers.get_uf_show(self.contact.show)
if self.contact.status:
stats += ': ' + self.contact.status
status_label = self.xml.get_widget('status_label')
status_label.set_max_width_chars(15)
status_label.set_text(stats)
tip = gtk.Tooltips()
status_label_eventbox = self.xml.get_widget('status_label_eventbox')
tip.set_tip(status_label_eventbox, stats)
def fill_contact_page(self):
tooltips = gtk.Tooltips()
self.xml.get_widget('nickname_label').set_markup(
'<b><span size="x-large">' +
self.contact.get_shown_name() +
'</span></b>')
self.xml.get_widget('local_jid_label').set_text(self.contact.jid)
log = True
if self.contact.jid in gajim.config.get_per('accounts', self.account,
'no_log_for').split(' '):
log = False
checkbutton = self.xml.get_widget('log_history_checkbutton')
checkbutton.set_active(log)
checkbutton.connect('toggled', self.on_log_history_checkbutton_toggled)
resources = '%s (%s)' % (self.contact.resource, unicode(
self.contact.priority))
uf_resources = self.contact.resource + _(' resource with priority ')\
+ unicode(self.contact.priority)
if not self.contact.status:
self.contact.status = ''
# Request list time status
# gajim.connections[self.account].request_last_status_time(self.contact.jid,
# self.contact.resource)
self.xml.get_widget('resource_prio_label').set_text(resources)
resource_prio_label_eventbox = self.xml.get_widget(
'resource_prio_label_eventbox')
tooltips.set_tip(resource_prio_label_eventbox, uf_resources)
self.fill_status_label()
# gajim.connections[self.account].request_vcard(self.contact.jid, self.is_fake)
def fill_personal_page(self):
contact = gajim.connections[gajim.ZEROCONF_ACC_NAME].roster.getItem(self.contact.jid)
for key in ('1st', 'last', 'jid', 'email'):
if not contact['txt_dict'].has_key(key):
contact['txt_dict'][key] = ''
self.xml.get_widget('first_name_label').set_text(contact['txt_dict']['1st'])
self.xml.get_widget('last_name_label').set_text(contact['txt_dict']['last'])
self.xml.get_widget('jabber_id_label').set_text(contact['txt_dict']['jid'])
self.xml.get_widget('email_label').set_text(contact['txt_dict']['email'])
def on_close_button_clicked(self, widget):
self.window.destroy()