From 4ae64ff6e67ecffbf3a0947659c10dc106b783a1 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 31 Oct 2009 19:03:03 +0100 Subject: [PATCH 01/29] ability to configure out/inmsgtxt color in preference window. Fixes #5372 --- data/glade/preferences_window.glade | 597 ++++++++++++++++++---------- src/config.py | 100 +++-- src/dialogs.py | 6 +- src/gajim.py | 2 + 4 files changed, 468 insertions(+), 237 deletions(-) diff --git a/data/glade/preferences_window.glade b/data/glade/preferences_window.glade index 73bb13a6b..82eb6b817 100644 --- a/data/glade/preferences_window.glade +++ b/data/glade/preferences_window.glade @@ -1,13 +1,13 @@ - - - + + + 6 Preferences preferences - + True @@ -27,7 +27,7 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 - GTK_SHADOW_NONE + none True @@ -41,28 +41,29 @@ 6 + Display a_vatars of contacts in roster True True + False If checked, Gajim will display avatars of contacts in roster window and in group chats - Display a_vatars of contacts in roster True - 0 True False False + 0 + Display status _messages of contacts in roster True True + False If checked, Gajim will display status messages of contacts under the contact name in roster window and in group chats - Display status _messages of contacts in roster True - 0 True @@ -74,12 +75,12 @@ + Display m_ood of contacts in roster True True + False If checked, Gajim will display the mood of contacts in the roster window - Display m_ood of contacts in roster True - 0 True @@ -91,12 +92,12 @@ + Display _activity of contacts in roster True True + False If checked, Gajim will display the activity of contacts in the roster window - Display _activity of contacts in roster True - 0 True @@ -108,12 +109,12 @@ + Display _tunes of contacts in roster True True + False If checked, Gajim will display the tunes of contacts in the roster window - Display _tunes of contacts in roster True - 0 True @@ -134,15 +135,16 @@ False + 0 + in _roster True True - in _roster + False True - 0 True @@ -153,11 +155,11 @@ + in _group chats True True - in _group chats + False True - 0 True @@ -188,6 +190,7 @@ False + 0 @@ -195,7 +198,7 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 - GTK_SHADOW_NONE + none True @@ -255,11 +258,11 @@ - True - Hide all buttons in chat windows Ma_ke message windows compact + True + False + Hide all buttons in chat windows True - 0 True @@ -272,12 +275,12 @@ + _Ignore rich content in incoming messages True True + False Some messages may include rich content (formatting, colors etc). If checked, Gajim will just display the raw message text. - _Ignore rich content in incoming messages True - 0 True @@ -290,11 +293,11 @@ - True - If checked, Gajim will highlight spelling errors in input fields of chat windows. If no language is explicitly set via right click on the input field, the default language will be used for this contact or group chat. _Highlight misspelled words + True + False + If checked, Gajim will highlight spelling errors in input fields of chat windows. If no language is explicitly set via right click on the input field, the default language will be used for this contact or group chat. True - 0 True @@ -352,8 +355,8 @@ Detached roster with chat grouped by type General - tab False + tab @@ -365,7 +368,7 @@ Detached roster with chat grouped by type True 0 - GTK_SHADOW_NONE + none True @@ -389,6 +392,7 @@ Detached roster with chat grouped by type False + 0 @@ -405,15 +409,18 @@ Show only in roster + + 0 + + Notify me about contacts that sign _in True True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Notify me about contacts that sign _in True - 0 True @@ -423,12 +430,12 @@ Show only in roster + Notify me about contacts that sign _out True True + False Gajim will notify you via a popup window in the bottom right of the screen about contacts that just signed out - Notify me about contacts that sign _out True - 0 True @@ -440,11 +447,11 @@ Show only in roster + Allow popup/notifications when I'm _away/na/busy/invisible True True - Allow popup/notifications when I'm _away/na/busy/invisible + False True - 0 True @@ -460,7 +467,7 @@ Show only in roster False True 0 - GTK_SHADOW_NONE + none True @@ -471,28 +478,29 @@ Show only in roster 6 + Notify on new _GMail email True True + False If checked, Gajim will show a notification when a new e-mail is received via GMail - Notify on new _GMail email True - 0 True False False + 0 + Display _extra email details True True + False If checked, Gajim will also include information about the sender of the new emails - Display _extra email details True - 0 True @@ -534,6 +542,7 @@ Show only in roster False + 0 @@ -561,17 +570,17 @@ Always True + Advanced... True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Advanced... - 0 False False + 0 @@ -599,6 +608,7 @@ Always False + 0 @@ -606,7 +616,7 @@ Always True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 - GTK_SHADOW_NONE + none True @@ -623,26 +633,26 @@ Always 6 + Play _sounds True True - Play _sounds + False True - 0 True False + 0 + Ma_nage... True True True - Ma_nage... True - 0 @@ -651,14 +661,17 @@ Always + + 0 + + Allow sound when I'm _busy True True - Allow sound when I'm _busy + False True - 0 True @@ -699,9 +712,9 @@ Always Notifications - tab 1 False + tab @@ -714,7 +727,7 @@ Always True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 - GTK_SHADOW_NONE + none True @@ -811,6 +824,7 @@ Disabled False + 0 @@ -825,9 +839,9 @@ Disabled Personal Events - tab 2 False + tab @@ -840,7 +854,7 @@ Disabled True True 0 - GTK_SHADOW_NONE + none True @@ -855,12 +869,12 @@ Disabled 6 + _Away after: True True + False If checked, Gajim will change status to Away when the computer is unused. - _Away after: True - 0 True @@ -871,12 +885,12 @@ Disabled + _Not available after: True True + False If checked, Gajim will change status to Not Available when the computer has not been used even longer - _Not available after: True - 0 True @@ -1013,13 +1027,14 @@ $T will be replaced by auto-not-available timeout False False + 0 True 0 - GTK_SHADOW_NONE + none True @@ -1042,34 +1057,36 @@ $T will be replaced by auto-not-available timeout False False + 0 - + True 14 + Sign _in True True - Sign _in + False True - 0 True False False + 0 + Sign _out True True - Sign _out + False True - 0 True @@ -1090,6 +1107,7 @@ $T will be replaced by auto-not-available timeout False False + 0 @@ -1097,9 +1115,9 @@ $T will be replaced by auto-not-available timeout True True If enabled, Gajim will not ask for a status message. The specified default message will be used instead. - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + automatic + automatic + in True @@ -1134,7 +1152,7 @@ $T will be replaced by auto-not-available timeout True 0 - GTK_SHADOW_NONE + none True @@ -1148,36 +1166,40 @@ $T will be replaced by auto-not-available timeout 2 6 6 - - - True 5 - GTK_BUTTONBOX_START + start + gtk-new True True True - gtk-new + False True - 0 + + False + False + 0 + + gtk-delete True True True - gtk-delete + False True - 0 + False + False 1 @@ -1193,9 +1215,9 @@ $T will be replaced by auto-not-available timeout True True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + automatic + automatic + in True @@ -1211,15 +1233,15 @@ $T will be replaced by auto-not-available timeout True True - GTK_POLICY_NEVER - GTK_POLICY_NEVER - GTK_SHADOW_IN + never + never + in True True 3 - GTK_WRAP_WORD + word @@ -1229,6 +1251,9 @@ $T will be replaced by auto-not-available timeout GTK_FILL + + + @@ -1259,9 +1284,9 @@ $T will be replaced by auto-not-available timeout Status - tab 3 False + tab @@ -1273,7 +1298,7 @@ $T will be replaced by auto-not-available timeout True 0 - GTK_SHADOW_NONE + none True @@ -1287,14 +1312,11 @@ $T will be replaced by auto-not-available timeout 2 6 6 - - - True True - 0 + False @@ -1304,11 +1326,11 @@ $T will be replaced by auto-not-available timeout + Use system _default True True - Use system _default + False True - 0 True @@ -1330,6 +1352,9 @@ $T will be replaced by auto-not-available timeout GTK_FILL + + + @@ -1348,6 +1373,7 @@ $T will be replaced by auto-not-available timeout False False + 0 @@ -1355,7 +1381,7 @@ $T will be replaced by auto-not-available timeout True GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 - GTK_SHADOW_NONE + none True @@ -1370,20 +1396,14 @@ $T will be replaced by auto-not-available timeout 3 6 6 - - - - - - + Use _transports icons True True + False If checked, Gajim will use protocol-specific status icons. (eg. A contact from MSN will have the equivalent msn icon for status online, away, busy, etc...) - Use _transports icons True - 0 True @@ -1435,12 +1455,12 @@ $T will be replaced by auto-not-available timeout + Ma_nage... True True + False Configure color and font of the interface - Ma_nage... True - 0 @@ -1463,6 +1483,12 @@ $T will be replaced by auto-not-available timeout GTK_FILL + + + + + + @@ -1488,7 +1514,7 @@ $T will be replaced by auto-not-available timeout True 0 - GTK_SHADOW_NONE + none True @@ -1497,7 +1523,7 @@ $T will be replaced by auto-not-available timeout True - 3 + 4 4 12 6 @@ -1505,40 +1531,22 @@ $T will be replaced by auto-not-available timeout True 0 - _Incoming message: + Contact's nickname: True - GTK_JUSTIFY_CENTER - True - incoming_msg_colorbutton + center GTK_FILL - - - True - True - 0 - - - - 1 - 2 - - - - True 0 - _Outgoing message: + Your nickname: True - GTK_JUSTIFY_CENTER - True - outgoing_msg_colorbutton + center 2 @@ -1547,82 +1555,33 @@ $T will be replaced by auto-not-available timeout - - - True - True - 0 - - - - 3 - 4 - GTK_FILL - - - True 0 _Status message: True - GTK_JUSTIFY_CENTER - True - status_msg_colorbutton + center - 1 - 2 + 2 + 3 GTK_FILL - - - True - True - 0 - - - - 1 - 2 - 1 - 2 - - - - True 0 _URL highlight: True - url_msg_colorbutton 2 3 - 1 - 2 - GTK_FILL - - - - - - True - True - 0 - - - - 3 - 4 - 1 - 2 + 2 + 3 GTK_FILL @@ -1636,14 +1595,15 @@ $T will be replaced by auto-not-available timeout False + 0 True True + False False - 0 @@ -1662,6 +1622,7 @@ $T will be replaced by auto-not-available timeout False False + 0 @@ -1690,9 +1651,225 @@ $T will be replaced by auto-not-available timeout 4 + 3 + 4 + GTK_FILL + + + + + True + 0 + Contact's message: + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + Your message: + + + 2 + 3 + 1 + 2 + GTK_FILL + + + + + + True + + + True + True + False + True + + + + 0 + + + + + True + True + True + 0 + #000000000000 + + + + 1 + + + + + 1 + 2 + 1 + 2 + + + + + + + True + + + True + True + False + True + + + + 0 + + + + + True + True + True + 0 + #000000000000 + + + + 1 + + + + + 3 + 4 + 1 + 2 + + + + + + + True + + + True + True + True + 0 + #000000000000 + + + + 0 + + + + + + + + 1 + 2 + + + + + + + True + + + True + True + True + 0 + #000000000000 + + + + 0 + + + + + + + + 1 + 2 2 3 - GTK_FILL + + + + + + + True + + + True + True + True + 0 + #000000000000 + + + + 0 + + + + + + + + 3 + 4 + + + + + + + True + + + True + True + True + 0 + #000000000000 + + + + 0 + + + + + + + + 3 + 4 + 2 + 3 + + @@ -1727,9 +1904,9 @@ $T will be replaced by auto-not-available timeout Style - tab 4 False + tab @@ -1741,7 +1918,7 @@ $T will be replaced by auto-not-available timeout True 0 - GTK_SHADOW_NONE + none True @@ -1761,11 +1938,14 @@ Always use Xfce default applications Custom + + 0 + 0 - GTK_SHADOW_NONE + none True @@ -1898,6 +2078,7 @@ Custom False False + 0 @@ -1905,7 +2086,7 @@ Custom True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 - GTK_SHADOW_NONE + none True @@ -1921,12 +2102,12 @@ Custom 6 + _Ignore events from contacts not in the roster True True + False If checked, Gajim will ignore incoming events from unauthorized contacts. Use with caution, because it blocks all messages from any contact that is not in the roster - _Ignore events from contacts not in the roster True - 0 True @@ -1936,12 +2117,12 @@ Custom + Allow _OS information to be sent True True + False If checked, Gajim will allow others to detect the operation system you are using - Allow _OS information to be sent True - 0 True @@ -1952,12 +2133,12 @@ Custom + Log _encrypted chat session True True + False If checked, Gajim will keep logs for encrypted messages. Please note that when using E2E encryption the remote party has to agree on logging, else the messages will not be logged. - Log _encrypted chat session True - 0 True @@ -1991,7 +2172,7 @@ Custom True 0 - GTK_SHADOW_NONE + none True @@ -2003,26 +2184,27 @@ Custom 6 + _Log status changes of contacts True True - _Log status changes of contacts + False True - 0 True False False + 0 + Check on startup if Gajim is the _default Jabber client True True - Check on startup if Gajim is the _default Jabber client + False True - 0 True @@ -2057,7 +2239,7 @@ Custom True 0 - GTK_SHADOW_NONE + none True @@ -2069,7 +2251,7 @@ Custom True True - 0 + False @@ -2088,6 +2270,7 @@ Custom False False + 0 @@ -2138,28 +2321,36 @@ Custom Advanced - tab 5 False + tab + + 0 + True 15 - GTK_BUTTONBOX_END + end + gtk-close True True True - gtk-close + False True - 0 + + False + False + 0 + diff --git a/src/config.py b/src/config.py index 3d999f689..a8e3b0902 100644 --- a/src/config.py +++ b/src/config.py @@ -228,25 +228,8 @@ class PreferencesWindow: st = gajim.config.get('use_transports_iconsets') self.xml.get_widget('transports_iconsets_checkbutton').set_active(st) - # Color for incoming messages - colSt = gajim.config.get('inmsgcolor') - self.xml.get_widget('incoming_msg_colorbutton').set_color( - gtk.gdk.color_parse(colSt)) - - # Color for outgoing messages - colSt = gajim.config.get('outmsgcolor') - self.xml.get_widget('outgoing_msg_colorbutton').set_color( - gtk.gdk.color_parse(colSt)) - - # Color for status messages - colSt = gajim.config.get('statusmsgcolor') - self.xml.get_widget('status_msg_colorbutton').set_color( - gtk.gdk.color_parse(colSt)) - - # Color for hyperlinks - colSt = gajim.config.get('urlmsgcolor') - self.xml.get_widget('url_msg_colorbutton').set_color( - gtk.gdk.color_parse(colSt)) + # Color widgets + self.draw_color_widgets() # Font for messages font = gajim.config.get('conversation_font') @@ -823,12 +806,18 @@ class PreferencesWindow: for win in gajim.interface.msg_win_mgr.windows(): win.update_font() - def on_incoming_msg_colorbutton_color_set(self, widget): + def on_incoming_nick_colorbutton_color_set(self, widget): self.on_preference_widget_color_set(widget, 'inmsgcolor') - def on_outgoing_msg_colorbutton_color_set(self, widget): + def on_outgoing_nick_colorbutton_color_set(self, widget): self.on_preference_widget_color_set(widget, 'outmsgcolor') + def on_incoming_msg_colorbutton_color_set(self, widget): + self.on_preference_widget_color_set(widget, 'inmsgtxtcolor') + + def on_outgoing_msg_colorbutton_color_set(self, widget): + self.on_preference_widget_color_set(widget, 'outmsgtxtcolor') + def on_url_msg_colorbutton_color_set(self, widget): self.on_preference_widget_color_set(widget, 'urlmsgcolor') @@ -846,22 +835,71 @@ class PreferencesWindow: else: font_widget.set_sensitive(True) self.on_preference_widget_font_set(font_widget, 'conversation_font') + + def draw_color_widgets(self): + col_to_widget = {'inmsgcolor': 'incoming_nick_colorbutton', + 'outmsgcolor': 'outgoing_nick_colorbutton', + 'inmsgtxtcolor': ['incoming_msg_colorbutton', + 'incoming_msg_checkbutton'], + 'outmsgtxtcolor': ['outgoing_msg_colorbutton', + 'outgoing_msg_checkbutton'], + 'statusmsgcolor': 'status_msg_colorbutton', + 'urlmsgcolor': 'url_msg_colorbutton'} + for c in col_to_widget: + col = gajim.config.get(c) + if col: + if isinstance(col_to_widget[c], list): + self.xml.get_widget(col_to_widget[c][0]).set_color( + gtk.gdk.color_parse(col)) + self.xml.get_widget(col_to_widget[c][0]).set_sensitive(True) + self.xml.get_widget(col_to_widget[c][1]).set_active(True) + else: + self.xml.get_widget(col_to_widget[c]).set_color( + gtk.gdk.color_parse(col)) + else: + if isinstance(col_to_widget[c], list): + self.xml.get_widget(col_to_widget[c][0]).set_color( + gtk.gdk.color_parse('#000000')) + self.xml.get_widget(col_to_widget[c][0]).set_sensitive(False) + self.xml.get_widget(col_to_widget[c][1]).set_active(False) + else: + self.xml.get_widget(col_to_widget[c]).set_color( + gtk.gdk.color_parse('#000000')) def on_reset_colors_button_clicked(self, widget): - for i in ('inmsgcolor', 'outmsgcolor', 'statusmsgcolor', 'urlmsgcolor'): - gajim.config.set(i, gajim.interface.default_colors[i]) + col_to_widget = {'inmsgcolor': 'incoming_nick_colorbutton', + 'outmsgcolor': 'outgoing_nick_colorbutton', + 'inmsgtxtcolor': 'incoming_msg_colorbutton', + 'outmsgtxtcolor': 'outgoing_msg_colorbutton', + 'statusmsgcolor': 'status_msg_colorbutton', + 'urlmsgcolor': 'url_msg_colorbutton'} + for c in col_to_widget: + gajim.config.set(c, gajim.interface.default_colors[c]) + self.draw_color_widgets() - self.xml.get_widget('incoming_msg_colorbutton').set_color(\ - gtk.gdk.color_parse(gajim.config.get('inmsgcolor'))) - self.xml.get_widget('outgoing_msg_colorbutton').set_color(\ - gtk.gdk.color_parse(gajim.config.get('outmsgcolor'))) - self.xml.get_widget('status_msg_colorbutton').set_color(\ - gtk.gdk.color_parse(gajim.config.get('statusmsgcolor'))) - self.xml.get_widget('url_msg_colorbutton').set_color(\ - gtk.gdk.color_parse(gajim.config.get('urlmsgcolor'))) self.update_text_tags() gajim.interface.save_config() + def _set_color(self, state, widget_name, option): + ''' set color value in prefs and update the UI ''' + if state: + color = self.xml.get_widget(widget_name).get_color() + color_string = gtkgui_helpers.make_color_string(color) + else: + color_string = '' + gajim.config.set(option, color_string) + gajim.interface.save_config() + + def on_incoming_msg_checkbutton_toggled(self, widget): + state = widget.get_active() + self.xml.get_widget('incoming_msg_colorbutton').set_sensitive(state) + self._set_color(state, 'incoming_msg_colorbutton', 'inmsgtxtcolor') + + def on_outgoing_msg_checkbutton_toggled(self, widget): + state = widget.get_active() + self.xml.get_widget('outgoing_msg_colorbutton').set_sensitive(state) + self._set_color(state, 'outgoing_msg_colorbutton', 'outmsgtxtcolor') + def on_auto_away_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'autoaway', [self.auto_away_time_spinbutton, self.auto_away_message_entry]) diff --git a/src/dialogs.py b/src/dialogs.py index a0fc9f9b7..1f8c1abaf 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4346,7 +4346,7 @@ class TransformChatToMUC: gajim.automatic_rooms[self.account][room_jid]['invities'] = guest_list gajim.automatic_rooms[self.account][room_jid]['continue_tag'] = True gajim.interface.join_gc_room(self.account, room_jid, - gajim.nicks[self.account], None, is_continued=True) + gajim.nicks[self.account], None, is_continued=True) self.window.destroy() def on_cancel_button_clicked(self, widget): @@ -4354,8 +4354,8 @@ class TransformChatToMUC: def unique_room_id_error(self, server): self.unique_room_id_supported(server, - gajim.nicks[self.account].lower().replace(' ','') + str(randrange( - 9999999))) + gajim.nicks[self.account].lower().replace(' ','') + str(randrange( + 9999999))) class DataFormWindow(Dialog): def __init__(self, form, on_response_ok): diff --git a/src/gajim.py b/src/gajim.py index ba6826581..2808da3b2 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -3530,6 +3530,8 @@ class Interface: self.default_colors = { 'inmsgcolor': gajim.config.get('inmsgcolor'), 'outmsgcolor': gajim.config.get('outmsgcolor'), + 'inmsgtxtcolor': gajim.config.get('inmsgtxtcolor'), + 'outmsgtxtcolor': gajim.config.get('outmsgtxtcolor'), 'statusmsgcolor': gajim.config.get('statusmsgcolor'), 'urlmsgcolor': gajim.config.get('urlmsgcolor'), } From e10ff2c9079e8999c979bbe4d44b3cad5accd256 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 31 Oct 2009 19:15:18 +0100 Subject: [PATCH 02/29] fix typo in a string --- src/gajim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 2808da3b2..81479c6ba 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -454,8 +454,8 @@ class PassphraseRequest: return elif result == 'expired': dialogs.ErrorDialog(_('GPG key expired'), - _('Your GPG key has expied, you will be connected to %s without ' - 'OpenPGP.') % account) + _('Your GPG key has expired, you will be connected to %s without' + ' OpenPGP.') % account) # Don't try to connect with GPG gajim.connections[account].continue_connect_info[2] = False self.complete(None) From 48c82ae7fa65b0d2003ec27505b6e245a037cdd8 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 31 Oct 2009 19:35:46 +0100 Subject: [PATCH 03/29] [Mattj] improve join groupchat behaviour. Fixes #5383 --- src/conversation_textview.py | 2 +- src/dialogs.py | 36 +++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/conversation_textview.py b/src/conversation_textview.py index f4d4a2804..837e2a0ce 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -853,7 +853,7 @@ class ConversationTextview(gobject.GObject): gajim.interface.instances[self.account]['join_gc'].window.present() else: try: - dialogs.JoinGroupchatWindow(account=None, room_jid=room_jid) + dialogs.JoinGroupchatWindow(account=self.account, room_jid=room_jid) except GajimGeneralException: pass diff --git a/src/dialogs.py b/src/dialogs.py index 1f8c1abaf..56246179d 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -1922,7 +1922,7 @@ class JoinGroupchatWindow: '''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''' - self.xml = gtkgui_helpers.get_glade('join_groupchat_window.glade') + if account: if room_jid != '' and room_jid in gajim.gc_connected[account] and\ gajim.gc_connected[account][room_jid]: @@ -1934,21 +1934,27 @@ class JoinGroupchatWindow: ErrorDialog(_('You are not connected to the server'), _('You can not join a group chat unless you are connected.')) raise GajimGeneralException, 'You must be connected to join a groupchat' - else: - account_label = self.xml.get_widget('account_label') - account_combobox = self.xml.get_widget('account_combobox') - account_label.set_no_show_all(False) - account_combobox.set_no_show_all(False) - liststore = gtk.ListStore(str) - account_combobox.set_model(liststore) - cell = gtk.CellRendererText() - account_combobox.pack_start(cell, True) - account_combobox.add_attribute(cell, 'text', 0) - for acct in [a for a in gajim.connections if \ - gajim.account_is_connected(a)]: - account_combobox.append_text(acct) - account_combobox.set_active(-1) + self.xml = gtkgui_helpers.get_glade('join_groupchat_window.glade') + + account_label = self.xml.get_widget('account_label') + account_combobox = self.xml.get_widget('account_combobox') + account_label.set_no_show_all(False) + account_combobox.set_no_show_all(False) + liststore = gtk.ListStore(str) + account_combobox.set_model(liststore) + cell = gtk.CellRendererText() + account_combobox.pack_start(cell, True) + account_combobox.add_attribute(cell, 'text', 0) + account_combobox.set_active(-1) + + # Add accounts, set current as active if it matches 'account' + for acct in [a for a in gajim.connections if \ + gajim.account_is_connected(a)]: + account_combobox.append_text(acct) + if account and account == acct: + account_combobox.set_active(liststore.iter_n_children(None)-1) + self.account = account self.automatic = automatic self._empty_required_widgets = [] From 4824db0923bce84c1bf0d9db9c79be78fe69a461 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 31 Oct 2009 22:57:14 +0100 Subject: [PATCH 04/29] [Grigroy] align colors buttons. Fixes #5385 --- data/glade/preferences_window.glade | 52 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/data/glade/preferences_window.glade b/data/glade/preferences_window.glade index 82eb6b817..fa912df8b 100644 --- a/data/glade/preferences_window.glade +++ b/data/glade/preferences_window.glade @@ -1709,6 +1709,8 @@ $T will be replaced by auto-not-available timeout + False + end 1 @@ -1747,6 +1749,8 @@ $T will be replaced by auto-not-available timeout + False + end 1 @@ -1763,6 +1767,9 @@ $T will be replaced by auto-not-available timeout True + + + True @@ -1773,23 +1780,25 @@ $T will be replaced by auto-not-available timeout - 0 + False + end + 1 - - - 1 2 - + GTK_FILL True + + + True @@ -1800,25 +1809,27 @@ $T will be replaced by auto-not-available timeout - 0 + False + end + 1 - - - 1 2 2 3 - + GTK_FILL True + + + True @@ -1829,23 +1840,25 @@ $T will be replaced by auto-not-available timeout - 0 + False + end + 1 - - - 3 4 - + GTK_FILL True + + + True @@ -1856,19 +1869,18 @@ $T will be replaced by auto-not-available timeout - 0 + False + end + 1 - - - 3 4 2 3 - + GTK_FILL From d5351f63281285d039faa1f54fb3495f8114c254 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 31 Oct 2009 23:22:12 +0100 Subject: [PATCH 05/29] ignore unknown show types when we receive strange stanza. --- src/common/connection_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index cc84e75bc..b3f95691c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -2259,7 +2259,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, is_gc = True status = prs.getStatus() or '' show = prs.getShow() - if not show in gajim.SHOW_LIST: + if show not in ('chat', 'away', 'xa', 'dnd'): show = '' # We ignore unknown show if not ptype and not show: show = 'online' From b0712e177470645a7b174e001d08e33e75c80bf5 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 31 Oct 2009 23:57:14 +0100 Subject: [PATCH 06/29] replace all %d / %s things in SQL queries by ? for security reasons --- src/common/logger.py | 81 ++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index d4af560f1..08929b417 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -149,9 +149,12 @@ class Logger: self.open_db() self.get_jids_already_in_db() - def simple_commit(self, sql_to_commit): + def simple_commit(self, sql_to_commit, values=None): '''helper to commit''' - self.cur.execute(sql_to_commit) + if values: + self.cur.execute(sql_to_commit, values) + else: + self.cur.execute(sql_to_commit) try: self.con.commit() except sqlite.OperationalError, e: @@ -383,21 +386,19 @@ class Logger: def insert_unread_events(self, message_id, jid_id): ''' add unread message with id: message_id''' - sql = 'INSERT INTO unread_messages VALUES (%d, %d, 0)' % (message_id, - jid_id) - self.simple_commit(sql) + sql = 'INSERT INTO unread_messages VALUES (?, ?, 0)' + self.simple_commit(sql, values=(message_id, jid_id)) def set_read_messages(self, message_ids): ''' mark all messages with ids in message_ids as read''' ids = ','.join([str(i) for i in message_ids]) - sql = 'DELETE FROM unread_messages WHERE message_id IN (%s)' % ids - self.simple_commit(sql) + sql = 'DELETE FROM unread_messages WHERE message_id IN (?)' + self.simple_commit(sql, values=(ids,)) def set_shown_unread_msgs(self, msg_id): ''' mark unread message as shown un GUI ''' - sql = 'UPDATE unread_messages SET shown = 1 where message_id = %s' % \ - msg_id - self.simple_commit(sql) + sql = 'UPDATE unread_messages SET shown = 1 where message_id = ?' + self.simple_commit(sql, values=(msg_id,)) def reset_shown_unread_messages(self): ''' Set shown field to False in unread_messages table ''' @@ -423,8 +424,8 @@ class Logger: SELECT logs.log_line_id, logs.message, logs.time, logs.subject, jids.jid FROM logs, jids - WHERE logs.log_line_id = %d AND logs.jid_id = jids.jid_id - ''' % msg_id + WHERE logs.log_line_id = ? AND logs.jid_id = jids.jid_id + ''', (msg_id,) ) results = self.cur.fetchall() if len(results) == 0: @@ -536,9 +537,9 @@ class Logger: try: self.cur.execute(''' SELECT time, kind, message FROM logs - WHERE (%s) AND kind IN (%d, %d, %d, %d, %d) AND time > %d - ORDER BY time DESC LIMIT %d OFFSET %d - ''' % (where_sql, constants.KIND_SINGLE_MSG_RECV, + WHERE (?) AND kind IN (?, ?, ?, ?, ?) AND time > ? + ORDER BY time DESC LIMIT ? OFFSET ? + ''', (where_sql, constants.KIND_SINGLE_MSG_RECV, constants.KIND_CHAT_MSG_RECV, constants.KIND_SINGLE_MSG_SENT, constants.KIND_CHAT_MSG_SENT, constants.KIND_ERROR, timed_out, restore_how_many_rows, pending_how_many) @@ -577,10 +578,10 @@ class Logger: self.cur.execute(''' SELECT contact_name, time, kind, show, message, subject FROM logs - WHERE (%s) - AND time BETWEEN %d AND %d + WHERE (?) + AND time BETWEEN ? AND ? ORDER BY time - ''' % (where_sql, start_of_day, last_second_of_day)) + ''', (where_sql, start_of_day, last_second_of_day)) results = self.cur.fetchall() return results @@ -607,9 +608,9 @@ class Logger: like_sql = '%' + query.replace("'", "''") + '%' self.cur.execute(''' SELECT contact_name, time, kind, show, message, subject FROM logs - WHERE (%s) AND message LIKE '%s' + WHERE (?) AND message LIKE '?' ORDER BY time - ''' % (where_sql, like_sql)) + ''', (where_sql, like_sql)) results = self.cur.fetchall() return results @@ -635,11 +636,11 @@ class Logger: # Now we have timestamps of time 0:00 of every day with logs self.cur.execute(''' SELECT DISTINCT time/(86400)*86400 FROM logs - WHERE (%s) - AND time BETWEEN %d AND %d - AND kind NOT IN (%d, %d) + WHERE (?) + AND time BETWEEN ? AND ? + AND kind NOT IN (?, ?) ORDER BY time - ''' % (where_sql, start_of_month, last_second_of_month, + ''', (where_sql, start_of_month, last_second_of_month, constants.KIND_STATUS, constants.KIND_GCSTATUS)) result = self.cur.fetchall() @@ -664,9 +665,9 @@ class Logger: where_sql = 'jid_id = %s' % jid_id self.cur.execute(''' SELECT MAX(time) FROM logs - WHERE (%s) - AND kind NOT IN (%d, %d) - ''' % (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS)) + WHERE (?) + AND kind NOT IN (?, ?) + ''', (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS)) results = self.cur.fetchone() if results is not None: @@ -686,8 +687,8 @@ class Logger: where_sql = 'jid_id = %s' % jid_id self.cur.execute(''' SELECT time FROM rooms_last_message_time - WHERE (%s) - ''' % (where_sql)) + WHERE (?) + ''', (where_sql,)) results = self.cur.fetchone() if results is not None: @@ -701,9 +702,8 @@ class Logger: we had logs for that room in rooms_last_message_time table''' jid_id = self.get_jid_id(jid, 'ROOM') # jid_id is unique in this table, create or update : - sql = 'REPLACE INTO rooms_last_message_time VALUES (%d, %d)' % \ - (jid_id, time) - self.simple_commit(sql) + sql = 'REPLACE INTO rooms_last_message_time VALUES (?, ?)' + self.simple_commit(sql, (jid_id, time)) def _build_contact_where(self, account, jid): '''build the where clause for a jid, including metacontacts @@ -733,18 +733,17 @@ class Logger: # unknown type return self.cur.execute( - 'SELECT type from transports_cache WHERE transport = "%s"' % jid) + 'SELECT type from transports_cache WHERE transport = "?"', (jid,)) results = self.cur.fetchall() if results: result = results[0][0] if result == type_id: return - sql = 'UPDATE transports_cache SET type = %d WHERE transport = "%s"' %\ - (type_id, jid) - self.simple_commit(sql) + sql = 'UPDATE transports_cache SET type = ? WHERE transport = "?"' + self.simple_commit(sql, values=(type_id, jid)) return - sql = 'INSERT INTO transports_cache VALUES ("%s", %d)' % (jid, type_id) - self.simple_commit(sql) + sql = 'INSERT INTO transports_cache VALUES ("?", ?)' + self.simple_commit(sql, values=(jid, type_id)) def get_transports_type(self): '''return all the type of the transports in DB''' @@ -815,9 +814,9 @@ class Logger: # yield the row yield hash_method, hash_, identities, features for hash_method, hash_ in to_be_removed: - sql = '''DELETE FROM caps_cache WHERE hash_method = "%s" AND - hash = "%s"''' % (hash_method, hash_) - self.simple_commit(sql) + sql = '''DELETE FROM caps_cache WHERE hash_method = "?" AND + hash = "?"''' + self.simple_commit(sql, values=(hash_method, hash_)) def add_caps_entry(self, hash_method, hash_, identities, features): data = [] From 09496b1fbd3aed6239268b42218097d6dba82c06 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 1 Nov 2009 09:43:31 +0100 Subject: [PATCH 07/29] [Gotham48] added new wroop iconset --- THANKS.artists | 2 ++ data/iconsets/wroop/16x16/away.png | Bin 0 -> 929 bytes data/iconsets/wroop/16x16/chat.png | Bin 0 -> 938 bytes data/iconsets/wroop/16x16/closed.png | Bin 0 -> 210 bytes data/iconsets/wroop/16x16/connecting.gif | Bin 0 -> 286 bytes data/iconsets/wroop/16x16/dnd.png | Bin 0 -> 941 bytes data/iconsets/wroop/16x16/error.png | Bin 0 -> 466 bytes data/iconsets/wroop/16x16/event.gif | Bin 0 -> 2234 bytes data/iconsets/wroop/16x16/invisible.png | Bin 0 -> 901 bytes data/iconsets/wroop/16x16/muc_active.png | Bin 0 -> 949 bytes data/iconsets/wroop/16x16/muc_inactive.png | Bin 0 -> 895 bytes data/iconsets/wroop/16x16/not_in_roster.png | Bin 0 -> 924 bytes data/iconsets/wroop/16x16/offline.png | Bin 0 -> 939 bytes data/iconsets/wroop/16x16/online.png | Bin 0 -> 937 bytes data/iconsets/wroop/16x16/opened.png | Bin 0 -> 204 bytes data/iconsets/wroop/16x16/requested.png | Bin 0 -> 929 bytes data/iconsets/wroop/16x16/xa.png | Bin 0 -> 929 bytes data/iconsets/wroop/32x32/away.png | Bin 0 -> 2437 bytes data/iconsets/wroop/32x32/chat.png | Bin 0 -> 2430 bytes data/iconsets/wroop/32x32/dnd.png | Bin 0 -> 2443 bytes data/iconsets/wroop/32x32/error.png | Bin 0 -> 739 bytes data/iconsets/wroop/32x32/invisible.png | Bin 0 -> 2202 bytes data/iconsets/wroop/32x32/muc_active.png | Bin 0 -> 2414 bytes data/iconsets/wroop/32x32/muc_inactive.png | Bin 0 -> 2237 bytes data/iconsets/wroop/32x32/not_in_roster.png | Bin 0 -> 2347 bytes data/iconsets/wroop/32x32/offline.png | Bin 0 -> 2503 bytes data/iconsets/wroop/32x32/online.png | Bin 0 -> 2453 bytes data/iconsets/wroop/32x32/requested.png | Bin 0 -> 2437 bytes data/iconsets/wroop/32x32/xa.png | Bin 0 -> 2437 bytes data/iconsets/wroop/48x48/away.png | Bin 0 -> 4331 bytes data/iconsets/wroop/48x48/chat.png | Bin 0 -> 4301 bytes data/iconsets/wroop/48x48/dnd.png | Bin 0 -> 4257 bytes data/iconsets/wroop/48x48/error.png | Bin 0 -> 1039 bytes data/iconsets/wroop/48x48/invisible.png | Bin 0 -> 3841 bytes data/iconsets/wroop/48x48/muc_active.png | Bin 0 -> 4079 bytes data/iconsets/wroop/48x48/muc_inactive.png | Bin 0 -> 3824 bytes data/iconsets/wroop/48x48/not_in_roster.png | Bin 0 -> 3975 bytes data/iconsets/wroop/48x48/offline.png | Bin 0 -> 4422 bytes data/iconsets/wroop/48x48/online.png | Bin 0 -> 4323 bytes data/iconsets/wroop/48x48/requested.png | Bin 0 -> 4331 bytes data/iconsets/wroop/48x48/xa.png | Bin 0 -> 4331 bytes gajim.nsi | 6 ++++++ 42 files changed, 8 insertions(+) create mode 100644 data/iconsets/wroop/16x16/away.png create mode 100644 data/iconsets/wroop/16x16/chat.png create mode 100644 data/iconsets/wroop/16x16/closed.png create mode 100644 data/iconsets/wroop/16x16/connecting.gif create mode 100644 data/iconsets/wroop/16x16/dnd.png create mode 100644 data/iconsets/wroop/16x16/error.png create mode 100644 data/iconsets/wroop/16x16/event.gif create mode 100644 data/iconsets/wroop/16x16/invisible.png create mode 100644 data/iconsets/wroop/16x16/muc_active.png create mode 100644 data/iconsets/wroop/16x16/muc_inactive.png create mode 100644 data/iconsets/wroop/16x16/not_in_roster.png create mode 100644 data/iconsets/wroop/16x16/offline.png create mode 100644 data/iconsets/wroop/16x16/online.png create mode 100644 data/iconsets/wroop/16x16/opened.png create mode 100644 data/iconsets/wroop/16x16/requested.png create mode 100644 data/iconsets/wroop/16x16/xa.png create mode 100644 data/iconsets/wroop/32x32/away.png create mode 100644 data/iconsets/wroop/32x32/chat.png create mode 100644 data/iconsets/wroop/32x32/dnd.png create mode 100644 data/iconsets/wroop/32x32/error.png create mode 100644 data/iconsets/wroop/32x32/invisible.png create mode 100644 data/iconsets/wroop/32x32/muc_active.png create mode 100644 data/iconsets/wroop/32x32/muc_inactive.png create mode 100644 data/iconsets/wroop/32x32/not_in_roster.png create mode 100644 data/iconsets/wroop/32x32/offline.png create mode 100644 data/iconsets/wroop/32x32/online.png create mode 100644 data/iconsets/wroop/32x32/requested.png create mode 100644 data/iconsets/wroop/32x32/xa.png create mode 100644 data/iconsets/wroop/48x48/away.png create mode 100644 data/iconsets/wroop/48x48/chat.png create mode 100644 data/iconsets/wroop/48x48/dnd.png create mode 100644 data/iconsets/wroop/48x48/error.png create mode 100644 data/iconsets/wroop/48x48/invisible.png create mode 100644 data/iconsets/wroop/48x48/muc_active.png create mode 100644 data/iconsets/wroop/48x48/muc_inactive.png create mode 100644 data/iconsets/wroop/48x48/not_in_roster.png create mode 100644 data/iconsets/wroop/48x48/offline.png create mode 100644 data/iconsets/wroop/48x48/online.png create mode 100644 data/iconsets/wroop/48x48/requested.png create mode 100644 data/iconsets/wroop/48x48/xa.png diff --git a/THANKS.artists b/THANKS.artists index b6501e6e8..5cdff9134 100644 --- a/THANKS.artists +++ b/THANKS.artists @@ -2,7 +2,9 @@ Anders Ström Christophe Got Dennis Craven Guillaume Morin +Gvorcek Spajreh Josef Vybíral Membris Khan Rederick Asher Jakub Szypulka + diff --git a/data/iconsets/wroop/16x16/away.png b/data/iconsets/wroop/16x16/away.png new file mode 100644 index 0000000000000000000000000000000000000000..ab36438126538966cbc73530498645a6fa365dd1 GIT binary patch literal 929 zcmV;S177@zP)ym;_p;?0W?6{GR8uA6AUm>3MzgE4B;*KWIyA{6_A?sj*&@5k)? zy_q)R@BaHv-zNzn*u9C#umlVO{XU;R1W+jC<3I*TMe_&e|cKywCch zZ2)=?b`c63r_0<=BC&Aq`X|?3i$-Eo{{ki^!@XCpzW#GCIDGQXuk(CAHH|DwsHzHp zZQD4G;L^o&yztx^5{dZT8#g|^5RJsvm8Dcd3Je84dhw;P3qMRvOW)r96PKo;8wQ51 z;c}@UE7)zpTyh13;MsE_b8YqSu4~snzNrF(m&dP+W$vxZx29&%G#$6wAUJe@^JfnM z@W;$U5~(d(w&2#(EGLc}k}r>68B3;;gEBCnyWO7Y#T6W6R8^z5$4B2WKejDl+Y)`p z{PgztP*n{Fnd!w9bhq0B^s8R4_jIc*X=_^+vZA3GIzF=#t7Ic28GxOVjnC{vGfKz} zo7G1at+u3jz22Y-P;1njjckSWTm{YOpj>ruXyP~u09mU^wcMs$Z?l#y;c~gCHEIrk zS}Yb9oAqkRZ+g1oe`QIo6zJ~WPiiI)l7tWf2^f0PPx`?Qt)_$D^wO+XOT}U_p#qsg zE|=>&YWjam7?Oj+My}5I=m4dv1AwPP=9if@gruP927O0OQOM;3qRx5PwGbq*D{4~A6wpBwQcq-7t^CMk5cW(c9vr;L4r7R|v_~OgY z=XFIH?H@XPu-??we8onf^3bx`%r|fx2U&sO@oq-Xdg>dQ_k^?a$2Sx*0MS=NV-jp=bYYnnC$&}=r-|CjSWcHwnRxNw#l)Kzx2Q8369d>8vxAuj=Y}rCNQ6P5K$nhzOMt@q`btaR zEARb1m>7ujyMMpKm+vPDAvm1Tv6KYFfi7Kd?gJY0twe>&I z|IxYB`Bxi*fq?2$Jwia;w%zhxrSe^R;)l80bMH@%r|$tAqg_mCAxxrVMUtGF#`SRyOljG_8^3rlfN^~baIeq%&pQDg~3ot5k#xGp^P%wTw9Lzr2lOSx8Qa zs+mVoeOOl21yD@WT->i6ly$wirL^^c()J9s))uz%GbkQ`K;XC@nvM-%7G}Y5Xx3ZU zuN{<4)66KqYN256M53|gopn{xRGa$V8i}W0$FU6nWHrpa>@_si0aPNbsNU zY4gaHPoC`iAejvGsI?o9`m8lclS=^9oF**a31MqPg-oWhw6HM$;gKsJmMI(#zZdI@ zpL!(X>S5i`0gPg}xU=OcuI5&LHw@$BhsyLYomExs2C!{=<^Od48$IuRQuokO1ONa4 M07*qoM6N<$f@UedCjbBd literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/closed.png b/data/iconsets/wroop/16x16/closed.png new file mode 100644 index 0000000000000000000000000000000000000000..2cded1ea86aa1239853fa3166bd7aa1acc6ba12a GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CO!3HF4Hmy+sQjEnx?oJHr&dIz4aySb-B8wRq zxP?KOkzv*x36Sy^)r^LnJOwonXkvV8Fq&@c*UbuMgkqT&Q;Jo0_u?TSwE(x3f~! zN;g;caWFLOj=yxM!Q~=X*19#LV0FMhJw4NZvcbhPZm}Y26hG=paP(Y0t^hy@)f)8{4;QKSh!IcMgf(4BvA6r8 iZp3h6SEhbu*CQg7nV;G97(*E+hNbRjc0EB=W(@!+hFbvu literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/dnd.png b/data/iconsets/wroop/16x16/dnd.png new file mode 100644 index 0000000000000000000000000000000000000000..93f03b92f86301b0f282658d510348001968f90e GIT binary patch literal 941 zcmV;e15*5nP)GK~y-6b(2j@n{@!jfA42Ix^``0IGn7`j6(U^hoFaH!eUIo z$hK%j4tkmzy?HY6;=zlFH!p5cXEY{SYFW%ooVTHoWd@L8`4}Ig?bwphLOiYuYxdR$(Mat6e*nWH;WO{N{qC1t9i30*H`A<6 zO|iLt7l4N5BOL4L!Q0SCe12~2(&rc7j7DPD{{;+>gb!c1@YeY8Q2VjTYd6c%jcko4K7Z+A9U;d&$8j0Ok7H-c;p|;Sc1JA$EKQT5Y{rdfP=$eMp z>13ZnM^_bOBx;t$^5i5!z^T)H-n;ki)nB~y*;N&2Jv(%6Ff~6X-;9p3PuFlc92`B_ zO6bf<02Z&$keFUXR4Uwzj`DagD4!iVH@J9vu~i0wb*koGy)l7eSvX~dra%+TZ2^R7 zBTSp-wg63mCY-W@Vp*)-n4nJ8+(3)!@pw9fY3jK|0$q}DY8r08kIH5NsrJv(m23gG z--lDvuq27>S^{C3y2s<`Q~@gGa;=bBXFHL^rD{~OMYb&y$r1>lxRJ&xKEy5*DBf8^ z(;QUFgLT`hY3{`&M*maY604F~HA%QQUQLw0(Gs%_(~uV=@u z77fFgQ-S5QVQd}>2L0>0E@`Go`A(AVH(tlg<^jK zXZuzrlL?*beI{5-r=2^g6e5$MF!d)cw;MZ`ClS5E&VvUCpt-M)BSSA0uKoVo)lzBu zTV;N3fv>;$YL@-`pKA#``IuZTtMe{R0;|Hx&N>U010PF(?&;kLW_V;Jv56cVj_+KCGx$_a@o6Y(F@_v6Vttz?Ym8C!O`TQr3l<84A>$=_sV4CLA|LOcUL3xHrD!a0T P00000NkvXXu0mjfdYiq- literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/error.png b/data/iconsets/wroop/16x16/error.png new file mode 100644 index 0000000000000000000000000000000000000000..a28a58d89087ee5c60e70a7a0b49c37cf2203ba4 GIT binary patch literal 466 zcmV;@0WJQCP)SG*jZapKxJY=LoiysCvPa+C6ARQBzxNXpL5ULxf7Xb78Ns9 zMOEMD!a+Q_C;TTIJ_Wu^zs<>#ib}@~+F*xNL&I@{b5#`&=OZ>&v8!q=E%7Pxw zN9=8berA&#B;5seA$T6F$;<_IlWuY_?w^T|G{;Uu7aJm?;>W+Ij=%=K&au<3iOF3U zMs#Z4HGFPQA=xbX@G7&q*I=wq5zpENuQN~X;-(rrR5NemH9!P-g#>Feg#Z8m07*qo IM6N<$fv%+mDtHar{ zsd@2pc+-63+_5?tkIBL^Cm3ap^;~B>=(s`oO^ZY0Bn}CNDJPe1`q|^v2{L&uUX$Ii zx|x+-h?zlRx4(i6Q-Q=jSwHm`vK$SI5>~unWIL(adZR{d=2@i$3@4G17F`TEOt& z>xy&h+KDwgv_K)fOX-OS2fNO3ckR7Sd<>O&_7;o}5+3~2IH-6~;|FVNL*vDpuU#vz ztXjwLgK?|X`(E5;BP9@a4LL_q4)#^cR7$xDWTs!{W^VBDHY<>@yz%a$wLnwTLf-~y zmZ_U&8^7$j$JL;C@zj-1cv7ANJg_a8G`N5UXRMlbdF8$}PZlw32r@2^Fq5+1$-L10 z(8{dld->CtRJzxSuLZhxY11lTNL1{3c`iSG{z4-E-8$4L?82q+aKqjU6W@1H#730a&_YDi8 z7=gK??M0Pf6n}cN=Iqmg*pjdZA*0KWurv7;tYGCk(VWwu{f&=9L*#&qgy0PwrT~U3 zIk(na2bOT>UKC?3;qbWov+T!2h980K?F#(|xc~8SEXg_G{6_#-Uc6kwvM2&nd?qfv zD^9ArYuPy%oN9LX7rZ%;_{Dk-U*QWu2A39rgDz*-JXBRLDS-@o|gC z`X1bQ4=upUInui(1sCioIOC;Pq`**c|Ff`y!(2uNmoIe(nHxE5#0nc2FaJE^*(;JF z8o=K@EoXu*U V_tDi`tk5Tl@dLBlN?3hu4FGt&B6|P; literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/invisible.png b/data/iconsets/wroop/16x16/invisible.png new file mode 100644 index 0000000000000000000000000000000000000000..433ed39f3b2701d19ebb5154560db74f5c72c83c GIT binary patch literal 901 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 VNQg643!4A{002ovPDHLkV1lEPsUQFV literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/not_in_roster.png b/data/iconsets/wroop/16x16/not_in_roster.png new file mode 100644 index 0000000000000000000000000000000000000000..c346512a1031a24c73161aa1f234f104a2f8ac7e GIT binary patch literal 924 zcmV;N17rM&P)DkG}2g zjl8n6v%}KT5{X0tfM_(z`Sa&#X=!17eeL(zZ?0Zmm|wgFfH8)#iILXWax9rnZ=30< zDdYG1jS#{JDUVhNO$;nEE)s+>>wvVB+XX`WRbmP_8nd=e=U%YT>cr_lES7v5FYx<>R zP%12~v9xATD*B}at+_HY!)iP(FJ8Db41^`nscl^wyRnSxx@2sdtk#_Ks*HGs5zlbW ztCH24jBVq(F0mWSXj|6;9ab@Aneb}E&^vNTdD?n+6kkX$|}fHBMkYPjq3VR;^& zkfc&6%H^^h3K(Tb_c~uYfUv8-ss!}<6lMpC%6vwMpsS1Ht^(dAOJpe18&#$|# zyWiA&qVWk}pyCp#xLgZ`0EiTeWUHDxmIfhcYCeJMy8HQjeoX>fd%66ha96u=9EUbz zXz>iU>*~1KaGaYB$GKfsM~i1@GX}?T2zRxcyCSvM=Nx4I zBI_LEK}`+HvXDx#W7`}Gi86+s{?iP-cjlj4zpPv@6dwPe*4H-p?)$m8WBUhRjkGoI z9(dLt`;U=Qp|wUS1;BxA)85y|+rw`aA7*y%C6ikpFU&9IpQ&;*+V@%WOQD{7zb2Z} zf9{H6u?T?UIGlXB(Y*dfH2ZKnw~HIJCdSd}TKX1DL0000u;HG1=8;>Cj(6K`JJqRwc%K<%j6ft$``WJFfxHrkH1h;vXGl(zKGk z&x479IDhxwcltiTwr%!ra%w^VbU^d@T897%h5Ry*0+ONW@PUJ{A22yJ(LQ{1_>(s# z-h53}Rn_H^Mcam2t=2FM!}vZt^TX{sw?7O`hco{HOioR_^6oqDeKjx;?9A?LvlNZ8 zzWxA!!-tPBGBQG2+Y`j&v9(KITzo4u9lrK2U~+1r{lbN}e-8HdchAkuab@-jqA22& zod8%hi$2O9$ti*)A-oDRHoqF|TWOi1V3C*A=3aYB2 zDhe)HMv@%V>vf{hD7I}gIy&02v9aO5c|@6Kykb2=xRsr?VwSIMjiCt z4m!HpQQU42VDaW6vP<>=n#1e$4p>%A&TM3G$u1OC!QbYmTr>~_5rA^hz~AOaQUAm= zt8A=iu&kQw^?HL20JCZ~Ha8!#wY5dFr}QClY*vq+J$^^2S`f;!Tww} zo9%o4*ho*QSaIiaIm|ti`+q#Z>+xWkCfDb0kk93@ZJVRR!yG^HQt_uBuU$2a%6C#c zmf)LjzrN#gDlconBTqi6+Kx)4j3i0y7I(?zay0671VKR8dwKE1v(?P~?O)UB)cc|7 zaOr@`gM&jKwLcT+UrqlO$UiI#_4*?KB*{VRA&=dyb!9WZXA{fIcRxO$^8T1wT3SBn z(exKopVNoa^8MqovtSggLMom7Wp{V?)5pg2xSVBK?ggmTYRUi0`EPP2f3`K$?hpU~ N002ovPDHLkV1jmo%CrCg literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/online.png b/data/iconsets/wroop/16x16/online.png new file mode 100644 index 0000000000000000000000000000000000000000..d868f9a2e450ce115dbbaa5bc57b5f948e321e0d GIT binary patch literal 937 zcmV;a16KTrP)#z2|>M?qnS8vG$A2#OtzM>EMQAvz=3{npy#wb{dr+x zAkO#xeJQ%uLVb{sWkp93Q@R_4;QcBO`;hQ&7_Df5@ERwTT7l_$@akWu8JH?B0Trk{qs{^qMW<{>93UGL9^LJom~C-%Lq+m=q_T@@I1&m0J*uW@3BRERc1sz2hKv^Q5e`SN-4ZDOEN#0b z!r=(ofe*)%*(qiiXjcQ8ruC`-Wykic;~WQWhCo=u@s21sRgggdg|bDt=HSXUma~Ch z)hIi*2cWvHyPCI(`R1lqqU&~njJ-_MZDDzT8A%ZcA)qeP5gcUGUcr^BG)G#UMWF_Vd6qcJ)0VY2H8VY1 zI(6lEyzQfofk>CvwR+ZR*wX8o4S?(h(U003Wh1ekS;=o~Z*P5m>dGf&(slh-tTi?m zjH(TRwrU9Ac*|F^_i9HwyF1H{<9zW_nO>%|q9_*uJkMMEKb`*uE0uijwdvb;00000 LNkvXXu0mjf@(sGP literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/opened.png b/data/iconsets/wroop/16x16/opened.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb9170041d378f8e67c5cc708de115da08cc1f6 GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CO!3HF4Hmy+sQjEnx?oJHr&dIz4aySb-B8wRq zxP?KOkzv*x36SyS|xv6<249-QVi6yBi3gww4 z84B*6z5(HleBwYwmYyz-ArhC9QyiGLZQJ($;!!?%yE7cg#zH|o>pD9dg%Yx}f6KF7 sxM^T&k!w-%LLjVg(%d5+SsIKCQ5%(${4PG74K#$o)78&qol`;+0Q!A7o&W#< literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/16x16/requested.png b/data/iconsets/wroop/16x16/requested.png new file mode 100644 index 0000000000000000000000000000000000000000..ab36438126538966cbc73530498645a6fa365dd1 GIT binary patch literal 929 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 literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/dnd.png b/data/iconsets/wroop/32x32/dnd.png new file mode 100644 index 0000000000000000000000000000000000000000..3c28fc49a664aa23e1f4126560fc1cd3f1f7ada3 GIT binary patch literal 2443 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 JPDHLkV1nx*mHPky literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/error.png b/data/iconsets/wroop/32x32/error.png new file mode 100644 index 0000000000000000000000000000000000000000..4b6c6661e7adb270d95d5d76e09d5a220479c270 GIT binary patch literal 739 zcmV<90v!E`P)dt0fZ%+tbm^v-phcN`-Zd0-aj%z2*k zgZtk1y;rt5&*(YV=@$S%Z#SZ{bQf3%Ylf!_*3zEs$w=o2hdVC75zC27Vmb&A1F zVVEjOGhG3cr6IvmSQBig4UKyQOJ!;3)BwQTf&MVIvhXAgAnDm^W?=4A0I#KMKrSfl z)?jShuBHKu+tnJ30lPstmw0vtP=`A}rjZNQChfhn;fU(bR+gI|t|>zQ%<*eCx*Oq$T(e_Eh0kH0(>P z>5Tvq&7ptf*&aYqv*31^_rV^~n(s#du_pFx3(x~^fm$OsEPDW@u76W{z!BZZWVbuwI_^ozZdA@Zus8=`~hvw V!@cY&(f$Ac002ovPDHLkV1hN!Nyh*H literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/invisible.png b/data/iconsets/wroop/32x32/invisible.png new file mode 100644 index 0000000000000000000000000000000000000000..a5aea01e4d8a34d63af367c109fe37951a8734aa GIT binary patch literal 2202 zcmV;L2xa$)P)fTU&uqH@Z3s$=s&31SA>Y#NJ*OHA+JdaYA&<{QtAsgV*|!M*vIG0 zzV%_o=F%tyH6tyJW@fLozS(=P%a)W94{{woa%e5E1sDW|>eq9?OYqaQx^_2-{^ewVS@WLc&{ z4EW&j!Pm}Hb*~WwK`V`@>Ta~DMdO@{!Kd)Xn{S-?>)UUA4*dGgOA|q6hbsM0L^A`_1b8z#9^&8eUnt7fQl@VV7I9KAlum84w7b>X$tFg{| zT>X7THzhDuXB$U14IDi1>=^K3?Q?Yoo^G{Tt?xeZy{%$mUK9;+(Gh~jc~=?oj?(oA z-qF`gtRFFqjTMZI6|5gI^feQq9#hsj&et}gqfrzRq|BcD{tvdbTCEmUVp|2+zh}?h zArWCs|Juy^fOGD)p-|nUIM%il&4xw*(vh*DY^ZNyruoO6Q)7-|i)Z16s6 zgNiAxX1bUnkq9Y4g3?8V2$HIIH2_IK1d@d=KZ#J0}2Z zGOWoN?jOczj}n7cf)a&Nssa$9*XxGPa>tcr8G)M$m;%!5?CjEwnYmj4T2-Z0>#DFM zz-r5yMn8pVkdj{AXfaX$v8FH$)-?LD+L8dH4O-W>lm_6|!s62O^z<#N?rBQZZPS^l z3sdc~lNRQ0%_OZfMw!*BrnM#{u*R}^-6)%fHqzJVCo=_^Dd;Qu*gUk6&Fe<7#u5^% z@J1VqGH9ilUz|y))Jy08eSRJ|bNtwewy8t;!^z2seUI&a%>QfhO#9n=o*b~oVuK~K z86gI`y)G_#3X}8Dz%c7u>*`RhwsA~}kRn~*1BA?GWH!SZi!~ObHB(o{Z{2G9-o(WC zB30kGnoHj6bUN+R|2Tc^=B3`1iJ5a&vFvNQn`!|0Yu1cMs70@!H2+7 zxlBUHvz*-K6@;lEre;6$-RoRCdFteJr_*Tz@2vvV%kbLSv!5-@%*-yGJ3T(xb!~E+ zQ`mwcD`?~m8hL}v0Y~QZO z*ELLQwZ5+unUxN>3d1UFS2o?s(k=UW&Yu5xZvJANK6UEk?9_#+>)g|lePL4yy!!Ci z!~I*gZCl$nq}M$5o$Zfod+3`s_*nIYAyg^$P5`+bw%RBruYcku&&*typXs#EojrT= z!o>@Vz?V0r_dvYTXf*qFY~L|3FgTEp?Q3k^z2jTML&cbo)ECYG5}E5>Oy?)g%w79D zT)KSe^3u7_&&@9{cb2%{w)~#uI0}r6j&5vh-n6;D(adfCSQ;GLwqdxxuRqW8!T|Jo zUEk?+%BvS{%yp*K{NmzrIX*Goo}Rw4T(9|q9Zc?kSUs9$S=K)=Fpv$b9mtx^K4YvA zV~kQtNeCg8y)rBb%707*qoM6N<$f~`k4%m4rY literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/muc_active.png b/data/iconsets/wroop/32x32/muc_active.png new file mode 100644 index 0000000000000000000000000000000000000000..e0544aa5635076b9d0e949f2de93aab4bea5b8fe GIT binary patch literal 2414 zcmV-!36b`RP)dYn$DKHE2uNF>AIfr+*=ZQ=KizdtxSdSM*c2OMZ= zZZ*C&fX1eVD&Y4*h@x9>*;IXf{rY99s;V>pdwTqw`1m9MtFO3<>gvkOSj)1kLx>-$veL4GK!1Rj_w5&xlMxa|5|9Xy z@=r}6a5{A6&C8?VwoS6K!bdO^9NqoguK$EWp)ugGmgd$Ez7{}ZQ^Oh|#P4pp@yC_* z*ImC<(==_*-shFJwoiqkDyXVTZf-8+r3)!5D+3@H4ALJOAR3KgSr)csvvTES@~*9S z+LmRRhu=EX@#<@@_@$JOd>+Ic0BPF)bi;;?OE%uHad|M*r~d69yHq1#;BqY1b9%gzvfy1e@uH)AJAvQMPDchn zy5OEHDK7Cn_~5VW&UK!1?b`jks%Z*URh8VirIDh-LL?GJfJDr24?-Z2*b)SS;gJzu zc&UZnUO%R3aqm5MS<99zo8Gze&+mr^!vSE&OkpasmT-#@qG-#OrsbAxD$hK-M@2~1 z*I&m2JGN6)R7g53Qi5fhjK{-_$HQ2*iIk8k2}w~=ArI`>&ieZ6K)^H4?olnFev)FIo4ab&H<(`%}67fqt>Fww7ORzdu8_ln|Qiy;};*EI_X?ncTJ_oq1iH^C5)bRNJSTs;WZQbp`M$K)%QA(Jj-o zyL$qvX`1*dDwrwc@kB~yLI`r5UUD4nv?yeT0gxae$KfW|=|u>Ul4&BG)&lq{DliO# z^XL0i%QS6|+oJ>d+6=;^Y1$K`6O4>Zke4?Pgo40M1rvZ28BnQN1=#70D77ora%(pDlv?7s6 z5*Sl}al0T zKYp0*@s9{bI>^f^L{&a(89F*U0jOSFo{Ys~Ny9K~U|azLfE*qko~o{_iURlpp8){a zwBjLV5^y?vh@J0lV>}+t{2z~p+4=4^PKOVr(;(Qi;-NWnw0CqNgrIim^2qS;@Kl<< zfY#F78fk25I2-5-cs-u{7*L=n%ACQ=3hP)`vyHd9cV|GHe)SOfSqlLeix154Ustn@ zWrcN__tDWY{DA_kdQWmR#)NF+_azc)&%Z)66*kytF&G}J$+rT|o30zgw7 z+;#aQtS-Bm{bwJgXY>?FI{|>MIH@kUoJ}hpnzM3%*WNhD@efbpbm}zRd|OZ3nYNKw zEEWNdz}$Xd^Lc-0k7e1ez0d7pVM*~NAZD9on+!&~0VvL`K~=O%d!uC;1-bKw-Z^$O4E(dDx%K7Q3;-OkEZdRga#2#e;46azsES5u-qJ7qf9$>EJht<3 z;?vVqFDZ_f7gdbD``)n;U>G7D5P0 zN7w|uaxt25Hq-xnp-8P93YGXSK@wL*ff z>l{AvHo;Jc&_I}AC`2$AVlo=N1e8P9F)a(*wlm*^5X>*hO>DS!{TXB0m_Bjh#Km|# zHpSPq<*Tl`x(y*~zi9OWq^@h0%jrtycy!Ao;h2gTni01Y3FNvRmZvz|C|yuAURkho zKoPnb3kpnmVUYEYp&)*p%7V7c?$jyl5DP5#|56n$5N^x|}MCA{@Fy02oO_+P0lOm%qcy g=~saH&aUVG1yKYPA*hfl9{>OV07*qoM6N<$f{9v_wg3PC literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/muc_inactive.png b/data/iconsets/wroop/32x32/muc_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..80e31bf313e9efe974df0b114303c94a0a0e08c4 GIT binary patch literal 2237 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 literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/online.png b/data/iconsets/wroop/32x32/online.png new file mode 100644 index 0000000000000000000000000000000000000000..013102b236ee4243aefec6c71e5fa88efc83f4ff GIT binary patch literal 2453 zcmV;G32OFNh=c3|n?Qyn_KYt* z({t^f?%n%RRhx(2V~i0Y5wx_W*50dXef6)ZT5E|c%Xl}xW5fBWM6^77Iu@Dy>NCr-WePXUe{KYCaQ@vTQc`KhUg9(?#t zt+lo;k9W=Pk}zwlJ+DT#Db3c3Tk3ajr=VpL!-WraZ0QrXdL33Mk@~dC{ z+7YA=QX89J{_ZqqUwICRL~Dam8m$ykDkMVyPlopn>m1HmtWEKFK77w3-2eVhV{|da z%jEmt|K5wO`PMA(m5q%lZ&BkhA;j1dPaNN$;bj!}&GYA;;>^pZAU6Uo_}WOI8Yd)Fo8k`5)~*+K`}B?VPvF&DJWF_&E&Q^7=oNV{|r|b-lP|G zg7LBO>O&7cJOMnO`$Xp;57g`R`Uj4DaH=17OzcAS^w}RGb%r(yZ4@eyXe}rP1rF>x zz|_Ppwl>B9SX^y$<=SN~&0Hc%99~K^iVTIv^7Pps@}*CGLuFZJK5*oNQ!oGSTzhS8 z?E&B?8zDG+;K1}KKriYPUU=oFEOl-mwL}?#HUg~`wR)W+hwf!+`y{18ku;4-)0k4B z$kg^pjvTs|TD^|e3T*_+2&9%Qb#Cy&tIrZfodQ7a>o9=KrMPR)?!BXfq^De#ii?+j zg9-%N3;|Ri2?~aNlY24Rka|mE6XG-?P9qYVka~;JLz|#rP{9VgL<}W!|Hn2abt$Qj*i^1kER;}MqBKYYS_@WtizF_RN-1HCkw8NN zqxD+dIBR`uL$x+&BeXzifzlFb1m$7{r4_M@NL@tg{_1!GBy};Vi-=u>(u#7if;1wB z5C|<;4Ht;hNI7eLy;e8Cs0PT$NUimWjj~R-M0#TIamC zFkFscGsM(RUS>qyg-@yc#uIbbDfkupFUAkP_NxBCo>iCLMKO|Q+)&32_L_MCTqcXJx4MXm&-B4ja|!vqBm z?|hW$=KU0l6|5Lw#eib5%yjd94)1&v6BJmC=14_|GTfYJcXJx&otLiIo1L5O0yqOKpgkjjdcGm;^5>*5k43cqp_unmo6@bVb}%E++GiS4*2|s?)z}Fy0uvC*8Z?(wtGR&uD_aJ z$HQ=_TvDl^s5T8xfm_Er@HWHQ43~mSGF+N5x#=L2)%W>o=k7PUi&6K5U!QFOKRS8h z)RVW@1E+!Zg;&lm+P(~Rf9w22V?R@))58l%4Ob*h4iX5g>0wRpCY&^4XrqQV=+x*m z6OH}o{?>Whm*It1&MyM(+!ueNK(4|r;5(B$C%5d|cV|7WUK>j{UvEaN$+`Y{V%hnt z8o3Gp-Z5NxzUqRnv9ofBB6V+;ceD!KiT2FQ>ua-fvrGJ=lI#|U!@#$8zGvsi)UI7s zN>M4<{>GH3^eRjCExP_1P7lz*FcdJ9-Xz|FG_EN_DB834C+5DVhsSS@0^AeY}eBX4r4YG(fr{##ivMbXjY> zC0moNUbmP0`Heq!27|EAJ1xs^3&*#Cv1W67X=2C3h*X)(Hm_FY=Czuv#06Qh1pq(L zvF{gRzg+K#(@Byf>G7|$X19U-U2`g>6oCl@K$0ez_kN>S`}Z}cZv*phYtR1z$?~Q^ TX#>_700000NkvXXu0mjfJFca9 literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/32x32/requested.png b/data/iconsets/wroop/32x32/requested.png new file mode 100644 index 0000000000000000000000000000000000000000..2e37e9cd57fe0149cdf75b6631d7508fe4217ec8 GIT binary patch literal 2437 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 Z{{jzB`Enc5tV{p^002ovPDHLkV1o2rkN^Mx literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/chat.png b/data/iconsets/wroop/48x48/chat.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1b4213cede0743a8ac90594daa3c4d8b3050a1 GIT binary patch literal 4301 zcmV;;5HjzHP)5kI>5!uTjreq|J?t*_sqE?y!X5p$G-i0JAj__ zq4l@!?q%V|4I8@$hlV-;Zj6tY&YeFuKR-VgEip3%oCIDtc;L|Wp9jY~Ca`b+ z-ag>dz+->}cy!N)1|E52&)R_%19`2r`oHcs#@LC;iP>Yvj$S!(^!pR(*A93Qc;?`N zL(}gGfqnb;W>esw0K(gU;M)Fy>HXTO#?!RMZmpxc<=Gv0RoRpoFD;@5Hf9x z`FijDx%1~HzV`Qjce+-qIp7NL^uYs%rhZ`p`}XhM3_JyNf9#{bGW7Uwd}_OtQl^em z&<^J;AV7Eo5JeHi;v4{-oy!Qr5D(x*lZ=!SAyku)bI$$eH~-`H?;ifnI4}=<`QU*= zXMb)2`}XhM1N;G?KmWPk-ZJvg$UQ;`y!TjRaNZ;&@Qhy{<=oj*j9z$?;`|KtsG1N0 zz>^8v=J35!~!AF0M z?eBjSDHH-hm}x^P4Z!uW>(hV!<)=?L=M36K3dYaiY7p@ETyM(*+6Q>_$5y zryqG}HVV@PV>!YzlyOjrSd%0N(mu^ zc>M8CZ41Lt0-s1R-km_oFnscf&us~VP#F`mFn?P(YkBEA&oX-cbmCA8bP8B$i3%h- zxC1;;EhO&tU69z92d@=!dk=J+_doC^S}dJSLXBi5O^%b`nECfaX>!tnJ4d; zQi^)LO0}{8IF22DHi=T&-ANP^r%K6Y%VaYQ-?y3mfkAo;1=?~s0IHP=x2LAKIdOx_ zKR!#nR>N8kc<_#7x{`Pt-V3}I96S6hYlb&rDlzqrZo)7tp7_j@_y6rz{`$2P>pw1) z&Wc2*4!H+A2%rD%>XrQp;n&tq&%rM!T(#)o03cIkp*XP)?iGGiPL6coHgJK-dntP z6b1%Z*1fE0r`5KU0xh9hnPu|EDBfF2#Tn_G^SgHL8fwMdAg~o+XmDj7@WqAO!a2*8 zi?5+1>68gd(gd%RAP6;sYgQu=IA`$A;*7!B2pdP3xQ>bI*f_%32xrp&ID0Ad|@i&N-^(dEu>P?AlqhrWqNf1X6-j z5NJtnp`Snnco*Y+3f`F{(K9jDEWUQmBqY4WyO=-)i48503S=`_4QavHwX=9@sg~!3 zbB;_V6SQJ(%tHBGE~k}}!h7dqQ$tEcy)v5&ur&G&jRsl~WHRJ(IUEk-B9s+4C9pQl zLA^st3(!m*tc`Fk!rKVrA{?GvE=Q2bq|&nZbYgM}oDBj+ z0c2V|2#1D-+P%j|Q9a31qAJ$blc>m+=txk3B%8?)1c_r|Vw{a}CTZ5jSQlfg!B~?3 zX!+gQ7-3@YmLSNG&190rF!j;u)7m;Fs^X0$it2ce9~v5J2f}^(_jX9q(nq1tpK;Fl zdc7=+sZm;(MynP*>IA%0kO?wm+A=69z}7J~8Kl<4iHuB)k7AsOa3)HxlizGQppA|3 zwvLj5Ok0LbkU=UE63tX4@n4+3g^8=w>t*4b^MyixCRI*Pb0JGK>b!H-o2W*uw!qx< zH9GUXgket-kyJ{7mS`0q0B0=I`edP^&(XstrQ-CmKtL#NGb3ZtVkXi6&~*s0^T`nDlMNz-r5)?b2vnj zj!Tb;iWG}8REo1y%4Lj87V89yLF_N~;@)+Rp!YAPbyt4^#6Jw() zQLRX&beka4LtL-0Fn5c|u{SA~=CNtItyJfu7b84~*LGnaMZs;}x{hNUaHjEHVgC zDk;uc8)2h}*wk?*8Q4PR=vlFidbJF`jx!asYRq5>C=axEF2lJPsWUj|ys_4m%Vk4S z=`kW8PADKu?@7-!3bK}eu9g~4rLYgkjpn-Xz-j<_;S zy);3+G(lXM#zgaYQ^J}u*cyewZ3Ida1|iOt5yXppH2n4tZopeZnC)=RdtWRTYps|Y z1l|O=a{1D%v)1K03$b%CD+V{-A<)vag5vGbB&pK@xsF~|tlEk6b$na~Q^K1PJ}xKC zl#=h_GSb&sv1%u|j$V`wP&%=t)!Y3j2`dIS;#^Fwvk*IL-IdFiW?L~g2)qh#`iz? zhr-Zia54G9=9074k6yfRs|72q5(0PuaED(!JXWpM;!N0A4Z8X)&anF4hmj)b0lB0n zw1V=?S;o$MgUZ51q!b8|WRg-OH6kTJh$Ky}EL>#l%r_{{oK08xJ846+30Qsa!{7`- zSD(#i^L%}%%l>I-iX72jgMr84Jz61(Q^L91jOiA@LHy=&-S z`97SF=@=X-Yu#NdE-XYZpEy2AQhtA?74V|6O1io5yfMao>)S6}j^o(o+BVIGy~B0y z3`4^Y)3t0BQqUx^R8oy|pcl7ADftq)G;DP(TgA}u!+2*1dxz_}woS8f9NTYw`-RKK z7zaGRbOU5*U8vCuSFT>WT6+1!53W1sTxaJ8retTq01OO2LQnseyONM{v8)>4TV=LX z>kS({{aY9qek1|aSumZQADD8^xtCA;;QG~TS4$+N#b=g+{h7tx4}bV0gB#ax=mSr= zbfn+Up3f=;QDuVS^c&z77LAJr;O2S(i!>jb#d=3}(b>0+FgK7q4COb}+B+V-34(JM z&QHJe()VugPKvuNBsK$21A5ORdj{68-`E2X#jp1<^TRGB97=lX<*U@@&mpNSA`s|= z5Tt<3VgimF+3t;G+Sg#6zPBjaT-Ss5fspw6$rfyYX#Or zoEwNm_JdPO^wq4j?$oIt-8^&l%q+ji2FH?~cnatq7#Pg$+O>P2J(tq}PF(K_;*))0 zeOd>?Hy?PE0x2x`dg>*E^D3!IJ8%|aFNiXICJsiXrC+rGP_9(WE3cfGn3%Xx;oaC` zY9Z0w{p|9(_sk5eN z<4v>P=L|qSj_kz?=V#8GJu~Z^gLiY2uZ6^-ZPqXh<>vb~=ZA(?cC@8E(mb3KGFS+e zp3MSSldrkBD?-Xe&Z?D)86O`jo&E9I*(iz}@6|Tz5+ZNg=+5W!;mVb(+Y8H==d#(X z)>_|fdd3*zYPFh~x^=5EHg>H%J3HIhxqt6By6-~dr@m^SwU+I~cwwT#Di$N~Dfj2We5Z6kVw# z=W2i$Zqqu7pah6x2Q88mMNyzYQMdWiZHfX#+rUM7B~AZHWJNuY%68m3b|gENEk}|i zM{;#g6eUrlNQx`&ImbKt$IPyll+@IT9iRg|%$wbr_xpZ-*L(AOBdoQ&AIIK(ds={w z7`}Qmc zJ^^e6x*Gp$Yisw`u36i*w6CuP;QIJ@>D>8qb8~aI!Ue-j0mp#n59~j9^=HBHt_kej zx2F^MEbtLP0z9<)L%q9p?OxH_)0=Z$*ZJR`S4!!L$%*_cuN=92_{a|@lAjIm67cN< z`wz~%F9i1P+mlIvKMZ(FmMqD9@wdLTsjqKYhm=yh8=FH^o z*v+0JCgQ(m1orOR(*=A5SdqzOAd60|lZmq?Eq}KufNjl;>d+I@p+uloBDGIw50>`PYB@m$&}w#s3@!=76sr z*njZs&rD$NzCF8v-vit)Jn@@DTeoaoC4|6Qi&hF_6j+PFGBGm3$d8XR`u1r`H)jd` zY77j^)dicLHdo2>dEwu}1Uu;lmvJ-hWWKbt}=RKnNLI<2HUsT+14Z zF@~1*cDDZ7ud?Bxhw4`F(oHz73&7QDS7-k8t4|#@#wg$`_p-!Y2_!!M7eL#SfB47S z2l@w=07Sk|82EtY(y3D%`ObH^b>jxoaS%@I63E0g5Y$}VTCl{xF$QA{RvV1gw0C#2 z`!kbJhb~my<4|zU1hDsFBL^ltr7-4 z&wc&tTt0OQ$8~Xp#7RKQq%Z0u7GxS>F(#fkv;`wAuC-`mL0XP|?|V#*jq&i8zl=$1 zK!~PAi|p1dTUU*aUb=Po$Pa7SzS%ggT%i3<;9<}6h48;~0(*jGjEnvrj*bRuSdG9Mw_@ zA%uAJ(NAskJWm1-Cm8QeAYte|{@4>kUdnTniYU$A6h>=a+5ZfqCr;uZkaeNAEl>m5 z*k9Q0dY_OuA{i6G=!uiOvi}*3)|6&%3Z){vl;=GD*b_s<>#NV+l|Zt#`A9C8^EPZ4 zUS_RjZe~Vkr5HJWoa2WM#hOb5)vX{Jv?&Cs1X#*(=;&EYN6%tXi4{^(-t`$r#ADXR zKYr*CBgc=Um11sYMp$dvuwi&vE|>Fwk0e;{h=ET4^0Ci7e!rAbgw-lRp#awK+B46@ zS`q>wM54jlR;c-Zx+%?yErWEeTuMiO7nznU0Dh^$?3F2|Mz3@E&9ej*AB+WIK^Sb} ze%2UdLg=+;o}qvJI&>5fwziSVWW-~id;I>t{@P!jNU;9>LKoT!Fwj5H32^JigfvET z=I~((lan|C97u?8+w6(|FJ9Wo=1=aXe|R-*-5p3FF)Ge5Qb^jmJLn%?&E`+;X7SR_ z*pdxn)W(cuKw)x{GlvgjjONyjN!f_4hP6&0S~psP z3lS4(Fujz5zV$0vv-1H`84s&1Mrm}UP=1K=Lv*AtN@KMpmGM}!^8xzSuWYbltx>mt z6mas{=g?t9wNOASWj)VxHf`G633Mfx>lWAsuzlP1K4Xk6=Vzr+iW`?ksOIxHfaJEX zI*9lhL0d;VOIG$`l8MPGg$@-eh){mgf(RWdtWt5_*M=o4dui)vpEp)5w&H83=JVXR zG=kBZa(-4CW9;^A+xr?Z*9Z&)^etVM18i~jrZ7e`cKigASSwOc@7Dt{<ZWZ(CD{LttigU3cw~9 z>zHc^6oe(EL0G&OO2mBi&YwbZcZ3uNT<`OM$EM=l*?wbuH#5yt+A2s zBc+3%&m*|4eWxzKA(c+k)RM(&LllIF#GjP}#{$|Q9Swld2CXzY3Ndx-gjj89YRQsH zr?E<7gvKFWdPo5RfuGN#qX=y@)*9+5R{SJY28))2<=C4EQBH2uYphQNrF69kRX*!<9X@WHR~jjtOOLNr->?6 z!b({fV{BJfcRKN$j`~Cvzn~bawYIA2U6iKBs zXsxX=#%iT%uyKJ|fT^kJs>K>NofbwZ(#_3i-=}zOoa*>>nijRtoXgSDxft1z!;ucw z2wcY{s#Xv}U<4@Qw<_0ItQ!-s%AleM9mVcrf&k@5xS1@*2ps7kutbF-rJJ)<@_DMI zGCCoYZf?dXg_}+ri#1bI)71q3tb1Vp!P4G+d%_!2lU1X&byA)(N+E?HoylN)A7L$F zp@=FJDUMyCczKN8;SK0=kyJKIn4cxv*#V0d#j68t(b^)D3X4%16GfOXA}Uu2ibX0j zH}P^EMCA&FiD@QIpQdta4ntgHsintMCW91lns!p2GFo$EYO)H12lgK<)m$iFE|wJDd&inz*GPopzH zcd=OX+uGWiQf)0JDwbvUnl)4!2-M<$C_LT0P zib!r04Flu*yBT`o^DG&@ zAJ=tBrBdYIIS;;nd#-6XXIooY)YXO2nwIXRRjrLZdFrjHM$9h2f&B+B?cKL$YGmXh zrE)3EX0vX(tKIn5rsRqpJGl753wPB{Y4kFcfqpWB>u_9`Om7dvzw;&hiAgGxHz;3? z>$TbC%V_TDA=BG~_5-3y1?{?2u8mU~yX}-GhTnzWFBp?Cf1hjQ{Wm;mi#EAO9GR9^+>wvyIp^2Xn4{q}(YdLMic6GgP`+*(dGxB10FA$;xVk0y!B@850& zJTLVon+wk=rOfwVcz!gBBHi4-E}t1*8Ca$0-?@{v<;(AIrG-SCd$f5g+q;a>wtPAL ziR)yBR|d`f>+(?)>F>Yr{HRjO0M9Ml09iP9)U0rM?8;c_wWF_IHO83s;SWx^tGX1x z@?E>=92$DpBz_wB&Y>Ze@7e``yQ)jI4}WmV7-L>L`s&rOD`O?%(&9Yr!~dbU`{56N zbm`jr)^r*yg_DQ7qhseY5~xm0Fnj70x=^^M#v5Ho-=jILt#oYMNK%5!dZUc!y;3>e}y=!;xefO>H0H}_>)fxQY%~q)l(pt)6V-(Mw z!&WMHCBZ#_i)@xfYuC~|Fn|#P<4V#W-BM^;`M?aog$w6qkGyhZf_RVM-IdoHNYpnt zh6aann>T-;+i@JJiZhwu_>a2mI}<5sL0XHtb&Ftfl4xoQTd5$Gnn%F6E+U&H)zwA1 zrw6yaJq9j7tnZD|n}2!AUDWAotg`2 z0aX5Kt2utGQ;f{GQrmfcj@A$a@gV6mI8Hr%8o)RLySh`!&0A;O-0}iIxl&QDzkYOL zV&Zy*_hO5wfkb_iPfE$q;85|L>d_8(So=U03B(nBOR|%cf2D_ZP&kcfMu!Ck>>kw&EQDYdV?c@Thhe#{8|l9Bv1MI`i6vfaSMPOf&gedjL;>>ybu|cBOr>i}z&S`eJ%x<-%^T4|WYVSCl1741leQNN;!wX7mpK&;6us1@T_lzp? zJYK&b9oU2)h)krw;`i`&m@eHoBaJ-`p93c&^u2L->UFc9%5+@kKoD9=&?l$hV~@_m z2sN?h0hTy5#5%dFBHRCyL^nXLlHlsF}SgAXU5mw3g#E(u!4 zf_5d;8s=urt~BA0V8LLcjs}3ihG3xyhi1*L)X}K@cs-oBXF5`c_!y9d;IiP88M7O8 zdw>D_KM)$I+{BF87u)rQ(f9gkdPD}xqjsLb!EuJ2H z6MEFwL12F2#9;v61MmodT>y3fNVnZq0O$Z%1#kwy+ou*!U-_!KzchjQg%bw=ya?bi z0J|rzRjXCAXZN1!?9QE40BEnRWh*yVvMkGJo0yvbeg@$6Q;Vmse;FKKn85tPiCq9* z0dO3E27tLEbInH|eQfW84;fl?@Sx(Kyz{f=OP4S8 z;%yAz4FD%kEuLQeMhMI=oTx_NF9Ap!jfVaGAN;|6-}%mW_qUp@dI`21pLK``Lp3n!id@RtDWbI(4%?~nfQj~}tt8eoX~dwUqzU(t!JKocJJfA{rJ6ru(7bUYE82uIC3EJ@a$1g0Z$Ij zJ2>w_-orTuM-GK6P`Cn)9Gr9En)h(t!IKNmvImieBL`NDN~Nj=WRz0;)RRvge&*?C z_X21Ecr{}CYyt?c%6sOH%r$@arI#OK7ExMkt=3Rp2+le9sL(qPKLIKL3Ba)Jw(~H9x;_OlhSA zr0eyDVQ>J$G*;%wf!M?O0^~gk@8EnfK{665@Am}?UwWbl&qnV|%4RU?^}2DCt0YbI z!on*L8)FoJmmI13cO(#Vo8z@w%{=kMllKr2n*?c+ zRzgUy2gix){RFTWI^nx`9*5Rpl>dio0Vu`L8WWA*G1FLIl2!ms2>^NOsi*I))oKR7 z@rd;{54;FKzi|Bf`_nYF0@BUtrXdbiBBo$9_8{S4arPkQ5YObnLa0w^?7GY1z=106 zzB`Nq2ddb0mqmR_gQc{1c-F@-B4N04o-jq79@A6P#`^+|dLwz^`1kim%rD-Fz+(XL z;DbkZ3k#=JTeUU;6Gx!ILc!$@6yvqDtg(!pvk9i^3aM3q07L>J0R%{`!c<*h=WGIF zd5folEEhmi(B;{KiD7L5Wu*c@jvae=&m`6{ftb(l-oO7qqfu|9EQD&UW{4OR-|?jv zD6YE11d`zHYE`4zOdt>tOSCM3h(Q$3nL#WN2%60V)v6AL5gIRu#GBS<3A%|mU}2K! zsp-o8{RbKVc1O%(4}1>*jy-g2mY9S%PimD02yvzL0St2(!BPbF45I{T74RiNVt8VB zzimV^!Ps;5D2wO1BD-1sDN=2b4lP40628;(2Ov1rCzB(0YVal z3xQ=qL774bp;FUeAFNKqAW!2-S;SFJdWwc1X0Vs=tk71OQ7%a!B_x0XY+_Oc$l-er z@0i4F<3GCqpfNR7AtI?%r_4Ap1CLW+md(&%W+aIMh~bSuv49yNr7k~XGAEFUR2m^u z6$M2KNuuB!gET>N$csWU2na&>LV&cInylw!YI?dliMb@OBS{i%jZw@j)>uVC5DAv$ zc+MQiKmsVGRET*YXeCf8SW7`+drc$p8At%nqF}^?if#Noq7DgOU;`^bzA;u2<&s#d zk|fd2xg7vtWA`9!wOT1N3pBJbNmN>z3&H9LX|jk!>k7OGAPg@YbN)y|1)u<+l#Gc( z6a;`$8U!6ORH$(Z&Io|5*hnfQhgKU9dj$ZkRx4dyUFG?O6H^wk_0eoLEio~KlEm7v zl{fz)7Ag`{Y6)Q2!wZ$@Lh2mRa;(U-Xq2or>jZ&S3PvYE7D`V{=tI=UK$4_zlq-nl zX0vHmS62aGwhSRKaVRV_u^{B_N@>VMQ9kwx7!VB+hNtLy2t+ZIAb6>a2>?(W!%9O` zC^0n@yaKcmKs2Cu?6IJS!@bZ@Mng!zEFu)@kMghp+5ph&_Z>4c1io+?tf`D|Vn}>b z@TLcZK@4ap6kJYr2_xgT5moVG@_SELyCL;Si|=M&Ug&mm?>S!WGDgFe-8sp5R#^3^2ud zn4_~;;&*@}M&Sq>d4_@>cVBLl+u%JlCqxFO2bQ-n2s1CjQ++5nH$Bt-^JG?#d#Es7@xJ;!rBDY zq=v*)p%h?^24qwIcD+g|+2aaOQ*Y5YzIAA0+v>> z+QMjqR;?M|E(#(FL;~j-g(o;qaKvCPU!@8Vg;uQzqYbRKV{dD%Z_`)e=nvMN2ur)& z9!+8{37i3d>(@W(lP7Mkc6vl^40!xGF&q)J3H5CytzdR~78(W$1_~Nl8EDurRQOK8 zK*L~mdKQ(m5^7_kf~;-wCv_7!411$Nkr(vQM^}53m`eh01Hjd5*Lt2vIJ1Mb{_<_$ zjW)q*#zGqn=LyzWSYyy?G;wJ60W@k;uqJ`g35>B}=mgdzXw;@~X!ilM8ckRe}-$M0fa8`%h#@5?LP3pgFA12++FVO zyLo83x-$r88(yUnzzc`=VCD)~)~Hu%&@q-PNs3mzwWY1XAaHz!A=t=A@XSb)6p2X! z_(-BWH`<^(youqOTce!(($eM51XfIj5CFUmfX|*g+Z~NY&TXjSg=-&dnCRYeBqj+8 ztwmxJq)7?}&>Icl*$2xh1FZq0HH_BbTm?X$J$j=77zp5PlvT=|&}EFq()F_&iqfpz zZja8MJKGH}X1snIf!LpaqbLeKd+uzTh^2jfc;iNUX;Y|;By57zreP%E)0zZ>BE#CC zi?!i8`q>6HiVPb?hJLnzwc$F}23-t_4AvxDo=t5eGNHp7iyQ4F6dkoX9B%jzfBE5B ze0{z#xtFlLD{&G)aplTVzu)iWZlpHO{OW@n{d{fH`gUR@6F`y_m81e=Eua)ezCbVA zKrh?C$QOW8FxH}yRL1wGHbs(blNr6x&)0By>Cy@nI$K#;9$dMy)JJH)pPU4|g+Sa~ zc#WCm=Rbe4p+52$t(TAU#ol19YXBbN6%G>j3PL`;IgZ95^_oltS|$@A>w(zkB!H&7;Z_O>`@0OQ(8V z3<0qeP+L3F6YqL!41V1^Cl}70zro$$2L#=5I}2sx|z()&9!RPs?l1j`dzAZ=-BN3eY+0Vch~NN z(donv&8HAh8pH)|j+Ss^?drzr)neJLn?bMFD|-FDyL9Q&dcWUu_&TuWOTUg2a|>R%49XyJv50cIVD&rBcxV zV2n|sI}rKQpBiYbRhp#A zCRPK0cb*GZNL-92kKg=H4eoUOzx=Vs=N-SPpWFN&vHYA0hxLb$00000NkvXXu0mjf DgHj&| literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/muc_active.png b/data/iconsets/wroop/48x48/muc_active.png new file mode 100644 index 0000000000000000000000000000000000000000..a6aa8c3ba2b6aa67c6b4081d4c172192d9ca5da0 GIT binary patch literal 4079 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 literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/muc_inactive.png b/data/iconsets/wroop/48x48/muc_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..1ef4c99475d3f284d56545a121ae6dbcec6733bd GIT binary patch literal 3824 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 literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/offline.png b/data/iconsets/wroop/48x48/offline.png new file mode 100644 index 0000000000000000000000000000000000000000..ca40bb48c30977cf4af976f83695d9d4fa3c84d3 GIT binary patch literal 4422 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 M07*qoM6N<$f~(21RR910 literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/online.png b/data/iconsets/wroop/48x48/online.png new file mode 100644 index 0000000000000000000000000000000000000000..35a6b9e044fbc9074aed867615d99483ff8e4d16 GIT binary patch literal 4323 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 Ra54Y@002ovPDHLkV1jasV!Z$W literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/requested.png b/data/iconsets/wroop/48x48/requested.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2846c257aca229785a044493113b909e251b4a GIT binary patch literal 4331 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 Z{{jzB`Enc5tV{p^002ovPDHLkV1o2rkN^Mx literal 0 HcmV?d00001 diff --git a/data/iconsets/wroop/48x48/xa.png b/data/iconsets/wroop/48x48/xa.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2846c257aca229785a044493113b909e251b4a GIT binary patch literal 4331 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 Z{{jzB`Enc5tV{p^002ovPDHLkV1o2rkN^Mx literal 0 HcmV?d00001 diff --git a/gajim.nsi b/gajim.nsi index b4a87e1bf..3a5c8910f 100644 --- a/gajim.nsi +++ b/gajim.nsi @@ -286,6 +286,11 @@ Section "sun" SecIconsetsSun File /r "data\iconsets\sun" SectionEnd +Section "wroop" SecIconsetsSun + SetOutPath "$INSTDIR\data\iconsets" + File /r "data\iconsets\wroop" +SectionEnd + Section "transports" SecIconsetsTransports SetOutPath "$INSTDIR\data\iconsets" File /r "data\iconsets\transports" @@ -692,6 +697,7 @@ Section "Uninstall" RMDir /r "$INSTDIR\data\iconsets\gota" RMDir /r "$INSTDIR\data\iconsets\jabberbulb" RMDir /r "$INSTDIR\data\iconsets\sun" + RMDir /r "$INSTDIR\data\iconsets\wroop" RMDir /r "$INSTDIR\data\iconsets\transports" RMDir "$INSTDIR\data\iconsets" RMDir "$INSTDIR\data" From e9264abb8a439c7e2a479c632d5ca083a115d300 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 1 Nov 2009 09:52:33 +0100 Subject: [PATCH 08/29] fix variable name --- src/groupchat_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 5e68a6975..aad08ff67 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -647,7 +647,7 @@ class GroupchatControl(ChatControlBase): bookmark_separator = xml.get_widget('bookmark_separator') separatormenuitem2 = xml.get_widget('separatormenuitem2') - if hide_buttonbar_entries: + if hide_buttonbar_items: change_nick_menuitem.hide() change_subject_menuitem.hide() bookmark_room_menuitem.hide() From 21ffce890c35b4d8c3a1a2de872408372948c99a Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 1 Nov 2009 12:14:42 +0100 Subject: [PATCH 09/29] Backed out changeset f169c518cd8d sqlite with ? doesn't work as expected --- src/common/logger.py | 81 ++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 08929b417..d4af560f1 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -149,12 +149,9 @@ class Logger: self.open_db() self.get_jids_already_in_db() - def simple_commit(self, sql_to_commit, values=None): + def simple_commit(self, sql_to_commit): '''helper to commit''' - if values: - self.cur.execute(sql_to_commit, values) - else: - self.cur.execute(sql_to_commit) + self.cur.execute(sql_to_commit) try: self.con.commit() except sqlite.OperationalError, e: @@ -386,19 +383,21 @@ class Logger: def insert_unread_events(self, message_id, jid_id): ''' add unread message with id: message_id''' - sql = 'INSERT INTO unread_messages VALUES (?, ?, 0)' - self.simple_commit(sql, values=(message_id, jid_id)) + sql = 'INSERT INTO unread_messages VALUES (%d, %d, 0)' % (message_id, + jid_id) + self.simple_commit(sql) def set_read_messages(self, message_ids): ''' mark all messages with ids in message_ids as read''' ids = ','.join([str(i) for i in message_ids]) - sql = 'DELETE FROM unread_messages WHERE message_id IN (?)' - self.simple_commit(sql, values=(ids,)) + sql = 'DELETE FROM unread_messages WHERE message_id IN (%s)' % ids + self.simple_commit(sql) def set_shown_unread_msgs(self, msg_id): ''' mark unread message as shown un GUI ''' - sql = 'UPDATE unread_messages SET shown = 1 where message_id = ?' - self.simple_commit(sql, values=(msg_id,)) + sql = 'UPDATE unread_messages SET shown = 1 where message_id = %s' % \ + msg_id + self.simple_commit(sql) def reset_shown_unread_messages(self): ''' Set shown field to False in unread_messages table ''' @@ -424,8 +423,8 @@ class Logger: SELECT logs.log_line_id, logs.message, logs.time, logs.subject, jids.jid FROM logs, jids - WHERE logs.log_line_id = ? AND logs.jid_id = jids.jid_id - ''', (msg_id,) + WHERE logs.log_line_id = %d AND logs.jid_id = jids.jid_id + ''' % msg_id ) results = self.cur.fetchall() if len(results) == 0: @@ -537,9 +536,9 @@ class Logger: try: self.cur.execute(''' SELECT time, kind, message FROM logs - WHERE (?) AND kind IN (?, ?, ?, ?, ?) AND time > ? - ORDER BY time DESC LIMIT ? OFFSET ? - ''', (where_sql, constants.KIND_SINGLE_MSG_RECV, + WHERE (%s) AND kind IN (%d, %d, %d, %d, %d) AND time > %d + ORDER BY time DESC LIMIT %d OFFSET %d + ''' % (where_sql, constants.KIND_SINGLE_MSG_RECV, constants.KIND_CHAT_MSG_RECV, constants.KIND_SINGLE_MSG_SENT, constants.KIND_CHAT_MSG_SENT, constants.KIND_ERROR, timed_out, restore_how_many_rows, pending_how_many) @@ -578,10 +577,10 @@ class Logger: self.cur.execute(''' SELECT contact_name, time, kind, show, message, subject FROM logs - WHERE (?) - AND time BETWEEN ? AND ? + WHERE (%s) + AND time BETWEEN %d AND %d ORDER BY time - ''', (where_sql, start_of_day, last_second_of_day)) + ''' % (where_sql, start_of_day, last_second_of_day)) results = self.cur.fetchall() return results @@ -608,9 +607,9 @@ class Logger: like_sql = '%' + query.replace("'", "''") + '%' self.cur.execute(''' SELECT contact_name, time, kind, show, message, subject FROM logs - WHERE (?) AND message LIKE '?' + WHERE (%s) AND message LIKE '%s' ORDER BY time - ''', (where_sql, like_sql)) + ''' % (where_sql, like_sql)) results = self.cur.fetchall() return results @@ -636,11 +635,11 @@ class Logger: # Now we have timestamps of time 0:00 of every day with logs self.cur.execute(''' SELECT DISTINCT time/(86400)*86400 FROM logs - WHERE (?) - AND time BETWEEN ? AND ? - AND kind NOT IN (?, ?) + WHERE (%s) + AND time BETWEEN %d AND %d + AND kind NOT IN (%d, %d) ORDER BY time - ''', (where_sql, start_of_month, last_second_of_month, + ''' % (where_sql, start_of_month, last_second_of_month, constants.KIND_STATUS, constants.KIND_GCSTATUS)) result = self.cur.fetchall() @@ -665,9 +664,9 @@ class Logger: where_sql = 'jid_id = %s' % jid_id self.cur.execute(''' SELECT MAX(time) FROM logs - WHERE (?) - AND kind NOT IN (?, ?) - ''', (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS)) + WHERE (%s) + AND kind NOT IN (%d, %d) + ''' % (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS)) results = self.cur.fetchone() if results is not None: @@ -687,8 +686,8 @@ class Logger: where_sql = 'jid_id = %s' % jid_id self.cur.execute(''' SELECT time FROM rooms_last_message_time - WHERE (?) - ''', (where_sql,)) + WHERE (%s) + ''' % (where_sql)) results = self.cur.fetchone() if results is not None: @@ -702,8 +701,9 @@ class Logger: we had logs for that room in rooms_last_message_time table''' jid_id = self.get_jid_id(jid, 'ROOM') # jid_id is unique in this table, create or update : - sql = 'REPLACE INTO rooms_last_message_time VALUES (?, ?)' - self.simple_commit(sql, (jid_id, time)) + sql = 'REPLACE INTO rooms_last_message_time VALUES (%d, %d)' % \ + (jid_id, time) + self.simple_commit(sql) def _build_contact_where(self, account, jid): '''build the where clause for a jid, including metacontacts @@ -733,17 +733,18 @@ class Logger: # unknown type return self.cur.execute( - 'SELECT type from transports_cache WHERE transport = "?"', (jid,)) + 'SELECT type from transports_cache WHERE transport = "%s"' % jid) results = self.cur.fetchall() if results: result = results[0][0] if result == type_id: return - sql = 'UPDATE transports_cache SET type = ? WHERE transport = "?"' - self.simple_commit(sql, values=(type_id, jid)) + sql = 'UPDATE transports_cache SET type = %d WHERE transport = "%s"' %\ + (type_id, jid) + self.simple_commit(sql) return - sql = 'INSERT INTO transports_cache VALUES ("?", ?)' - self.simple_commit(sql, values=(jid, type_id)) + sql = 'INSERT INTO transports_cache VALUES ("%s", %d)' % (jid, type_id) + self.simple_commit(sql) def get_transports_type(self): '''return all the type of the transports in DB''' @@ -814,9 +815,9 @@ class Logger: # yield the row yield hash_method, hash_, identities, features for hash_method, hash_ in to_be_removed: - sql = '''DELETE FROM caps_cache WHERE hash_method = "?" AND - hash = "?"''' - self.simple_commit(sql, values=(hash_method, hash_)) + sql = '''DELETE FROM caps_cache WHERE hash_method = "%s" AND + hash = "%s"''' % (hash_method, hash_) + self.simple_commit(sql) def add_caps_entry(self, hash_method, hash_, identities, features): data = [] From a75fba495bb657aebe3dacfe886610a5468a3df9 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 1 Nov 2009 12:40:29 +0100 Subject: [PATCH 10/29] better error message --- src/dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs.py b/src/dialogs.py index 56246179d..3233855ea 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -2070,7 +2070,7 @@ class JoinGroupchatWindow: user, server, resource = helpers.decompose_jid(room_jid) if not user or not server or resource: ErrorDialog(_('Invalid group chat Jabber ID'), - _('The group chat Jabber ID has not allowed characters.')) + _('Please enter the group chat Jabber ID as room@server.')) return try: room_jid = helpers.parse_jid(room_jid) From ea973ddc2ebab4c31cbe2d7e54545a5e0eddcd4f Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 1 Nov 2009 13:50:34 +0100 Subject: [PATCH 11/29] fix exception handling in command system --- src/command_system/framework.py | 2 +- src/command_system/implementation/standard.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/command_system/framework.py b/src/command_system/framework.py index f46f701c5..3faa9ea68 100644 --- a/src/command_system/framework.py +++ b/src/command_system/framework.py @@ -158,7 +158,7 @@ class Command(object): # in case if they was not set by the one who raised an exception. except CommandError, error: if not error.command and not error.name: - raise CommandError(exception.message, self) + raise CommandError(error.message, self) raise # This one is a little bit too wide, but as Python does not have diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py index d46d07a09..50623cb9a 100644 --- a/src/command_system/implementation/standard.py +++ b/src/command_system/implementation/standard.py @@ -22,6 +22,7 @@ from common import gajim from common import helpers from common.exceptions import GajimGeneralException +from ..errors import CommandError from ..framework import CommandContainer, command, documentation from ..mapping import generate_usage From cb1fcc8cf80b56f670622c067082fe52fa7016aa Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 1 Nov 2009 16:50:27 +0100 Subject: [PATCH 12/29] update ft proxies list --- configure.ac | 2 +- src/common/config.py | 3 +- src/common/defs.py | 2 +- src/common/optparser.py | 94 ++++++++++++++++++++++------------------- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/configure.ac b/configure.ac index abe3115e7..157d41760 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_INIT([Gajim - A Jabber Instant Messager], - [0.12.5.7-dev],[http://trac.gajim.org/],[gajim]) + [0.12.5.8-dev],[http://trac.gajim.org/],[gajim]) AC_PREREQ([2.59]) AC_CONFIG_HEADER(config.h) diff --git a/src/common/config.py b/src/common/config.py index 2b0262d3b..92279c6d3 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -326,8 +326,7 @@ class Config: 'http_auth': [opt_str, 'ask'], # yes, no, ask 'dont_ack_subscription': [opt_bool, False, _('Jabberd2 workaround')], # proxy65 for FT - 'file_transfer_proxies': [opt_str, - 'proxy65.talkonaut.com, proxy.jabber.org, proxy.netlab.cz, transfer.jabber.freenet.de, proxy.jabber.cd.chalmers.se'], + 'file_transfer_proxies': [opt_str, 'proxy.eu.jabber.org, proxy.jabber.ru, proxy.jabbim.cz'], 'use_ft_proxies': [opt_bool, True, _('If checked, Gajim will use your IP and proxies defined in file_transfer_proxies option for file transfer.'), True], 'msgwin-x-position': [opt_int, -1], # Default is to let the wm decide 'msgwin-y-position': [opt_int, -1], # Default is to let the wm decide diff --git a/src/common/defs.py b/src/common/defs.py index 0f3bd1296..2886838a1 100644 --- a/src/common/defs.py +++ b/src/common/defs.py @@ -27,7 +27,7 @@ docdir = '../' datadir = '../' localedir = '../po' -version = '0.12.5.7-dev' +version = '0.12.5.8-dev' import sys, os.path for base in ('.', 'common'): diff --git a/src/common/optparser.py b/src/common/optparser.py index 0e0b463a6..783e332a5 100644 --- a/src/common/optparser.py +++ b/src/common/optparser.py @@ -216,12 +216,51 @@ class OptionsParser: self.update_config_to_01256() if old < [0, 12, 5, 7] and new >= [0, 12, 5, 7]: self.update_config_to_01257() + if old < [0, 12, 5, 8] and new >= [0, 12, 5, 8]: + self.update_config_to_01258() gajim.logger.init_vars() gajim.config.set('version', new_version) caps.capscache.initialize_from_db() + def assert_unread_msgs_table_exists(self): + '''create table unread_messages if there is no such table''' + back = os.getcwd() + os.chdir(logger.LOG_DB_FOLDER) + con = sqlite.connect(logger.LOG_DB_FILE) + os.chdir(back) + cur = con.cursor() + try: + cur.executescript( + ''' + CREATE TABLE unread_messages ( + message_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, + jid_id INTEGER + ); + ''' + ) + con.commit() + gajim.logger.init_vars() + except sqlite.OperationalError: + pass + con.close() + + def update_ft_proxies(self, to_remove=[], to_add=[]): + for account in gajim.config.get_per('accounts'): + proxies_str = gajim.config.get_per('accounts', account, + 'file_transfer_proxies') + proxies = [p.strip() for p in proxies_str.split(',')] + for wrong_proxy in to_remove: + if wrong_proxy in proxies: + proxies.remove(wrong_proxy) + for new_proxy in to_add: + if new_proxy not in proxies: + proxies.append(new_proxy) + proxies_str = ', '.join(proxies) + gajim.config.set_per('accounts', account, 'file_transfer_proxies', + proxies_str) + def update_config_x_to_09(self): # Var name that changed: # avatar_width /height -> chat_avatar_width / height @@ -262,38 +301,10 @@ class OptionsParser: theme = gajim.config.get_per('themes')[0] gajim.config.set('roster_theme', theme) # new proxies in accounts.name.file_transfer_proxies - for account in gajim.config.get_per('accounts'): - proxies = gajim.config.get_per('accounts', account, - 'file_transfer_proxies') - if proxies.find('proxy.netlab.cz') < 0: - proxies += ', ' + 'proxy.netlab.cz' - gajim.config.set_per('accounts', account, 'file_transfer_proxies', - proxies) + self.update_ft_proxies(to_add=['proxy.netlab.cz']) gajim.config.set('version', '0.9') - def assert_unread_msgs_table_exists(self): - '''create table unread_messages if there is no such table''' - back = os.getcwd() - os.chdir(logger.LOG_DB_FOLDER) - con = sqlite.connect(logger.LOG_DB_FILE) - os.chdir(back) - cur = con.cursor() - try: - cur.executescript( - ''' - CREATE TABLE unread_messages ( - message_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, - jid_id INTEGER - ); - ''' - ) - con.commit() - gajim.logger.init_vars() - except sqlite.OperationalError: - pass - con.close() - def update_config_09_to_010(self): if 'usetabbedchat' in self.old_values and not \ self.old_values['usetabbedchat']: @@ -311,21 +322,8 @@ class OptionsParser: self.old_values['always_compact_view_gc'] != 'False': gajim.config.set('always_hide_groupchat_buttons', True) - for account in gajim.config.get_per('accounts'): - proxies_str = gajim.config.get_per('accounts', account, - 'file_transfer_proxies') - proxies = proxies_str.split(',') - for i in range(0, len(proxies)): - proxies[i] = proxies[i].strip() - for wrong_proxy in ('proxy65.jabber.autocom.pl', - 'proxy65.jabber.ccc.de'): - if wrong_proxy in proxies: - proxies.remove(wrong_proxy) - if not 'transfer.jabber.freenet.de' in proxies: - proxies.append('transfer.jabber.freenet.de') - proxies_str = ', '.join(proxies) - gajim.config.set_per('accounts', account, 'file_transfer_proxies', - proxies_str) + self.update_ft_proxies(to_remove=['proxy65.jabber.autocom.pl', + 'proxy65.jabber.ccc.de'], to_add=['transfer.jabber.freenet.de']) # create unread_messages table if needed self.assert_unread_msgs_table_exists() @@ -811,4 +809,12 @@ class OptionsParser: 'simplebulb', 'stellar'): gajim.config.set('iconset', gajim.config.DEFAULT_ICONSET) gajim.config.set('version', '0.12.5.7') + + def update_config_to_01258(self): + self.update_ft_proxies(to_remove=['proxy65.talkonaut.com', + 'proxy.jabber.org', 'proxy.netlab.cz', 'transfer.jabber.freenet.de', + 'proxy.jabber.cd.chalmers.se'], to_add=['proxy.eu.jabber.org', + 'proxy.jabber.ru', 'proxy.jabbim.cz']) + gajim.config.set('version', '0.12.5.8') + # vim: se ts=3: From d19df32c161237ffe7ec79cd7606cf625b36082d Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Mon, 2 Nov 2009 06:59:56 +0100 Subject: [PATCH 13/29] reorder imports in history_manager so that it can bu run. Fixes #5391 --- src/history_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/history_manager.py b/src/history_manager.py index 30565d6a5..9ab638364 100644 --- a/src/history_manager.py +++ b/src/history_manager.py @@ -83,15 +83,15 @@ common.configpaths.gajimpaths.init(config_path) del config_path common.configpaths.gajimpaths.init_profile() from common import exceptions -import dialogs +from common import gajim 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 +import dialogs # time, message, subject ( From aae1dd6c386e89e5ed2835ceb0b0ed887e944e18 Mon Sep 17 00:00:00 2001 From: red-agent Date: Mon, 2 Nov 2009 17:42:00 +0200 Subject: [PATCH 14/29] Fixed an issue with the bare command prefix --- src/command_system/framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_system/framework.py b/src/command_system/framework.py index 3faa9ea68..a7f391781 100644 --- a/src/command_system/framework.py +++ b/src/command_system/framework.py @@ -65,7 +65,7 @@ class CommandProcessor(object): Try to process text as a command. Returns True if it has been processed as a command and False otherwise. """ - if not text.startswith(self.COMMAND_PREFIX): + if not (text.startswith(self.COMMAND_PREFIX) and len(text) > 1): return False body = text[len(self.COMMAND_PREFIX):] From b4d45a120f6231ed07aee91a3f907375dafd6259 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Mon, 2 Nov 2009 22:41:43 +0100 Subject: [PATCH 15/29] Changeleog for 0.13 --- ChangeLog | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ChangeLog b/ChangeLog index 62d716b12..9e4f70e5e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,26 @@ +Gajim 0.13 (XX November 2009) + + * Improve gtkspell (fix memleak) + * BOSH connection + * Roster versioning + * Ability to send contacts + * GUI to send XHTML messages + * Improve sessions handling + * pubsub storage (for bookmarks) + * Ability to select account when joining a groupchat + * Better Gnome keyring support + * Ability to ignore occupants in groupchats + * Ability to show / hide self contact row + * Automatically go away when screensaver is enabled under windows + * Ability to enable / disable accounts + * better URL recognition + * groupchat autoreconnect + * Store passwords in KDE wallet is available + * Better MUC errors handling + * Fix sound player launch (don't create zombies anymore) + * Optional shell like completion + * New color theme + Gajim 0.12.5 (08 August 2009) * Don't depend on GTK 2.14 From 0e38897445887b0efb70c61f5da22c808d9121a4 Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 3 Nov 2009 10:14:23 +0200 Subject: [PATCH 16/29] Minor refactoring --- src/command_system/framework.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/command_system/framework.py b/src/command_system/framework.py index a7f391781..db9eb2e78 100644 --- a/src/command_system/framework.py +++ b/src/command_system/framework.py @@ -65,7 +65,9 @@ class CommandProcessor(object): Try to process text as a command. Returns True if it has been processed as a command and False otherwise. """ - if not (text.startswith(self.COMMAND_PREFIX) and len(text) > 1): + prefix = text.startswith(self.COMMAND_PREFIX) + length = len(text) > len(self.COMMAND_PREFIX) + if not (prefix and length): return False body = text[len(self.COMMAND_PREFIX):] From a202367fda46ba340914923dbf10a7dcc400b8c8 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 3 Nov 2009 19:53:11 +0100 Subject: [PATCH 17/29] remove unused file from windows installer script --- ChangeLog | 2 +- gajim.nsi | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9e4f70e5e..6f77eb6d3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,7 +15,7 @@ Gajim 0.13 (XX November 2009) * Ability to enable / disable accounts * better URL recognition * groupchat autoreconnect - * Store passwords in KDE wallet is available + * Store passwords in KDE wallet if available * Better MUC errors handling * Fix sound player launch (don't create zombies anymore) * Optional shell like completion diff --git a/gajim.nsi b/gajim.nsi index 3a5c8910f..a8ae08281 100644 --- a/gajim.nsi +++ b/gajim.nsi @@ -174,7 +174,6 @@ Section "Gajim" SecGajim File "bin\pywintypes25.dll" File "bin\OpenSSL.rand.pyd" File "bin\select.pyd" - File "bin\Crypto.Hash.SHA256.pyd" File "bin\sqlite3.dll" File "bin\ssleay32.dll" File "bin\OpenSSL.SSL.pyd" @@ -651,7 +650,6 @@ Section "Uninstall" Delete "$INSTDIR\bin\bz2.pyd" Delete "$INSTDIR\bin\cairo._cairo.pyd" Delete "$INSTDIR\bin\Crypto.Cipher.AES.pyd" - Delete "$INSTDIR\bin\Crypto.Hash.SHA256.pyd" Delete "$INSTDIR\bin\gajim.exe" Delete "$INSTDIR\bin\gobject._gobject.pyd" Delete "$INSTDIR\bin\gtk._gtk.pyd" From 255c16c79f80c09815db06c66ece515545106702 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 3 Nov 2009 19:59:39 +0100 Subject: [PATCH 18/29] fix window installer script --- gajim.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gajim.nsi b/gajim.nsi index a8ae08281..79c2e19e4 100644 --- a/gajim.nsi +++ b/gajim.nsi @@ -285,7 +285,7 @@ Section "sun" SecIconsetsSun File /r "data\iconsets\sun" SectionEnd -Section "wroop" SecIconsetsSun +Section "wroop" SecIconsetsWroop SetOutPath "$INSTDIR\data\iconsets" File /r "data\iconsets\wroop" SectionEnd From 3a98a4170c6c13fb20115f79e1f0a2680304e69c Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Tue, 3 Nov 2009 22:14:19 +0100 Subject: [PATCH 19/29] Move Interface() god class from gajim.py to gui_interface.py. --- src/gajim.py | 3399 +---------------- test/runtests.py | 3 +- ...rface.py => test_gui_event_integration.py} | 28 - test/test_gui_interface.py | 111 + 4 files changed, 116 insertions(+), 3425 deletions(-) rename test/{test_misc_interface.py => test_gui_event_integration.py} (83%) create mode 100644 test/test_gui_interface.py diff --git a/src/gajim.py b/src/gajim.py index 81479c6ba..a2e58f4e7 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -226,36 +226,9 @@ if not hasattr(gobject, 'timeout_add_seconds'): return gobject.timeout_add(time_sec * 1000, *args) gobject.timeout_add_seconds = timeout_add_seconds_fake -import re + import signal -import time -import math - import gtkgui_helpers -import notify -import message_control - -from chat_control import ChatControlBase -from chat_control import ChatControl -from groupchat_control import GroupchatControl -from groupchat_control import PrivateChatControl - -from atom_window import AtomWindow -from session import ChatControlSession - -import common.sleepy - -from common.xmpp import idlequeue -from common.zeroconf import connection_zeroconf -from common import resolver -from common import proxy65_manager -from common import socks5 -from common import helpers -from common import optparser -from common import dataforms -from common import passwords -from common import pep -from common import caps gajimpaths = common.configpaths.gajimpaths @@ -264,8 +237,8 @@ config_filename = gajimpaths['CONFIG_FILE'] import traceback import errno - import dialogs + def pid_alive(): try: pf = open(pid_filename) @@ -394,3374 +367,8 @@ def on_exit(): import atexit atexit.register(on_exit) - -parser = optparser.OptionsParser(config_filename) - -import roster_window -import profile_window -import config -from threading import Thread - - -class PassphraseRequest: - def __init__(self, keyid): - self.keyid = keyid - self.callbacks = [] - self.dialog_created = False - self.dialog = None - self.completed = False - - def interrupt(self): - self.dialog.window.destroy() - self.callbacks = [] - - def run_callback(self, account, callback): - gajim.connections[account].gpg_passphrase(self.passphrase) - callback() - - def add_callback(self, account, cb): - if self.completed: - self.run_callback(account, cb) - else: - self.callbacks.append((account, cb)) - if not self.dialog_created: - self.create_dialog(account) - - def complete(self, passphrase): - self.passphrase = passphrase - self.completed = True - if passphrase is not None: - gobject.timeout_add_seconds(30, gajim.interface.forget_gpg_passphrase, - self.keyid) - for (account, cb) in self.callbacks: - self.run_callback(account, cb) - del self.callbacks - - def create_dialog(self, account): - title = _('Passphrase Required') - second = _('Enter GPG key passphrase for key %(keyid)s (account ' - '%(account)s).') % {'keyid': self.keyid, 'account': account} - - def _cancel(): - # user cancelled, continue without GPG - self.complete(None) - - def _ok(passphrase, checked, count): - result = gajim.connections[account].test_gpg_passphrase(passphrase) - if result == 'ok': - # passphrase is good - self.complete(passphrase) - return - elif result == 'expired': - dialogs.ErrorDialog(_('GPG key expired'), - _('Your GPG key has expired, you will be connected to %s without' - ' OpenPGP.') % account) - # Don't try to connect with GPG - gajim.connections[account].continue_connect_info[2] = False - self.complete(None) - return - - if count < 3: - # ask again - dialogs.PassphraseDialog(_('Wrong Passphrase'), - _('Please retype your GPG passphrase or press Cancel.'), - ok_handler=(_ok, count + 1), cancel_handler=_cancel) - else: - # user failed 3 times, continue without GPG - self.complete(None) - - self.dialog = dialogs.PassphraseDialog(title, second, ok_handler=(_ok, 1), - cancel_handler=_cancel) - self.dialog_created = True - - -class ThreadInterface: - def __init__(self, func, func_args, callback, callback_args): - '''Call a function in a thread - - :param func: the function to call in the thread - :param func_args: list or arguments for this function - :param callback: callback to call once function is finished - :param callback_args: list of arguments for this callback - ''' - def thread_function(func, func_args, callback, callback_args): - output = func(*func_args) - gobject.idle_add(callback, output, *callback_args) - Thread(target=thread_function, args=(func, func_args, callback, - callback_args)).start() - -class Interface: - -################################################################################ -### Methods handling events from connection -################################################################################ - - def handle_event_roster(self, account, data): - #('ROSTER', account, array) - # FIXME: Those methods depend to highly on each other - # and the order in which they are called - self.roster.fill_contacts_and_groups_dicts(data, account) - self.roster.add_account_contacts(account) - self.roster.fire_up_unread_messages_events(account) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('Roster', (account, data)) - - def handle_event_warning(self, unused, data): - #('WARNING', account, (title_text, section_text)) - dialogs.WarningDialog(data[0], data[1]) - - def handle_event_error(self, unused, data): - #('ERROR', account, (title_text, section_text)) - dialogs.ErrorDialog(data[0], data[1]) - - def handle_event_information(self, unused, data): - #('INFORMATION', account, (title_text, section_text)) - dialogs.InformationDialog(data[0], data[1]) - - def handle_event_ask_new_nick(self, account, data): - #('ASK_NEW_NICK', account, (room_jid,)) - room_jid = data[0] - title = _('Unable to join group chat') - prompt = _('Your desired nickname in group chat %s is in use or ' - 'registered by another occupant.\nPlease specify another nickname ' - 'below:') % room_jid - check_text = _('Always use this nickname when there is a conflict') - if 'change_nick_dialog' in self.instances: - self.instances['change_nick_dialog'].add_room(account, room_jid, - prompt) - else: - self.instances['change_nick_dialog'] = dialogs.ChangeNickDialog( - account, room_jid, title, prompt) - - def handle_event_http_auth(self, account, data): - #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg)) - def response(account, iq_obj, answer): - self.dialog.destroy() - gajim.connections[account].build_http_auth_answer(iq_obj, answer) - - def on_yes(is_checked, account, iq_obj): - response(account, iq_obj, 'yes') - - sec_msg = _('Do you accept this request?') - if gajim.get_number_of_connected_accounts() > 1: - sec_msg = _('Do you accept this request on account %s?') % account - if data[4]: - sec_msg = data[4] + '\n' + sec_msg - self.dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for ' - '%(url)s (id: %(id)s)') % {'method': data[0], 'url': data[1], - 'id': data[2]}, sec_msg, on_response_yes=(on_yes, account, data[3]), - on_response_no=(response, account, data[3], 'no')) - - def handle_event_error_answer(self, account, array): - #('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode)) - id_, jid_from, errmsg, errcode = array - if unicode(errcode) in ('400', '403', '406') and id_: - # show the error dialog - ft = self.instances['file_transfers'] - sid = id_ - if len(id_) > 3 and id_[2] == '_': - sid = id_[3:] - if sid in ft.files_props['s']: - file_props = ft.files_props['s'][sid] - if unicode(errcode) == '400': - file_props['error'] = -3 - else: - file_props['error'] = -4 - self.handle_event_file_request_error(account, - (jid_from, file_props, errmsg)) - conn = gajim.connections[account] - conn.disconnect_transfer(file_props) - return - elif unicode(errcode) == '404': - conn = gajim.connections[account] - sid = id_ - if len(id_) > 3 and id_[2] == '_': - sid = id_[3:] - if sid in conn.files_props: - file_props = conn.files_props[sid] - self.handle_event_file_send_error(account, - (jid_from, file_props)) - conn.disconnect_transfer(file_props) - return - - ctrl = self.msg_win_mgr.get_control(jid_from, account) - if ctrl and ctrl.type_id == message_control.TYPE_GC: - ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) - - def handle_event_con_type(self, account, con_type): - # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'plain' - gajim.con_types[account] = con_type - self.roster.draw_account(account) - - def handle_event_connection_lost(self, account, array): - # ('CONNECTION_LOST', account, [title, text]) - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'connection_lost.png') - path = gtkgui_helpers.get_path_to_generic_or_avatar(path) - notify.popup(_('Connection Failed'), account, account, - 'connection_failed', path, array[0], array[1]) - - def unblock_signed_in_notifications(self, account): - gajim.block_signed_in_notifications[account] = False - - def handle_event_status(self, account, show): # OUR status - #('STATUS', account, show) - model = self.roster.status_combobox.get_model() - if show in ('offline', 'error'): - for name in self.instances[account]['online_dialog'].keys(): - # .keys() is needed to not have a dictionary length changed during - # iteration error - self.instances[account]['online_dialog'][name].destroy() - del self.instances[account]['online_dialog'][name] - for request in self.gpg_passphrase.values(): - if request: - request.interrupt() - # .keys() is needed because dict changes during loop - for account in self.pass_dialog.keys(): - self.pass_dialog[account].window.destroy() - if show == 'offline': - # sensitivity for this menuitem - if gajim.get_number_of_connected_accounts() == 0: - model[self.roster.status_message_menuitem_iter][3] = False - gajim.block_signed_in_notifications[account] = True - else: - # 30 seconds after we change our status to sth else than offline - # we stop blocking notifications of any kind - # this prevents from getting the roster items as 'just signed in' - # contacts. 30 seconds should be enough time - gobject.timeout_add_seconds(30, self.unblock_signed_in_notifications, account) - # sensitivity for this menuitem - model[self.roster.status_message_menuitem_iter][3] = True - - # Inform all controls for this account of the connection state change - ctrls = self.msg_win_mgr.get_controls() - if account in self.minimized_controls: - # Can not be the case when we remove account - ctrls += self.minimized_controls[account].values() - for ctrl in ctrls: - if ctrl.account == account: - if show == 'offline' or (show == 'invisible' and \ - gajim.connections[account].is_zeroconf): - ctrl.got_disconnected() - else: - # Other code rejoins all GCs, so we don't do it here - if not ctrl.type_id == message_control.TYPE_GC: - ctrl.got_connected() - if ctrl.parent_win: - ctrl.parent_win.redraw_tab(ctrl) - - self.roster.on_status_changed(account, show) - if account in self.show_vcard_when_connect and show not in ('offline', - 'error'): - self.edit_own_details(account) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('AccountPresence', (show, account)) - - def edit_own_details(self, account): - jid = gajim.get_jid_from_account(account) - if 'profile' not in self.instances[account]: - self.instances[account]['profile'] = \ - profile_window.ProfileWindow(account) - gajim.connections[account].request_vcard(jid) - - def handle_event_notify(self, account, array): - # 'NOTIFY' (account, (jid, status, status message, resource, - # priority, # keyID, timestamp, contact_nickname)) - # - # Contact changed show - - # FIXME: Drop and rewrite... - - statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd', - 'invisible'] - # Ignore invalid show - if array[1] not in statuss: - return - old_show = 0 - new_show = statuss.index(array[1]) - status_message = array[2] - jid = array[0].split('/')[0] - keyID = array[5] - contact_nickname = array[7] - - # Get the proper keyID - keyID = helpers.prepare_and_validate_gpg_keyID(account, jid, keyID) - - resource = array[3] - if not resource: - resource = '' - priority = array[4] - if gajim.jid_is_transport(jid): - # It must be an agent - ji = jid.replace('@', '') - else: - ji = jid - - highest = gajim.contacts. \ - get_contact_with_highest_priority(account, jid) - was_highest = (highest and highest.resource == resource) - - conn = gajim.connections[account] - - # Update contact - jid_list = gajim.contacts.get_jid_list(account) - if ji in jid_list or jid == gajim.get_jid_from_account(account): - lcontact = gajim.contacts.get_contacts(account, ji) - contact1 = None - resources = [] - for c in lcontact: - resources.append(c.resource) - if c.resource == resource: - contact1 = c - break - - if contact1: - if contact1.show in statuss: - old_show = statuss.index(contact1.show) - # nick changed - if contact_nickname is not None and \ - contact1.contact_name != contact_nickname: - contact1.contact_name = contact_nickname - self.roster.draw_contact(jid, account) - - if old_show == new_show and contact1.status == status_message and \ - contact1.priority == priority: # no change - return - else: - contact1 = gajim.contacts.get_first_contact_from_jid(account, ji) - if not contact1: - # Presence of another resource of our - # jid - # Create self contact and add to roster - if resource == conn.server_resource: - return - # Ignore offline presence of unknown self resource - if new_show < 2: - return - contact1 = gajim.contacts.create_contact(jid=ji, - name=gajim.nicks[account], groups=['self_contact'], - show=array[1], status=status_message, sub='both', ask='none', - priority=priority, keyID=keyID, resource=resource, - mood=conn.mood, tune=conn.tune, activity=conn.activity) - old_show = 0 - gajim.contacts.add_contact(account, contact1) - lcontact.append(contact1) - 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: - # Another resource of an existing contact connected - old_show = 0 - contact1 = gajim.contacts.copy_contact(contact1) - lcontact.append(contact1) - contact1.resource = resource - - self.roster.add_contact(contact1.jid, account) - - 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) - if contact1.jid in gajim.to_be_removed[account]: - gajim.to_be_removed[account].remove(contact1.jid) - gobject.timeout_add_seconds(5, self.roster.remove_newly_added, - contact1.jid, account) - elif old_show > 1 and new_show == 0 and conn.connected > 1: - if not contact1.jid in gajim.to_be_removed[account]: - gajim.to_be_removed[account].append(contact1.jid) - if contact1.jid in gajim.newly_added[account]: - gajim.newly_added[account].remove(contact1.jid) - self.roster.draw_contact(contact1.jid, account) - gobject.timeout_add_seconds(5, self.roster.remove_to_be_removed, - contact1.jid, account) - - # unset custom status - if (old_show == 0 and new_show > 1) or (old_show > 1 and new_show == 0\ - and conn.connected > 1): - if account in self.status_sent_to_users and \ - jid in self.status_sent_to_users[account]: - del self.status_sent_to_users[account][jid] - - contact1.show = array[1] - contact1.status = status_message - contact1.priority = priority - contact1.keyID = keyID - timestamp = array[6] - if timestamp: - contact1.last_status_time = timestamp - elif not gajim.block_signed_in_notifications[account]: - # We're connected since more that 30 seconds - contact1.last_status_time = time.localtime() - contact1.contact_nickname = contact_nickname - - if gajim.jid_is_transport(jid): - # It must be an agent - if ji in jid_list: - # Update existing iter and group counting - self.roster.draw_contact(ji, account) - self.roster.draw_group(_('Transports'), account) - if new_show > 1 and ji in gajim.transport_avatar[account]: - # transport just signed in. - # request avatars - for jid_ in gajim.transport_avatar[account][ji]: - conn.request_vcard(jid_) - # transport just signed in/out, don't show - # popup notifications for 30s - account_ji = account + '/' + ji - gajim.block_signed_in_notifications[account_ji] = True - gobject.timeout_add_seconds(30, - self.unblock_signed_in_notifications, account_ji) - locations = (self.instances, self.instances[account]) - for location in locations: - if 'add_contact' in location: - if old_show == 0 and new_show > 1: - location['add_contact'].transport_signed_in(jid) - break - elif old_show > 1 and new_show == 0: - location['add_contact'].transport_signed_out(jid) - break - elif ji in jid_list: - # It isn't an agent - # reset chatstate if needed: - # (when contact signs out or has errors) - if array[1] in ('offline', 'error'): - contact1.our_chatstate = contact1.chatstate = \ - contact1.composing_xep = None - - # TODO: This causes problems when another - # resource signs off! - conn.remove_transfers_for_contact(contact1) - - # disable encryption, since if any messages are - # lost they'll be not decryptable (note that - # this contradicts XEP-0201 - trying to get that - # in the XEP, though) - - # there won't be any sessions here if the contact terminated - # their sessions before going offline (which we do) - for sess in conn.get_sessions(ji): - if (ji+'/'+resource) != str(sess.jid): - continue - if sess.control: - sess.control.no_autonegotiation = False - if sess.enable_encryption: - sess.terminate_e2e() - conn.delete_session(jid, sess.thread_id) - - self.roster.chg_contact_status(contact1, array[1], status_message, - account) - # Notifications - if old_show < 2 and new_show > 1: - notify.notify('contact_connected', jid, account, status_message) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('ContactPresence', (account, - array)) - - elif old_show > 1 and new_show < 2: - notify.notify('contact_disconnected', jid, account, status_message) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('ContactAbsence', (account, array)) - # FIXME: stop non active file transfers - # Status change (not connected/disconnected or - # error (<1)) - elif new_show > 1: - notify.notify('status_change', jid, account, [new_show, - status_message]) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('ContactStatus', (account, array)) - else: - # FIXME: MSN transport (CMSN1.2.1 and PyMSN) don't - # follow the XEP, still the case in 2008. - # It's maybe a GC_NOTIFY (specialy for MSN gc) - self.handle_event_gc_notify(account, (jid, array[1], status_message, - array[3], None, None, None, None, None, [], None, None)) - - highest = gajim.contacts.get_contact_with_highest_priority(account, jid) - is_highest = (highest and highest.resource == resource) - - # disconnect the session from the ctrl if the highest resource has changed - if (was_highest and not is_highest) or (not was_highest and is_highest): - ctrl = self.msg_win_mgr.get_control(jid, account) - - if ctrl: - ctrl.no_autonegotiation = False - ctrl.set_session(None) - ctrl.contact = highest - - def handle_event_msgerror(self, account, array): - #'MSGERROR' (account, (jid, error_code, error_msg, msg, time[, session])) - full_jid_with_resource = array[0] - jids = full_jid_with_resource.split('/', 1) - jid = jids[0] - - if array[1] == '503': - # If we get server-not-found error, stop sending chatstates - for contact in gajim.contacts.get_contacts(account, jid): - contact.composing_xep = False - - session = None - if len(array) > 5: - session = array[5] - - gc_control = self.msg_win_mgr.get_gc_control(jid, account) - if not gc_control and \ - jid in self.minimized_controls[account]: - gc_control = self.minimized_controls[account][jid] - 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 session: - ctrl = session.control - else: - ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) - - if not ctrl: - 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) - ctrl = self.new_private_chat(gc_c, account, session) - - ctrl.print_conversation(_('Error %(code)s: %(msg)s') % { - 'code': array[1], 'msg': array[2]}, 'status') - return - - gc_control.print_conversation(_('Error %(code)s: %(msg)s') % { - 'code': array[1], 'msg': array[2]}, 'status') - if gc_control.parent_win and 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] - if array[3]: - msg = _('error while sending %(message)s ( %(error)s )') % { - 'message': array[3], 'error': msg} - if session: - session.roster_message(jid, msg, array[4], msg_type='error') - - def handle_event_msgsent(self, account, array): - #('MSGSENT', account, (jid, msg, keyID)) - msg = array[1] - # do not play sound when standalone chatstate message (eg no msg) - if msg and gajim.config.get_per('soundevents', 'message_sent', 'enabled'): - helpers.play_sound('message_sent') - - def handle_event_msgnotsent(self, account, array): - #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session)) - msg = _('error while sending %(message)s ( %(error)s )') % { - 'message': array[2], 'error': array[1]} - if not array[4]: - # No session. This can happen when sending a message from gajim-remote - log.warn(msg) - return - array[4].roster_message(array[0], msg, array[3], account, - msg_type='error') - - def handle_event_subscribe(self, account, array): - #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172 - if self.remote_ctrl: - self.remote_ctrl.raise_signal('Subscribe', (account, array)) - - jid = array[0] - text = array[1] - nick = array[2] - if helpers.allow_popup_window(account) or not self.systray_enabled: - dialogs.SubscriptionRequestWindow(jid, text, account, nick) - return - - self.add_event(account, jid, 'subscription_request', (text, nick)) - - if helpers.allow_showing_notification(account): - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'subscription_request.png') - path = gtkgui_helpers.get_path_to_generic_or_avatar(path) - event_type = _('Subscription request') - notify.popup(event_type, jid, account, 'subscription_request', path, - event_type, jid) - - def handle_event_subscribed(self, account, array): - #('SUBSCRIBED', account, (jid, resource)) - jid = array[0] - if jid in gajim.contacts.get_jid_list(account): - c = gajim.contacts.get_first_contact_from_jid(account, jid) - c.resource = array[1] - self.roster.remove_contact_from_groups(c.jid, account, - [_('Not in Roster'), _('Observers')], update=False) - else: - keyID = '' - attached_keys = gajim.config.get_per('accounts', account, - 'attached_gpg_keys').split() - if jid in attached_keys: - keyID = attached_keys[attached_keys.index(jid) + 1] - name = jid.split('@', 1)[0] - name = name.split('%', 1)[0] - contact1 = gajim.contacts.create_contact(jid=jid, name=name, - groups=[], show='online', status='online', - ask='to', resource=array[1], keyID=keyID) - gajim.contacts.add_contact(account, contact1) - self.roster.add_contact(jid, account) - dialogs.InformationDialog(_('Authorization accepted'), - _('The contact "%s" has authorized you to see his or her status.') - % jid) - if not gajim.config.get_per('accounts', account, 'dont_ack_subscription'): - gajim.connections[account].ack_subscribed(jid) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('Subscribed', (account, array)) - - def show_unsubscribed_dialog(self, account, contact): - def on_yes(is_checked, list_): - self.roster.on_req_usub(None, list_) - list_ = [(contact, account)] - dialogs.YesNoDialog( - _('Contact "%s" removed subscription from you') % contact.jid, - _('You will always see him or her as offline.\nDo you want to ' - 'remove him or her from your contact list?'), - on_response_yes=(on_yes, list_)) - # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does - # not show deny - - def handle_event_unsubscribed(self, account, jid): - #('UNSUBSCRIBED', account, jid) - gajim.connections[account].ack_unsubscribed(jid) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('Unsubscribed', (account, jid)) - - contact = gajim.contacts.get_first_contact_from_jid(account, jid) - if not contact: - return - - if helpers.allow_popup_window(account) or not self.systray_enabled: - self.show_unsubscribed_dialog(account, contact) - - self.add_event(account, jid, 'unsubscribed', contact) - - if helpers.allow_showing_notification(account): - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'unsubscribed.png') - path = gtkgui_helpers.get_path_to_generic_or_avatar(path) - event_type = _('Unsubscribed') - notify.popup(event_type, jid, account, 'unsubscribed', path, - event_type, jid) - - def handle_event_agent_info_error(self, account, agent): - #('AGENT_ERROR_INFO', account, (agent)) - try: - gajim.connections[account].services_cache.agent_info_error(agent) - except AttributeError: - return - - def handle_event_agent_items_error(self, account, agent): - #('AGENT_ERROR_INFO', account, (agent)) - try: - gajim.connections[account].services_cache.agent_items_error(agent) - except AttributeError: - return - - def handle_event_agent_removed(self, account, agent): - # remove transport's contacts from treeview - jid_list = gajim.contacts.get_jid_list(account) - for jid in jid_list: - if jid.endswith('@' + agent): - c = gajim.contacts.get_first_contact_from_jid(account, jid) - gajim.log.debug( - 'Removing contact %s due to unregistered transport %s'\ - % (jid, agent)) - gajim.connections[account].unsubscribe(c.jid) - # Transport contacts can't have 2 resources - if c.jid in gajim.to_be_removed[account]: - # This way we'll really remove it - gajim.to_be_removed[account].remove(c.jid) - self.roster.remove_contact(c.jid, account, backend=True) - - def handle_event_register_agent_info(self, account, array): - # ('REGISTER_AGENT_INFO', account, (agent, infos, is_form)) - # info in a dataform if is_form is True - if array[2] or 'instructions' in array[1]: - 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.')) - - def handle_event_agent_info_items(self, account, array): - #('AGENT_INFO_ITEMS', account, (agent, node, items)) - our_jid = gajim.get_jid_from_account(account) - if 'pep_services' in gajim.interface.instances[account] and \ - array[0] == our_jid: - gajim.interface.instances[account]['pep_services'].items_received( - array[2]) - try: - gajim.connections[account].services_cache.agent_items(array[0], - array[1], array[2]) - except AttributeError: - return - - def handle_event_agent_info_info(self, account, array): - #('AGENT_INFO_INFO', account, (agent, node, identities, features, data)) - try: - gajim.connections[account].services_cache.agent_info(array[0], - array[1], array[2], array[3], array[4]) - except AttributeError: - return - - def handle_event_new_acc_connected(self, account, array): - #('NEW_ACC_CONNECTED', account, (infos, is_form, ssl_msg, ssl_err, - # ssl_cert, ssl_fingerprint)) - if 'account_creation_wizard' in self.instances: - self.instances['account_creation_wizard'].new_acc_connected(array[0], - array[1], array[2], array[3], array[4], array[5]) - - def handle_event_new_acc_not_connected(self, account, array): - #('NEW_ACC_NOT_CONNECTED', account, (reason)) - if 'account_creation_wizard' in self.instances: - self.instances['account_creation_wizard'].new_acc_not_connected(array) - - def handle_event_acc_ok(self, account, array): - #('ACC_OK', account, (config)) - if 'account_creation_wizard' in self.instances: - self.instances['account_creation_wizard'].acc_is_ok(array) - - if self.remote_ctrl: - self.remote_ctrl.raise_signal('NewAccount', (account, array)) - - def handle_event_acc_not_ok(self, account, array): - #('ACC_NOT_OK', account, (reason)) - if 'account_creation_wizard' in self.instances: - self.instances['account_creation_wizard'].acc_is_not_ok(array) - - def handle_event_quit(self, p1, p2): - self.roster.quit_gtkgui_interface() - - def handle_event_myvcard(self, account, array): - nick = '' - if 'NICKNAME' in array and array['NICKNAME']: - gajim.nicks[account] = array['NICKNAME'] - elif 'FN' in array and array['FN']: - gajim.nicks[account] = array['FN'] - if 'profile' in self.instances[account]: - win = self.instances[account]['profile'] - win.set_values(array) - if account in self.show_vcard_when_connect: - self.show_vcard_when_connect.remove(account) - jid = array['jid'] - if jid in self.instances[account]['infos']: - self.instances[account]['infos'][jid].set_values(array) - - def handle_event_vcard(self, account, vcard): - # ('VCARD', account, data) - '''vcard holds the vcard data''' - jid = vcard['jid'] - resource = vcard.get('resource', '') - fjid = jid + '/' + str(resource) - - # vcard window - win = None - if jid in self.instances[account]['infos']: - win = self.instances[account]['infos'][jid] - elif resource and fjid in self.instances[account]['infos']: - win = self.instances[account]['infos'][fjid] - if win: - win.set_values(vcard) - - # show avatar in chat - ctrl = None - if resource and self.msg_win_mgr.has_window(fjid, account): - win = self.msg_win_mgr.get_window(fjid, account) - ctrl = win.get_control(fjid, account) - elif self.msg_win_mgr.has_window(jid, account): - win = self.msg_win_mgr.get_window(jid, account) - ctrl = win.get_control(jid, account) - - if ctrl and ctrl.type_id != message_control.TYPE_GC: - ctrl.show_avatar() - - # Show avatar in roster or gc_roster - gc_ctrl = self.msg_win_mgr.get_gc_control(jid, account) - if not gc_ctrl and \ - jid in self.minimized_controls[account]: - gc_ctrl = self.minimized_controls[account][jid] - if gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC: - gc_ctrl.draw_avatar(resource) - else: - self.roster.draw_avatar(jid, account) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('VcardInfo', (account, vcard)) - - def handle_event_last_status_time(self, account, array): - # ('LAST_STATUS_TIME', account, (jid, resource, seconds, status)) - tim = array[2] - if tim < 0: - # Ann error occured - return - win = None - if array[0] in self.instances[account]['infos']: - win = self.instances[account]['infos'][array[0]] - elif array[0] + '/' + array[1] in self.instances[account]['infos']: - win = self.instances[account]['infos'][array[0] + '/' + array[1]] - c = gajim.contacts.get_contact(account, array[0], array[1]) - if c: # c can be none if it's a gc contact - c.last_status_time = time.localtime(time.time() - tim) - if array[3]: - c.status = array[3] - self.roster.draw_contact(c.jid, account) # draw offline status - if win: - win.set_last_status_time() - if self.remote_ctrl: - 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 array[0] in self.instances[account]['infos']: - win = self.instances[account]['infos'][array[0]] - elif array[0] + '/' + array[1] in self.instances[account]['infos']: - win = self.instances[account]['infos'][array[0] + '/' + array[1]] - if win: - win.set_os_info(array[1], array[2], array[3]) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('OsInfo', (account, array)) - - def handle_event_entity_time(self, account, array): - #'ENTITY_TIME' (account, (jid, resource, time_info)) - win = None - if array[0] in self.instances[account]['infos']: - win = self.instances[account]['infos'][array[0]] - elif array[0] + '/' + array[1] in self.instances[account]['infos']: - win = self.instances[account]['infos'][array[0] + '/' + array[1]] - if win: - win.set_entity_time(array[1], array[2]) - if self.remote_ctrl: - self.remote_ctrl.raise_signal('EntityTime', (account, array)) - - def handle_event_gc_notify(self, account, array): - #'GC_NOTIFY' (account, (room_jid, show, status, nick, - # role, affiliation, jid, reason, actor, statusCode, newNick, avatar_sha)) - nick = array[3] - if not nick: - return - room_jid = array[0] - fjid = room_jid + '/' + nick - show = array[1] - status = array[2] - conn = gajim.connections[account] - - # Get the window and control for the updated status, this may be a - # PrivateChatControl - control = self.msg_win_mgr.get_gc_control(room_jid, account) - - if not control and \ - room_jid in self.minimized_controls[account]: - control = self.minimized_controls[account][room_jid] - - if not control or (control and control.type_id != message_control.TYPE_GC): - return - - control.chg_contact_status(nick, show, status, array[4], array[5], - array[6], array[7], array[8], array[9], array[10], array[11]) - - contact = gajim.contacts.\ - get_contact_with_highest_priority(account, room_jid) - if contact: - self.roster.draw_contact(room_jid, account) - - # print status in chat window and update status/GPG image - ctrl = self.msg_win_mgr.get_control(fjid, account) - if ctrl: - statusCode = array[9] - if '303' in statusCode: - new_nick = array[10] - ctrl.print_conversation(_('%(nick)s is now known as %(new_nick)s') \ - % {'nick': nick, 'new_nick': new_nick}, 'status') - gc_c = gajim.contacts.get_gc_contact(account, room_jid, new_nick) - c = gajim.contacts.contact_from_gc_contact(gc_c) - ctrl.gc_contact = gc_c - ctrl.contact = c - if ctrl.session: - # stop e2e - if ctrl.session.enable_encryption: - thread_id = ctrl.session.thread_id - ctrl.session.terminate_e2e() - conn.delete_session(fjid, thread_id) - ctrl.no_autonegotiation = False - ctrl.draw_banner() - old_jid = room_jid + '/' + nick - new_jid = room_jid + '/' + new_nick - self.msg_win_mgr.change_key(old_jid, new_jid, account) - else: - contact = ctrl.contact - contact.show = show - contact.status = status - gc_contact = ctrl.gc_contact - gc_contact.show = show - gc_contact.status = status - uf_show = helpers.get_uf_show(show) - ctrl.print_conversation(_('%(nick)s is now %(status)s') % { - 'nick': nick, 'status': uf_show}, 'status') - if status: - ctrl.print_conversation(' (', 'status', simple=True) - ctrl.print_conversation('%s' % (status), 'status', simple=True) - ctrl.print_conversation(')', 'status', simple=True) - ctrl.parent_win.redraw_tab(ctrl) - ctrl.update_ui() - 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, has_timestamp, htmlmsg, - # [status_codes])) - jids = array[0].split('/', 1) - room_jid = jids[0] - - msg = array[1] - - gc_control = self.msg_win_mgr.get_gc_control(room_jid, account) - if not gc_control and \ - room_jid in self.minimized_controls[account]: - gc_control = self.minimized_controls[account][room_jid] - - 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, msg, array[2], array[3], xhtml, array[5]) - - if self.remote_ctrl: - highlight = gc_control.needs_visual_notification(msg) - array += (highlight,) - self.remote_ctrl.raise_signal('GCMessage', (account, array)) - - def handle_event_gc_subject(self, account, array): - #('GC_SUBJECT', account, (jid, subject, body, has_timestamp)) - jids = array[0].split('/', 1) - jid = jids[0] - - gc_control = self.msg_win_mgr.get_gc_control(jid, account) - - if not gc_control and \ - jid in self.minimized_controls[account]: - gc_control = self.minimized_controls[account][jid] - - contact = gajim.contacts.\ - get_contact_with_highest_priority(account, jid) - if contact: - contact.status = array[1] - self.roster.draw_contact(jid, account) - - if not gc_control: - return - gc_control.set_subject(array[1]) - # Standard way, the message comes from the occupant who set the subject - text = None - if len(jids) > 1: - text = _('%(jid)s has set the subject to %(subject)s') % { - 'jid': jids[1], 'subject': 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, form)) config is a dict - room_jid = array[0].split('/')[0] - if room_jid in gajim.automatic_rooms[account]: - if 'continue_tag' in gajim.automatic_rooms[account][room_jid]: - # We're converting chat to muc. allow participants to invite - form = dataforms.ExtendForm(node = array[1]) - for f in form.iter_fields(): - if f.var == 'muc#roomconfig_allowinvites': - f.value = True - elif f.var == 'muc#roomconfig_publicroom': - f.value = False - elif f.var == 'muc#roomconfig_membersonly': - f.value = True - elif f.var == 'public_list': - f.value = False - gajim.connections[account].send_gc_config(room_jid, form) - else: - # use default configuration - gajim.connections[account].send_gc_config(room_jid, array[1]) - # invite contacts - # check if it is necessary to add - continue_tag = False - if 'continue_tag' in gajim.automatic_rooms[account][room_jid]: - continue_tag = True - if 'invities' in gajim.automatic_rooms[account][room_jid]: - for jid in gajim.automatic_rooms[account][room_jid]['invities']: - gajim.connections[account].send_invite(room_jid, jid, - continue_tag=continue_tag) - del gajim.automatic_rooms[account][room_jid] - elif room_jid not in self.instances[account]['gc_config']: - self.instances[account]['gc_config'][room_jid] = \ - config.GroupchatConfigWindow(account, room_jid, array[1]) - - def handle_event_gc_config_change(self, account, array): - #('GC_CONFIG_CHANGE', account, (jid, statusCode)) statuscode is a list - # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify - # http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes-init - jid = array[0] - statusCode = array[1] - - gc_control = self.msg_win_mgr.get_gc_control(jid, account) - if not gc_control and \ - jid in self.minimized_controls[account]: - gc_control = self.minimized_controls[account][jid] - if not gc_control: - return - - changes = [] - if '100' in statusCode: - # Can be a presence (see chg_contact_status in groupchat_control.py) - changes.append(_('Any occupant is allowed to see your full JID')) - gc_control.is_anonymous = False - if '102' in statusCode: - changes.append(_('Room now shows unavailable member')) - if '103' in statusCode: - changes.append(_('room now does not show unavailable members')) - if '104' in statusCode: - changes.append( - _('A non-privacy-related room configuration change has occurred')) - if '170' in statusCode: - # Can be a presence (see chg_contact_status in groupchat_control.py) - changes.append(_('Room logging is now enabled')) - if '171' in statusCode: - changes.append(_('Room logging is now disabled')) - if '172' in statusCode: - changes.append(_('Room is now non-anonymous')) - gc_control.is_anonymous = False - if '173' in statusCode: - changes.append(_('Room is now semi-anonymous')) - gc_control.is_anonymous = True - if '174' in statusCode: - changes.append(_('Room is now fully-anonymous')) - gc_control.is_anonymous = True - - for change in changes: - gc_control.print_conversation(change) - - def handle_event_gc_affiliation(self, account, array): - #('GC_AFFILIATION', account, (room_jid, users_dict)) - room_jid = array[0] - if room_jid in self.instances[account]['gc_config']: - self.instances[account]['gc_config'][room_jid].\ - affiliation_list_received(array[1]) - - def handle_event_gc_password_required(self, account, array): - #('GC_PASSWORD_REQUIRED', account, (room_jid, nick)) - room_jid = array[0] - nick = array[1] - - def on_ok(text): - gajim.connections[account].join_gc(nick, room_jid, text) - gajim.gc_passwords[room_jid] = text - - def on_cancel(): - # get and destroy window - if room_jid in gajim.interface.minimized_controls[account]: - self.roster.on_disconnect(None, room_jid, account) - else: - win = self.msg_win_mgr.get_window(room_jid, account) - ctrl = self.msg_win_mgr.get_gc_control(room_jid, account) - win.remove_tab(ctrl, 3) - - dlg = dialogs.InputDialog(_('Password Required'), - _('A Password is required to join the room %s. Please type it.') % \ - room_jid, is_modal=False, ok_handler=on_ok, cancel_handler=on_cancel) - dlg.input_entry.set_visibility(False) - - def handle_event_gc_invitation(self, account, array): - #('GC_INVITATION', (room_jid, jid_from, reason, password, is_continued)) - jid = gajim.get_jid_without_resource(array[1]) - room_jid = array[0] - if helpers.allow_popup_window(account) or not self.systray_enabled: - dialogs.InvitationReceivedDialog(account, room_jid, jid, array[3], - array[2], is_continued=array[4]) - return - - self.add_event(account, jid, 'gc-invitation', (room_jid, array[2], - array[3], array[4])) - - 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) - event_type = _('Groupchat Invitation') - notify.popup(event_type, jid, account, 'gc-invitation', path, - event_type, room_jid) - - def forget_gpg_passphrase(self, keyid): - if keyid in self.gpg_passphrase: - del self.gpg_passphrase[keyid] - return False - - def handle_event_bad_passphrase(self, account, array): - #('BAD_PASSPHRASE', account, ()) - use_gpg_agent = gajim.config.get('use_gpg_agent') - sectext = '' - if use_gpg_agent: - sectext = _('You configured Gajim to use GPG agent, but there is no ' - 'GPG agent running or it returned a wrong passphrase.\n') - sectext += _('You are currently connected without your OpenPGP key.') - dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext) - else: - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'warning.png') - notify.popup('warning', account, account, 'warning', path, - _('OpenGPG Passphrase Incorrect'), - _('You are currently connected without your OpenPGP key.')) - keyID = gajim.config.get_per('accounts', account, 'keyid') - self.forget_gpg_passphrase(keyID) - - def handle_event_gpg_password_required(self, account, array): - #('GPG_PASSWORD_REQUIRED', account, (callback,)) - callback = array[0] - keyid = gajim.config.get_per('accounts', account, 'keyid') - if keyid in self.gpg_passphrase: - request = self.gpg_passphrase[keyid] - else: - request = PassphraseRequest(keyid) - self.gpg_passphrase[keyid] = request - request.add_callback(account, callback) - - def handle_event_gpg_always_trust(self, account, callback): - #('GPG_ALWAYS_TRUST', account, callback) - def on_yes(checked): - if checked: - gajim.connections[account].gpg.always_trust = True - callback(True) - - def on_no(): - callback(False) - - dialogs.YesNoDialog(_('GPG key not trusted'), _('The GPG key used to ' - 'encrypt this chat is not trusted. Do you really want to encrypt this ' - 'message?'), checktext=_('Do _not ask me again'), - on_response_yes=on_yes, on_response_no=on_no) - - def handle_event_password_required(self, account, array): - #('PASSWORD_REQUIRED', account, None) - if account in self.pass_dialog: - return - text = _('Enter your password for account %s') % account - if passwords.USER_HAS_GNOMEKEYRING and \ - not passwords.USER_USES_GNOMEKEYRING: - text += '\n' + _('Gnome Keyring is installed but not \ - correctly started (environment variable probably not \ - correctly set)') - - def on_ok(passphrase, save): - if save: - gajim.config.set_per('accounts', account, 'savepass', True) - passwords.save_password(account, passphrase) - gajim.connections[account].set_password(passphrase) - del self.pass_dialog[account] - - def on_cancel(): - self.roster.set_state(account, 'offline') - self.roster.update_status_combobox() - del self.pass_dialog[account] - - self.pass_dialog[account] = dialogs.PassphraseDialog( - _('Password Required'), text, _('Save password'), ok_handler=on_ok, - cancel_handler=on_cancel) - - def handle_event_roster_info(self, account, array): - #('ROSTER_INFO', account, (jid, name, sub, ask, groups)) - jid = array[0] - name = array[1] - sub = array[2] - ask = array[3] - groups = array[4] - contacts = gajim.contacts.get_contacts(account, jid) - if (not sub or sub == 'none') and (not ask or ask == 'none') and \ - not name and not groups: - # contact removed us. - if contacts: - self.roster.remove_contact(jid, account, backend=True) - return - elif not contacts: - if sub == 'remove': - return - # Add new contact to roster - contact = gajim.contacts.create_contact(jid=jid, name=name, - groups=groups, show='offline', sub=sub, ask=ask) - gajim.contacts.add_contact(account, contact) - self.roster.add_contact(jid, account) - else: - # it is an existing contact that might has changed - re_place = False - # If contact has changed (sub, ask or group) update roster - # Mind about observer status changes: - # According to xep 0162, a contact is not an observer anymore when - # we asked for auth, so also remove him if ask changed - old_groups = contacts[0].groups - if contacts[0].sub != sub or contacts[0].ask != ask\ - or old_groups != groups: - re_place = True - # c.get_shown_groups() has changed. Reflect that in roster_winodow - self.roster.remove_contact(jid, account, force=True) - for contact in contacts: - contact.name = name or '' - contact.sub = sub - contact.ask = ask - contact.groups = groups or [] - if re_place: - self.roster.add_contact(jid, account) - # Refilter and update old groups - for group in old_groups: - self.roster.draw_group(group, account) - else: - self.roster.draw_contact(jid, account) - - if self.remote_ctrl: - self.remote_ctrl.raise_signal('RosterInfo', (account, array)) - - def handle_event_bookmarks(self, account, bms): - # ('BOOKMARKS', account, [{name,jid,autojoin,password,nick}, {}]) - # We received a bookmark item from the server (JEP48) - # Auto join GC windows if neccessary - - self.roster.set_actions_menu_needs_rebuild() - invisible_show = gajim.SHOW_LIST.index('invisible') - # do not autojoin if we are invisible - if gajim.connections[account].connected == invisible_show: - return - - self.auto_join_bookmarks(account) - - def handle_event_file_send_error(self, account, array): - jid = array[0] - file_props = array[1] - ft = self.instances['file_transfers'] - ft.set_status(file_props['type'], file_props['sid'], 'stop') - - if helpers.allow_popup_window(account): - ft.show_send_error(file_props) - return - - self.add_event(account, jid, 'file-send-error', file_props) - - if helpers.allow_showing_notification(account): - img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'ft_error.png') - path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - event_type = _('File Transfer Error') - notify.popup(event_type, jid, account, 'file-send-error', path, - event_type, file_props['name']) - - def handle_event_gmail_notify(self, account, array): - jid = array[0] - gmail_new_messages = int(array[1]) - gmail_messages_list = array[2] - if gajim.config.get('notify_on_new_gmail_email'): - img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'new_email_recv.png') - title = _('New mail on %(gmail_mail_address)s') % \ - {'gmail_mail_address': jid} - 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'): - cnt = 0 - 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 - if cnt >=5: - break - senders = ',\n '.join(reversed(gmessage['From'])) - text += _('\n\nFrom: %(from_address)s\nSubject: %(subject)s\n%(snippet)s') % \ - {'from_address': senders, 'subject': gmessage['Subject'], - 'snippet': gmessage['Snippet']} - cnt += 1 - - if gajim.config.get_per('soundevents', 'gmail_received', 'enabled'): - helpers.play_sound('gmail_received') - path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - notify.popup(_('New E-mail'), jid, account, 'gmail', - path_to_image=path, title=title, - text=text) - - if self.remote_ctrl: - self.remote_ctrl.raise_signal('NewGmail', (account, array)) - - def handle_event_file_request_error(self, account, array): - # ('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, errmsg) - else: - ft.show_request_error(file_props) - return - - if errno in (-4, -5): - msg_type = 'file-error' - else: - msg_type = 'file-request-error' - - self.add_event(account, jid, msg_type, file_props) - - if helpers.allow_showing_notification(account): - # check if we should be notified - img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'ft_error.png') - - path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - event_type = _('File Transfer Error') - notify.popup(event_type, jid, account, msg_type, path, - title = event_type, text = file_props['name']) - - def handle_event_file_request(self, account, array): - jid = array[0] - if jid not in gajim.contacts.get_jid_list(account): - keyID = '' - attached_keys = gajim.config.get_per('accounts', account, - 'attached_gpg_keys').split() - if jid in attached_keys: - keyID = attached_keys[attached_keys.index(jid) + 1] - contact = gajim.contacts.create_contact(jid=jid, name='', - groups=[_('Not in Roster')], show='not in roster', status='', - sub='none', keyID=keyID) - gajim.contacts.add_contact(account, contact) - self.roster.add_contact(contact.jid, account) - file_props = array[1] - contact = gajim.contacts.get_first_contact_from_jid(account, jid) - - if helpers.allow_popup_window(account): - self.instances['file_transfers'].show_file_request(account, contact, - file_props) - return - - self.add_event(account, jid, 'file-request', file_props) - - if helpers.allow_showing_notification(account): - img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'ft_request.png') - txt = _('%s wants to send you a file.') % gajim.get_name_from_jid( - account, jid) - path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - event_type = _('File Transfer Request') - notify.popup(event_type, jid, account, 'file-request', - path_to_image = path, title = event_type, text = txt) - - def handle_event_file_error(self, title, message): - dialogs.ErrorDialog(title, message) - - def handle_event_file_progress(self, account, file_props): - 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'] - if file_props['error'] == 0: - ft.set_progress(file_props['type'], file_props['sid'], - file_props['received-len']) - else: - ft.set_status(file_props['type'], file_props['sid'], 'stop') - if 'stalled' in file_props and file_props['stalled'] or \ - 'paused' in file_props and file_props['paused']: - return - if file_props['type'] == 'r': # we receive a file - jid = unicode(file_props['sender']) - else: # we send a file - jid = unicode(file_props['receiver']) - - if helpers.allow_popup_window(account): - if file_props['error'] == 0: - if gajim.config.get('notify_on_file_complete'): - ft.show_completed(jid, file_props) - elif file_props['error'] == -1: - ft.show_stopped(jid, file_props, - error_msg=_('Remote contact stopped transfer')) - elif file_props['error'] == -6: - ft.show_stopped(jid, file_props, error_msg=_('Error opening file')) - return - - msg_type = '' - event_type = '' - if file_props['error'] == 0 and gajim.config.get( - 'notify_on_file_complete'): - msg_type = 'file-completed' - event_type = _('File Transfer Completed') - elif file_props['error'] in (-1, -6): - msg_type = 'file-stopped' - event_type = _('File Transfer Stopped') - - if event_type == '': - # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs) - # this should never happen but it does. see process_result() in socks5.py - # who calls this func (sth is really wrong unless this func is also registered - # as progress_cb - return - - if msg_type: - self.add_event(account, jid, msg_type, file_props) - - if file_props is not None: - if file_props['type'] == 'r': - # get the name of the sender, as it is in the roster - sender = unicode(file_props['sender']).split('/')[0] - name = gajim.contacts.get_first_contact_from_jid(account, - sender).get_shown_name() - filename = os.path.basename(file_props['file-name']) - if event_type == _('File Transfer Completed'): - txt = _('You successfully received %(filename)s from %(name)s.')\ - % {'filename': filename, 'name': name} - img = 'ft_done.png' - else: # ft stopped - txt = _('File transfer of %(filename)s from %(name)s stopped.')\ - % {'filename': filename, 'name': name} - img = 'ft_stopped.png' - else: - receiver = file_props['receiver'] - if hasattr(receiver, 'jid'): - receiver = receiver.jid - receiver = receiver.split('/')[0] - # get the name of the contact, as it is in the roster - name = gajim.contacts.get_first_contact_from_jid(account, - receiver).get_shown_name() - filename = os.path.basename(file_props['file-name']) - if event_type == _('File Transfer Completed'): - txt = _('You successfully sent %(filename)s to %(name)s.')\ - % {'filename': filename, 'name': name} - img = 'ft_done.png' - else: # ft stopped - txt = _('File transfer of %(filename)s to %(name)s stopped.')\ - % {'filename': filename, 'name': name} - img = 'ft_stopped.png' - img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', img) - path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - else: - txt = '' - - if gajim.config.get('notify_on_file_complete') and \ - (gajim.config.get('autopopupaway') or \ - gajim.connections[account].connected in (2, 3)): - # we want to be notified and we are online/chat or we don't mind - # bugged when away/na/busy - notify.popup(event_type, jid, account, msg_type, path_to_image = path, - title = event_type, text = txt) - - def handle_event_stanza_arrived(self, account, stanza): - if account not in self.instances: - return - if 'xml_console' in self.instances[account]: - self.instances[account]['xml_console'].print_stanza(stanza, 'incoming') - - def handle_event_stanza_sent(self, account, stanza): - if account not in self.instances: - return - if 'xml_console' in self.instances[account]: - self.instances[account]['xml_console'].print_stanza(stanza, 'outgoing') - - def handle_event_vcard_published(self, account, array): - if 'profile' in self.instances[account]: - win = self.instances[account]['profile'] - win.vcard_published() - for gc_control in self.msg_win_mgr.get_controls(message_control.TYPE_GC) + \ - self.minimized_controls[account].values(): - if gc_control.account == account: - show = gajim.SHOW_LIST[gajim.connections[account].connected] - status = gajim.connections[account].status - gajim.connections[account].send_gc_status(gc_control.nick, - gc_control.room_jid, show, status) - - def handle_event_vcard_not_published(self, account, array): - if 'profile' in self.instances[account]: - win = self.instances[account]['profile'] - win.vcard_not_published() - - def ask_offline_status(self, account): - for contact in gajim.contacts.iter_contacts(account): - gajim.connections[account].request_last_status_time(contact.jid, - contact.resource) - - def handle_event_signed_in(self, account, empty): - '''SIGNED_IN event is emitted when we sign in, so handle it''' - # block signed in notifications for 30 seconds - gajim.block_signed_in_notifications[account] = True - self.roster.set_actions_menu_needs_rebuild() - self.roster.draw_account(account) - state = self.sleeper.getState() - connected = gajim.connections[account].connected - if gajim.config.get('ask_offline_status_on_connection'): - # Ask offline status in 1 minute so w'are sure we got all online - # presences - gobject.timeout_add_seconds(60, self.ask_offline_status, account) - if state != common.sleepy.STATE_UNKNOWN and connected in (2, 3): - # we go online or free for chat, so we activate auto status - gajim.sleeper_state[account] = 'online' - elif not ((state == common.sleepy.STATE_AWAY and connected == 4) or \ - (state == common.sleepy.STATE_XA and connected == 5)): - # If we are autoaway/xa and come back after a disconnection, do nothing - # Else disable autoaway - gajim.sleeper_state[account] = 'off' - invisible_show = gajim.SHOW_LIST.index('invisible') - # We cannot join rooms if we are invisible - if gajim.connections[account].connected == invisible_show: - return - # join already open groupchats - for gc_control in self.msg_win_mgr.get_controls(message_control.TYPE_GC) \ - + self.minimized_controls[account].values(): - if account != gc_control.account: - continue - room_jid = gc_control.room_jid - if room_jid in gajim.gc_connected[account] and \ - gajim.gc_connected[account][room_jid]: - continue - nick = gc_control.nick - password = gajim.gc_passwords.get(room_jid, '') - gajim.connections[account].join_gc(nick, room_jid, password) - # send currently played music - if gajim.connections[account].pep_supported and dbus_support.supported \ - and gajim.config.get_per('accounts', account, 'publish_tune'): - self.enable_music_listener() - - def handle_event_metacontacts(self, account, tags_list): - gajim.contacts.define_metacontacts(account, tags_list) - self.roster.redraw_metacontacts(account) - - def handle_atom_entry(self, account, data): - atom_entry, = data - AtomWindow.newAtomEntry(atom_entry) - - def handle_event_failed_decrypt(self, account, data): - jid, tim, session = data - - details = _('Unable to decrypt message from ' - '%s\nIt may have been tampered with.') % jid - - ctrl = session.control - if ctrl: - ctrl.print_conversation_line(details, 'status', '', tim) - else: - dialogs.WarningDialog(_('Unable to decrypt message'), - details) - - # terminate the session - session.terminate_e2e() - session.conn.delete_session(jid, session.thread_id) - - # restart the session - if ctrl: - ctrl.begin_e2e_negotiation() - - def handle_event_privacy_lists_received(self, account, data): - # ('PRIVACY_LISTS_RECEIVED', account, list) - if account not in self.instances: - return - if 'privacy_lists' in self.instances[account]: - self.instances[account]['privacy_lists'].privacy_lists_received(data) - - def handle_event_privacy_list_received(self, account, data): - # ('PRIVACY_LIST_RECEIVED', account, (name, rules)) - if account not in self.instances: - return - name = data[0] - rules = data[1] - if 'privacy_list_%s' % name in self.instances[account]: - self.instances[account]['privacy_list_%s' % name].\ - privacy_list_received(rules) - if name == 'block': - gajim.connections[account].blocked_contacts = [] - gajim.connections[account].blocked_groups = [] - gajim.connections[account].blocked_list = [] - gajim.connections[account].blocked_all = False - for rule in rules: - if not 'type' in rule: - gajim.connections[account].blocked_all = True - elif rule['type'] == 'jid' and rule['action'] == 'deny': - gajim.connections[account].blocked_contacts.append(rule['value']) - elif rule['type'] == 'group' and rule['action'] == 'deny': - gajim.connections[account].blocked_groups.append(rule['value']) - gajim.connections[account].blocked_list.append(rule) - #elif rule['type'] == "group" and action == "deny": - # text_item = _('%s group "%s"') % _(rule['action']), rule['value'] - # self.store.append([text_item]) - # self.global_rules.append(rule) - #else: - # self.global_rules_to_append.append(rule) - if 'blocked_contacts' in self.instances[account]: - self.instances[account]['blocked_contacts'].\ - privacy_list_received(rules) - - def handle_event_privacy_lists_active_default(self, account, data): - if not data: - return - # Send to all privacy_list_* windows as we can't know which one asked - for win in self.instances[account]: - if win.startswith('privacy_list_'): - self.instances[account][win].check_active_default(data) - - def handle_event_privacy_list_removed(self, account, name): - # ('PRIVACY_LISTS_REMOVED', account, name) - if account not in self.instances: - return - if 'privacy_lists' in self.instances[account]: - self.instances[account]['privacy_lists'].privacy_list_removed(name) - - def handle_event_zc_name_conflict(self, account, data): - def on_ok(new_name): - 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, '') - def on_cancel(): - gajim.connections[account].change_status('offline','') - - dlg = dialogs.InputDialog(_('Username Conflict'), - _('Please type a new username for your local account'), input_str=data, - is_modal=True, ok_handler=on_ok, cancel_handler=on_cancel) - - def handle_event_ping_sent(self, account, contact): - if contact.jid == contact.get_full_jid(): - # If contact is a groupchat user - jids = [contact.jid] - else: - jids = [contact.jid, contact.get_full_jid()] - for jid in jids: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - ctrl.print_conversation(_('Ping?'), 'status') - - def handle_event_ping_reply(self, account, data): - contact = data[0] - seconds = data[1] - if contact.jid == contact.get_full_jid(): - # If contact is a groupchat user - jids = [contact.jid] - else: - jids = [contact.jid, contact.get_full_jid()] - for jid in jids: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - ctrl.print_conversation(_('Pong! (%s s.)') % seconds, 'status') - - def handle_event_ping_error(self, account, contact): - if contact.jid == contact.get_full_jid(): - # If contact is a groupchat user - jids = [contact.jid] - else: - jids = [contact.jid, contact.get_full_jid()] - for jid in jids: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - ctrl.print_conversation(_('Error.'), 'status') - - def handle_event_search_form(self, account, data): - # ('SEARCH_FORM', account, (jid, dataform, is_dataform)) - if data[0] not in self.instances[account]['search']: - return - self.instances[account]['search'][data[0]].on_form_arrived(data[1], - data[2]) - - def handle_event_search_result(self, account, data): - # ('SEARCH_RESULT', account, (jid, dataform, is_dataform)) - if data[0] not in self.instances[account]['search']: - return - self.instances[account]['search'][data[0]].on_result_arrived(data[1], - data[2]) - - def handle_event_resource_conflict(self, account, data): - # ('RESOURCE_CONFLICT', account, ()) - # First we go offline, but we don't overwrite status message - self.roster.send_status(account, 'offline', - gajim.connections[account].status) - def on_ok(new_resource): - gajim.config.set_per('accounts', account, 'resource', new_resource) - self.roster.send_status(account, gajim.connections[account].old_show, - gajim.connections[account].status) - proposed_resource = gajim.connections[account].server_resource - proposed_resource += gajim.config.get('gc_proposed_nick_char') - dlg = dialogs.ResourceConflictDialog(_('Resource Conflict'), - _('You are already connected to this account with the same resource. ' - 'Please type a new one'), resource=proposed_resource, ok_handler=on_ok) - - def handle_event_jingle_incoming(self, account, data): - # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, - # data...)) - # TODO: conditional blocking if peer is not in roster - - # unpack data - peerjid, sid, contents = data - content_types = set(c[0] for c in contents) - - # check type of jingle session - if 'audio' in content_types or 'video' in content_types: - # a voip session... - # we now handle only voip, so the only thing we will do here is - # not to return from function - pass - else: - # unknown session type... it should be declined in common/jingle.py - return - - jid = gajim.get_jid_without_resource(peerjid) - resource = gajim.get_resource_from_jid(peerjid) - ctrl = self.msg_win_mgr.get_control(peerjid, account) - if not ctrl: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - if 'audio' in content_types: - ctrl.set_audio_state('connection_received', sid) - if 'video' in content_types: - ctrl.set_video_state('connection_received', sid) - - dlg = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) - if dlg: - dlg.add_contents(content_types) - return - - if helpers.allow_popup_window(account): - dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) - return - - self.add_event(account, peerjid, 'jingle-incoming', (peerjid, sid, - content_types)) - - if helpers.allow_showing_notification(account): - # TODO: we should use another pixmap ;-) - img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', - 'ft_request.png') - txt = _('%s wants to start a voice chat.') % gajim.get_name_from_jid( - account, peerjid) - path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - event_type = _('Voice Chat Request') - notify.popup(event_type, peerjid, account, 'jingle-incoming', - path_to_image = path, title = event_type, text = txt) - - def handle_event_jingle_connected(self, account, data): - # ('JINGLE_CONNECTED', account, (peerjid, sid, media)) - peerjid, sid, media = data - if media in ('audio', 'video'): - jid = gajim.get_jid_without_resource(peerjid) - resource = gajim.get_resource_from_jid(peerjid) - ctrl = self.msg_win_mgr.get_control(peerjid, account) - if not ctrl: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - if media == 'audio': - ctrl.set_audio_state('connected', sid) - else: - ctrl.set_video_state('connected', sid) - - def handle_event_jingle_disconnected(self, account, data): - # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) - peerjid, sid, media, reason = data - jid = gajim.get_jid_without_resource(peerjid) - resource = gajim.get_resource_from_jid(peerjid) - ctrl = self.msg_win_mgr.get_control(peerjid, account) - if not ctrl: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - if media in ('audio', None): - ctrl.set_audio_state('stop', sid=sid, reason=reason) - if media in ('video', None): - ctrl.set_video_state('stop', sid=sid, reason=reason) - dialog = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) - if dialog: - dialog.dialog.destroy() - - def handle_event_jingle_error(self, account, data): - # ('JINGLE_ERROR', account, (peerjid, sid, reason)) - peerjid, sid, reason = data - jid = gajim.get_jid_without_resource(peerjid) - resource = gajim.get_resource_from_jid(peerjid) - ctrl = self.msg_win_mgr.get_control(peerjid, account) - if not ctrl: - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - ctrl.set_audio_state('error', reason=reason) - - def handle_event_pep_config(self, account, data): - # ('PEP_CONFIG', account, (node, form)) - if 'pep_services' in self.instances[account]: - self.instances[account]['pep_services'].config(data[0], data[1]) - - def handle_event_roster_item_exchange(self, account, data): - # data = (action in [add, delete, modify], exchange_list, jid_from) - dialogs.RosterItemExchangeWindow(account, data[0], data[1], data[2]) - - def handle_event_unique_room_id_supported(self, account, data): - '''Receive confirmation that unique_room_id are supported''' - # ('UNIQUE_ROOM_ID_SUPPORTED', server, instance, room_id) - instance = data[1] - instance.unique_room_id_supported(data[0], data[2]) - - def handle_event_unique_room_id_unsupported(self, account, data): - # ('UNIQUE_ROOM_ID_UNSUPPORTED', server, instance) - instance = data[1] - instance.unique_room_id_error(data[0]) - - def handle_event_ssl_error(self, account, data): - # ('SSL_ERROR', account, (text, errnum, cert, sha1_fingerprint)) - server = gajim.config.get_per('accounts', account, 'hostname') - - def on_ok(is_checked): - del self.instances[account]['online_dialog']['ssl_error'] - if is_checked[0]: - # Check if cert is already in file - certs = '' - if os.path.isfile(gajim.MY_CACERTS): - f = open(gajim.MY_CACERTS) - certs = f.read() - f.close() - if data[2] in certs: - dialogs.ErrorDialog(_('Certificate Already in File'), - _('This certificate is already in file %s, so it\'s not added again.') % gajim.MY_CACERTS) - else: - f = open(gajim.MY_CACERTS, 'a') - f.write(server + '\n') - f.write(data[2] + '\n\n') - f.close() - gajim.config.set_per('accounts', account, 'ssl_fingerprint_sha1', - data[3]) - if is_checked[1]: - ignore_ssl_errors = gajim.config.get_per('accounts', account, - 'ignore_ssl_errors').split() - ignore_ssl_errors.append(str(data[1])) - gajim.config.set_per('accounts', account, 'ignore_ssl_errors', - ' '.join(ignore_ssl_errors)) - gajim.connections[account].ssl_certificate_accepted() - - def on_cancel(): - del self.instances[account]['online_dialog']['ssl_error'] - gajim.connections[account].disconnect(on_purpose=True) - self.handle_event_status(account, 'offline') - - pritext = _('Error verifying SSL certificate') - sectext = _('There was an error verifying the SSL certificate of your jabber server: %(error)s\nDo you still want to connect to this server?') % {'error': data[0]} - if data[1] in (18, 27): - checktext1 = _('Add this certificate to the list of trusted certificates.\nSHA1 fingerprint of the certificate:\n%s') % data[3] - else: - checktext1 = '' - checktext2 = _('Ignore this error for this certificate.') - if 'ssl_error' in self.instances[account]['online_dialog']: - self.instances[account]['online_dialog']['ssl_error'].destroy() - self.instances[account]['online_dialog']['ssl_error'] = \ - dialogs.ConfirmationDialogDubbleCheck(pritext, sectext, checktext1, - checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel) - - def handle_event_fingerprint_error(self, account, data): - # ('FINGERPRINT_ERROR', account, (new_fingerprint,)) - def on_yes(is_checked): - del self.instances[account]['online_dialog']['fingerprint_error'] - gajim.config.set_per('accounts', account, 'ssl_fingerprint_sha1', - data[0]) - # Reset the ignored ssl errors - gajim.config.set_per('accounts', account, 'ignore_ssl_errors', '') - gajim.connections[account].ssl_certificate_accepted() - def on_no(): - del self.instances[account]['online_dialog']['fingerprint_error'] - gajim.connections[account].disconnect(on_purpose=True) - self.handle_event_status(account, 'offline') - pritext = _('SSL certificate error') - sectext = _('It seems the SSL certificate of account %(account)s has ' - 'changed or your connection is being hacked.\nOld fingerprint: %(old)s' - '\nNew fingerprint: %(new)s\n\nDo you still want to connect and update' - ' the fingerprint of the certificate?') % {'account': account, - 'old': gajim.config.get_per('accounts', account, - 'ssl_fingerprint_sha1'), 'new': data[0]} - if 'fingerprint_error' in self.instances[account]['online_dialog']: - self.instances[account]['online_dialog']['fingerprint_error'].destroy() - self.instances[account]['online_dialog']['fingerprint_error'] = \ - dialogs.YesNoDialog(pritext, sectext, on_response_yes=on_yes, - on_response_no=on_no) - - def handle_event_plain_connection(self, account, data): - # ('PLAIN_CONNECTION', account, (connection)) - server = gajim.config.get_per('accounts', account, 'hostname') - def on_ok(is_checked): - if not is_checked[0]: - on_cancel() - return - # On cancel call del self.instances, so don't call it another time - # before - del self.instances[account]['online_dialog']['plain_connection'] - if is_checked[1]: - gajim.config.set_per('accounts', account, - 'warn_when_plaintext_connection', False) - gajim.connections[account].connection_accepted(data[0], 'plain') - def on_cancel(): - del self.instances[account]['online_dialog']['plain_connection'] - gajim.connections[account].disconnect(on_purpose=True) - self.handle_event_status(account, 'offline') - pritext = _('Insecure connection') - sectext = _('You are about to send your password on an unencrypted ' - 'connection. Are you sure you want to do that?') - checktext1 = _('Yes, I really want to connect insecurely') - checktext2 = _('Do _not ask me again') - if 'plain_connection' in self.instances[account]['online_dialog']: - self.instances[account]['online_dialog']['plain_connection'].destroy() - self.instances[account]['online_dialog']['plain_connection'] = \ - dialogs.ConfirmationDialogDubbleCheck(pritext, sectext, - checktext1, checktext2, on_response_ok=on_ok, - on_response_cancel=on_cancel, is_modal=False) - - def handle_event_insecure_ssl_connection(self, account, data): - # ('INSECURE_SSL_CONNECTION', account, (connection, connection_type)) - server = gajim.config.get_per('accounts', account, 'hostname') - def on_ok(is_checked): - del self.instances[account]['online_dialog']['insecure_ssl'] - if not is_checked[0]: - on_cancel() - return - if is_checked[1]: - gajim.config.set_per('accounts', account, - 'warn_when_insecure_ssl_connection', False) - if gajim.connections[account].connected == 0: - # We have been disconnecting (too long time since window is opened) - # re-connect with auto-accept - gajim.connections[account].connection_auto_accepted = True - show, msg = gajim.connections[account].continue_connect_info[:2] - self.roster.send_status(account, show, msg) - return - gajim.connections[account].connection_accepted(data[0], data[1]) - def on_cancel(): - del self.instances[account]['online_dialog']['insecure_ssl'] - gajim.connections[account].disconnect(on_purpose=True) - self.handle_event_status(account, 'offline') - pritext = _('Insecure connection') - sectext = _('You are about to send your password on an insecure ' - 'connection. You should install PyOpenSSL to prevent that. Are you sure you want to do that?') - checktext1 = _('Yes, I really want to connect insecurely') - checktext2 = _('Do _not ask me again') - if 'insecure_ssl' in self.instances[account]['online_dialog']: - self.instances[account]['online_dialog']['insecure_ssl'].destroy() - self.instances[account]['online_dialog']['insecure_ssl'] = \ - dialogs.ConfirmationDialogDubbleCheck(pritext, sectext, - checktext1, checktext2, on_response_ok=on_ok, - on_response_cancel=on_cancel, is_modal=False) - - def handle_event_pubsub_node_removed(self, account, data): - # ('PUBSUB_NODE_REMOVED', account, (jid, node)) - if 'pep_services' in self.instances[account]: - if data[0] == gajim.get_jid_from_account(account): - self.instances[account]['pep_services'].node_removed(data[1]) - - def handle_event_pubsub_node_not_removed(self, account, data): - # ('PUBSUB_NODE_NOT_REMOVED', account, (jid, node, msg)) - if data[0] == gajim.get_jid_from_account(account): - dialogs.WarningDialog(_('PEP node was not removed'), - _('PEP node %(node)s was not removed: %(message)s') % { - 'node': data[1], 'message': data[2]}) - - def register_handlers(self): - self.handlers = { - 'ROSTER': self.handle_event_roster, - 'WARNING': self.handle_event_warning, - 'ERROR': self.handle_event_error, - 'INFORMATION': self.handle_event_information, - 'ERROR_ANSWER': self.handle_event_error_answer, - 'STATUS': self.handle_event_status, - 'NOTIFY': self.handle_event_notify, - 'MSGERROR': self.handle_event_msgerror, - 'MSGSENT': self.handle_event_msgsent, - 'MSGNOTSENT': self.handle_event_msgnotsent, - 'SUBSCRIBED': self.handle_event_subscribed, - 'UNSUBSCRIBED': self.handle_event_unsubscribed, - 'SUBSCRIBE': self.handle_event_subscribe, - 'AGENT_ERROR_INFO': self.handle_event_agent_info_error, - 'AGENT_ERROR_ITEMS': self.handle_event_agent_items_error, - 'AGENT_REMOVED': self.handle_event_agent_removed, - 'REGISTER_AGENT_INFO': self.handle_event_register_agent_info, - 'AGENT_INFO_ITEMS': self.handle_event_agent_info_items, - 'AGENT_INFO_INFO': self.handle_event_agent_info_info, - 'QUIT': self.handle_event_quit, - 'NEW_ACC_CONNECTED': self.handle_event_new_acc_connected, - 'NEW_ACC_NOT_CONNECTED': self.handle_event_new_acc_not_connected, - 'ACC_OK': self.handle_event_acc_ok, - 'ACC_NOT_OK': self.handle_event_acc_not_ok, - 'MYVCARD': self.handle_event_myvcard, - 'VCARD': self.handle_event_vcard, - 'LAST_STATUS_TIME': self.handle_event_last_status_time, - 'OS_INFO': self.handle_event_os_info, - 'ENTITY_TIME': self.handle_event_entity_time, - 'GC_NOTIFY': self.handle_event_gc_notify, - 'GC_MSG': self.handle_event_gc_msg, - 'GC_SUBJECT': self.handle_event_gc_subject, - 'GC_CONFIG': self.handle_event_gc_config, - 'GC_CONFIG_CHANGE': self.handle_event_gc_config_change, - 'GC_INVITATION': self.handle_event_gc_invitation, - 'GC_AFFILIATION': self.handle_event_gc_affiliation, - 'GC_PASSWORD_REQUIRED': self.handle_event_gc_password_required, - 'BAD_PASSPHRASE': self.handle_event_bad_passphrase, - 'ROSTER_INFO': self.handle_event_roster_info, - 'BOOKMARKS': self.handle_event_bookmarks, - 'CON_TYPE': self.handle_event_con_type, - 'CONNECTION_LOST': self.handle_event_connection_lost, - 'FILE_REQUEST': self.handle_event_file_request, - 'GMAIL_NOTIFY': self.handle_event_gmail_notify, - 'FILE_REQUEST_ERROR': self.handle_event_file_request_error, - 'FILE_SEND_ERROR': self.handle_event_file_send_error, - 'STANZA_ARRIVED': self.handle_event_stanza_arrived, - 'STANZA_SENT': self.handle_event_stanza_sent, - 'HTTP_AUTH': self.handle_event_http_auth, - 'VCARD_PUBLISHED': self.handle_event_vcard_published, - 'VCARD_NOT_PUBLISHED': self.handle_event_vcard_not_published, - 'ASK_NEW_NICK': self.handle_event_ask_new_nick, - 'SIGNED_IN': self.handle_event_signed_in, - 'METACONTACTS': self.handle_event_metacontacts, - 'ATOM_ENTRY': self.handle_atom_entry, - 'FAILED_DECRYPT': self.handle_event_failed_decrypt, - 'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received, - 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, - 'PRIVACY_LISTS_ACTIVE_DEFAULT': \ - self.handle_event_privacy_lists_active_default, - 'PRIVACY_LIST_REMOVED': self.handle_event_privacy_list_removed, - 'ZC_NAME_CONFLICT': self.handle_event_zc_name_conflict, - 'PING_SENT': self.handle_event_ping_sent, - 'PING_REPLY': self.handle_event_ping_reply, - 'PING_ERROR': self.handle_event_ping_error, - 'SEARCH_FORM': self.handle_event_search_form, - 'SEARCH_RESULT': self.handle_event_search_result, - 'RESOURCE_CONFLICT': self.handle_event_resource_conflict, - 'ROSTERX': self.handle_event_roster_item_exchange, - 'PEP_CONFIG': self.handle_event_pep_config, - 'UNIQUE_ROOM_ID_UNSUPPORTED': \ - self.handle_event_unique_room_id_unsupported, - 'UNIQUE_ROOM_ID_SUPPORTED': self.handle_event_unique_room_id_supported, - 'GPG_PASSWORD_REQUIRED': self.handle_event_gpg_password_required, - 'GPG_ALWAYS_TRUST': self.handle_event_gpg_always_trust, - 'PASSWORD_REQUIRED': self.handle_event_password_required, - 'SSL_ERROR': self.handle_event_ssl_error, - 'FINGERPRINT_ERROR': self.handle_event_fingerprint_error, - 'PLAIN_CONNECTION': self.handle_event_plain_connection, - 'INSECURE_SSL_CONNECTION': self.handle_event_insecure_ssl_connection, - 'PUBSUB_NODE_REMOVED': self.handle_event_pubsub_node_removed, - 'PUBSUB_NODE_NOT_REMOVED': self.handle_event_pubsub_node_not_removed, - 'JINGLE_INCOMING': self.handle_event_jingle_incoming, - 'JINGLE_CONNECTED': self.handle_event_jingle_connected, - 'JINGLE_DISCONNECTED': self.handle_event_jingle_disconnected, - 'JINGLE_ERROR': self.handle_event_jingle_error, - } - - def dispatch(self, event, account, data): - ''' - Dispatches an network event to the event handlers of this class - ''' - if event not in self.handlers: - log.warning('Unknown event %s dispatched to GUI: %s' % (event, data)) - else: - log.debug('Event %s distpached to GUI: %s' % (event, data)) - self.handlers[event](account, data) - -################################################################################ -### Methods dealing with gajim.events -################################################################################ - - def add_event(self, account, jid, type_, event_args): - '''add an event to the gajim.events var''' - # We add it to the gajim.events queue - # Do we have a queue? - jid = gajim.get_jid_without_resource(jid) - no_queue = len(gajim.events.get_events(account, jid)) == 0 - # type_ can be gc-invitation file-send-error file-error file-request-error - # file-request file-completed file-stopped jingle-incoming - # event_type can be in advancedNotificationWindow.events_list - event_types = {'file-request': 'ft_request', - 'file-completed': 'ft_finished'} - event_type = event_types.get(type_) - show_in_roster = notify.get_show_in_roster(event_type, account, jid) - show_in_systray = notify.get_show_in_systray(event_type, account, jid) - event = gajim.events.create_event(type_, event_args, - show_in_roster=show_in_roster, - show_in_systray=show_in_systray) - gajim.events.add_event(account, jid, event) - - 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): - if type_ == 'gc-invitation': - self.roster.add_groupchat(jid, account, status='offline') - else: - # add contact to roster ("Not In The Roster") if he is not - self.roster.add_to_not_in_the_roster(account, jid) - else: - self.roster.draw_contact(jid, account) - - # Select the big brother contact in roster, it's visible because it has - # events. - family = gajim.contacts.get_metacontacts_family(account, jid) - if family: - nearby_family, bb_jid, bb_account = \ - self.roster._get_nearby_family_and_big_brother(family, account) - else: - bb_jid, bb_account = jid, account - self.roster.select_contact(bb_jid, bb_account) - - def handle_event(self, account, fjid, type_): - w = None - ctrl = None - session = None - - resource = gajim.get_resource_from_jid(fjid) - jid = gajim.get_jid_without_resource(fjid) - - if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'): - w = self.msg_win_mgr.get_window(jid, account) - if jid in self.minimized_controls[account]: - self.roster.on_groupchat_maximized(None, jid, account) - return - else: - ctrl = self.msg_win_mgr.get_gc_control(jid, account) - - elif type_ in ('printed_chat', 'chat', ''): - # '' is for log in/out notifications - - if type_ != '': - event = gajim.events.get_first_event(account, fjid, type_) - if not event: - event = gajim.events.get_first_event(account, jid, type_) - if not event: - return - - if type_ == 'printed_chat': - ctrl = event.parameters[0] - elif type_ == 'chat': - session = event.parameters[8] - ctrl = session.control - elif type_ == '': - ctrl = self.msg_win_mgr.get_control(fjid, account) - - if not ctrl: - 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): - # remove resource of events too - gajim.events.change_jid(account, fjid, jid) - resource = None - fjid = jid - contact = None - if resource: - contact = gajim.contacts.get_contact(account, jid, resource) - if not contact: - contact = highest_contact - - ctrl = self.new_chat(contact, account, resource = resource, session = session) - - gajim.last_message_time[account][jid] = 0 # long time ago - - w = ctrl.parent_win - elif type_ in ('printed_pm', 'pm'): - # assume that the most recently updated control we have for this party - # is the one that this event was in - event = gajim.events.get_first_event(account, fjid, type_) - if not event: - event = gajim.events.get_first_event(account, jid, type_) - - if type_ == 'printed_pm': - ctrl = event.parameters[0] - elif type_ == 'pm': - session = event.parameters[8] - - if session and session.control: - ctrl = session.control - elif not ctrl: - room_jid = jid - nick = resource - gc_contact = gajim.contacts.get_gc_contact(account, room_jid, - nick) - if gc_contact: - show = gc_contact.show - else: - show = 'offline' - gc_contact = gajim.contacts.create_gc_contact( - room_jid = room_jid, name = nick, show = show) - - if not session: - session = gajim.connections[account].make_new_session( - fjid, None, type_='pm') - - self.new_private_chat(gc_contact, account, session=session) - ctrl = session.control - - w = ctrl.parent_win - 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, fjid, type_) - if not event: - # default to jid without resource - event = gajim.events.get_first_event(account, jid, type_) - if not event: - return - # Open the window - self.roster.open_event(account, jid, event) - else: - # Open the window - self.roster.open_event(account, fjid, event) - elif type_ == 'gmail': - url=gajim.connections[account].gmail_url - if url: - helpers.launch_browser_mailer('url', url) - elif type_ == 'gc-invitation': - event = gajim.events.get_first_event(account, jid, type_) - data = event.parameters - dialogs.InvitationReceivedDialog(account, data[0], jid, data[2], - data[1], data[3]) - gajim.events.remove_events(account, jid, event) - self.roster.draw_contact(jid, account) - elif type_ == 'subscription_request': - event = gajim.events.get_first_event(account, jid, type_) - data = event.parameters - dialogs.SubscriptionRequestWindow(jid, data[0], account, data[1]) - gajim.events.remove_events(account, jid, event) - self.roster.draw_contact(jid, account) - elif type_ == 'unsubscribed': - event = gajim.events.get_first_event(account, jid, type_) - contact = event.parameters - self.show_unsubscribed_dialog(account, contact) - gajim.events.remove_events(account, jid, event) - self.roster.draw_contact(jid, account) - elif type_ == 'jingle-incoming': - event = gajim.events.get_first_event(account, jid, type_) - peerjid, sid, content_types = event.parameters - dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) - gajim.events.remove_events(account, jid, event) - if w: - w.set_active_tab(ctrl) - w.window.window.focus(gtk.get_current_event_time()) - # Using isinstance here because we want to catch all derived types - if isinstance(ctrl, ChatControlBase): - tv = ctrl.conv_textview - tv.scroll_to_end() - -################################################################################ -### Methods dealing with emoticons -################################################################################ - - def image_is_ok(self, image): - if not os.path.exists(image): - return False - img = gtk.Image() - try: - img.set_from_file(image) - except Exception: - return False - t = img.get_storage_type() - if t != gtk.IMAGE_PIXBUF and t != gtk.IMAGE_ANIMATION: - return False - return True - - @property - def basic_pattern_re(self): - try: - return self._basic_pattern_re - except AttributeError: - self._basic_pattern_re = re.compile(self.basic_pattern, re.IGNORECASE) - return self._basic_pattern_re - - @property - def emot_and_basic_re(self): - try: - return self._emot_and_basic_re - except AttributeError: - self._emot_and_basic_re = re.compile(self.emot_and_basic, - re.IGNORECASE + re.UNICODE) - return self._emot_and_basic_re - - @property - def sth_at_sth_dot_sth_re(self): - try: - return self._sth_at_sth_dot_sth_re - except AttributeError: - self._sth_at_sth_dot_sth_re = re.compile(self.sth_at_sth_dot_sth) - return self._sth_at_sth_dot_sth_re - - @property - def invalid_XML_chars_re(self): - try: - return self._invalid_XML_chars_re - except AttributeError: - self._invalid_XML_chars_re = re.compile(self.invalid_XML_chars) - return self._invalid_XML_chars_re - - def make_regexps(self): - # regexp meta characters are: . ^ $ * + ? { } [ ] \ | ( ) - # one escapes the metachars with \ - # \S matches anything but ' ' '\t' '\n' '\r' '\f' and '\v' - # \s matches any whitespace character - # \w any alphanumeric character - # \W any non-alphanumeric character - # \b means word boundary. This is a zero-width assertion that - # matches only at the beginning or end of a word. - # ^ matches at the beginning of lines - # - # * means 0 or more times - # + means 1 or more times - # ? means 0 or 1 time - # | means or - # [^*] anything but '*' (inside [] you don't have to escape metachars) - # [^\s*] anything but whitespaces and '*' - # (? in the matching string don't match ? or ) etc.. if at the end - # so http://be) will match http://be and http://be)be) will match http://be)be - - legacy_prefixes = r"((?<=\()(www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+(?=\)))"\ - r"|((www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\ - r"\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)" - # NOTE: it's ok to catch www.gr such stuff exist! - - #FIXME: recognize xmpp: and treat it specially - links = r"((?<=\()[A-Za-z][A-Za-z0-9\+\.\-]*:"\ - r"([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\ - r"(?=\)))|([A-Za-z][A-Za-z0-9\+\.\-]*:([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)" - - #2nd one: at_least_one_char@at_least_one_char.at_least_one_char - mail = r'\bmailto:\S*[^\s\W]|' r'\b\S+@\S+\.\S*[^\s\W]' - - #detects eg. *b* *bold* *bold bold* test *bold* *bold*! (*bold*) - #doesn't detect (it's a feature :P) * bold* *bold * * bold * test*bold* - formatting = r'|(?> sys.stderr, err_str - # it is good to notify the user - # in case he or she cannot see the output of the console - dialogs.ErrorDialog(_('Could not save your settings and preferences'), - err_str) - sys.exit() - - def save_avatar_files(self, jid, photo, puny_nick = None, local = False): - '''Saves an avatar to a separate file, and generate files for dbus notifications. An avatar can be given as a pixmap directly or as an decoded image.''' - puny_jid = helpers.sanitize_filename(jid) - path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) - if puny_nick: - path_to_file = os.path.join(path_to_file, puny_nick) - # remove old avatars - for typ in ('jpeg', 'png'): - if local: - path_to_original_file = path_to_file + '_local'+ '.' + typ - else: - path_to_original_file = path_to_file + '.' + typ - if os.path.isfile(path_to_original_file): - os.remove(path_to_original_file) - if local and photo: - pixbuf = photo - typ = 'png' - extension = '_local.png' # save local avatars as png file - else: - pixbuf, typ = gtkgui_helpers.get_pixbuf_from_data(photo, want_type = True) - if pixbuf is None: - return - extension = '.' + typ - if typ not in ('jpeg', 'png'): - gajim.log.debug('gtkpixbuf cannot save other than jpeg and png formats. saving %s\'avatar as png file (originaly %s)' % (jid, typ)) - typ = 'png' - extension = '.png' - path_to_original_file = path_to_file + extension - try: - pixbuf.save(path_to_original_file, typ) - except Exception, e: - log.error('Error writing avatar file %s: %s' % (path_to_original_file, - str(e))) - # Generate and save the resized, color avatar - pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'notification') - if pixbuf: - path_to_normal_file = path_to_file + '_notif_size_colored' + extension - try: - pixbuf.save(path_to_normal_file, 'png') - except Exception, e: - log.error('Error writing avatar file %s: %s' % \ - (path_to_original_file, str(e))) - # Generate and save the resized, black and white avatar - bwbuf = gtkgui_helpers.get_scaled_pixbuf( - gtkgui_helpers.make_pixbuf_grayscale(pixbuf), 'notification') - if bwbuf: - path_to_bw_file = path_to_file + '_notif_size_bw' + extension - try: - bwbuf.save(path_to_bw_file, 'png') - except Exception, e: - log.error('Error writing avatar file %s: %s' % \ - (path_to_original_file, str(e))) - - def remove_avatar_files(self, jid, puny_nick = None, local = False): - '''remove avatar files of a jid''' - puny_jid = helpers.sanitize_filename(jid) - path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) - if puny_nick: - path_to_file = os.path.join(path_to_file, puny_nick) - for ext in ('.jpeg', '.png'): - if local: - ext = '_local' + ext - path_to_original_file = path_to_file + ext - if os.path.isfile(path_to_file + ext): - os.remove(path_to_file + ext) - if os.path.isfile(path_to_file + '_notif_size_colored' + ext): - os.remove(path_to_file + '_notif_size_colored' + ext) - if os.path.isfile(path_to_file + '_notif_size_bw' + ext): - os.remove(path_to_file + '_notif_size_bw' + ext) - - def auto_join_bookmarks(self, account): - '''autojoin bookmarked GCs that have 'auto join' on for this account''' - for bm in gajim.connections[account].bookmarks: - if bm['autojoin'] in ('1', 'true'): - jid = bm['jid'] - # Only join non-opened groupchats. Opened one are already - # auto-joined on re-connection - if not jid in gajim.gc_connected[account]: - # we are not already connected - minimize = bm['minimize'] in ('1', 'true') - gajim.interface.join_gc_room(account, jid, bm['nick'], - bm['password'], minimize = minimize) - elif jid in self.minimized_controls[account]: - # more or less a hack: - # On disconnect the minimized gc contact instances - # were set to offline. Reconnect them to show up in the roster. - self.roster.add_groupchat(jid, account) - - def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password, - nick): - '''add a bookmark for this account, sorted in bookmark list''' - bm = { - 'name': name, - 'jid': jid, - 'autojoin': autojoin, - 'minimize': minimize, - 'password': password, - 'nick': nick - } - place_found = False - index = 0 - # check for duplicate entry and respect alpha order - for bookmark in gajim.connections[account].bookmarks: - if bookmark['jid'] == bm['jid']: - dialogs.ErrorDialog( - _('Bookmark already set'), - _('Group Chat "%s" is already in your bookmarks.') % bm['jid']) - return - if bookmark['name'] > bm['name']: - place_found = True - break - index += 1 - if place_found: - gajim.connections[account].bookmarks.insert(index, bm) - else: - gajim.connections[account].bookmarks.append(bm) - gajim.connections[account].store_bookmarks() - self.roster.set_actions_menu_needs_rebuild() - dialogs.InformationDialog( - _('Bookmark has been added successfully'), - _('You can manage your bookmarks via Actions menu in your roster.')) - - - # does JID exist only within a groupchat? - def is_pm_contact(self, fjid, account): - bare_jid = gajim.get_jid_without_resource(fjid) - - gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account) - - if not gc_ctrl and \ - bare_jid in self.minimized_controls[account]: - gc_ctrl = self.minimized_controls[account][bare_jid] - - return gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC - - def create_ipython_window(self): - try: - from ipython_view import IPythonView - except ImportError: - print 'ipython_view not found' - return - import pango - - if os.name == 'nt': - font = 'Lucida Console 9' - else: - font = 'Luxi Mono 10' - - window = gtk.Window() - window.set_size_request(750,550) - window.set_resizable(True) - sw = gtk.ScrolledWindow() - sw.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) - view = IPythonView() - view.modify_font(pango.FontDescription(font)) - view.set_wrap_mode(gtk.WRAP_CHAR) - sw.add(view) - window.add(sw) - window.show_all() - def on_delete(win, event): - win.hide() - return True - window.connect('delete_event',on_delete) - view.updateNamespace({'gajim': gajim}) - gajim.ipython_window = window - - def run(self): - if self.systray_capabilities and gajim.config.get('trayicon') != 'never': - self.show_systray() - - self.roster = roster_window.RosterWindow() - for account in gajim.connections: - gajim.connections[account].load_roster_from_db() - - # get instances for windows/dialogs that will show_all()/hide() - self.instances['file_transfers'] = dialogs.FileTransfersWindow() - - gobject.timeout_add(100, self.autoconnect) - timeout, in_seconds = gajim.idlequeue.PROCESS_TIMEOUT - if in_seconds: - gobject.timeout_add_seconds(timeout, self.process_connections) - else: - gobject.timeout_add(timeout, self.process_connections) - gobject.timeout_add_seconds(gajim.config.get( - 'check_idle_every_foo_seconds'), self.read_sleepy) - - # when using libasyncns we need to process resolver in regular intervals - if resolver.USE_LIBASYNCNS: - gobject.timeout_add(200, gajim.resolver.process) - - # setup the indicator - if gajim.HAVE_INDICATOR: - notify.setup_indicator_server() - - def remote_init(): - if gajim.config.get('remote_control'): - try: - import remote_control - self.remote_ctrl = remote_control.Remote() - except Exception: - pass - gobject.timeout_add_seconds(5, remote_init) - - def __init__(self): - gajim.interface = self - gajim.thread_interface = ThreadInterface - # This is the manager and factory of message windows set by the module - self.msg_win_mgr = None - self.jabber_state_images = {'16': {}, '32': {}, 'opened': {}, - 'closed': {}} - self.emoticons_menu = None - # handler when an emoticon is clicked in emoticons_menu - self.emoticon_menuitem_clicked = None - self.minimized_controls = {} - self.status_sent_to_users = {} - self.status_sent_to_groups = {} - self.gpg_passphrase = {} - self.pass_dialog = {} - self.default_colors = { - 'inmsgcolor': gajim.config.get('inmsgcolor'), - 'outmsgcolor': gajim.config.get('outmsgcolor'), - 'inmsgtxtcolor': gajim.config.get('inmsgtxtcolor'), - 'outmsgtxtcolor': gajim.config.get('outmsgtxtcolor'), - 'statusmsgcolor': gajim.config.get('statusmsgcolor'), - 'urlmsgcolor': gajim.config.get('urlmsgcolor'), - } - - cfg_was_read = parser.read() - gajim.logger.reset_shown_unread_messages() - # override logging settings from config (don't take care of '-q' option) - if gajim.config.get('verbose'): - logging_helpers.set_verbose() - - # Is Gajim default app? - if os.name != 'nt' and gajim.config.get('check_if_gajim_is_default'): - gtkgui_helpers.possibly_set_gajim_as_xmpp_handler() - - for account in gajim.config.get_per('accounts'): - if gajim.config.get_per('accounts', account, 'is_zeroconf'): - gajim.ZEROCONF_ACC_NAME = account - break - # Is gnome configured to activate row on single click ? - try: - import gconf - client = gconf.client_get_default() - click_policy = client.get_string( - '/apps/nautilus/preferences/click_policy') - if click_policy == 'single': - gajim.single_click = True - except Exception: - pass - # add default status messages if there is not in the config file - if len(gajim.config.get_per('statusmsg')) == 0: - default = gajim.config.statusmsg_default - for msg in default: - gajim.config.add_per('statusmsg', msg) - gajim.config.set_per('statusmsg', msg, 'message', default[msg][0]) - gajim.config.set_per('statusmsg', msg, 'activity', default[msg][1]) - gajim.config.set_per('statusmsg', msg, 'subactivity', - default[msg][2]) - gajim.config.set_per('statusmsg', msg, 'activity_text', - default[msg][3]) - gajim.config.set_per('statusmsg', msg, 'mood', default[msg][4]) - gajim.config.set_per('statusmsg', msg, 'mood_text', default[msg][5]) - #add default themes if there is not in the config file - theme = gajim.config.get('roster_theme') - if not theme in gajim.config.get_per('themes'): - gajim.config.set('roster_theme', _('default')) - if len(gajim.config.get_per('themes')) == 0: - d = ['accounttextcolor', 'accountbgcolor', 'accountfont', - 'accountfontattrs', 'grouptextcolor', 'groupbgcolor', 'groupfont', - 'groupfontattrs', 'contacttextcolor', 'contactbgcolor', - 'contactfont', 'contactfontattrs', 'bannertextcolor', - 'bannerbgcolor'] - - default = gajim.config.themes_default - for theme_name in default: - gajim.config.add_per('themes', theme_name) - theme = default[theme_name] - for o in d: - gajim.config.set_per('themes', theme_name, o, - theme[d.index(o)]) - - if gajim.config.get('autodetect_browser_mailer') or not cfg_was_read: - gtkgui_helpers.autodetect_browser_mailer() - - gajim.idlequeue = idlequeue.get_idlequeue() - # resolve and keep current record of resolved hosts - gajim.resolver = resolver.get_resolver(gajim.idlequeue) - gajim.socks5queue = socks5.SocksQueue(gajim.idlequeue, - self.handle_event_file_rcv_completed, - self.handle_event_file_progress, - self.handle_event_file_error) - gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue) - gajim.default_session_type = ChatControlSession - self.register_handlers() - if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'active') \ - and gajim.HAVE_ZEROCONF: - gajim.connections[gajim.ZEROCONF_ACC_NAME] = \ - connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) - for account in gajim.config.get_per('accounts'): - if not gajim.config.get_per('accounts', account, 'is_zeroconf') and \ - gajim.config.get_per('accounts', account, 'active'): - 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') - gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url') - - self.instances = {} - - for a in gajim.connections: - self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {}, - 'search': {}, 'online_dialog': {}} - # online_dialog contains all dialogs that have a meaning only when we - # are not disconnected - self.minimized_controls[a] = {} - gajim.contacts.add_account(a) - gajim.groups[a] = {} - gajim.gc_connected[a] = {} - gajim.automatic_rooms[a] = {} - gajim.newly_added[a] = [] - gajim.to_be_removed[a] = [] - gajim.nicks[a] = gajim.config.get_per('accounts', a, 'name') - gajim.block_signed_in_notifications[a] = True - gajim.sleeper_state[a] = 0 - gajim.encrypted_chats[a] = [] - gajim.last_message_time[a] = {} - gajim.status_before_autoaway[a] = '' - gajim.transport_avatar[a] = {} - gajim.gajim_optional_features[a] = [] - gajim.caps_hash[a] = '' - - helpers.update_optional_features() - # prepopulate data which we are sure of; note: we do not log these info - for account in gajim.connections: - gajimcaps = caps.capscache[('sha-1', gajim.caps_hash[account])] - gajimcaps.identities = [gajim.gajim_identity] - gajimcaps.features = gajim.gajim_common_features + \ - gajim.gajim_optional_features[account] - - self.remote_ctrl = None - - if gajim.config.get('networkmanager_support') and dbus_support.supported: - import network_manager_listener - - # Handle gnome screensaver - if dbus_support.supported: - def gnome_screensaver_ActiveChanged_cb(active): - if not active: - for account in gajim.connections: - if gajim.sleeper_state[account] == 'autoaway-forced': - # We came back online ofter gnome-screensaver autoaway - self.roster.send_status(account, 'online', - gajim.status_before_autoaway[account]) - gajim.status_before_autoaway[account] = '' - gajim.sleeper_state[account] = 'online' - return - if not gajim.config.get('autoaway'): - # Don't go auto away if user disabled the option - return - for account in gajim.connections: - if account not in gajim.sleeper_state or \ - not gajim.sleeper_state[account]: - continue - if gajim.sleeper_state[account] == 'online': - # we save out online status - gajim.status_before_autoaway[account] = \ - gajim.connections[account].status - # we go away (no auto status) [we pass True to auto param] - auto_message = gajim.config.get('autoaway_message') - if not auto_message: - auto_message = gajim.connections[account].status - else: - auto_message = auto_message.replace('$S','%(status)s') - auto_message = auto_message.replace('$T','%(time)s') - auto_message = auto_message % { - 'status': gajim.status_before_autoaway[account], - 'time': gajim.config.get('autoxatime') - } - self.roster.send_status(account, 'away', auto_message, - auto=True) - gajim.sleeper_state[account] = 'autoaway-forced' - - try: - bus = dbus.SessionBus() - bus.add_signal_receiver(gnome_screensaver_ActiveChanged_cb, - 'ActiveChanged', 'org.gnome.ScreenSaver') - except Exception: - pass - - self.show_vcard_when_connect = [] - - self.sleeper = common.sleepy.Sleepy( - gajim.config.get('autoawaytime') * 60, # make minutes to seconds - gajim.config.get('autoxatime') * 60) - - gtkgui_helpers.make_jabber_state_images() - - self.systray_enabled = False - self.systray_capabilities = False - - if (os.name == 'nt'): - 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() - else: - gajim.config.set('trayicon', 'never') - - path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'gajim.png') - pix = gtk.gdk.pixbuf_new_from_file(path_to_file) - # set the icon to all windows - gtk.window_set_default_icon(pix) - - self.init_emoticons() - self.make_regexps() - - # get transports type from DB - gajim.transport_type = gajim.logger.get_transports_type() - - # test is dictionnary is present for speller - if gajim.config.get('use_speller'): - lang = gajim.config.get('speller_language') - if not lang: - lang = gajim.LANG - tv = gtk.TextView() - try: - import gtkspell - spell = gtkspell.Spell(tv, lang) - except (ImportError, TypeError, RuntimeError, OSError): - dialogs.AspellDictError(lang) - - if gajim.config.get('soundplayer') == '': - # only on first time Gajim starts - commands = ('aplay', 'play', 'esdplay', 'artsplay', 'ossplay') - for command in commands: - if helpers.is_in_path(command): - if command == 'aplay': - command += ' -q' - gajim.config.set('soundplayer', command) - break - - self.last_ftwindow_update = 0 - - self.music_track_changed_signal = None +from gui_interface import Interface if __name__ == '__main__': def sigint_cb(num, stack): diff --git a/test/runtests.py b/test/runtests.py index fa95009f4..bb7e86191 100755 --- a/test/runtests.py +++ b/test/runtests.py @@ -41,11 +41,12 @@ modules = ( 'test_xmpp_dispatcher_nb', 'test_resolver', 'test_caps', 'test_contacts', + 'test_gui_interface', ) #modules = () if use_x: - modules += ('test_misc_interface', + modules += ('test_gui_event_integration', 'test_roster', 'test_sessions', ) diff --git a/test/test_misc_interface.py b/test/test_gui_event_integration.py similarity index 83% rename from test/test_misc_interface.py rename to test/test_gui_event_integration.py index 4e31b35dc..00ed5c696 100644 --- a/test/test_misc_interface.py +++ b/test/test_gui_event_integration.py @@ -14,34 +14,6 @@ gajim.logger = MockLogger() Interface() -class TestMiscInterface(unittest.TestCase): - - def test_links_regexp_entire(self): - def assert_matches_all(str_): - m = gajim.interface.basic_pattern_re.match(str_) - - # the match should equal the string - str_span = (0, len(str_)) - self.assertEqual(m.span(), str_span) - - # these entire strings should be parsed as links - assert_matches_all('http://google.com/') - assert_matches_all('http://google.com') - assert_matches_all('http://www.google.ca/search?q=xmpp') - - assert_matches_all('http://tools.ietf.org/html/draft-saintandre-rfc3920bis-05#section-12.3') - - assert_matches_all('http://en.wikipedia.org/wiki/Protocol_(computing)') - assert_matches_all( - 'http://en.wikipedia.org/wiki/Protocol_%28computing%29') - - assert_matches_all('mailto:test@example.org') - - assert_matches_all('xmpp:example-node@example.com') - assert_matches_all('xmpp:example-node@example.com/some-resource') - assert_matches_all('xmpp:example-node@example.com?message') - assert_matches_all('xmpp://guest@example.com/support@example.com?message') - import time from data import * diff --git a/test/test_gui_interface.py b/test/test_gui_interface.py new file mode 100644 index 000000000..b2acc019f --- /dev/null +++ b/test/test_gui_interface.py @@ -0,0 +1,111 @@ +import unittest + +import lib +lib.setup_env() + +from gajim_mocks import * +gajim.logger = MockLogger() + +from common import logging_helpers +logging_helpers.set_quiet() + +from interface import Interface + +class Test(unittest.TestCase): + + def test_instantiation(self): + ''' Test that we can proper initialize and do not fail on globals ''' + interface = Interface() + interface.run() + + def test_dispatch(self): + ''' Test dispatcher forwarding network events to handler_* methods ''' + sut = Interface() + + success = sut.dispatch('No Such Event', None, None) + self.assertFalse(success, msg="Unexisting event handled") + + success = sut.dispatch('STANZA_ARRIVED', None, None) + self.assertTrue(success, msg="Existing event must be handled") + + def test_register_unregister_single_handler(self): + ''' Register / Unregister a custom event handler ''' + sut = Interface() + event = 'TESTS_ARE_COOL_EVENT' + + self.called = False + def handler(account, data): + self.assertEqual(account, 'account') + self.assertEqual(data, 'data') + self.called = True + + self.assertFalse(self.called) + sut.register_handler('TESTS_ARE_COOL_EVENT', handler) + sut.dispatch(event, 'account', 'data') + self.assertTrue(self.called, msg="Handler should have been called") + + self.called = False + sut.unregister_handler('TESTS_ARE_COOL_EVENT', handler) + sut.dispatch(event, 'account', 'data') + self.assertFalse(self.called, msg="Handler should no longer be called") + + + def test_dispatch_to_multiple_handlers(self): + ''' Register and dispatch a single event to multiple handlers ''' + sut = Interface() + event = 'SINGLE_EVENT' + + self.called_a = False + self.called_b = False + + def handler_a(account, data): + self.assertFalse(self.called_a, msg="One must only be notified once") + self.called_a = True + + def handler_b(account, data): + self.assertFalse(self.called_b, msg="One must only be notified once") + self.called_b = True + + sut.register_handler(event, handler_a) + sut.register_handler(event, handler_b) + + # register again + sut.register_handler('SOME_OTHER_EVENT', handler_b) + sut.register_handler(event, handler_a) + + sut.dispatch(event, 'account', 'data') + self.assertTrue(self.called_a and self.called_b, + msg="Both handlers should have been called") + + +def test_links_regexp_entire(self): + sut = Interface() + def assert_matches_all(str_): + m = sut.basic_pattern_re.match(str_) + + # the match should equal the string + str_span = (0, len(str_)) + self.assertEqual(m.span(), str_span) + + # these entire strings should be parsed as links + assert_matches_all('http://google.com/') + assert_matches_all('http://google.com') + assert_matches_all('http://www.google.ca/search?q=xmpp') + + assert_matches_all('http://tools.ietf.org/html/draft-saintandre-rfc3920bis-05#section-12.3') + + assert_matches_all('http://en.wikipedia.org/wiki/Protocol_(computing)') + assert_matches_all( + 'http://en.wikipedia.org/wiki/Protocol_%28computing%29') + + assert_matches_all('mailto:test@example.org') + + assert_matches_all('xmpp:example-node@example.com') + assert_matches_all('xmpp:example-node@example.com/some-resource') + assert_matches_all('xmpp:example-node@example.com?message') + assert_matches_all('xmpp://guest@example.com/support@example.com?message') + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.test'] + unittest.main() \ No newline at end of file From e1ccec089c56d3dc127bf99299be1e8e23d879c6 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Tue, 3 Nov 2009 23:37:11 +0100 Subject: [PATCH 20/29] Add missing file --- src/gui_interface.py | 3460 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3460 insertions(+) create mode 100644 src/gui_interface.py diff --git a/src/gui_interface.py b/src/gui_interface.py new file mode 100644 index 000000000..591294cb6 --- /dev/null +++ b/src/gui_interface.py @@ -0,0 +1,3460 @@ +# -*- coding:utf-8 -*- +## src/gajim.py +## +## Copyright (C) 2003-2008 Yann Leboulanger +## Copyright (C) 2004-2005 Vincent Hanquez +## Copyright (C) 2005 Alex Podaras +## Norman Rasmussen +## Stéphan Kochen +## Copyright (C) 2005-2006 Dimitur Kirov +## Alex Mauer +## Copyright (C) 2005-2007 Travis Shirk +## Nikos Kouremenos +## Copyright (C) 2006 Junglecow J +## Stefan Bethge +## Copyright (C) 2006-2008 Jean-Marie Traissard +## Copyright (C) 2007 Lukas Petrovicky +## James Newton +## Copyright (C) 2007-2008 Brendan Taylor +## Julien Pivotto +## Stephan Erb +## Copyright (C) 2008 Jonathan Schleifer +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +import os +import sys +import re +import time +import math + +import gtk +import gobject + +from common import i18n +from common import gajim + +from common import dbus_support +if dbus_support.supported: + from music_track_listener import MusicTrackListener + import dbus + +import gtkgui_helpers + + +import dialogs +import notify +import message_control + +from chat_control import ChatControlBase +from chat_control import ChatControl +from groupchat_control import GroupchatControl +from groupchat_control import PrivateChatControl + +from atom_window import AtomWindow +from session import ChatControlSession + +import common.sleepy + +from common.xmpp import idlequeue +from common.zeroconf import connection_zeroconf +from common import resolver +from common import caps +from common import proxy65_manager +from common import socks5 +from common import helpers +from common import dataforms +from common import passwords +from common import pep +from common import logging_helpers + +import roster_window +import profile_window +import config +from threading import Thread + +gajimpaths = common.configpaths.gajimpaths +config_filename = gajimpaths['CONFIG_FILE'] + +from common import optparser +parser = optparser.OptionsParser(config_filename) + + +import logging +log = logging.getLogger('gajim.interface') + +class Interface: + +################################################################################ +### Methods handling events from connection +################################################################################ + + def handle_event_roster(self, account, data): + #('ROSTER', account, array) + # FIXME: Those methods depend to highly on each other + # and the order in which they are called + self.roster.fill_contacts_and_groups_dicts(data, account) + self.roster.add_account_contacts(account) + self.roster.fire_up_unread_messages_events(account) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('Roster', (account, data)) + + def handle_event_warning(self, unused, data): + #('WARNING', account, (title_text, section_text)) + dialogs.WarningDialog(data[0], data[1]) + + def handle_event_error(self, unused, data): + #('ERROR', account, (title_text, section_text)) + dialogs.ErrorDialog(data[0], data[1]) + + def handle_event_information(self, unused, data): + #('INFORMATION', account, (title_text, section_text)) + dialogs.InformationDialog(data[0], data[1]) + + def handle_event_ask_new_nick(self, account, data): + #('ASK_NEW_NICK', account, (room_jid,)) + room_jid = data[0] + title = _('Unable to join group chat') + prompt = _('Your desired nickname in group chat %s is in use or ' + 'registered by another occupant.\nPlease specify another nickname ' + 'below:') % room_jid + check_text = _('Always use this nickname when there is a conflict') + if 'change_nick_dialog' in self.instances: + self.instances['change_nick_dialog'].add_room(account, room_jid, + prompt) + else: + self.instances['change_nick_dialog'] = dialogs.ChangeNickDialog( + account, room_jid, title, prompt) + + def handle_event_http_auth(self, account, data): + #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg)) + def response(account, iq_obj, answer): + self.dialog.destroy() + gajim.connections[account].build_http_auth_answer(iq_obj, answer) + + def on_yes(is_checked, account, iq_obj): + response(account, iq_obj, 'yes') + + sec_msg = _('Do you accept this request?') + if gajim.get_number_of_connected_accounts() > 1: + sec_msg = _('Do you accept this request on account %s?') % account + if data[4]: + sec_msg = data[4] + '\n' + sec_msg + self.dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for ' + '%(url)s (id: %(id)s)') % {'method': data[0], 'url': data[1], + 'id': data[2]}, sec_msg, on_response_yes=(on_yes, account, data[3]), + on_response_no=(response, account, data[3], 'no')) + + def handle_event_error_answer(self, account, array): + #('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode)) + id_, jid_from, errmsg, errcode = array + if unicode(errcode) in ('400', '403', '406') and id_: + # show the error dialog + ft = self.instances['file_transfers'] + sid = id_ + if len(id_) > 3 and id_[2] == '_': + sid = id_[3:] + if sid in ft.files_props['s']: + file_props = ft.files_props['s'][sid] + if unicode(errcode) == '400': + file_props['error'] = -3 + else: + file_props['error'] = -4 + self.handle_event_file_request_error(account, + (jid_from, file_props, errmsg)) + conn = gajim.connections[account] + conn.disconnect_transfer(file_props) + return + elif unicode(errcode) == '404': + conn = gajim.connections[account] + sid = id_ + if len(id_) > 3 and id_[2] == '_': + sid = id_[3:] + if sid in conn.files_props: + file_props = conn.files_props[sid] + self.handle_event_file_send_error(account, + (jid_from, file_props)) + conn.disconnect_transfer(file_props) + return + + ctrl = self.msg_win_mgr.get_control(jid_from, account) + if ctrl and ctrl.type_id == message_control.TYPE_GC: + ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) + + def handle_event_con_type(self, account, con_type): + # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'plain' + gajim.con_types[account] = con_type + self.roster.draw_account(account) + + def handle_event_connection_lost(self, account, array): + # ('CONNECTION_LOST', account, [title, text]) + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'connection_lost.png') + path = gtkgui_helpers.get_path_to_generic_or_avatar(path) + notify.popup(_('Connection Failed'), account, account, + 'connection_failed', path, array[0], array[1]) + + def unblock_signed_in_notifications(self, account): + gajim.block_signed_in_notifications[account] = False + + def handle_event_status(self, account, show): # OUR status + #('STATUS', account, show) + model = self.roster.status_combobox.get_model() + if show in ('offline', 'error'): + for name in self.instances[account]['online_dialog'].keys(): + # .keys() is needed to not have a dictionary length changed during + # iteration error + self.instances[account]['online_dialog'][name].destroy() + del self.instances[account]['online_dialog'][name] + for request in self.gpg_passphrase.values(): + if request: + request.interrupt() + # .keys() is needed because dict changes during loop + for account in self.pass_dialog.keys(): + self.pass_dialog[account].window.destroy() + if show == 'offline': + # sensitivity for this menuitem + if gajim.get_number_of_connected_accounts() == 0: + model[self.roster.status_message_menuitem_iter][3] = False + gajim.block_signed_in_notifications[account] = True + else: + # 30 seconds after we change our status to sth else than offline + # we stop blocking notifications of any kind + # this prevents from getting the roster items as 'just signed in' + # contacts. 30 seconds should be enough time + gobject.timeout_add_seconds(30, self.unblock_signed_in_notifications, account) + # sensitivity for this menuitem + model[self.roster.status_message_menuitem_iter][3] = True + + # Inform all controls for this account of the connection state change + ctrls = self.msg_win_mgr.get_controls() + if account in self.minimized_controls: + # Can not be the case when we remove account + ctrls += self.minimized_controls[account].values() + for ctrl in ctrls: + if ctrl.account == account: + if show == 'offline' or (show == 'invisible' and \ + gajim.connections[account].is_zeroconf): + ctrl.got_disconnected() + else: + # Other code rejoins all GCs, so we don't do it here + if not ctrl.type_id == message_control.TYPE_GC: + ctrl.got_connected() + if ctrl.parent_win: + ctrl.parent_win.redraw_tab(ctrl) + + self.roster.on_status_changed(account, show) + if account in self.show_vcard_when_connect and show not in ('offline', + 'error'): + self.edit_own_details(account) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('AccountPresence', (show, account)) + + def edit_own_details(self, account): + jid = gajim.get_jid_from_account(account) + if 'profile' not in self.instances[account]: + self.instances[account]['profile'] = \ + profile_window.ProfileWindow(account) + gajim.connections[account].request_vcard(jid) + + def handle_event_notify(self, account, array): + # 'NOTIFY' (account, (jid, status, status message, resource, + # priority, # keyID, timestamp, contact_nickname)) + # + # Contact changed show + + # FIXME: Drop and rewrite... + + statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd', + 'invisible'] + # Ignore invalid show + if array[1] not in statuss: + return + old_show = 0 + new_show = statuss.index(array[1]) + status_message = array[2] + jid = array[0].split('/')[0] + keyID = array[5] + contact_nickname = array[7] + + # Get the proper keyID + keyID = helpers.prepare_and_validate_gpg_keyID(account, jid, keyID) + + resource = array[3] + if not resource: + resource = '' + priority = array[4] + if gajim.jid_is_transport(jid): + # It must be an agent + ji = jid.replace('@', '') + else: + ji = jid + + highest = gajim.contacts. \ + get_contact_with_highest_priority(account, jid) + was_highest = (highest and highest.resource == resource) + + conn = gajim.connections[account] + + # Update contact + jid_list = gajim.contacts.get_jid_list(account) + if ji in jid_list or jid == gajim.get_jid_from_account(account): + lcontact = gajim.contacts.get_contacts(account, ji) + contact1 = None + resources = [] + for c in lcontact: + resources.append(c.resource) + if c.resource == resource: + contact1 = c + break + + if contact1: + if contact1.show in statuss: + old_show = statuss.index(contact1.show) + # nick changed + if contact_nickname is not None and \ + contact1.contact_name != contact_nickname: + contact1.contact_name = contact_nickname + self.roster.draw_contact(jid, account) + + if old_show == new_show and contact1.status == status_message and \ + contact1.priority == priority: # no change + return + else: + contact1 = gajim.contacts.get_first_contact_from_jid(account, ji) + if not contact1: + # Presence of another resource of our + # jid + # Create self contact and add to roster + if resource == conn.server_resource: + return + # Ignore offline presence of unknown self resource + if new_show < 2: + return + contact1 = gajim.contacts.create_contact(jid=ji, + name=gajim.nicks[account], groups=['self_contact'], + show=array[1], status=status_message, sub='both', ask='none', + priority=priority, keyID=keyID, resource=resource, + mood=conn.mood, tune=conn.tune, activity=conn.activity) + old_show = 0 + gajim.contacts.add_contact(account, contact1) + lcontact.append(contact1) + 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: + # Another resource of an existing contact connected + old_show = 0 + contact1 = gajim.contacts.copy_contact(contact1) + lcontact.append(contact1) + contact1.resource = resource + + self.roster.add_contact(contact1.jid, account) + + 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) + if contact1.jid in gajim.to_be_removed[account]: + gajim.to_be_removed[account].remove(contact1.jid) + gobject.timeout_add_seconds(5, self.roster.remove_newly_added, + contact1.jid, account) + elif old_show > 1 and new_show == 0 and conn.connected > 1: + if not contact1.jid in gajim.to_be_removed[account]: + gajim.to_be_removed[account].append(contact1.jid) + if contact1.jid in gajim.newly_added[account]: + gajim.newly_added[account].remove(contact1.jid) + self.roster.draw_contact(contact1.jid, account) + gobject.timeout_add_seconds(5, self.roster.remove_to_be_removed, + contact1.jid, account) + + # unset custom status + if (old_show == 0 and new_show > 1) or (old_show > 1 and new_show == 0\ + and conn.connected > 1): + if account in self.status_sent_to_users and \ + jid in self.status_sent_to_users[account]: + del self.status_sent_to_users[account][jid] + + contact1.show = array[1] + contact1.status = status_message + contact1.priority = priority + contact1.keyID = keyID + timestamp = array[6] + if timestamp: + contact1.last_status_time = timestamp + elif not gajim.block_signed_in_notifications[account]: + # We're connected since more that 30 seconds + contact1.last_status_time = time.localtime() + contact1.contact_nickname = contact_nickname + + if gajim.jid_is_transport(jid): + # It must be an agent + if ji in jid_list: + # Update existing iter and group counting + self.roster.draw_contact(ji, account) + self.roster.draw_group(_('Transports'), account) + if new_show > 1 and ji in gajim.transport_avatar[account]: + # transport just signed in. + # request avatars + for jid_ in gajim.transport_avatar[account][ji]: + conn.request_vcard(jid_) + # transport just signed in/out, don't show + # popup notifications for 30s + account_ji = account + '/' + ji + gajim.block_signed_in_notifications[account_ji] = True + gobject.timeout_add_seconds(30, + self.unblock_signed_in_notifications, account_ji) + locations = (self.instances, self.instances[account]) + for location in locations: + if 'add_contact' in location: + if old_show == 0 and new_show > 1: + location['add_contact'].transport_signed_in(jid) + break + elif old_show > 1 and new_show == 0: + location['add_contact'].transport_signed_out(jid) + break + elif ji in jid_list: + # It isn't an agent + # reset chatstate if needed: + # (when contact signs out or has errors) + if array[1] in ('offline', 'error'): + contact1.our_chatstate = contact1.chatstate = \ + contact1.composing_xep = None + + # TODO: This causes problems when another + # resource signs off! + conn.remove_transfers_for_contact(contact1) + + # disable encryption, since if any messages are + # lost they'll be not decryptable (note that + # this contradicts XEP-0201 - trying to get that + # in the XEP, though) + + # there won't be any sessions here if the contact terminated + # their sessions before going offline (which we do) + for sess in conn.get_sessions(ji): + if (ji+'/'+resource) != str(sess.jid): + continue + if sess.control: + sess.control.no_autonegotiation = False + if sess.enable_encryption: + sess.terminate_e2e() + conn.delete_session(jid, sess.thread_id) + + self.roster.chg_contact_status(contact1, array[1], status_message, + account) + # Notifications + if old_show < 2 and new_show > 1: + notify.notify('contact_connected', jid, account, status_message) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('ContactPresence', (account, + array)) + + elif old_show > 1 and new_show < 2: + notify.notify('contact_disconnected', jid, account, status_message) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('ContactAbsence', (account, array)) + # FIXME: stop non active file transfers + # Status change (not connected/disconnected or + # error (<1)) + elif new_show > 1: + notify.notify('status_change', jid, account, [new_show, + status_message]) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('ContactStatus', (account, array)) + else: + # FIXME: MSN transport (CMSN1.2.1 and PyMSN) don't + # follow the XEP, still the case in 2008. + # It's maybe a GC_NOTIFY (specialy for MSN gc) + self.handle_event_gc_notify(account, (jid, array[1], status_message, + array[3], None, None, None, None, None, [], None, None)) + + highest = gajim.contacts.get_contact_with_highest_priority(account, jid) + is_highest = (highest and highest.resource == resource) + + # disconnect the session from the ctrl if the highest resource has changed + if (was_highest and not is_highest) or (not was_highest and is_highest): + ctrl = self.msg_win_mgr.get_control(jid, account) + + if ctrl: + ctrl.no_autonegotiation = False + ctrl.set_session(None) + ctrl.contact = highest + + def handle_event_msgerror(self, account, array): + #'MSGERROR' (account, (jid, error_code, error_msg, msg, time[, session])) + full_jid_with_resource = array[0] + jids = full_jid_with_resource.split('/', 1) + jid = jids[0] + + if array[1] == '503': + # If we get server-not-found error, stop sending chatstates + for contact in gajim.contacts.get_contacts(account, jid): + contact.composing_xep = False + + session = None + if len(array) > 5: + session = array[5] + + gc_control = self.msg_win_mgr.get_gc_control(jid, account) + if not gc_control and \ + jid in self.minimized_controls[account]: + gc_control = self.minimized_controls[account][jid] + 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 session: + ctrl = session.control + else: + ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) + + if not ctrl: + 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) + ctrl = self.new_private_chat(gc_c, account, session) + + ctrl.print_conversation(_('Error %(code)s: %(msg)s') % { + 'code': array[1], 'msg': array[2]}, 'status') + return + + gc_control.print_conversation(_('Error %(code)s: %(msg)s') % { + 'code': array[1], 'msg': array[2]}, 'status') + if gc_control.parent_win and 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] + if array[3]: + msg = _('error while sending %(message)s ( %(error)s )') % { + 'message': array[3], 'error': msg} + if session: + session.roster_message(jid, msg, array[4], msg_type='error') + + def handle_event_msgsent(self, account, array): + #('MSGSENT', account, (jid, msg, keyID)) + msg = array[1] + # do not play sound when standalone chatstate message (eg no msg) + if msg and gajim.config.get_per('soundevents', 'message_sent', 'enabled'): + helpers.play_sound('message_sent') + + def handle_event_msgnotsent(self, account, array): + #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session)) + msg = _('error while sending %(message)s ( %(error)s )') % { + 'message': array[2], 'error': array[1]} + if not array[4]: + # No session. This can happen when sending a message from gajim-remote + log.warn(msg) + return + array[4].roster_message(array[0], msg, array[3], account, + msg_type='error') + + def handle_event_subscribe(self, account, array): + #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172 + if self.remote_ctrl: + self.remote_ctrl.raise_signal('Subscribe', (account, array)) + + jid = array[0] + text = array[1] + nick = array[2] + if helpers.allow_popup_window(account) or not self.systray_enabled: + dialogs.SubscriptionRequestWindow(jid, text, account, nick) + return + + self.add_event(account, jid, 'subscription_request', (text, nick)) + + if helpers.allow_showing_notification(account): + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'subscription_request.png') + path = gtkgui_helpers.get_path_to_generic_or_avatar(path) + event_type = _('Subscription request') + notify.popup(event_type, jid, account, 'subscription_request', path, + event_type, jid) + + def handle_event_subscribed(self, account, array): + #('SUBSCRIBED', account, (jid, resource)) + jid = array[0] + if jid in gajim.contacts.get_jid_list(account): + c = gajim.contacts.get_first_contact_from_jid(account, jid) + c.resource = array[1] + self.roster.remove_contact_from_groups(c.jid, account, + [_('Not in Roster'), _('Observers')], update=False) + else: + keyID = '' + attached_keys = gajim.config.get_per('accounts', account, + 'attached_gpg_keys').split() + if jid in attached_keys: + keyID = attached_keys[attached_keys.index(jid) + 1] + name = jid.split('@', 1)[0] + name = name.split('%', 1)[0] + contact1 = gajim.contacts.create_contact(jid=jid, name=name, + groups=[], show='online', status='online', + ask='to', resource=array[1], keyID=keyID) + gajim.contacts.add_contact(account, contact1) + self.roster.add_contact(jid, account) + dialogs.InformationDialog(_('Authorization accepted'), + _('The contact "%s" has authorized you to see his or her status.') + % jid) + if not gajim.config.get_per('accounts', account, 'dont_ack_subscription'): + gajim.connections[account].ack_subscribed(jid) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('Subscribed', (account, array)) + + def show_unsubscribed_dialog(self, account, contact): + def on_yes(is_checked, list_): + self.roster.on_req_usub(None, list_) + list_ = [(contact, account)] + dialogs.YesNoDialog( + _('Contact "%s" removed subscription from you') % contact.jid, + _('You will always see him or her as offline.\nDo you want to ' + 'remove him or her from your contact list?'), + on_response_yes=(on_yes, list_)) + # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does + # not show deny + + def handle_event_unsubscribed(self, account, jid): + #('UNSUBSCRIBED', account, jid) + gajim.connections[account].ack_unsubscribed(jid) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('Unsubscribed', (account, jid)) + + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + if not contact: + return + + if helpers.allow_popup_window(account) or not self.systray_enabled: + self.show_unsubscribed_dialog(account, contact) + + self.add_event(account, jid, 'unsubscribed', contact) + + if helpers.allow_showing_notification(account): + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'unsubscribed.png') + path = gtkgui_helpers.get_path_to_generic_or_avatar(path) + event_type = _('Unsubscribed') + notify.popup(event_type, jid, account, 'unsubscribed', path, + event_type, jid) + + def handle_event_agent_info_error(self, account, agent): + #('AGENT_ERROR_INFO', account, (agent)) + try: + gajim.connections[account].services_cache.agent_info_error(agent) + except AttributeError: + return + + def handle_event_agent_items_error(self, account, agent): + #('AGENT_ERROR_INFO', account, (agent)) + try: + gajim.connections[account].services_cache.agent_items_error(agent) + except AttributeError: + return + + def handle_event_agent_removed(self, account, agent): + # remove transport's contacts from treeview + jid_list = gajim.contacts.get_jid_list(account) + for jid in jid_list: + if jid.endswith('@' + agent): + c = gajim.contacts.get_first_contact_from_jid(account, jid) + gajim.log.debug( + 'Removing contact %s due to unregistered transport %s'\ + % (jid, agent)) + gajim.connections[account].unsubscribe(c.jid) + # Transport contacts can't have 2 resources + if c.jid in gajim.to_be_removed[account]: + # This way we'll really remove it + gajim.to_be_removed[account].remove(c.jid) + self.roster.remove_contact(c.jid, account, backend=True) + + def handle_event_register_agent_info(self, account, array): + # ('REGISTER_AGENT_INFO', account, (agent, infos, is_form)) + # info in a dataform if is_form is True + if array[2] or 'instructions' in array[1]: + 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.')) + + def handle_event_agent_info_items(self, account, array): + #('AGENT_INFO_ITEMS', account, (agent, node, items)) + our_jid = gajim.get_jid_from_account(account) + if 'pep_services' in gajim.interface.instances[account] and \ + array[0] == our_jid: + gajim.interface.instances[account]['pep_services'].items_received( + array[2]) + try: + gajim.connections[account].services_cache.agent_items(array[0], + array[1], array[2]) + except AttributeError: + return + + def handle_event_agent_info_info(self, account, array): + #('AGENT_INFO_INFO', account, (agent, node, identities, features, data)) + try: + gajim.connections[account].services_cache.agent_info(array[0], + array[1], array[2], array[3], array[4]) + except AttributeError: + return + + def handle_event_new_acc_connected(self, account, array): + #('NEW_ACC_CONNECTED', account, (infos, is_form, ssl_msg, ssl_err, + # ssl_cert, ssl_fingerprint)) + if 'account_creation_wizard' in self.instances: + self.instances['account_creation_wizard'].new_acc_connected(array[0], + array[1], array[2], array[3], array[4], array[5]) + + def handle_event_new_acc_not_connected(self, account, array): + #('NEW_ACC_NOT_CONNECTED', account, (reason)) + if 'account_creation_wizard' in self.instances: + self.instances['account_creation_wizard'].new_acc_not_connected(array) + + def handle_event_acc_ok(self, account, array): + #('ACC_OK', account, (config)) + if 'account_creation_wizard' in self.instances: + self.instances['account_creation_wizard'].acc_is_ok(array) + + if self.remote_ctrl: + self.remote_ctrl.raise_signal('NewAccount', (account, array)) + + def handle_event_acc_not_ok(self, account, array): + #('ACC_NOT_OK', account, (reason)) + if 'account_creation_wizard' in self.instances: + self.instances['account_creation_wizard'].acc_is_not_ok(array) + + def handle_event_quit(self, p1, p2): + self.roster.quit_gtkgui_interface() + + def handle_event_myvcard(self, account, array): + nick = '' + if 'NICKNAME' in array and array['NICKNAME']: + gajim.nicks[account] = array['NICKNAME'] + elif 'FN' in array and array['FN']: + gajim.nicks[account] = array['FN'] + if 'profile' in self.instances[account]: + win = self.instances[account]['profile'] + win.set_values(array) + if account in self.show_vcard_when_connect: + self.show_vcard_when_connect.remove(account) + jid = array['jid'] + if jid in self.instances[account]['infos']: + self.instances[account]['infos'][jid].set_values(array) + + def handle_event_vcard(self, account, vcard): + # ('VCARD', account, data) + '''vcard holds the vcard data''' + jid = vcard['jid'] + resource = vcard.get('resource', '') + fjid = jid + '/' + str(resource) + + # vcard window + win = None + if jid in self.instances[account]['infos']: + win = self.instances[account]['infos'][jid] + elif resource and fjid in self.instances[account]['infos']: + win = self.instances[account]['infos'][fjid] + if win: + win.set_values(vcard) + + # show avatar in chat + ctrl = None + if resource and self.msg_win_mgr.has_window(fjid, account): + win = self.msg_win_mgr.get_window(fjid, account) + ctrl = win.get_control(fjid, account) + elif self.msg_win_mgr.has_window(jid, account): + win = self.msg_win_mgr.get_window(jid, account) + ctrl = win.get_control(jid, account) + + if ctrl and ctrl.type_id != message_control.TYPE_GC: + ctrl.show_avatar() + + # Show avatar in roster or gc_roster + gc_ctrl = self.msg_win_mgr.get_gc_control(jid, account) + if not gc_ctrl and \ + jid in self.minimized_controls[account]: + gc_ctrl = self.minimized_controls[account][jid] + if gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC: + gc_ctrl.draw_avatar(resource) + else: + self.roster.draw_avatar(jid, account) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('VcardInfo', (account, vcard)) + + def handle_event_last_status_time(self, account, array): + # ('LAST_STATUS_TIME', account, (jid, resource, seconds, status)) + tim = array[2] + if tim < 0: + # Ann error occured + return + win = None + if array[0] in self.instances[account]['infos']: + win = self.instances[account]['infos'][array[0]] + elif array[0] + '/' + array[1] in self.instances[account]['infos']: + win = self.instances[account]['infos'][array[0] + '/' + array[1]] + c = gajim.contacts.get_contact(account, array[0], array[1]) + if c: # c can be none if it's a gc contact + c.last_status_time = time.localtime(time.time() - tim) + if array[3]: + c.status = array[3] + self.roster.draw_contact(c.jid, account) # draw offline status + if win: + win.set_last_status_time() + if self.remote_ctrl: + 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 array[0] in self.instances[account]['infos']: + win = self.instances[account]['infos'][array[0]] + elif array[0] + '/' + array[1] in self.instances[account]['infos']: + win = self.instances[account]['infos'][array[0] + '/' + array[1]] + if win: + win.set_os_info(array[1], array[2], array[3]) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('OsInfo', (account, array)) + + def handle_event_entity_time(self, account, array): + #'ENTITY_TIME' (account, (jid, resource, time_info)) + win = None + if array[0] in self.instances[account]['infos']: + win = self.instances[account]['infos'][array[0]] + elif array[0] + '/' + array[1] in self.instances[account]['infos']: + win = self.instances[account]['infos'][array[0] + '/' + array[1]] + if win: + win.set_entity_time(array[1], array[2]) + if self.remote_ctrl: + self.remote_ctrl.raise_signal('EntityTime', (account, array)) + + def handle_event_gc_notify(self, account, array): + #'GC_NOTIFY' (account, (room_jid, show, status, nick, + # role, affiliation, jid, reason, actor, statusCode, newNick, avatar_sha)) + nick = array[3] + if not nick: + return + room_jid = array[0] + fjid = room_jid + '/' + nick + show = array[1] + status = array[2] + conn = gajim.connections[account] + + # Get the window and control for the updated status, this may be a + # PrivateChatControl + control = self.msg_win_mgr.get_gc_control(room_jid, account) + + if not control and \ + room_jid in self.minimized_controls[account]: + control = self.minimized_controls[account][room_jid] + + if not control or (control and control.type_id != message_control.TYPE_GC): + return + + control.chg_contact_status(nick, show, status, array[4], array[5], + array[6], array[7], array[8], array[9], array[10], array[11]) + + contact = gajim.contacts.\ + get_contact_with_highest_priority(account, room_jid) + if contact: + self.roster.draw_contact(room_jid, account) + + # print status in chat window and update status/GPG image + ctrl = self.msg_win_mgr.get_control(fjid, account) + if ctrl: + statusCode = array[9] + if '303' in statusCode: + new_nick = array[10] + ctrl.print_conversation(_('%(nick)s is now known as %(new_nick)s') \ + % {'nick': nick, 'new_nick': new_nick}, 'status') + gc_c = gajim.contacts.get_gc_contact(account, room_jid, new_nick) + c = gajim.contacts.contact_from_gc_contact(gc_c) + ctrl.gc_contact = gc_c + ctrl.contact = c + if ctrl.session: + # stop e2e + if ctrl.session.enable_encryption: + thread_id = ctrl.session.thread_id + ctrl.session.terminate_e2e() + conn.delete_session(fjid, thread_id) + ctrl.no_autonegotiation = False + ctrl.draw_banner() + old_jid = room_jid + '/' + nick + new_jid = room_jid + '/' + new_nick + self.msg_win_mgr.change_key(old_jid, new_jid, account) + else: + contact = ctrl.contact + contact.show = show + contact.status = status + gc_contact = ctrl.gc_contact + gc_contact.show = show + gc_contact.status = status + uf_show = helpers.get_uf_show(show) + ctrl.print_conversation(_('%(nick)s is now %(status)s') % { + 'nick': nick, 'status': uf_show}, 'status') + if status: + ctrl.print_conversation(' (', 'status', simple=True) + ctrl.print_conversation('%s' % (status), 'status', simple=True) + ctrl.print_conversation(')', 'status', simple=True) + ctrl.parent_win.redraw_tab(ctrl) + ctrl.update_ui() + 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, has_timestamp, htmlmsg, + # [status_codes])) + jids = array[0].split('/', 1) + room_jid = jids[0] + + msg = array[1] + + gc_control = self.msg_win_mgr.get_gc_control(room_jid, account) + if not gc_control and \ + room_jid in self.minimized_controls[account]: + gc_control = self.minimized_controls[account][room_jid] + + 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, msg, array[2], array[3], xhtml, array[5]) + + if self.remote_ctrl: + highlight = gc_control.needs_visual_notification(msg) + array += (highlight,) + self.remote_ctrl.raise_signal('GCMessage', (account, array)) + + def handle_event_gc_subject(self, account, array): + #('GC_SUBJECT', account, (jid, subject, body, has_timestamp)) + jids = array[0].split('/', 1) + jid = jids[0] + + gc_control = self.msg_win_mgr.get_gc_control(jid, account) + + if not gc_control and \ + jid in self.minimized_controls[account]: + gc_control = self.minimized_controls[account][jid] + + contact = gajim.contacts.\ + get_contact_with_highest_priority(account, jid) + if contact: + contact.status = array[1] + self.roster.draw_contact(jid, account) + + if not gc_control: + return + gc_control.set_subject(array[1]) + # Standard way, the message comes from the occupant who set the subject + text = None + if len(jids) > 1: + text = _('%(jid)s has set the subject to %(subject)s') % { + 'jid': jids[1], 'subject': 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, form)) config is a dict + room_jid = array[0].split('/')[0] + if room_jid in gajim.automatic_rooms[account]: + if 'continue_tag' in gajim.automatic_rooms[account][room_jid]: + # We're converting chat to muc. allow participants to invite + form = dataforms.ExtendForm(node = array[1]) + for f in form.iter_fields(): + if f.var == 'muc#roomconfig_allowinvites': + f.value = True + elif f.var == 'muc#roomconfig_publicroom': + f.value = False + elif f.var == 'muc#roomconfig_membersonly': + f.value = True + elif f.var == 'public_list': + f.value = False + gajim.connections[account].send_gc_config(room_jid, form) + else: + # use default configuration + gajim.connections[account].send_gc_config(room_jid, array[1]) + # invite contacts + # check if it is necessary to add + continue_tag = False + if 'continue_tag' in gajim.automatic_rooms[account][room_jid]: + continue_tag = True + if 'invities' in gajim.automatic_rooms[account][room_jid]: + for jid in gajim.automatic_rooms[account][room_jid]['invities']: + gajim.connections[account].send_invite(room_jid, jid, + continue_tag=continue_tag) + del gajim.automatic_rooms[account][room_jid] + elif room_jid not in self.instances[account]['gc_config']: + self.instances[account]['gc_config'][room_jid] = \ + config.GroupchatConfigWindow(account, room_jid, array[1]) + + def handle_event_gc_config_change(self, account, array): + #('GC_CONFIG_CHANGE', account, (jid, statusCode)) statuscode is a list + # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify + # http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes-init + jid = array[0] + statusCode = array[1] + + gc_control = self.msg_win_mgr.get_gc_control(jid, account) + if not gc_control and \ + jid in self.minimized_controls[account]: + gc_control = self.minimized_controls[account][jid] + if not gc_control: + return + + changes = [] + if '100' in statusCode: + # Can be a presence (see chg_contact_status in groupchat_control.py) + changes.append(_('Any occupant is allowed to see your full JID')) + gc_control.is_anonymous = False + if '102' in statusCode: + changes.append(_('Room now shows unavailable member')) + if '103' in statusCode: + changes.append(_('room now does not show unavailable members')) + if '104' in statusCode: + changes.append( + _('A non-privacy-related room configuration change has occurred')) + if '170' in statusCode: + # Can be a presence (see chg_contact_status in groupchat_control.py) + changes.append(_('Room logging is now enabled')) + if '171' in statusCode: + changes.append(_('Room logging is now disabled')) + if '172' in statusCode: + changes.append(_('Room is now non-anonymous')) + gc_control.is_anonymous = False + if '173' in statusCode: + changes.append(_('Room is now semi-anonymous')) + gc_control.is_anonymous = True + if '174' in statusCode: + changes.append(_('Room is now fully-anonymous')) + gc_control.is_anonymous = True + + for change in changes: + gc_control.print_conversation(change) + + def handle_event_gc_affiliation(self, account, array): + #('GC_AFFILIATION', account, (room_jid, users_dict)) + room_jid = array[0] + if room_jid in self.instances[account]['gc_config']: + self.instances[account]['gc_config'][room_jid].\ + affiliation_list_received(array[1]) + + def handle_event_gc_password_required(self, account, array): + #('GC_PASSWORD_REQUIRED', account, (room_jid, nick)) + room_jid = array[0] + nick = array[1] + + def on_ok(text): + gajim.connections[account].join_gc(nick, room_jid, text) + gajim.gc_passwords[room_jid] = text + + def on_cancel(): + # get and destroy window + if room_jid in gajim.interface.minimized_controls[account]: + self.roster.on_disconnect(None, room_jid, account) + else: + win = self.msg_win_mgr.get_window(room_jid, account) + ctrl = self.msg_win_mgr.get_gc_control(room_jid, account) + win.remove_tab(ctrl, 3) + + dlg = dialogs.InputDialog(_('Password Required'), + _('A Password is required to join the room %s. Please type it.') % \ + room_jid, is_modal=False, ok_handler=on_ok, cancel_handler=on_cancel) + dlg.input_entry.set_visibility(False) + + def handle_event_gc_invitation(self, account, array): + #('GC_INVITATION', (room_jid, jid_from, reason, password, is_continued)) + jid = gajim.get_jid_without_resource(array[1]) + room_jid = array[0] + if helpers.allow_popup_window(account) or not self.systray_enabled: + dialogs.InvitationReceivedDialog(account, room_jid, jid, array[3], + array[2], is_continued=array[4]) + return + + self.add_event(account, jid, 'gc-invitation', (room_jid, array[2], + array[3], array[4])) + + 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) + event_type = _('Groupchat Invitation') + notify.popup(event_type, jid, account, 'gc-invitation', path, + event_type, room_jid) + + def forget_gpg_passphrase(self, keyid): + if keyid in self.gpg_passphrase: + del self.gpg_passphrase[keyid] + return False + + def handle_event_bad_passphrase(self, account, array): + #('BAD_PASSPHRASE', account, ()) + use_gpg_agent = gajim.config.get('use_gpg_agent') + sectext = '' + if use_gpg_agent: + sectext = _('You configured Gajim to use GPG agent, but there is no ' + 'GPG agent running or it returned a wrong passphrase.\n') + sectext += _('You are currently connected without your OpenPGP key.') + dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext) + else: + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'warning.png') + notify.popup('warning', account, account, 'warning', path, + _('OpenGPG Passphrase Incorrect'), + _('You are currently connected without your OpenPGP key.')) + keyID = gajim.config.get_per('accounts', account, 'keyid') + self.forget_gpg_passphrase(keyID) + + def handle_event_gpg_password_required(self, account, array): + #('GPG_PASSWORD_REQUIRED', account, (callback,)) + callback = array[0] + keyid = gajim.config.get_per('accounts', account, 'keyid') + if keyid in self.gpg_passphrase: + request = self.gpg_passphrase[keyid] + else: + request = PassphraseRequest(keyid) + self.gpg_passphrase[keyid] = request + request.add_callback(account, callback) + + def handle_event_gpg_always_trust(self, account, callback): + #('GPG_ALWAYS_TRUST', account, callback) + def on_yes(checked): + if checked: + gajim.connections[account].gpg.always_trust = True + callback(True) + + def on_no(): + callback(False) + + dialogs.YesNoDialog(_('GPG key not trusted'), _('The GPG key used to ' + 'encrypt this chat is not trusted. Do you really want to encrypt this ' + 'message?'), checktext=_('Do _not ask me again'), + on_response_yes=on_yes, on_response_no=on_no) + + def handle_event_password_required(self, account, array): + #('PASSWORD_REQUIRED', account, None) + if account in self.pass_dialog: + return + text = _('Enter your password for account %s') % account + if passwords.USER_HAS_GNOMEKEYRING and \ + not passwords.USER_USES_GNOMEKEYRING: + text += '\n' + _('Gnome Keyring is installed but not \ + correctly started (environment variable probably not \ + correctly set)') + + def on_ok(passphrase, save): + if save: + gajim.config.set_per('accounts', account, 'savepass', True) + passwords.save_password(account, passphrase) + gajim.connections[account].set_password(passphrase) + del self.pass_dialog[account] + + def on_cancel(): + self.roster.set_state(account, 'offline') + self.roster.update_status_combobox() + del self.pass_dialog[account] + + self.pass_dialog[account] = dialogs.PassphraseDialog( + _('Password Required'), text, _('Save password'), ok_handler=on_ok, + cancel_handler=on_cancel) + + def handle_event_roster_info(self, account, array): + #('ROSTER_INFO', account, (jid, name, sub, ask, groups)) + jid = array[0] + name = array[1] + sub = array[2] + ask = array[3] + groups = array[4] + contacts = gajim.contacts.get_contacts(account, jid) + if (not sub or sub == 'none') and (not ask or ask == 'none') and \ + not name and not groups: + # contact removed us. + if contacts: + self.roster.remove_contact(jid, account, backend=True) + return + elif not contacts: + if sub == 'remove': + return + # Add new contact to roster + contact = gajim.contacts.create_contact(jid=jid, name=name, + groups=groups, show='offline', sub=sub, ask=ask) + gajim.contacts.add_contact(account, contact) + self.roster.add_contact(jid, account) + else: + # it is an existing contact that might has changed + re_place = False + # If contact has changed (sub, ask or group) update roster + # Mind about observer status changes: + # According to xep 0162, a contact is not an observer anymore when + # we asked for auth, so also remove him if ask changed + old_groups = contacts[0].groups + if contacts[0].sub != sub or contacts[0].ask != ask\ + or old_groups != groups: + re_place = True + # c.get_shown_groups() has changed. Reflect that in roster_winodow + self.roster.remove_contact(jid, account, force=True) + for contact in contacts: + contact.name = name or '' + contact.sub = sub + contact.ask = ask + contact.groups = groups or [] + if re_place: + self.roster.add_contact(jid, account) + # Refilter and update old groups + for group in old_groups: + self.roster.draw_group(group, account) + else: + self.roster.draw_contact(jid, account) + + if self.remote_ctrl: + self.remote_ctrl.raise_signal('RosterInfo', (account, array)) + + def handle_event_bookmarks(self, account, bms): + # ('BOOKMARKS', account, [{name,jid,autojoin,password,nick}, {}]) + # We received a bookmark item from the server (JEP48) + # Auto join GC windows if neccessary + + self.roster.set_actions_menu_needs_rebuild() + invisible_show = gajim.SHOW_LIST.index('invisible') + # do not autojoin if we are invisible + if gajim.connections[account].connected == invisible_show: + return + + self.auto_join_bookmarks(account) + + def handle_event_file_send_error(self, account, array): + jid = array[0] + file_props = array[1] + ft = self.instances['file_transfers'] + ft.set_status(file_props['type'], file_props['sid'], 'stop') + + if helpers.allow_popup_window(account): + ft.show_send_error(file_props) + return + + self.add_event(account, jid, 'file-send-error', file_props) + + if helpers.allow_showing_notification(account): + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'ft_error.png') + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + event_type = _('File Transfer Error') + notify.popup(event_type, jid, account, 'file-send-error', path, + event_type, file_props['name']) + + def handle_event_gmail_notify(self, account, array): + jid = array[0] + gmail_new_messages = int(array[1]) + gmail_messages_list = array[2] + if gajim.config.get('notify_on_new_gmail_email'): + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'new_email_recv.png') + title = _('New mail on %(gmail_mail_address)s') % \ + {'gmail_mail_address': jid} + 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'): + cnt = 0 + 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 + if cnt >=5: + break + senders = ',\n '.join(reversed(gmessage['From'])) + text += _('\n\nFrom: %(from_address)s\nSubject: %(subject)s\n%(snippet)s') % \ + {'from_address': senders, 'subject': gmessage['Subject'], + 'snippet': gmessage['Snippet']} + cnt += 1 + + if gajim.config.get_per('soundevents', 'gmail_received', 'enabled'): + helpers.play_sound('gmail_received') + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + notify.popup(_('New E-mail'), jid, account, 'gmail', + path_to_image=path, title=title, + text=text) + + if self.remote_ctrl: + self.remote_ctrl.raise_signal('NewGmail', (account, array)) + + def handle_event_file_request_error(self, account, array): + # ('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, errmsg) + else: + ft.show_request_error(file_props) + return + + if errno in (-4, -5): + msg_type = 'file-error' + else: + msg_type = 'file-request-error' + + self.add_event(account, jid, msg_type, file_props) + + if helpers.allow_showing_notification(account): + # check if we should be notified + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'ft_error.png') + + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + event_type = _('File Transfer Error') + notify.popup(event_type, jid, account, msg_type, path, + title = event_type, text = file_props['name']) + + def handle_event_file_request(self, account, array): + jid = array[0] + if jid not in gajim.contacts.get_jid_list(account): + keyID = '' + attached_keys = gajim.config.get_per('accounts', account, + 'attached_gpg_keys').split() + if jid in attached_keys: + keyID = attached_keys[attached_keys.index(jid) + 1] + contact = gajim.contacts.create_contact(jid=jid, name='', + groups=[_('Not in Roster')], show='not in roster', status='', + sub='none', keyID=keyID) + gajim.contacts.add_contact(account, contact) + self.roster.add_contact(contact.jid, account) + file_props = array[1] + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + + if helpers.allow_popup_window(account): + self.instances['file_transfers'].show_file_request(account, contact, + file_props) + return + + self.add_event(account, jid, 'file-request', file_props) + + if helpers.allow_showing_notification(account): + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'ft_request.png') + txt = _('%s wants to send you a file.') % gajim.get_name_from_jid( + account, jid) + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + event_type = _('File Transfer Request') + notify.popup(event_type, jid, account, 'file-request', + path_to_image = path, title = event_type, text = txt) + + def handle_event_file_error(self, title, message): + dialogs.ErrorDialog(title, message) + + def handle_event_file_progress(self, account, file_props): + 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'] + if file_props['error'] == 0: + ft.set_progress(file_props['type'], file_props['sid'], + file_props['received-len']) + else: + ft.set_status(file_props['type'], file_props['sid'], 'stop') + if 'stalled' in file_props and file_props['stalled'] or \ + 'paused' in file_props and file_props['paused']: + return + if file_props['type'] == 'r': # we receive a file + jid = unicode(file_props['sender']) + else: # we send a file + jid = unicode(file_props['receiver']) + + if helpers.allow_popup_window(account): + if file_props['error'] == 0: + if gajim.config.get('notify_on_file_complete'): + ft.show_completed(jid, file_props) + elif file_props['error'] == -1: + ft.show_stopped(jid, file_props, + error_msg=_('Remote contact stopped transfer')) + elif file_props['error'] == -6: + ft.show_stopped(jid, file_props, error_msg=_('Error opening file')) + return + + msg_type = '' + event_type = '' + if file_props['error'] == 0 and gajim.config.get( + 'notify_on_file_complete'): + msg_type = 'file-completed' + event_type = _('File Transfer Completed') + elif file_props['error'] in (-1, -6): + msg_type = 'file-stopped' + event_type = _('File Transfer Stopped') + + if event_type == '': + # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs) + # this should never happen but it does. see process_result() in socks5.py + # who calls this func (sth is really wrong unless this func is also registered + # as progress_cb + return + + if msg_type: + self.add_event(account, jid, msg_type, file_props) + + if file_props is not None: + if file_props['type'] == 'r': + # get the name of the sender, as it is in the roster + sender = unicode(file_props['sender']).split('/')[0] + name = gajim.contacts.get_first_contact_from_jid(account, + sender).get_shown_name() + filename = os.path.basename(file_props['file-name']) + if event_type == _('File Transfer Completed'): + txt = _('You successfully received %(filename)s from %(name)s.')\ + % {'filename': filename, 'name': name} + img = 'ft_done.png' + else: # ft stopped + txt = _('File transfer of %(filename)s from %(name)s stopped.')\ + % {'filename': filename, 'name': name} + img = 'ft_stopped.png' + else: + receiver = file_props['receiver'] + if hasattr(receiver, 'jid'): + receiver = receiver.jid + receiver = receiver.split('/')[0] + # get the name of the contact, as it is in the roster + name = gajim.contacts.get_first_contact_from_jid(account, + receiver).get_shown_name() + filename = os.path.basename(file_props['file-name']) + if event_type == _('File Transfer Completed'): + txt = _('You successfully sent %(filename)s to %(name)s.')\ + % {'filename': filename, 'name': name} + img = 'ft_done.png' + else: # ft stopped + txt = _('File transfer of %(filename)s to %(name)s stopped.')\ + % {'filename': filename, 'name': name} + img = 'ft_stopped.png' + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', img) + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + else: + txt = '' + + if gajim.config.get('notify_on_file_complete') and \ + (gajim.config.get('autopopupaway') or \ + gajim.connections[account].connected in (2, 3)): + # we want to be notified and we are online/chat or we don't mind + # bugged when away/na/busy + notify.popup(event_type, jid, account, msg_type, path_to_image = path, + title = event_type, text = txt) + + def handle_event_stanza_arrived(self, account, stanza): + if account not in self.instances: + return + if 'xml_console' in self.instances[account]: + self.instances[account]['xml_console'].print_stanza(stanza, 'incoming') + + def handle_event_stanza_sent(self, account, stanza): + if account not in self.instances: + return + if 'xml_console' in self.instances[account]: + self.instances[account]['xml_console'].print_stanza(stanza, 'outgoing') + + def handle_event_vcard_published(self, account, array): + if 'profile' in self.instances[account]: + win = self.instances[account]['profile'] + win.vcard_published() + for gc_control in self.msg_win_mgr.get_controls(message_control.TYPE_GC) + \ + self.minimized_controls[account].values(): + if gc_control.account == account: + show = gajim.SHOW_LIST[gajim.connections[account].connected] + status = gajim.connections[account].status + gajim.connections[account].send_gc_status(gc_control.nick, + gc_control.room_jid, show, status) + + def handle_event_vcard_not_published(self, account, array): + if 'profile' in self.instances[account]: + win = self.instances[account]['profile'] + win.vcard_not_published() + + def ask_offline_status(self, account): + for contact in gajim.contacts.iter_contacts(account): + gajim.connections[account].request_last_status_time(contact.jid, + contact.resource) + + def handle_event_signed_in(self, account, empty): + '''SIGNED_IN event is emitted when we sign in, so handle it''' + # block signed in notifications for 30 seconds + gajim.block_signed_in_notifications[account] = True + self.roster.set_actions_menu_needs_rebuild() + self.roster.draw_account(account) + state = self.sleeper.getState() + connected = gajim.connections[account].connected + if gajim.config.get('ask_offline_status_on_connection'): + # Ask offline status in 1 minute so w'are sure we got all online + # presences + gobject.timeout_add_seconds(60, self.ask_offline_status, account) + if state != common.sleepy.STATE_UNKNOWN and connected in (2, 3): + # we go online or free for chat, so we activate auto status + gajim.sleeper_state[account] = 'online' + elif not ((state == common.sleepy.STATE_AWAY and connected == 4) or \ + (state == common.sleepy.STATE_XA and connected == 5)): + # If we are autoaway/xa and come back after a disconnection, do nothing + # Else disable autoaway + gajim.sleeper_state[account] = 'off' + invisible_show = gajim.SHOW_LIST.index('invisible') + # We cannot join rooms if we are invisible + if gajim.connections[account].connected == invisible_show: + return + # join already open groupchats + for gc_control in self.msg_win_mgr.get_controls(message_control.TYPE_GC) \ + + self.minimized_controls[account].values(): + if account != gc_control.account: + continue + room_jid = gc_control.room_jid + if room_jid in gajim.gc_connected[account] and \ + gajim.gc_connected[account][room_jid]: + continue + nick = gc_control.nick + password = gajim.gc_passwords.get(room_jid, '') + gajim.connections[account].join_gc(nick, room_jid, password) + # send currently played music + if gajim.connections[account].pep_supported and dbus_support.supported \ + and gajim.config.get_per('accounts', account, 'publish_tune'): + self.enable_music_listener() + + def handle_event_metacontacts(self, account, tags_list): + gajim.contacts.define_metacontacts(account, tags_list) + self.roster.redraw_metacontacts(account) + + def handle_atom_entry(self, account, data): + atom_entry, = data + AtomWindow.newAtomEntry(atom_entry) + + def handle_event_failed_decrypt(self, account, data): + jid, tim, session = data + + details = _('Unable to decrypt message from ' + '%s\nIt may have been tampered with.') % jid + + ctrl = session.control + if ctrl: + ctrl.print_conversation_line(details, 'status', '', tim) + else: + dialogs.WarningDialog(_('Unable to decrypt message'), + details) + + # terminate the session + session.terminate_e2e() + session.conn.delete_session(jid, session.thread_id) + + # restart the session + if ctrl: + ctrl.begin_e2e_negotiation() + + def handle_event_privacy_lists_received(self, account, data): + # ('PRIVACY_LISTS_RECEIVED', account, list) + if account not in self.instances: + return + if 'privacy_lists' in self.instances[account]: + self.instances[account]['privacy_lists'].privacy_lists_received(data) + + def handle_event_privacy_list_received(self, account, data): + # ('PRIVACY_LIST_RECEIVED', account, (name, rules)) + if account not in self.instances: + return + name = data[0] + rules = data[1] + if 'privacy_list_%s' % name in self.instances[account]: + self.instances[account]['privacy_list_%s' % name].\ + privacy_list_received(rules) + if name == 'block': + gajim.connections[account].blocked_contacts = [] + gajim.connections[account].blocked_groups = [] + gajim.connections[account].blocked_list = [] + gajim.connections[account].blocked_all = False + for rule in rules: + if not 'type' in rule: + gajim.connections[account].blocked_all = True + elif rule['type'] == 'jid' and rule['action'] == 'deny': + gajim.connections[account].blocked_contacts.append(rule['value']) + elif rule['type'] == 'group' and rule['action'] == 'deny': + gajim.connections[account].blocked_groups.append(rule['value']) + gajim.connections[account].blocked_list.append(rule) + #elif rule['type'] == "group" and action == "deny": + # text_item = _('%s group "%s"') % _(rule['action']), rule['value'] + # self.store.append([text_item]) + # self.global_rules.append(rule) + #else: + # self.global_rules_to_append.append(rule) + if 'blocked_contacts' in self.instances[account]: + self.instances[account]['blocked_contacts'].\ + privacy_list_received(rules) + + def handle_event_privacy_lists_active_default(self, account, data): + if not data: + return + # Send to all privacy_list_* windows as we can't know which one asked + for win in self.instances[account]: + if win.startswith('privacy_list_'): + self.instances[account][win].check_active_default(data) + + def handle_event_privacy_list_removed(self, account, name): + # ('PRIVACY_LISTS_REMOVED', account, name) + if account not in self.instances: + return + if 'privacy_lists' in self.instances[account]: + self.instances[account]['privacy_lists'].privacy_list_removed(name) + + def handle_event_zc_name_conflict(self, account, data): + def on_ok(new_name): + 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, '') + def on_cancel(): + gajim.connections[account].change_status('offline','') + + dlg = dialogs.InputDialog(_('Username Conflict'), + _('Please type a new username for your local account'), input_str=data, + is_modal=True, ok_handler=on_ok, cancel_handler=on_cancel) + + def handle_event_ping_sent(self, account, contact): + if contact.jid == contact.get_full_jid(): + # If contact is a groupchat user + jids = [contact.jid] + else: + jids = [contact.jid, contact.get_full_jid()] + for jid in jids: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.print_conversation(_('Ping?'), 'status') + + def handle_event_ping_reply(self, account, data): + contact = data[0] + seconds = data[1] + if contact.jid == contact.get_full_jid(): + # If contact is a groupchat user + jids = [contact.jid] + else: + jids = [contact.jid, contact.get_full_jid()] + for jid in jids: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.print_conversation(_('Pong! (%s s.)') % seconds, 'status') + + def handle_event_ping_error(self, account, contact): + if contact.jid == contact.get_full_jid(): + # If contact is a groupchat user + jids = [contact.jid] + else: + jids = [contact.jid, contact.get_full_jid()] + for jid in jids: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.print_conversation(_('Error.'), 'status') + + def handle_event_search_form(self, account, data): + # ('SEARCH_FORM', account, (jid, dataform, is_dataform)) + if data[0] not in self.instances[account]['search']: + return + self.instances[account]['search'][data[0]].on_form_arrived(data[1], + data[2]) + + def handle_event_search_result(self, account, data): + # ('SEARCH_RESULT', account, (jid, dataform, is_dataform)) + if data[0] not in self.instances[account]['search']: + return + self.instances[account]['search'][data[0]].on_result_arrived(data[1], + data[2]) + + def handle_event_resource_conflict(self, account, data): + # ('RESOURCE_CONFLICT', account, ()) + # First we go offline, but we don't overwrite status message + self.roster.send_status(account, 'offline', + gajim.connections[account].status) + def on_ok(new_resource): + gajim.config.set_per('accounts', account, 'resource', new_resource) + self.roster.send_status(account, gajim.connections[account].old_show, + gajim.connections[account].status) + proposed_resource = gajim.connections[account].server_resource + proposed_resource += gajim.config.get('gc_proposed_nick_char') + dlg = dialogs.ResourceConflictDialog(_('Resource Conflict'), + _('You are already connected to this account with the same resource. ' + 'Please type a new one'), resource=proposed_resource, ok_handler=on_ok) + + def handle_event_jingle_incoming(self, account, data): + # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, + # data...)) + # TODO: conditional blocking if peer is not in roster + + # unpack data + peerjid, sid, contents = data + content_types = set(c[0] for c in contents) + + # check type of jingle session + if 'audio' in content_types or 'video' in content_types: + # a voip session... + # we now handle only voip, so the only thing we will do here is + # not to return from function + pass + else: + # unknown session type... it should be declined in common/jingle.py + return + + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + if 'audio' in content_types: + ctrl.set_audio_state('connection_received', sid) + if 'video' in content_types: + ctrl.set_video_state('connection_received', sid) + + dlg = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) + if dlg: + dlg.add_contents(content_types) + return + + if helpers.allow_popup_window(account): + dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) + return + + self.add_event(account, peerjid, 'jingle-incoming', (peerjid, sid, + content_types)) + + if helpers.allow_showing_notification(account): + # TODO: we should use another pixmap ;-) + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'ft_request.png') + txt = _('%s wants to start a voice chat.') % gajim.get_name_from_jid( + account, peerjid) + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + event_type = _('Voice Chat Request') + notify.popup(event_type, peerjid, account, 'jingle-incoming', + path_to_image = path, title = event_type, text = txt) + + def handle_event_jingle_connected(self, account, data): + # ('JINGLE_CONNECTED', account, (peerjid, sid, media)) + peerjid, sid, media = data + if media in ('audio', 'video'): + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + if media == 'audio': + ctrl.set_audio_state('connected', sid) + else: + ctrl.set_video_state('connected', sid) + + def handle_event_jingle_disconnected(self, account, data): + # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) + peerjid, sid, media, reason = data + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + if media in ('audio', None): + ctrl.set_audio_state('stop', sid=sid, reason=reason) + if media in ('video', None): + ctrl.set_video_state('stop', sid=sid, reason=reason) + dialog = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) + if dialog: + dialog.dialog.destroy() + + def handle_event_jingle_error(self, account, data): + # ('JINGLE_ERROR', account, (peerjid, sid, reason)) + peerjid, sid, reason = data + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.set_audio_state('error', reason=reason) + + def handle_event_pep_config(self, account, data): + # ('PEP_CONFIG', account, (node, form)) + if 'pep_services' in self.instances[account]: + self.instances[account]['pep_services'].config(data[0], data[1]) + + def handle_event_roster_item_exchange(self, account, data): + # data = (action in [add, delete, modify], exchange_list, jid_from) + dialogs.RosterItemExchangeWindow(account, data[0], data[1], data[2]) + + def handle_event_unique_room_id_supported(self, account, data): + '''Receive confirmation that unique_room_id are supported''' + # ('UNIQUE_ROOM_ID_SUPPORTED', server, instance, room_id) + instance = data[1] + instance.unique_room_id_supported(data[0], data[2]) + + def handle_event_unique_room_id_unsupported(self, account, data): + # ('UNIQUE_ROOM_ID_UNSUPPORTED', server, instance) + instance = data[1] + instance.unique_room_id_error(data[0]) + + def handle_event_ssl_error(self, account, data): + # ('SSL_ERROR', account, (text, errnum, cert, sha1_fingerprint)) + server = gajim.config.get_per('accounts', account, 'hostname') + + def on_ok(is_checked): + del self.instances[account]['online_dialog']['ssl_error'] + if is_checked[0]: + # Check if cert is already in file + certs = '' + if os.path.isfile(gajim.MY_CACERTS): + f = open(gajim.MY_CACERTS) + certs = f.read() + f.close() + if data[2] in certs: + dialogs.ErrorDialog(_('Certificate Already in File'), + _('This certificate is already in file %s, so it\'s not added again.') % gajim.MY_CACERTS) + else: + f = open(gajim.MY_CACERTS, 'a') + f.write(server + '\n') + f.write(data[2] + '\n\n') + f.close() + gajim.config.set_per('accounts', account, 'ssl_fingerprint_sha1', + data[3]) + if is_checked[1]: + ignore_ssl_errors = gajim.config.get_per('accounts', account, + 'ignore_ssl_errors').split() + ignore_ssl_errors.append(str(data[1])) + gajim.config.set_per('accounts', account, 'ignore_ssl_errors', + ' '.join(ignore_ssl_errors)) + gajim.connections[account].ssl_certificate_accepted() + + def on_cancel(): + del self.instances[account]['online_dialog']['ssl_error'] + gajim.connections[account].disconnect(on_purpose=True) + self.handle_event_status(account, 'offline') + + pritext = _('Error verifying SSL certificate') + sectext = _('There was an error verifying the SSL certificate of your jabber server: %(error)s\nDo you still want to connect to this server?') % {'error': data[0]} + if data[1] in (18, 27): + checktext1 = _('Add this certificate to the list of trusted certificates.\nSHA1 fingerprint of the certificate:\n%s') % data[3] + else: + checktext1 = '' + checktext2 = _('Ignore this error for this certificate.') + if 'ssl_error' in self.instances[account]['online_dialog']: + self.instances[account]['online_dialog']['ssl_error'].destroy() + self.instances[account]['online_dialog']['ssl_error'] = \ + dialogs.ConfirmationDialogDubbleCheck(pritext, sectext, checktext1, + checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel) + + def handle_event_fingerprint_error(self, account, data): + # ('FINGERPRINT_ERROR', account, (new_fingerprint,)) + def on_yes(is_checked): + del self.instances[account]['online_dialog']['fingerprint_error'] + gajim.config.set_per('accounts', account, 'ssl_fingerprint_sha1', + data[0]) + # Reset the ignored ssl errors + gajim.config.set_per('accounts', account, 'ignore_ssl_errors', '') + gajim.connections[account].ssl_certificate_accepted() + def on_no(): + del self.instances[account]['online_dialog']['fingerprint_error'] + gajim.connections[account].disconnect(on_purpose=True) + self.handle_event_status(account, 'offline') + pritext = _('SSL certificate error') + sectext = _('It seems the SSL certificate of account %(account)s has ' + 'changed or your connection is being hacked.\nOld fingerprint: %(old)s' + '\nNew fingerprint: %(new)s\n\nDo you still want to connect and update' + ' the fingerprint of the certificate?') % {'account': account, + 'old': gajim.config.get_per('accounts', account, + 'ssl_fingerprint_sha1'), 'new': data[0]} + if 'fingerprint_error' in self.instances[account]['online_dialog']: + self.instances[account]['online_dialog']['fingerprint_error'].destroy() + self.instances[account]['online_dialog']['fingerprint_error'] = \ + dialogs.YesNoDialog(pritext, sectext, on_response_yes=on_yes, + on_response_no=on_no) + + def handle_event_plain_connection(self, account, data): + # ('PLAIN_CONNECTION', account, (connection)) + server = gajim.config.get_per('accounts', account, 'hostname') + def on_ok(is_checked): + if not is_checked[0]: + on_cancel() + return + # On cancel call del self.instances, so don't call it another time + # before + del self.instances[account]['online_dialog']['plain_connection'] + if is_checked[1]: + gajim.config.set_per('accounts', account, + 'warn_when_plaintext_connection', False) + gajim.connections[account].connection_accepted(data[0], 'plain') + def on_cancel(): + del self.instances[account]['online_dialog']['plain_connection'] + gajim.connections[account].disconnect(on_purpose=True) + self.handle_event_status(account, 'offline') + pritext = _('Insecure connection') + sectext = _('You are about to send your password on an unencrypted ' + 'connection. Are you sure you want to do that?') + checktext1 = _('Yes, I really want to connect insecurely') + checktext2 = _('Do _not ask me again') + if 'plain_connection' in self.instances[account]['online_dialog']: + self.instances[account]['online_dialog']['plain_connection'].destroy() + self.instances[account]['online_dialog']['plain_connection'] = \ + dialogs.ConfirmationDialogDubbleCheck(pritext, sectext, + checktext1, checktext2, on_response_ok=on_ok, + on_response_cancel=on_cancel, is_modal=False) + + def handle_event_insecure_ssl_connection(self, account, data): + # ('INSECURE_SSL_CONNECTION', account, (connection, connection_type)) + server = gajim.config.get_per('accounts', account, 'hostname') + def on_ok(is_checked): + del self.instances[account]['online_dialog']['insecure_ssl'] + if not is_checked[0]: + on_cancel() + return + if is_checked[1]: + gajim.config.set_per('accounts', account, + 'warn_when_insecure_ssl_connection', False) + if gajim.connections[account].connected == 0: + # We have been disconnecting (too long time since window is opened) + # re-connect with auto-accept + gajim.connections[account].connection_auto_accepted = True + show, msg = gajim.connections[account].continue_connect_info[:2] + self.roster.send_status(account, show, msg) + return + gajim.connections[account].connection_accepted(data[0], data[1]) + def on_cancel(): + del self.instances[account]['online_dialog']['insecure_ssl'] + gajim.connections[account].disconnect(on_purpose=True) + self.handle_event_status(account, 'offline') + pritext = _('Insecure connection') + sectext = _('You are about to send your password on an insecure ' + 'connection. You should install PyOpenSSL to prevent that. Are you sure you want to do that?') + checktext1 = _('Yes, I really want to connect insecurely') + checktext2 = _('Do _not ask me again') + if 'insecure_ssl' in self.instances[account]['online_dialog']: + self.instances[account]['online_dialog']['insecure_ssl'].destroy() + self.instances[account]['online_dialog']['insecure_ssl'] = \ + dialogs.ConfirmationDialogDubbleCheck(pritext, sectext, + checktext1, checktext2, on_response_ok=on_ok, + on_response_cancel=on_cancel, is_modal=False) + + def handle_event_pubsub_node_removed(self, account, data): + # ('PUBSUB_NODE_REMOVED', account, (jid, node)) + if 'pep_services' in self.instances[account]: + if data[0] == gajim.get_jid_from_account(account): + self.instances[account]['pep_services'].node_removed(data[1]) + + def handle_event_pubsub_node_not_removed(self, account, data): + # ('PUBSUB_NODE_NOT_REMOVED', account, (jid, node, msg)) + if data[0] == gajim.get_jid_from_account(account): + dialogs.WarningDialog(_('PEP node was not removed'), + _('PEP node %(node)s was not removed: %(message)s') % { + 'node': data[1], 'message': data[2]}) + + def register_handlers(self): + self.handlers = { + 'ROSTER': self.handle_event_roster, + 'WARNING': self.handle_event_warning, + 'ERROR': self.handle_event_error, + 'INFORMATION': self.handle_event_information, + 'ERROR_ANSWER': self.handle_event_error_answer, + 'STATUS': self.handle_event_status, + 'NOTIFY': self.handle_event_notify, + 'MSGERROR': self.handle_event_msgerror, + 'MSGSENT': self.handle_event_msgsent, + 'MSGNOTSENT': self.handle_event_msgnotsent, + 'SUBSCRIBED': self.handle_event_subscribed, + 'UNSUBSCRIBED': self.handle_event_unsubscribed, + 'SUBSCRIBE': self.handle_event_subscribe, + 'AGENT_ERROR_INFO': self.handle_event_agent_info_error, + 'AGENT_ERROR_ITEMS': self.handle_event_agent_items_error, + 'AGENT_REMOVED': self.handle_event_agent_removed, + 'REGISTER_AGENT_INFO': self.handle_event_register_agent_info, + 'AGENT_INFO_ITEMS': self.handle_event_agent_info_items, + 'AGENT_INFO_INFO': self.handle_event_agent_info_info, + 'QUIT': self.handle_event_quit, + 'NEW_ACC_CONNECTED': self.handle_event_new_acc_connected, + 'NEW_ACC_NOT_CONNECTED': self.handle_event_new_acc_not_connected, + 'ACC_OK': self.handle_event_acc_ok, + 'ACC_NOT_OK': self.handle_event_acc_not_ok, + 'MYVCARD': self.handle_event_myvcard, + 'VCARD': self.handle_event_vcard, + 'LAST_STATUS_TIME': self.handle_event_last_status_time, + 'OS_INFO': self.handle_event_os_info, + 'ENTITY_TIME': self.handle_event_entity_time, + 'GC_NOTIFY': self.handle_event_gc_notify, + 'GC_MSG': self.handle_event_gc_msg, + 'GC_SUBJECT': self.handle_event_gc_subject, + 'GC_CONFIG': self.handle_event_gc_config, + 'GC_CONFIG_CHANGE': self.handle_event_gc_config_change, + 'GC_INVITATION': self.handle_event_gc_invitation, + 'GC_AFFILIATION': self.handle_event_gc_affiliation, + 'GC_PASSWORD_REQUIRED': self.handle_event_gc_password_required, + 'BAD_PASSPHRASE': self.handle_event_bad_passphrase, + 'ROSTER_INFO': self.handle_event_roster_info, + 'BOOKMARKS': self.handle_event_bookmarks, + 'CON_TYPE': self.handle_event_con_type, + 'CONNECTION_LOST': self.handle_event_connection_lost, + 'FILE_REQUEST': self.handle_event_file_request, + 'GMAIL_NOTIFY': self.handle_event_gmail_notify, + 'FILE_REQUEST_ERROR': self.handle_event_file_request_error, + 'FILE_SEND_ERROR': self.handle_event_file_send_error, + 'STANZA_ARRIVED': self.handle_event_stanza_arrived, + 'STANZA_SENT': self.handle_event_stanza_sent, + 'HTTP_AUTH': self.handle_event_http_auth, + 'VCARD_PUBLISHED': self.handle_event_vcard_published, + 'VCARD_NOT_PUBLISHED': self.handle_event_vcard_not_published, + 'ASK_NEW_NICK': self.handle_event_ask_new_nick, + 'SIGNED_IN': self.handle_event_signed_in, + 'METACONTACTS': self.handle_event_metacontacts, + 'ATOM_ENTRY': self.handle_atom_entry, + 'FAILED_DECRYPT': self.handle_event_failed_decrypt, + 'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received, + 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, + 'PRIVACY_LISTS_ACTIVE_DEFAULT': \ + self.handle_event_privacy_lists_active_default, + 'PRIVACY_LIST_REMOVED': self.handle_event_privacy_list_removed, + 'ZC_NAME_CONFLICT': self.handle_event_zc_name_conflict, + 'PING_SENT': self.handle_event_ping_sent, + 'PING_REPLY': self.handle_event_ping_reply, + 'PING_ERROR': self.handle_event_ping_error, + 'SEARCH_FORM': self.handle_event_search_form, + 'SEARCH_RESULT': self.handle_event_search_result, + 'RESOURCE_CONFLICT': self.handle_event_resource_conflict, + 'ROSTERX': self.handle_event_roster_item_exchange, + 'PEP_CONFIG': self.handle_event_pep_config, + 'UNIQUE_ROOM_ID_UNSUPPORTED': \ + self.handle_event_unique_room_id_unsupported, + 'UNIQUE_ROOM_ID_SUPPORTED': self.handle_event_unique_room_id_supported, + 'GPG_PASSWORD_REQUIRED': self.handle_event_gpg_password_required, + 'GPG_ALWAYS_TRUST': self.handle_event_gpg_always_trust, + 'PASSWORD_REQUIRED': self.handle_event_password_required, + 'SSL_ERROR': self.handle_event_ssl_error, + 'FINGERPRINT_ERROR': self.handle_event_fingerprint_error, + 'PLAIN_CONNECTION': self.handle_event_plain_connection, + 'INSECURE_SSL_CONNECTION': self.handle_event_insecure_ssl_connection, + 'PUBSUB_NODE_REMOVED': self.handle_event_pubsub_node_removed, + 'PUBSUB_NODE_NOT_REMOVED': self.handle_event_pubsub_node_not_removed, + 'JINGLE_INCOMING': self.handle_event_jingle_incoming, + 'JINGLE_CONNECTED': self.handle_event_jingle_connected, + 'JINGLE_DISCONNECTED': self.handle_event_jingle_disconnected, + 'JINGLE_ERROR': self.handle_event_jingle_error, + } + + def dispatch(self, event, account, data): + ''' + Dispatches an network event to the event handlers of this class + ''' + if event not in self.handlers: + log.warning('Unknown event %s dispatched to GUI: %s' % (event, data)) + else: + log.debug('Event %s distpached to GUI: %s' % (event, data)) + self.handlers[event](account, data) + + +################################################################################ +### Methods dealing with gajim.events +################################################################################ + + def add_event(self, account, jid, type_, event_args): + '''add an event to the gajim.events var''' + # We add it to the gajim.events queue + # Do we have a queue? + jid = gajim.get_jid_without_resource(jid) + no_queue = len(gajim.events.get_events(account, jid)) == 0 + # type_ can be gc-invitation file-send-error file-error file-request-error + # file-request file-completed file-stopped jingle-incoming + # event_type can be in advancedNotificationWindow.events_list + event_types = {'file-request': 'ft_request', + 'file-completed': 'ft_finished'} + event_type = event_types.get(type_) + show_in_roster = notify.get_show_in_roster(event_type, account, jid) + show_in_systray = notify.get_show_in_systray(event_type, account, jid) + event = gajim.events.create_event(type_, event_args, + show_in_roster=show_in_roster, + show_in_systray=show_in_systray) + gajim.events.add_event(account, jid, event) + + 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): + if type_ == 'gc-invitation': + self.roster.add_groupchat(jid, account, status='offline') + else: + # add contact to roster ("Not In The Roster") if he is not + self.roster.add_to_not_in_the_roster(account, jid) + else: + self.roster.draw_contact(jid, account) + + # Select the big brother contact in roster, it's visible because it has + # events. + family = gajim.contacts.get_metacontacts_family(account, jid) + if family: + nearby_family, bb_jid, bb_account = \ + self.roster._get_nearby_family_and_big_brother(family, account) + else: + bb_jid, bb_account = jid, account + self.roster.select_contact(bb_jid, bb_account) + + def handle_event(self, account, fjid, type_): + w = None + ctrl = None + session = None + + resource = gajim.get_resource_from_jid(fjid) + jid = gajim.get_jid_without_resource(fjid) + + if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'): + w = self.msg_win_mgr.get_window(jid, account) + if jid in self.minimized_controls[account]: + self.roster.on_groupchat_maximized(None, jid, account) + return + else: + ctrl = self.msg_win_mgr.get_gc_control(jid, account) + + elif type_ in ('printed_chat', 'chat', ''): + # '' is for log in/out notifications + + if type_ != '': + event = gajim.events.get_first_event(account, fjid, type_) + if not event: + event = gajim.events.get_first_event(account, jid, type_) + if not event: + return + + if type_ == 'printed_chat': + ctrl = event.parameters[0] + elif type_ == 'chat': + session = event.parameters[8] + ctrl = session.control + elif type_ == '': + ctrl = self.msg_win_mgr.get_control(fjid, account) + + if not ctrl: + 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): + # remove resource of events too + gajim.events.change_jid(account, fjid, jid) + resource = None + fjid = jid + contact = None + if resource: + contact = gajim.contacts.get_contact(account, jid, resource) + if not contact: + contact = highest_contact + + ctrl = self.new_chat(contact, account, resource = resource, session = session) + + gajim.last_message_time[account][jid] = 0 # long time ago + + w = ctrl.parent_win + elif type_ in ('printed_pm', 'pm'): + # assume that the most recently updated control we have for this party + # is the one that this event was in + event = gajim.events.get_first_event(account, fjid, type_) + if not event: + event = gajim.events.get_first_event(account, jid, type_) + + if type_ == 'printed_pm': + ctrl = event.parameters[0] + elif type_ == 'pm': + session = event.parameters[8] + + if session and session.control: + ctrl = session.control + elif not ctrl: + room_jid = jid + nick = resource + gc_contact = gajim.contacts.get_gc_contact(account, room_jid, + nick) + if gc_contact: + show = gc_contact.show + else: + show = 'offline' + gc_contact = gajim.contacts.create_gc_contact( + room_jid = room_jid, name = nick, show = show) + + if not session: + session = gajim.connections[account].make_new_session( + fjid, None, type_='pm') + + self.new_private_chat(gc_contact, account, session=session) + ctrl = session.control + + w = ctrl.parent_win + 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, fjid, type_) + if not event: + # default to jid without resource + event = gajim.events.get_first_event(account, jid, type_) + if not event: + return + # Open the window + self.roster.open_event(account, jid, event) + else: + # Open the window + self.roster.open_event(account, fjid, event) + elif type_ == 'gmail': + url=gajim.connections[account].gmail_url + if url: + helpers.launch_browser_mailer('url', url) + elif type_ == 'gc-invitation': + event = gajim.events.get_first_event(account, jid, type_) + data = event.parameters + dialogs.InvitationReceivedDialog(account, data[0], jid, data[2], + data[1], data[3]) + gajim.events.remove_events(account, jid, event) + self.roster.draw_contact(jid, account) + elif type_ == 'subscription_request': + event = gajim.events.get_first_event(account, jid, type_) + data = event.parameters + dialogs.SubscriptionRequestWindow(jid, data[0], account, data[1]) + gajim.events.remove_events(account, jid, event) + self.roster.draw_contact(jid, account) + elif type_ == 'unsubscribed': + event = gajim.events.get_first_event(account, jid, type_) + contact = event.parameters + self.show_unsubscribed_dialog(account, contact) + gajim.events.remove_events(account, jid, event) + self.roster.draw_contact(jid, account) + elif type_ == 'jingle-incoming': + event = gajim.events.get_first_event(account, jid, type_) + peerjid, sid, content_types = event.parameters + dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) + gajim.events.remove_events(account, jid, event) + if w: + w.set_active_tab(ctrl) + w.window.window.focus(gtk.get_current_event_time()) + # Using isinstance here because we want to catch all derived types + if isinstance(ctrl, ChatControlBase): + tv = ctrl.conv_textview + tv.scroll_to_end() + +################################################################################ +### Methods dealing with emoticons +################################################################################ + + def image_is_ok(self, image): + if not os.path.exists(image): + return False + img = gtk.Image() + try: + img.set_from_file(image) + except Exception: + return False + t = img.get_storage_type() + if t != gtk.IMAGE_PIXBUF and t != gtk.IMAGE_ANIMATION: + return False + return True + + @property + def basic_pattern_re(self): + try: + return self._basic_pattern_re + except AttributeError: + self._basic_pattern_re = re.compile(self.basic_pattern, re.IGNORECASE) + return self._basic_pattern_re + + @property + def emot_and_basic_re(self): + try: + return self._emot_and_basic_re + except AttributeError: + self._emot_and_basic_re = re.compile(self.emot_and_basic, + re.IGNORECASE + re.UNICODE) + return self._emot_and_basic_re + + @property + def sth_at_sth_dot_sth_re(self): + try: + return self._sth_at_sth_dot_sth_re + except AttributeError: + self._sth_at_sth_dot_sth_re = re.compile(self.sth_at_sth_dot_sth) + return self._sth_at_sth_dot_sth_re + + @property + def invalid_XML_chars_re(self): + try: + return self._invalid_XML_chars_re + except AttributeError: + self._invalid_XML_chars_re = re.compile(self.invalid_XML_chars) + return self._invalid_XML_chars_re + + def make_regexps(self): + # regexp meta characters are: . ^ $ * + ? { } [ ] \ | ( ) + # one escapes the metachars with \ + # \S matches anything but ' ' '\t' '\n' '\r' '\f' and '\v' + # \s matches any whitespace character + # \w any alphanumeric character + # \W any non-alphanumeric character + # \b means word boundary. This is a zero-width assertion that + # matches only at the beginning or end of a word. + # ^ matches at the beginning of lines + # + # * means 0 or more times + # + means 1 or more times + # ? means 0 or 1 time + # | means or + # [^*] anything but '*' (inside [] you don't have to escape metachars) + # [^\s*] anything but whitespaces and '*' + # (? in the matching string don't match ? or ) etc.. if at the end + # so http://be) will match http://be and http://be)be) will match http://be)be + + legacy_prefixes = r"((?<=\()(www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+(?=\)))"\ + r"|((www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\ + r"\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)" + # NOTE: it's ok to catch www.gr such stuff exist! + + #FIXME: recognize xmpp: and treat it specially + links = r"((?<=\()[A-Za-z][A-Za-z0-9\+\.\-]*:"\ + r"([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\ + r"(?=\)))|([A-Za-z][A-Za-z0-9\+\.\-]*:([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)" + + #2nd one: at_least_one_char@at_least_one_char.at_least_one_char + mail = r'\bmailto:\S*[^\s\W]|' r'\b\S+@\S+\.\S*[^\s\W]' + + #detects eg. *b* *bold* *bold bold* test *bold* *bold*! (*bold*) + #doesn't detect (it's a feature :P) * bold* *bold * * bold * test*bold* + formatting = r'|(?> sys.stderr, err_str + # it is good to notify the user + # in case he or she cannot see the output of the console + dialogs.ErrorDialog(_('Could not save your settings and preferences'), + err_str) + sys.exit() + + def save_avatar_files(self, jid, photo, puny_nick = None, local = False): + '''Saves an avatar to a separate file, and generate files for dbus notifications. An avatar can be given as a pixmap directly or as an decoded image.''' + puny_jid = helpers.sanitize_filename(jid) + path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) + if puny_nick: + path_to_file = os.path.join(path_to_file, puny_nick) + # remove old avatars + for typ in ('jpeg', 'png'): + if local: + path_to_original_file = path_to_file + '_local'+ '.' + typ + else: + path_to_original_file = path_to_file + '.' + typ + if os.path.isfile(path_to_original_file): + os.remove(path_to_original_file) + if local and photo: + pixbuf = photo + typ = 'png' + extension = '_local.png' # save local avatars as png file + else: + pixbuf, typ = gtkgui_helpers.get_pixbuf_from_data(photo, want_type = True) + if pixbuf is None: + return + extension = '.' + typ + if typ not in ('jpeg', 'png'): + gajim.log.debug('gtkpixbuf cannot save other than jpeg and png formats. saving %s\'avatar as png file (originaly %s)' % (jid, typ)) + typ = 'png' + extension = '.png' + path_to_original_file = path_to_file + extension + try: + pixbuf.save(path_to_original_file, typ) + except Exception, e: + log.error('Error writing avatar file %s: %s' % (path_to_original_file, + str(e))) + # Generate and save the resized, color avatar + pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'notification') + if pixbuf: + path_to_normal_file = path_to_file + '_notif_size_colored' + extension + try: + pixbuf.save(path_to_normal_file, 'png') + except Exception, e: + log.error('Error writing avatar file %s: %s' % \ + (path_to_original_file, str(e))) + # Generate and save the resized, black and white avatar + bwbuf = gtkgui_helpers.get_scaled_pixbuf( + gtkgui_helpers.make_pixbuf_grayscale(pixbuf), 'notification') + if bwbuf: + path_to_bw_file = path_to_file + '_notif_size_bw' + extension + try: + bwbuf.save(path_to_bw_file, 'png') + except Exception, e: + log.error('Error writing avatar file %s: %s' % \ + (path_to_original_file, str(e))) + + def remove_avatar_files(self, jid, puny_nick = None, local = False): + '''remove avatar files of a jid''' + puny_jid = helpers.sanitize_filename(jid) + path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) + if puny_nick: + path_to_file = os.path.join(path_to_file, puny_nick) + for ext in ('.jpeg', '.png'): + if local: + ext = '_local' + ext + path_to_original_file = path_to_file + ext + if os.path.isfile(path_to_file + ext): + os.remove(path_to_file + ext) + if os.path.isfile(path_to_file + '_notif_size_colored' + ext): + os.remove(path_to_file + '_notif_size_colored' + ext) + if os.path.isfile(path_to_file + '_notif_size_bw' + ext): + os.remove(path_to_file + '_notif_size_bw' + ext) + + def auto_join_bookmarks(self, account): + '''autojoin bookmarked GCs that have 'auto join' on for this account''' + for bm in gajim.connections[account].bookmarks: + if bm['autojoin'] in ('1', 'true'): + jid = bm['jid'] + # Only join non-opened groupchats. Opened one are already + # auto-joined on re-connection + if not jid in gajim.gc_connected[account]: + # we are not already connected + minimize = bm['minimize'] in ('1', 'true') + gajim.interface.join_gc_room(account, jid, bm['nick'], + bm['password'], minimize = minimize) + elif jid in self.minimized_controls[account]: + # more or less a hack: + # On disconnect the minimized gc contact instances + # were set to offline. Reconnect them to show up in the roster. + self.roster.add_groupchat(jid, account) + + def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password, + nick): + '''add a bookmark for this account, sorted in bookmark list''' + bm = { + 'name': name, + 'jid': jid, + 'autojoin': autojoin, + 'minimize': minimize, + 'password': password, + 'nick': nick + } + place_found = False + index = 0 + # check for duplicate entry and respect alpha order + for bookmark in gajim.connections[account].bookmarks: + if bookmark['jid'] == bm['jid']: + dialogs.ErrorDialog( + _('Bookmark already set'), + _('Group Chat "%s" is already in your bookmarks.') % bm['jid']) + return + if bookmark['name'] > bm['name']: + place_found = True + break + index += 1 + if place_found: + gajim.connections[account].bookmarks.insert(index, bm) + else: + gajim.connections[account].bookmarks.append(bm) + gajim.connections[account].store_bookmarks() + self.roster.set_actions_menu_needs_rebuild() + dialogs.InformationDialog( + _('Bookmark has been added successfully'), + _('You can manage your bookmarks via Actions menu in your roster.')) + + + # does JID exist only within a groupchat? + def is_pm_contact(self, fjid, account): + bare_jid = gajim.get_jid_without_resource(fjid) + + gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account) + + if not gc_ctrl and \ + bare_jid in self.minimized_controls[account]: + gc_ctrl = self.minimized_controls[account][bare_jid] + + return gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC + + def create_ipython_window(self): + try: + from ipython_view import IPythonView + except ImportError: + print 'ipython_view not found' + return + import pango + + if os.name == 'nt': + font = 'Lucida Console 9' + else: + font = 'Luxi Mono 10' + + window = gtk.Window() + window.set_size_request(750,550) + window.set_resizable(True) + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) + view = IPythonView() + view.modify_font(pango.FontDescription(font)) + view.set_wrap_mode(gtk.WRAP_CHAR) + sw.add(view) + window.add(sw) + window.show_all() + def on_delete(win, event): + win.hide() + return True + window.connect('delete_event',on_delete) + view.updateNamespace({'gajim': gajim}) + gajim.ipython_window = window + + def run(self): + if self.systray_capabilities and gajim.config.get('trayicon') != 'never': + self.show_systray() + + self.roster = roster_window.RosterWindow() + for account in gajim.connections: + gajim.connections[account].load_roster_from_db() + + # get instances for windows/dialogs that will show_all()/hide() + self.instances['file_transfers'] = dialogs.FileTransfersWindow() + + gobject.timeout_add(100, self.autoconnect) + timeout, in_seconds = gajim.idlequeue.PROCESS_TIMEOUT + if in_seconds: + gobject.timeout_add_seconds(timeout, self.process_connections) + else: + gobject.timeout_add(timeout, self.process_connections) + gobject.timeout_add_seconds(gajim.config.get( + 'check_idle_every_foo_seconds'), self.read_sleepy) + + # when using libasyncns we need to process resolver in regular intervals + if resolver.USE_LIBASYNCNS: + gobject.timeout_add(200, gajim.resolver.process) + + # setup the indicator + if gajim.HAVE_INDICATOR: + notify.setup_indicator_server() + + def remote_init(): + if gajim.config.get('remote_control'): + try: + import remote_control + self.remote_ctrl = remote_control.Remote() + except Exception: + pass + gobject.timeout_add_seconds(5, remote_init) + + + def __init__(self): + gajim.interface = self + gajim.thread_interface = ThreadInterface + # This is the manager and factory of message windows set by the module + self.msg_win_mgr = None + self.jabber_state_images = {'16': {}, '32': {}, 'opened': {}, + 'closed': {}} + self.emoticons_menu = None + # handler when an emoticon is clicked in emoticons_menu + self.emoticon_menuitem_clicked = None + self.minimized_controls = {} + self.status_sent_to_users = {} + self.status_sent_to_groups = {} + self.gpg_passphrase = {} + self.pass_dialog = {} + self.default_colors = { + 'inmsgcolor': gajim.config.get('inmsgcolor'), + 'outmsgcolor': gajim.config.get('outmsgcolor'), + 'inmsgtxtcolor': gajim.config.get('inmsgtxtcolor'), + 'outmsgtxtcolor': gajim.config.get('outmsgtxtcolor'), + 'statusmsgcolor': gajim.config.get('statusmsgcolor'), + 'urlmsgcolor': gajim.config.get('urlmsgcolor'), + } + + cfg_was_read = parser.read() + gajim.logger.reset_shown_unread_messages() + # override logging settings from config (don't take care of '-q' option) + if gajim.config.get('verbose'): + logging_helpers.set_verbose() + + # Is Gajim default app? + if os.name != 'nt' and gajim.config.get('check_if_gajim_is_default'): + gtkgui_helpers.possibly_set_gajim_as_xmpp_handler() + + for account in gajim.config.get_per('accounts'): + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + gajim.ZEROCONF_ACC_NAME = account + break + # Is gnome configured to activate row on single click ? + try: + import gconf + client = gconf.client_get_default() + click_policy = client.get_string( + '/apps/nautilus/preferences/click_policy') + if click_policy == 'single': + gajim.single_click = True + except Exception: + pass + # add default status messages if there is not in the config file + if len(gajim.config.get_per('statusmsg')) == 0: + default = gajim.config.statusmsg_default + for msg in default: + gajim.config.add_per('statusmsg', msg) + gajim.config.set_per('statusmsg', msg, 'message', default[msg][0]) + gajim.config.set_per('statusmsg', msg, 'activity', default[msg][1]) + gajim.config.set_per('statusmsg', msg, 'subactivity', + default[msg][2]) + gajim.config.set_per('statusmsg', msg, 'activity_text', + default[msg][3]) + gajim.config.set_per('statusmsg', msg, 'mood', default[msg][4]) + gajim.config.set_per('statusmsg', msg, 'mood_text', default[msg][5]) + #add default themes if there is not in the config file + theme = gajim.config.get('roster_theme') + if not theme in gajim.config.get_per('themes'): + gajim.config.set('roster_theme', _('default')) + if len(gajim.config.get_per('themes')) == 0: + d = ['accounttextcolor', 'accountbgcolor', 'accountfont', + 'accountfontattrs', 'grouptextcolor', 'groupbgcolor', 'groupfont', + 'groupfontattrs', 'contacttextcolor', 'contactbgcolor', + 'contactfont', 'contactfontattrs', 'bannertextcolor', + 'bannerbgcolor'] + + default = gajim.config.themes_default + for theme_name in default: + gajim.config.add_per('themes', theme_name) + theme = default[theme_name] + for o in d: + gajim.config.set_per('themes', theme_name, o, + theme[d.index(o)]) + + if gajim.config.get('autodetect_browser_mailer') or not cfg_was_read: + gtkgui_helpers.autodetect_browser_mailer() + + gajim.idlequeue = idlequeue.get_idlequeue() + # resolve and keep current record of resolved hosts + gajim.resolver = resolver.get_resolver(gajim.idlequeue) + gajim.socks5queue = socks5.SocksQueue(gajim.idlequeue, + self.handle_event_file_rcv_completed, + self.handle_event_file_progress, + self.handle_event_file_error) + gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue) + gajim.default_session_type = ChatControlSession + self.register_handlers() + if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'active') \ + and gajim.HAVE_ZEROCONF: + gajim.connections[gajim.ZEROCONF_ACC_NAME] = \ + connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) + for account in gajim.config.get_per('accounts'): + if not gajim.config.get_per('accounts', account, 'is_zeroconf') and \ + gajim.config.get_per('accounts', account, 'active'): + 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') + gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url') + + self.instances = {} + + for a in gajim.connections: + self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {}, + 'search': {}, 'online_dialog': {}} + # online_dialog contains all dialogs that have a meaning only when we + # are not disconnected + self.minimized_controls[a] = {} + gajim.contacts.add_account(a) + gajim.groups[a] = {} + gajim.gc_connected[a] = {} + gajim.automatic_rooms[a] = {} + gajim.newly_added[a] = [] + gajim.to_be_removed[a] = [] + gajim.nicks[a] = gajim.config.get_per('accounts', a, 'name') + gajim.block_signed_in_notifications[a] = True + gajim.sleeper_state[a] = 0 + gajim.encrypted_chats[a] = [] + gajim.last_message_time[a] = {} + gajim.status_before_autoaway[a] = '' + gajim.transport_avatar[a] = {} + gajim.gajim_optional_features[a] = [] + gajim.caps_hash[a] = '' + + helpers.update_optional_features() + # prepopulate data which we are sure of; note: we do not log these info + for account in gajim.connections: + gajimcaps = caps.capscache[('sha-1', gajim.caps_hash[account])] + gajimcaps.identities = [gajim.gajim_identity] + gajimcaps.features = gajim.gajim_common_features + \ + gajim.gajim_optional_features[account] + + self.remote_ctrl = None + + if gajim.config.get('networkmanager_support') and dbus_support.supported: + import network_manager_listener + + # Handle gnome screensaver + if dbus_support.supported: + def gnome_screensaver_ActiveChanged_cb(active): + if not active: + for account in gajim.connections: + if gajim.sleeper_state[account] == 'autoaway-forced': + # We came back online ofter gnome-screensaver autoaway + self.roster.send_status(account, 'online', + gajim.status_before_autoaway[account]) + gajim.status_before_autoaway[account] = '' + gajim.sleeper_state[account] = 'online' + return + if not gajim.config.get('autoaway'): + # Don't go auto away if user disabled the option + return + for account in gajim.connections: + if account not in gajim.sleeper_state or \ + not gajim.sleeper_state[account]: + continue + if gajim.sleeper_state[account] == 'online': + # we save out online status + gajim.status_before_autoaway[account] = \ + gajim.connections[account].status + # we go away (no auto status) [we pass True to auto param] + auto_message = gajim.config.get('autoaway_message') + if not auto_message: + auto_message = gajim.connections[account].status + else: + auto_message = auto_message.replace('$S','%(status)s') + auto_message = auto_message.replace('$T','%(time)s') + auto_message = auto_message % { + 'status': gajim.status_before_autoaway[account], + 'time': gajim.config.get('autoxatime') + } + self.roster.send_status(account, 'away', auto_message, + auto=True) + gajim.sleeper_state[account] = 'autoaway-forced' + + try: + bus = dbus.SessionBus() + bus.add_signal_receiver(gnome_screensaver_ActiveChanged_cb, + 'ActiveChanged', 'org.gnome.ScreenSaver') + except Exception: + pass + + self.show_vcard_when_connect = [] + + self.sleeper = common.sleepy.Sleepy( + gajim.config.get('autoawaytime') * 60, # make minutes to seconds + gajim.config.get('autoxatime') * 60) + + gtkgui_helpers.make_jabber_state_images() + + self.systray_enabled = False + self.systray_capabilities = False + + if (os.name == 'nt'): + 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() + else: + gajim.config.set('trayicon', 'never') + + path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'gajim.png') + pix = gtk.gdk.pixbuf_new_from_file(path_to_file) + # set the icon to all windows + gtk.window_set_default_icon(pix) + + self.init_emoticons() + self.make_regexps() + + # get transports type from DB + gajim.transport_type = gajim.logger.get_transports_type() + + # test is dictionnary is present for speller + if gajim.config.get('use_speller'): + lang = gajim.config.get('speller_language') + if not lang: + lang = gajim.LANG + tv = gtk.TextView() + try: + import gtkspell + spell = gtkspell.Spell(tv, lang) + except (ImportError, TypeError, RuntimeError, OSError): + dialogs.AspellDictError(lang) + + if gajim.config.get('soundplayer') == '': + # only on first time Gajim starts + commands = ('aplay', 'play', 'esdplay', 'artsplay', 'ossplay') + for command in commands: + if helpers.is_in_path(command): + if command == 'aplay': + command += ' -q' + gajim.config.set('soundplayer', command) + break + + self.last_ftwindow_update = 0 + + self.music_track_changed_signal = None + + +class PassphraseRequest: + def __init__(self, keyid): + self.keyid = keyid + self.callbacks = [] + self.dialog_created = False + self.dialog = None + self.completed = False + + def interrupt(self): + self.dialog.window.destroy() + self.callbacks = [] + + def run_callback(self, account, callback): + gajim.connections[account].gpg_passphrase(self.passphrase) + callback() + + def add_callback(self, account, cb): + if self.completed: + self.run_callback(account, cb) + else: + self.callbacks.append((account, cb)) + if not self.dialog_created: + self.create_dialog(account) + + def complete(self, passphrase): + self.passphrase = passphrase + self.completed = True + if passphrase is not None: + gobject.timeout_add_seconds(30, gajim.interface.forget_gpg_passphrase, + self.keyid) + for (account, cb) in self.callbacks: + self.run_callback(account, cb) + del self.callbacks + + def create_dialog(self, account): + title = _('Passphrase Required') + second = _('Enter GPG key passphrase for key %(keyid)s (account ' + '%(account)s).') % {'keyid': self.keyid, 'account': account} + + def _cancel(): + # user cancelled, continue without GPG + self.complete(None) + + def _ok(passphrase, checked, count): + result = gajim.connections[account].test_gpg_passphrase(passphrase) + if result == 'ok': + # passphrase is good + self.complete(passphrase) + return + elif result == 'expired': + dialogs.ErrorDialog(_('GPG key expired'), + _('Your GPG key has expired, you will be connected to %s without' + ' OpenPGP.') % account) + # Don't try to connect with GPG + gajim.connections[account].continue_connect_info[2] = False + self.complete(None) + return + + if count < 3: + # ask again + dialogs.PassphraseDialog(_('Wrong Passphrase'), + _('Please retype your GPG passphrase or press Cancel.'), + ok_handler=(_ok, count + 1), cancel_handler=_cancel) + else: + # user failed 3 times, continue without GPG + self.complete(None) + + self.dialog = dialogs.PassphraseDialog(title, second, ok_handler=(_ok, 1), + cancel_handler=_cancel) + self.dialog_created = True + + +class ThreadInterface: + def __init__(self, func, func_args, callback, callback_args): + '''Call a function in a thread + + :param func: the function to call in the thread + :param func_args: list or arguments for this function + :param callback: callback to call once function is finished + :param callback_args: list of arguments for this callback + ''' + def thread_function(func, func_args, callback, callback_args): + output = func(*func_args) + gobject.idle_add(callback, output, *callback_args) + + Thread(target=thread_function, args=(func, func_args, callback, + callback_args)).start() From 124483ea49f525fd902a29f178d19855bb4db255 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 4 Nov 2009 20:37:22 +0100 Subject: [PATCH 21/29] prevent traceback. Fixes #5402 --- src/roster_window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/roster_window.py b/src/roster_window.py index cd3a8c9ef..9181c0e5b 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1234,6 +1234,9 @@ class RosterWindow: child_path = self.model.get_path(child_iter) path = self.modelfilter.convert_child_path_to_path(child_path) + if not path: + continue + if not self.tree.row_expanded(path) and icon_name != 'event': iterC = self.model.iter_children(child_iter) while iterC: From 4343d706a06db5bf3c8d1a16edb536a35a03385d Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 4 Nov 2009 21:17:57 +0100 Subject: [PATCH 22/29] use default port for bosh proxy when none is provided. Fixes #5400, #5401 --- src/common/xmpp/transports_nb.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 257be2840..3fc4571a7 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -56,9 +56,18 @@ def get_proxy_data_from_dict(proxy): proxy_type = proxy['type'] if proxy_type == 'bosh' and not proxy['bosh_useproxy']: # with BOSH not over proxy we have to parse the hostname from BOSH URI - tcp_host = urisplit(proxy['bosh_uri'])[1] - tcp_host, tcp_port = tcp_host.split(':', 1) - tcp_port = int(tcp_port) + proto, tcp_host, path = urisplit(proxy['bosh_uri']) + spl = tcp_host.split(':', 1) + if len(spl) == 1: + # No port were set + tcp_host = tcp_host[0] + if proto == 'https': + tcp_port = 443 + else: + tcp_port = 80 + else: + tcp_host, tcp_port = spl + tcp_port = int(tcp_port) else: # with proxy!=bosh or with bosh over HTTP proxy we're connecting to proxy # machine From 2aef55ad2abc70c06f007444a6d78f805a91a4c0 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 4 Nov 2009 21:56:33 +0100 Subject: [PATCH 23/29] better URI splitting code. see #5400, #5401 --- src/common/xmpp/bosh.py | 4 +-- src/common/xmpp/transports_nb.py | 42 +++++++++++++++----------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index a1a9bb60a..bb6ea4dd8 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -84,8 +84,8 @@ class NonBlockingBOSH(NonBlockingTransport): self.proxy_dict['type'] = 'http' # with SSL over proxy, we do HTTP CONNECT to proxy to open a channel to # BOSH Connection Manager - host, port = urisplit(self.bosh_uri)[1].split(':', 1) - self.proxy_dict['xmpp_server'] = (host, int(port)) + host, port = urisplit(self.bosh_uri)[1:3] + self.proxy_dict['xmpp_server'] = (host, port) self.proxy_dict['credentials'] = self.proxy_creds diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 3fc4571a7..eb428afcf 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -41,33 +41,33 @@ log = logging.getLogger('gajim.c.x.transports_nb') def urisplit(uri): ''' - Function for splitting URI string to tuple (protocol, host, path). - e.g. urisplit('http://httpcm.jabber.org/webclient') returns - ('http', 'httpcm.jabber.org', '/webclient') + Function for splitting URI string to tuple (protocol, host, port, path). + e.g. urisplit('http://httpcm.jabber.org:123/webclient') returns + ('http', 'httpcm.jabber.org', 123, '/webclient') + return 443 as default port if proto is https else 80 ''' import re - regex = '(([^:/]+)(://))?([^/]*)(/?.*)' + regex = '(([^:/]+)(://))?([^/]*)(:)*([^/]*)(/?.*)' grouped = re.match(regex, uri).groups() - proto, host, path = grouped[1], grouped[3], grouped[4] - return proto, host, path + proto, host, port, path = grouped[1], grouped[3], grouped[5], grouped[6] + if not port: + if proto == 'https': + port = 443 + else: + port = 80 + else: + try: + port = int(port) + except: + port = 80 + return proto, host, port, path def get_proxy_data_from_dict(proxy): tcp_host, tcp_port, proxy_user, proxy_pass = None, None, None, None proxy_type = proxy['type'] if proxy_type == 'bosh' and not proxy['bosh_useproxy']: # with BOSH not over proxy we have to parse the hostname from BOSH URI - proto, tcp_host, path = urisplit(proxy['bosh_uri']) - spl = tcp_host.split(':', 1) - if len(spl) == 1: - # No port were set - tcp_host = tcp_host[0] - if proto == 'https': - tcp_port = 443 - else: - tcp_port = 80 - else: - tcp_host, tcp_port = spl - tcp_port = int(tcp_port) + proto, tcp_host, tcp_port, path = urisplit(proxy['bosh_uri']) else: # with proxy!=bosh or with bosh over HTTP proxy we're connecting to proxy # machine @@ -611,10 +611,8 @@ class NonBlockingHTTP(NonBlockingTCP): NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, proxy_dict) - self.http_protocol, self.http_host, self.http_path = urisplit( - http_dict['http_uri']) - self.http_host, self.http_port = self.http_host.split(':', 1) - self.http_port = int(self.http_port) + self.http_protocol, self.http_host, self.http_port, self.http_path = \ + urisplit(http_dict['http_uri']) self.http_protocol = self.http_protocol or 'http' self.http_path = self.http_path or '/' self.http_version = http_dict['http_version'] From b2a4c92e7a72c6d87af384da2ba1afefb8cebbf0 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 4 Nov 2009 22:06:45 +0100 Subject: [PATCH 24/29] better error handling --- src/common/xmpp/transports_nb.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index eb428afcf..8f6364c4a 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -58,8 +58,13 @@ def urisplit(uri): else: try: port = int(port) - except: - port = 80 + except ValueError: + if proto == 'https': + port = 443 + else: + port = 80 + log.warn('port cannot be extracted from BOSH URL %s, using port %i', + uri, port) return proto, host, port, path def get_proxy_data_from_dict(proxy): From c1cbc076454a4d96dcf0c0c5fc53d73082fc492f Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Wed, 4 Nov 2009 23:05:20 +0100 Subject: [PATCH 25/29] Check for empty caps values and not just for None. See #5399 This prevents our caps cache from getting filled with invalid caps values. Furtheremore, NullClientCaps has been saveguarded so that it won't fail for people with already tainted caches. --- src/common/caps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/caps.py b/src/common/caps.py index f676ad265..b3fe72e9c 100644 --- a/src/common/caps.py +++ b/src/common/caps.py @@ -198,7 +198,7 @@ class NullClientCaps(AbstractClientCaps): def _lookup_in_cache(self, caps_cache): # lookup something which does not exist to get a new CacheItem created - cache_item = caps_cache[('old', '')] + cache_item = caps_cache[('dummy', '')] assert cache_item.queried == 0 return cache_item @@ -359,10 +359,10 @@ class ConnectionCaps(object): else: hash_method, node, caps_hash = caps_tag['hash'], caps_tag['node'], caps_tag['ver'] - if node is None or caps_hash is None: + if not node or not caps_hash: # improper caps in stanza, ignore client capabilities. client_caps = NullClientCaps() - elif hash_method is None: + elif not hash_method: client_caps = OldClientCaps(caps_hash, node) else: client_caps = ClientCaps(caps_hash, node, hash_method) From addaaa9242161d2147d5f6b4c8eea75c2c35704c Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Wed, 4 Nov 2009 23:46:16 +0100 Subject: [PATCH 26/29] Fix regex used in urisplit. It failed to split the host:port part. --- src/common/xmpp/transports_nb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 8f6364c4a..34e200d37 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -47,7 +47,7 @@ def urisplit(uri): return 443 as default port if proto is https else 80 ''' import re - regex = '(([^:/]+)(://))?([^/]*)(:)*([^/]*)(/?.*)' + regex = '(([^:/]+)(://))?([^:/]*)(:)?([^/]*)(/?.*)' grouped = re.match(regex, uri).groups() proto, host, port, path = grouped[1], grouped[3], grouped[5], grouped[6] if not port: From 1a76b72b5810d1949e898621bc1e475ab3a1caa4 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 5 Nov 2009 08:50:21 +0100 Subject: [PATCH 27/29] fix bosh url parsing (wrong regex replaced by urlparse.urlsplit() funxtion) --- src/common/xmpp/transports_nb.py | 23 +++++++++-------------- test/test_xmpp_transports_nb.py | 9 ++++++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 8f6364c4a..e23348bba 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -35,6 +35,7 @@ import errno import time import traceback import base64 +import urlparse import logging log = logging.getLogger('gajim.c.x.transports_nb') @@ -46,25 +47,19 @@ def urisplit(uri): ('http', 'httpcm.jabber.org', 123, '/webclient') return 443 as default port if proto is https else 80 ''' - import re - regex = '(([^:/]+)(://))?([^/]*)(:)*([^/]*)(/?.*)' - grouped = re.match(regex, uri).groups() - proto, host, port, path = grouped[1], grouped[3], grouped[5], grouped[6] + splitted = urlparse.urlsplit(uri) + proto, host, path = splitted.scheme, splitted.hostname, splitted.path + try: + port = splitted.port + except ValueError: + log.warn('port cannot be extracted from BOSH URL %s, using default port' \ + % uri) + port = '' if not port: if proto == 'https': port = 443 else: port = 80 - else: - try: - port = int(port) - except ValueError: - if proto == 'https': - port = 443 - else: - port = 80 - log.warn('port cannot be extracted from BOSH URL %s, using port %i', - uri, port) return proto, host, port, path def get_proxy_data_from_dict(proxy): diff --git a/test/test_xmpp_transports_nb.py b/test/test_xmpp_transports_nb.py index edc2f3ebb..d3b1ff9c9 100644 --- a/test/test_xmpp_transports_nb.py +++ b/test/test_xmpp_transports_nb.py @@ -17,13 +17,16 @@ class TestModuleLevelFunctions(unittest.TestCase): Test class for functions defined at module level ''' def test_urisplit(self): - def check_uri(uri, proto, host, path): - _proto, _host, _path = transports_nb.urisplit(uri) + def check_uri(uri, proto, host, port, path): + _proto, _host, _port, _path = transports_nb.urisplit(uri) self.assertEqual(proto, _proto) self.assertEqual(host, _host) + self.assertEqual(port, _port) self.assertEqual(path, _path) check_uri('http://httpcm.jabber.org/webclient', - proto='http', host='httpcm.jabber.org', path='/webclient') + proto='http', host='httpcm.jabber.org', port=80, path='/webclient') + check_uri('http://httpcm.jabber.org:5280/webclient', + proto='http', host='httpcm.jabber.org', port=5280, path='/webclient') def test_get_proxy_data_from_dict(self): def check_dict(proxy_dict, host, port, user, passwd): From 3157cf0b1bcc361677e23c3014e6ee470881602c Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Thu, 5 Nov 2009 08:53:19 +0100 Subject: [PATCH 28/29] Allow multiple event handlers for a single Interface event. We can use that until the plugin branch is available. --- src/gui_interface.py | 191 +++++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 88 deletions(-) diff --git a/src/gui_interface.py b/src/gui_interface.py index 591294cb6..3f8a50036 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1982,107 +1982,122 @@ class Interface: dialogs.WarningDialog(_('PEP node was not removed'), _('PEP node %(node)s was not removed: %(message)s') % { 'node': data[1], 'message': data[2]}) + + def register_handler(self, event, handler): + if event not in self.handlers: + self.handlers[event] = [] + + if handler not in self.handlers[event]: + self.handlers[event].append(handler) + + def unregister_handler(self, event, handler): + self.handlers[event].remove(handler) def register_handlers(self): self.handlers = { - 'ROSTER': self.handle_event_roster, - 'WARNING': self.handle_event_warning, - 'ERROR': self.handle_event_error, - 'INFORMATION': self.handle_event_information, - 'ERROR_ANSWER': self.handle_event_error_answer, - 'STATUS': self.handle_event_status, - 'NOTIFY': self.handle_event_notify, - 'MSGERROR': self.handle_event_msgerror, - 'MSGSENT': self.handle_event_msgsent, - 'MSGNOTSENT': self.handle_event_msgnotsent, - 'SUBSCRIBED': self.handle_event_subscribed, - 'UNSUBSCRIBED': self.handle_event_unsubscribed, - 'SUBSCRIBE': self.handle_event_subscribe, - 'AGENT_ERROR_INFO': self.handle_event_agent_info_error, - 'AGENT_ERROR_ITEMS': self.handle_event_agent_items_error, - 'AGENT_REMOVED': self.handle_event_agent_removed, - 'REGISTER_AGENT_INFO': self.handle_event_register_agent_info, - 'AGENT_INFO_ITEMS': self.handle_event_agent_info_items, - 'AGENT_INFO_INFO': self.handle_event_agent_info_info, - 'QUIT': self.handle_event_quit, - 'NEW_ACC_CONNECTED': self.handle_event_new_acc_connected, - 'NEW_ACC_NOT_CONNECTED': self.handle_event_new_acc_not_connected, - 'ACC_OK': self.handle_event_acc_ok, - 'ACC_NOT_OK': self.handle_event_acc_not_ok, - 'MYVCARD': self.handle_event_myvcard, - 'VCARD': self.handle_event_vcard, - 'LAST_STATUS_TIME': self.handle_event_last_status_time, - 'OS_INFO': self.handle_event_os_info, - 'ENTITY_TIME': self.handle_event_entity_time, - 'GC_NOTIFY': self.handle_event_gc_notify, - 'GC_MSG': self.handle_event_gc_msg, - 'GC_SUBJECT': self.handle_event_gc_subject, - 'GC_CONFIG': self.handle_event_gc_config, - 'GC_CONFIG_CHANGE': self.handle_event_gc_config_change, - 'GC_INVITATION': self.handle_event_gc_invitation, - 'GC_AFFILIATION': self.handle_event_gc_affiliation, - 'GC_PASSWORD_REQUIRED': self.handle_event_gc_password_required, - 'BAD_PASSPHRASE': self.handle_event_bad_passphrase, - 'ROSTER_INFO': self.handle_event_roster_info, - 'BOOKMARKS': self.handle_event_bookmarks, - 'CON_TYPE': self.handle_event_con_type, - 'CONNECTION_LOST': self.handle_event_connection_lost, - 'FILE_REQUEST': self.handle_event_file_request, - 'GMAIL_NOTIFY': self.handle_event_gmail_notify, - 'FILE_REQUEST_ERROR': self.handle_event_file_request_error, - 'FILE_SEND_ERROR': self.handle_event_file_send_error, - 'STANZA_ARRIVED': self.handle_event_stanza_arrived, - 'STANZA_SENT': self.handle_event_stanza_sent, - 'HTTP_AUTH': self.handle_event_http_auth, - 'VCARD_PUBLISHED': self.handle_event_vcard_published, - 'VCARD_NOT_PUBLISHED': self.handle_event_vcard_not_published, - 'ASK_NEW_NICK': self.handle_event_ask_new_nick, - 'SIGNED_IN': self.handle_event_signed_in, - 'METACONTACTS': self.handle_event_metacontacts, - 'ATOM_ENTRY': self.handle_atom_entry, - 'FAILED_DECRYPT': self.handle_event_failed_decrypt, - 'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received, - 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, + 'ROSTER': [self.handle_event_roster], + 'WARNING': [self.handle_event_warning], + 'ERROR': [self.handle_event_error], + 'INFORMATION': [self.handle_event_information], + 'ERROR_ANSWER': [self.handle_event_error_answer], + 'STATUS': [self.handle_event_status], + 'NOTIFY': [self.handle_event_notify], + 'MSGERROR': [self.handle_event_msgerror], + 'MSGSENT': [self.handle_event_msgsent], + 'MSGNOTSENT': [self.handle_event_msgnotsent], + 'SUBSCRIBED': [self.handle_event_subscribed], + 'UNSUBSCRIBED': [self.handle_event_unsubscribed], + 'SUBSCRIBE': [self.handle_event_subscribe], + 'AGENT_ERROR_INFO': [self.handle_event_agent_info_error], + 'AGENT_ERROR_ITEMS': [self.handle_event_agent_items_error], + 'AGENT_REMOVED': [self.handle_event_agent_removed], + 'REGISTER_AGENT_INFO': [self.handle_event_register_agent_info], + 'AGENT_INFO_ITEMS': [self.handle_event_agent_info_items], + 'AGENT_INFO_INFO': [self.handle_event_agent_info_info], + 'QUIT': [self.handle_event_quit], + 'NEW_ACC_CONNECTED': [self.handle_event_new_acc_connected], + 'NEW_ACC_NOT_CONNECTED': [self.handle_event_new_acc_not_connected], + 'ACC_OK': [self.handle_event_acc_ok], + 'ACC_NOT_OK': [self.handle_event_acc_not_ok], + 'MYVCARD': [self.handle_event_myvcard], + 'VCARD': [self.handle_event_vcard], + 'LAST_STATUS_TIME': [self.handle_event_last_status_time], + 'OS_INFO': [self.handle_event_os_info], + 'ENTITY_TIME': [self.handle_event_entity_time], + 'GC_NOTIFY': [self.handle_event_gc_notify], + 'GC_MSG': [self.handle_event_gc_msg], + 'GC_SUBJECT': [self.handle_event_gc_subject], + 'GC_CONFIG': [self.handle_event_gc_config], + 'GC_CONFIG_CHANGE': [self.handle_event_gc_config_change], + 'GC_INVITATION': [self.handle_event_gc_invitation], + 'GC_AFFILIATION': [self.handle_event_gc_affiliation], + 'GC_PASSWORD_REQUIRED': [self.handle_event_gc_password_required], + 'BAD_PASSPHRASE': [self.handle_event_bad_passphrase], + 'ROSTER_INFO': [self.handle_event_roster_info], + 'BOOKMARKS': [self.handle_event_bookmarks], + 'CON_TYPE': [self.handle_event_con_type], + 'CONNECTION_LOST': [self.handle_event_connection_lost], + 'FILE_REQUEST': [self.handle_event_file_request], + 'GMAIL_NOTIFY': [self.handle_event_gmail_notify], + 'FILE_REQUEST_ERROR': [self.handle_event_file_request_error], + 'FILE_SEND_ERROR': [self.handle_event_file_send_error], + 'STANZA_ARRIVED': [self.handle_event_stanza_arrived], + 'STANZA_SENT': [self.handle_event_stanza_sent], + 'HTTP_AUTH': [self.handle_event_http_auth], + 'VCARD_PUBLISHED': [self.handle_event_vcard_published], + 'VCARD_NOT_PUBLISHED': [self.handle_event_vcard_not_published], + 'ASK_NEW_NICK': [self.handle_event_ask_new_nick], + 'SIGNED_IN': [self.handle_event_signed_in], + 'METACONTACTS': [self.handle_event_metacontacts], + 'ATOM_ENTRY': [self.handle_atom_entry], + 'FAILED_DECRYPT': [self.handle_event_failed_decrypt], + 'PRIVACY_LISTS_RECEIVED': [self.handle_event_privacy_lists_received], + 'PRIVACY_LIST_RECEIVED': [self.handle_event_privacy_list_received], 'PRIVACY_LISTS_ACTIVE_DEFAULT': \ - self.handle_event_privacy_lists_active_default, - 'PRIVACY_LIST_REMOVED': self.handle_event_privacy_list_removed, - 'ZC_NAME_CONFLICT': self.handle_event_zc_name_conflict, - 'PING_SENT': self.handle_event_ping_sent, - 'PING_REPLY': self.handle_event_ping_reply, - 'PING_ERROR': self.handle_event_ping_error, - 'SEARCH_FORM': self.handle_event_search_form, - 'SEARCH_RESULT': self.handle_event_search_result, - 'RESOURCE_CONFLICT': self.handle_event_resource_conflict, - 'ROSTERX': self.handle_event_roster_item_exchange, - 'PEP_CONFIG': self.handle_event_pep_config, + [self.handle_event_privacy_lists_active_default], + 'PRIVACY_LIST_REMOVED': [self.handle_event_privacy_list_removed], + 'ZC_NAME_CONFLICT': [self.handle_event_zc_name_conflict], + 'PING_SENT': [self.handle_event_ping_sent], + 'PING_REPLY': [self.handle_event_ping_reply], + 'PING_ERROR': [self.handle_event_ping_error], + 'SEARCH_FORM': [self.handle_event_search_form], + 'SEARCH_RESULT': [self.handle_event_search_result], + 'RESOURCE_CONFLICT': [self.handle_event_resource_conflict], + 'ROSTERX': [self.handle_event_roster_item_exchange], + 'PEP_CONFIG': [self.handle_event_pep_config], 'UNIQUE_ROOM_ID_UNSUPPORTED': \ - self.handle_event_unique_room_id_unsupported, - 'UNIQUE_ROOM_ID_SUPPORTED': self.handle_event_unique_room_id_supported, - 'GPG_PASSWORD_REQUIRED': self.handle_event_gpg_password_required, - 'GPG_ALWAYS_TRUST': self.handle_event_gpg_always_trust, - 'PASSWORD_REQUIRED': self.handle_event_password_required, - 'SSL_ERROR': self.handle_event_ssl_error, - 'FINGERPRINT_ERROR': self.handle_event_fingerprint_error, - 'PLAIN_CONNECTION': self.handle_event_plain_connection, - 'INSECURE_SSL_CONNECTION': self.handle_event_insecure_ssl_connection, - 'PUBSUB_NODE_REMOVED': self.handle_event_pubsub_node_removed, - 'PUBSUB_NODE_NOT_REMOVED': self.handle_event_pubsub_node_not_removed, - 'JINGLE_INCOMING': self.handle_event_jingle_incoming, - 'JINGLE_CONNECTED': self.handle_event_jingle_connected, - 'JINGLE_DISCONNECTED': self.handle_event_jingle_disconnected, - 'JINGLE_ERROR': self.handle_event_jingle_error, + [self.handle_event_unique_room_id_unsupported], + 'UNIQUE_ROOM_ID_SUPPORTED': [self.handle_event_unique_room_id_supported], + 'GPG_PASSWORD_REQUIRED': [self.handle_event_gpg_password_required], + 'GPG_ALWAYS_TRUST': [self.handle_event_gpg_always_trust], + 'PASSWORD_REQUIRED': [self.handle_event_password_required], + 'SSL_ERROR': [self.handle_event_ssl_error], + 'FINGERPRINT_ERROR': [self.handle_event_fingerprint_error], + 'PLAIN_CONNECTION': [self.handle_event_plain_connection], + 'INSECURE_SSL_CONNECTION': [self.handle_event_insecure_ssl_connection], + 'PUBSUB_NODE_REMOVED': [self.handle_event_pubsub_node_removed], + 'PUBSUB_NODE_NOT_REMOVED': [self.handle_event_pubsub_node_not_removed], + 'JINGLE_INCOMING': [self.handle_event_jingle_incoming], + 'JINGLE_CONNECTED': [self.handle_event_jingle_connected], + 'JINGLE_DISCONNECTED': [self.handle_event_jingle_disconnected], + 'JINGLE_ERROR': [self.handle_event_jingle_error], } - def dispatch(self, event, account, data): + def dispatch(self, event, account, data): ''' - Dispatches an network event to the event handlers of this class + Dispatches an network event to the event handlers of this class. + + Return true if it could be dispatched to alteast one handler. ''' if event not in self.handlers: log.warning('Unknown event %s dispatched to GUI: %s' % (event, data)) + return False else: log.debug('Event %s distpached to GUI: %s' % (event, data)) - self.handlers[event](account, data) - + for handler in self.handlers[event]: + handler(account, data) + return len(self.handlers[event]) + ################################################################################ ### Methods dealing with gajim.events From 2e5bf4d0d2b205cc76221cbe253176bdbc56d3a3 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Thu, 5 Nov 2009 09:06:46 +0100 Subject: [PATCH 29/29] Organize tests into unit and integration tests. Integration tests can depend on UI, network or both. Unittests use neither. --- test/integration/__init__.py | 6 ++ .../test_gui_event_integration.py | 0 test/{ => integration}/test_resolver.py | 0 test/{ => integration}/test_roster.py | 0 test/{ => integration}/test_xmpp_client_nb.py | 0 .../test_xmpp_transports_nb.py | 59 +------------- test/lib/gajim_mocks.py | 4 + test/runtests.py | 23 +++--- test/unit/__init__.py | 5 ++ test/{ => unit}/test_caps.py | 0 test/{ => unit}/test_contacts.py | 0 test/{ => unit}/test_gui_interface.py | 13 +-- test/{ => unit}/test_sessions.py | 16 ++-- test/{ => unit}/test_xmpp_dispatcher_nb.py | 0 test/unit/test_xmpp_transports_nb.py | 80 +++++++++++++++++++ 15 files changed, 126 insertions(+), 80 deletions(-) create mode 100644 test/integration/__init__.py rename test/{ => integration}/test_gui_event_integration.py (100%) rename test/{ => integration}/test_resolver.py (100%) rename test/{ => integration}/test_roster.py (100%) rename test/{ => integration}/test_xmpp_client_nb.py (100%) rename test/{ => integration}/test_xmpp_transports_nb.py (82%) create mode 100644 test/unit/__init__.py rename test/{ => unit}/test_caps.py (100%) rename test/{ => unit}/test_contacts.py (100%) rename test/{ => unit}/test_gui_interface.py (95%) rename test/{ => unit}/test_sessions.py (99%) rename test/{ => unit}/test_xmpp_dispatcher_nb.py (100%) create mode 100644 test/unit/test_xmpp_transports_nb.py diff --git a/test/integration/__init__.py b/test/integration/__init__.py new file mode 100644 index 000000000..0adf844f1 --- /dev/null +++ b/test/integration/__init__.py @@ -0,0 +1,6 @@ +''' + +This package contains integration tests. Integration tests are tests +which require or include UI, network or both. + +''' \ No newline at end of file diff --git a/test/test_gui_event_integration.py b/test/integration/test_gui_event_integration.py similarity index 100% rename from test/test_gui_event_integration.py rename to test/integration/test_gui_event_integration.py diff --git a/test/test_resolver.py b/test/integration/test_resolver.py similarity index 100% rename from test/test_resolver.py rename to test/integration/test_resolver.py diff --git a/test/test_roster.py b/test/integration/test_roster.py similarity index 100% rename from test/test_roster.py rename to test/integration/test_roster.py diff --git a/test/test_xmpp_client_nb.py b/test/integration/test_xmpp_client_nb.py similarity index 100% rename from test/test_xmpp_client_nb.py rename to test/integration/test_xmpp_client_nb.py diff --git a/test/test_xmpp_transports_nb.py b/test/integration/test_xmpp_transports_nb.py similarity index 82% rename from test/test_xmpp_transports_nb.py rename to test/integration/test_xmpp_transports_nb.py index edc2f3ebb..f6cbab510 100644 --- a/test/test_xmpp_transports_nb.py +++ b/test/integration/test_xmpp_transports_nb.py @@ -1,5 +1,6 @@ ''' -Unit test for tranports classes. +Integration test for tranports classes. See unit for the ordinary +unit tests of this module. ''' import unittest @@ -12,62 +13,6 @@ from xmpp_mocks import IdleQueueThread, IdleMock from common.xmpp import transports_nb -class TestModuleLevelFunctions(unittest.TestCase): - ''' - Test class for functions defined at module level - ''' - def test_urisplit(self): - def check_uri(uri, proto, host, path): - _proto, _host, _path = transports_nb.urisplit(uri) - self.assertEqual(proto, _proto) - self.assertEqual(host, _host) - self.assertEqual(path, _path) - check_uri('http://httpcm.jabber.org/webclient', - proto='http', host='httpcm.jabber.org', path='/webclient') - - def test_get_proxy_data_from_dict(self): - def check_dict(proxy_dict, host, port, user, passwd): - _host, _port, _user, _passwd = transports_nb.get_proxy_data_from_dict( - proxy_dict) - self.assertEqual(_host, host) - self.assertEqual(_port, port) - self.assertEqual(_user, user) - self.assertEqual(_passwd, passwd) - - bosh_dict = {'bosh_content': u'text/xml; charset=utf-8', - 'bosh_hold': 2, - 'bosh_http_pipelining': False, - 'bosh_uri': u'http://gajim.org:5280/http-bind', - 'bosh_useproxy': False, - 'bosh_wait': 30, - 'bosh_wait_for_restart_response': False, - 'host': u'172.16.99.11', - 'pass': u'pass', - 'port': 3128, - 'type': u'bosh', - 'useauth': True, - 'user': u'user'} - check_dict(bosh_dict, host=u'gajim.org', port=5280, user=u'user', - passwd=u'pass') - - proxy_dict = {'bosh_content': u'text/xml; charset=utf-8', - 'bosh_hold': 2, - 'bosh_http_pipelining': False, - 'bosh_port': 5280, - 'bosh_uri': u'', - 'bosh_useproxy': True, - 'bosh_wait': 30, - 'bosh_wait_for_restart_response': False, - 'host': u'172.16.99.11', - 'pass': u'pass', - 'port': 3128, - 'type': 'socks5', - 'useauth': True, - 'user': u'user'} - check_dict(proxy_dict, host=u'172.16.99.11', port=3128, user=u'user', - passwd=u'pass') - - class AbstractTransportTest(unittest.TestCase): ''' Encapsulates Idlequeue instantiation for transports and more...''' diff --git a/test/lib/gajim_mocks.py b/test/lib/gajim_mocks.py index 44b18f037..d45e488e6 100644 --- a/test/lib/gajim_mocks.py +++ b/test/lib/gajim_mocks.py @@ -91,6 +91,7 @@ class MockChatControl(Mock): def __eq__(self, other): return self is other + class MockInterface(Mock): def __init__(self, *args): Mock.__init__(self, *args) @@ -113,14 +114,17 @@ class MockInterface(Mock): self.jabber_state_images = {'16': Mock(), '32': Mock(), 'opened': Mock(), 'closed': Mock()} + class MockLogger(Mock): def __init__(self): Mock.__init__(self, {'write': None, 'get_transports_type': {}}) + class MockContact(Mock): def __nonzero__(self): return True + import random class MockSession(Mock): diff --git a/test/runtests.py b/test/runtests.py index bb7e86191..5a9f5f663 100755 --- a/test/runtests.py +++ b/test/runtests.py @@ -4,7 +4,7 @@ ''' Runs Gajim's Test Suite -Non GUI related tests will be run on each commit. +Unit tests tests will be run on each commit. ''' import sys @@ -35,20 +35,21 @@ for o, a in opts: sys.exit(2) # new test modules need to be added manually -modules = ( 'test_xmpp_dispatcher_nb', - 'test_xmpp_client_nb', - 'test_xmpp_transports_nb', - 'test_resolver', - 'test_caps', - 'test_contacts', - 'test_gui_interface', +modules = ( 'unit.test_xmpp_dispatcher_nb', + 'unit.test_xmpp_transports_nb', + 'unit.test_caps', + 'unit.test_contacts', + 'unit.test_gui_interface', + 'unit.test_sessions', ) #modules = () if use_x: - modules += ('test_gui_event_integration', - 'test_roster', - 'test_sessions', + modules += ('integration.test_gui_event_integration', + 'integration.test_roster', + 'integration.test_resolver', + 'integration.test_xmpp_client_nb', + 'integration.test_xmpp_transports_nb' ) nb_errors = 0 diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 000000000..8e5f2ad7c --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,5 @@ +''' + +This package just contains plain unit tests + +''' \ No newline at end of file diff --git a/test/test_caps.py b/test/unit/test_caps.py similarity index 100% rename from test/test_caps.py rename to test/unit/test_caps.py diff --git a/test/test_contacts.py b/test/unit/test_contacts.py similarity index 100% rename from test/test_contacts.py rename to test/unit/test_contacts.py diff --git a/test/test_gui_interface.py b/test/unit/test_gui_interface.py similarity index 95% rename from test/test_gui_interface.py rename to test/unit/test_gui_interface.py index b2acc019f..57b91ad4b 100644 --- a/test/test_gui_interface.py +++ b/test/unit/test_gui_interface.py @@ -3,13 +3,15 @@ import unittest import lib lib.setup_env() -from gajim_mocks import * -gajim.logger = MockLogger() - from common import logging_helpers logging_helpers.set_quiet() -from interface import Interface +from common import gajim + +from gajim_mocks import MockLogger +gajim.logger = MockLogger() + +from gui_interface import Interface class Test(unittest.TestCase): @@ -76,9 +78,8 @@ class Test(unittest.TestCase): sut.dispatch(event, 'account', 'data') self.assertTrue(self.called_a and self.called_b, msg="Both handlers should have been called") - -def test_links_regexp_entire(self): + def test_links_regexp_entire(self): sut = Interface() def assert_matches_all(str_): m = sut.basic_pattern_re.match(str_) diff --git a/test/test_sessions.py b/test/unit/test_sessions.py similarity index 99% rename from test/test_sessions.py rename to test/unit/test_sessions.py index a28b9259b..724bac56d 100644 --- a/test/test_sessions.py +++ b/test/unit/test_sessions.py @@ -5,19 +5,27 @@ import time import lib lib.setup_env() + +import notify + from common import gajim from common import xmpp +from common.stanza_session import StanzaSession +from session import ChatControlSession + from mock import Mock, expectParams from gajim_mocks import * -from common.stanza_session import StanzaSession +gajim.interface = MockInterface() + # name to use for the test account account_name = 'test' class TestStanzaSession(unittest.TestCase): ''' Testclass for common/stanzasession.py ''' + def setUp(self): self.jid = 'test@example.org/Gajim' self.conn = MockConnection(account_name, {'send_stanza': None}) @@ -68,14 +76,10 @@ class TestStanzaSession(unittest.TestCase): calls = self.conn.mockGetNamedCalls('send_stanza') self.assertEqual(0, len(calls)) -from session import ChatControlSession - -gajim.interface = MockInterface() - -import notify class TestChatControlSession(unittest.TestCase): ''' Testclass for session.py ''' + def setUp(self): self.jid = 'test@example.org/Gajim' self.conn = MockConnection(account_name, {'send_stanza': None}) diff --git a/test/test_xmpp_dispatcher_nb.py b/test/unit/test_xmpp_dispatcher_nb.py similarity index 100% rename from test/test_xmpp_dispatcher_nb.py rename to test/unit/test_xmpp_dispatcher_nb.py diff --git a/test/unit/test_xmpp_transports_nb.py b/test/unit/test_xmpp_transports_nb.py new file mode 100644 index 000000000..b81f60d61 --- /dev/null +++ b/test/unit/test_xmpp_transports_nb.py @@ -0,0 +1,80 @@ +''' +Unit test for tranports classes. +''' + +import unittest + +import lib +lib.setup_env() + +from common.xmpp import transports_nb + + +class TestModuleLevelFunctions(unittest.TestCase): + ''' + Test class for functions defined at module level + ''' + def test_urisplit(self): + def check_uri(uri, proto, host, port, path): + _proto, _host, _port, _path = transports_nb.urisplit(uri) + self.assertEqual(proto, _proto) + self.assertEqual(host, _host) + self.assertEqual(path, _path) + self.assertEqual(port, _port) + + check_uri('http://httpcm.jabber.org:5280/webclient', proto='http', + host='httpcm.jabber.org', port=5280, path='/webclient') + + check_uri('http://httpcm.jabber.org/webclient', proto='http', + host='httpcm.jabber.org', port=80, path='/webclient') + + check_uri('https://httpcm.jabber.org/webclient', proto='https', + host='httpcm.jabber.org', port=443, path='/webclient') + + def test_get_proxy_data_from_dict(self): + def check_dict(proxy_dict, host, port, user, passwd): + _host, _port, _user, _passwd = transports_nb.get_proxy_data_from_dict( + proxy_dict) + self.assertEqual(_host, host) + self.assertEqual(_port, port) + self.assertEqual(_user, user) + self.assertEqual(_passwd, passwd) + + bosh_dict = {'bosh_content': u'text/xml; charset=utf-8', + 'bosh_hold': 2, + 'bosh_http_pipelining': False, + 'bosh_uri': u'http://gajim.org:5280/http-bind', + 'bosh_useproxy': False, + 'bosh_wait': 30, + 'bosh_wait_for_restart_response': False, + 'host': u'172.16.99.11', + 'pass': u'pass', + 'port': 3128, + 'type': u'bosh', + 'useauth': True, + 'user': u'user'} + check_dict(bosh_dict, host=u'gajim.org', port=5280, user=u'user', + passwd=u'pass') + + proxy_dict = {'bosh_content': u'text/xml; charset=utf-8', + 'bosh_hold': 2, + 'bosh_http_pipelining': False, + 'bosh_port': 5280, + 'bosh_uri': u'', + 'bosh_useproxy': True, + 'bosh_wait': 30, + 'bosh_wait_for_restart_response': False, + 'host': u'172.16.99.11', + 'pass': u'pass', + 'port': 3128, + 'type': 'socks5', + 'useauth': True, + 'user': u'user'} + check_dict(proxy_dict, host=u'172.16.99.11', port=3128, user=u'user', + passwd=u'pass') + + +if __name__ == '__main__': + unittest.main() + +# vim: se ts=3: