correctly set transient window for muc error dialogs. Fixes #6943
[gajim.git] / src / gui_interface.py
blobb027130c3a10019e58502118edd72a6589bbaab4
1 # -*- coding:utf-8 -*-
2 ## src/gajim.py
3 ##
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
6 ## Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
7 ## Norman Rasmussen <norman AT rasmussen.co.za>
8 ## Stéphan Kochen <stephan AT kochen.nl>
9 ## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
10 ## Alex Mauer <hawke AT hawkesnest.net>
11 ## Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
12 ## Nikos Kouremenos <kourem AT gmail.com>
13 ## Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
14 ## Stefan Bethge <stefan AT lanpartei.de>
15 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
16 ## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
17 ## James Newton <redshodan AT gmail.com>
18 ## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
19 ## Julien Pivotto <roidelapluie AT gmail.com>
20 ## Stephan Erb <steve-e AT h3c.de>
21 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
23 ## This file is part of Gajim.
25 ## Gajim is free software; you can redistribute it and/or modify
26 ## it under the terms of the GNU General Public License as published
27 ## by the Free Software Foundation; version 3 only.
29 ## Gajim is distributed in the hope that it will be useful,
30 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
31 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 ## GNU General Public License for more details.
34 ## You should have received a copy of the GNU General Public License
35 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
38 import os
39 import sys
40 import re
41 import time
42 import math
43 from subprocess import Popen
45 import gtk
46 import gobject
48 from common import i18n
49 from common import gajim
51 from common import dbus_support
52 if dbus_support.supported:
53 from music_track_listener import MusicTrackListener
54 from common import location_listener
55 import dbus
57 import gtkgui_helpers
59 import dialogs
60 import notify
61 import message_control
63 from chat_control import ChatControlBase
64 from chat_control import ChatControl
65 from groupchat_control import GroupchatControl
66 from groupchat_control import PrivateChatControl
68 from atom_window import AtomWindow
69 from session import ChatControlSession
71 import common.sleepy
73 from common.xmpp import idlequeue
74 from common.zeroconf import connection_zeroconf
75 from common import resolver
76 from common import caps_cache
77 from common import proxy65_manager
78 from common import socks5
79 from common import helpers
80 from common import dataforms
81 from common import passwords
82 from common import logging_helpers
83 from common.connection_handlers_events import OurShowEvent, \
84 FileRequestErrorEvent
85 from common.connection import Connection
87 import roster_window
88 import profile_window
89 import config
90 from threading import Thread
91 from common import ged
93 gajimpaths = common.configpaths.gajimpaths
94 config_filename = gajimpaths['CONFIG_FILE']
96 from common import optparser
97 parser = optparser.OptionsParser(config_filename)
99 import logging
100 log = logging.getLogger('gajim.interface')
102 class Interface:
104 ################################################################################
105 ### Methods handling events from connection
106 ################################################################################
108 def handle_event_error(self, unused, data):
109 #('ERROR', account, (title_text, section_text))
110 dialogs.ErrorDialog(data[0], data[1])
112 def handle_event_db_error(self, unused, data):
113 #('DB_ERROR', account, (title_text, section_text))
114 if self.db_error_dialog:
115 return
116 self.db_error_dialog = dialogs.ErrorDialog(data[0], data[1])
117 def destroyed(win):
118 self.db_error_dialog = None
119 self.db_error_dialog.connect('destroy', destroyed)
121 def handle_event_information(self, unused, data):
122 #('INFORMATION', account, (title_text, section_text))
123 dialogs.InformationDialog(data[0], data[1])
125 def handle_ask_new_nick(self, account, room_jid):
126 title = _('Unable to join group chat')
127 prompt = _('Your desired nickname in group chat %s is in use or '
128 'registered by another occupant.\nPlease specify another nickname '
129 'below:') % room_jid
130 check_text = _('Always use this nickname when there is a conflict')
131 if 'change_nick_dialog' in self.instances:
132 self.instances['change_nick_dialog'].add_room(account, room_jid,
133 prompt)
134 else:
135 self.instances['change_nick_dialog'] = dialogs.ChangeNickDialog(
136 account, room_jid, title, prompt)
138 def handle_event_http_auth(self, obj):
139 #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
140 def response(account, answer):
141 obj.conn.build_http_auth_answer(obj.stanza, answer)
143 def on_yes(is_checked, obj):
144 response(obj, 'yes')
146 account = obj.conn.name
147 sec_msg = _('Do you accept this request?')
148 if gajim.get_number_of_connected_accounts() > 1:
149 sec_msg = _('Do you accept this request on account %s?') % account
150 if obj.msg:
151 sec_msg = obj.msg + '\n' + sec_msg
152 dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for '
153 '%(url)s (id: %(id)s)') % {'method': obj.method, 'url': obj.url,
154 'id': obj.iq_id}, sec_msg, on_response_yes=(on_yes, obj),
155 on_response_no=(response, obj, 'no'))
157 def handle_event_iq_error(self, obj):
158 #('ERROR_ANSWER', account, (id_, fjid, errmsg, errcode))
159 if unicode(obj.errcode) in ('400', '403', '406') and obj.id_:
160 # show the error dialog
161 ft = self.instances['file_transfers']
162 sid = obj.id_
163 if len(obj.id_) > 3 and obj.id_[2] == '_':
164 sid = obj.id_[3:]
165 if sid in ft.files_props['s']:
166 file_props = ft.files_props['s'][sid]
167 if unicode(obj.errcode) == '400':
168 file_props['error'] = -3
169 else:
170 file_props['error'] = -4
171 gajim.nec.push_incoming_event(FileRequestErrorEvent(None,
172 conn=obj.conn, jid=obj.jid, file_props=file_props,
173 error_msg=obj.errmsg))
174 obj.conn.disconnect_transfer(file_props)
175 return
176 elif unicode(obj.errcode) == '404':
177 sid = obj.id_
178 if len(obj.id_) > 3 and obj.id_[2] == '_':
179 sid = obj.id_[3:]
180 if sid in obj.conn.files_props:
181 file_props = obj.conn.files_props[sid]
182 self.handle_event_file_send_error(obj.conn.name, (obj.fjid,
183 file_props))
184 obj.conn.disconnect_transfer(file_props)
185 return
187 ctrl = self.msg_win_mgr.get_control(obj.fjid, obj.conn.name)
188 if ctrl and ctrl.type_id == message_control.TYPE_GC:
189 ctrl.print_conversation('Error %s: %s' % (obj.errcode, obj.errmsg))
191 def handle_event_connection_lost(self, obj):
192 # ('CONNECTION_LOST', account, [title, text])
193 path = gtkgui_helpers.get_icon_path('gajim-connection_lost', 48)
194 account = obj.conn.name
195 notify.popup(_('Connection Failed'), account, account,
196 'connection_failed', path, obj.title, obj.msg)
198 def unblock_signed_in_notifications(self, account):
199 gajim.block_signed_in_notifications[account] = False
201 def handle_event_status(self, obj): # OUR status
202 #('STATUS', account, show)
203 account = obj.conn.name
204 if obj.show in ('offline', 'error'):
205 for name in self.instances[account]['online_dialog'].keys():
206 # .keys() is needed to not have a dictionary length changed
207 # during iteration error
208 self.instances[account]['online_dialog'][name].destroy()
209 if name in self.instances[account]['online_dialog']:
210 # destroy handler may have already removed it
211 del self.instances[account]['online_dialog'][name]
212 for request in self.gpg_passphrase.values():
213 if request:
214 request.interrupt(account=account)
215 if account in self.pass_dialog:
216 self.pass_dialog[account].window.destroy()
217 if obj.show == 'offline':
218 gajim.block_signed_in_notifications[account] = True
219 else:
220 # 30 seconds after we change our status to sth else than offline
221 # we stop blocking notifications of any kind
222 # this prevents from getting the roster items as 'just signed in'
223 # contacts. 30 seconds should be enough time
224 gobject.timeout_add_seconds(30,
225 self.unblock_signed_in_notifications, account)
227 if account in self.show_vcard_when_connect and obj.show not in (
228 'offline', 'error'):
229 self.edit_own_details(account)
231 def edit_own_details(self, account):
232 jid = gajim.get_jid_from_account(account)
233 if 'profile' not in self.instances[account]:
234 self.instances[account]['profile'] = \
235 profile_window.ProfileWindow(account)
236 gajim.connections[account].request_vcard(jid)
238 def handle_gc_error(self, gc_control, pritext, sectext):
239 if gc_control and gc_control.autorejoin is not None:
240 if gc_control.error_dialog:
241 gc_control.error_dialog.destroy()
242 def on_close(dummy):
243 gc_control.error_dialog.destroy()
244 gc_control.error_dialog = None
245 gc_control.error_dialog = dialogs.ErrorDialog(pritext, sectext,
246 on_response_ok=on_close, on_response_cancel=on_close)
247 gc_control.error_dialog.set_modal(False)
248 if gc_control.parent_win:
249 gc_control.error_dialog.set_transient_for(
250 gc_control.parent_win.window)
251 else:
252 d = dialogs.ErrorDialog(pritext, sectext)
253 if gc_control and gc_control.parent_win:
254 d.set_transient_for(gc_control.parent_win.window)
255 d.set_modal(False)
257 def handle_gc_password_required(self, account, room_jid, nick):
258 def on_ok(text):
259 gajim.connections[account].join_gc(nick, room_jid, text)
260 gajim.gc_passwords[room_jid] = text
261 gc_control.error_dialog = None
263 def on_cancel():
264 # get and destroy window
265 if room_jid in gajim.interface.minimized_controls[account]:
266 self.roster.on_disconnect(None, room_jid, account)
267 else:
268 win = self.msg_win_mgr.get_window(room_jid, account)
269 ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
270 win.remove_tab(ctrl, 3)
271 gc_control.error_dialog = None
273 gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
274 if gc_control:
275 if gc_control.error_dialog:
276 gc_control.error_dialog.destroy()
278 gc_control.error_dialog = dialogs.InputDialog(_('Password Required'),
279 _('A Password is required to join the room %s. Please type it.') % \
280 room_jid, is_modal=False, ok_handler=on_ok,
281 cancel_handler=on_cancel)
282 gc_control.error_dialog.input_entry.set_visibility(False)
284 def handle_event_gc_presence(self, obj):
285 gc_control = obj.gc_control
286 if obj.ptype == 'error':
287 if obj.errcode == '503':
288 # maximum user number reached
289 self.handle_gc_error(gc_control,
290 _('Unable to join group chat'),
291 _('Maximum number of users for <b>%s</b> has been reached')\
292 % obj.room_jid)
293 elif (obj.errcode == '401') or (obj.errcon == 'not-authorized'):
294 # password required to join
295 self.handle_gc_password_required(obj.conn.name, obj.room_jid,
296 obj.nick)
297 elif (obj.errcode == '403') or (obj.errcon == 'forbidden'):
298 # we are banned
299 self.handle_gc_error(gc_control, _('Unable to join group chat'),
300 _('You are banned from group chat <b>%s</b>.') % \
301 obj.room_jid)
302 elif (obj.errcode == '404') or (obj.errcon in ('item-not-found',
303 'remote-server-not-found')):
304 # group chat does not exist
305 self.handle_gc_error(gc_control, _('Unable to join group chat'),
306 _('Group chat <b>%s</b> does not exist.') % obj.room_jid)
307 elif (obj.errcode == '405') or (obj.errcon == 'not-allowed'):
308 self.handle_gc_error(gc_control, _('Unable to join group chat'),
309 _('Group chat creation is restricted.'))
310 elif (obj.errcode == '406') or (obj.errcon == 'not-acceptable'):
311 self.handle_gc_error(gc_control, _('Unable to join group chat'),
312 _('Your registered nickname must be used in group chat '
313 '<b>%s</b>.') % obj.room_jid)
314 elif (obj.errcode == '407') or (obj.errcon == \
315 'registration-required'):
316 self.handle_gc_error(gc_control, _('Unable to join group chat'),
317 _('You are not in the members list in groupchat %s.') % \
318 obj.room_jid)
319 elif (obj.errcode == '409') or (obj.errcon == 'conflict'):
320 self.handle_ask_new_nick(obj.conn.name, obj.room_jid)
321 elif gc_control:
322 gc_control.print_conversation('Error %s: %s' % (obj.errcode,
323 obj.errmsg))
324 if gc_control and gc_control.autorejoin:
325 gc_control.autorejoin = False
327 def handle_event_presence(self, obj):
328 # 'NOTIFY' (account, (jid, status, status message, resource,
329 # priority, # keyID, timestamp, contact_nickname))
331 # Contact changed show
333 account = obj.conn.name
334 jid = obj.jid
335 show = obj.show
336 status = obj.status
337 resource = obj.resource or ''
339 jid_list = gajim.contacts.get_jid_list(account)
341 # unset custom status
342 if (obj.old_show == 0 and obj.new_show > 1) or \
343 (obj.old_show > 1 and obj.new_show == 0 and obj.conn.connected > 1):
344 if account in self.status_sent_to_users and \
345 jid in self.status_sent_to_users[account]:
346 del self.status_sent_to_users[account][jid]
348 if gajim.jid_is_transport(jid):
349 # It must be an agent
351 # transport just signed in/out, don't show
352 # popup notifications for 30s
353 account_jid = account + '/' + jid
354 gajim.block_signed_in_notifications[account_jid] = True
355 gobject.timeout_add_seconds(30,
356 self.unblock_signed_in_notifications, account_jid)
358 highest = gajim.contacts.get_contact_with_highest_priority(account, jid)
359 is_highest = (highest and highest.resource == resource)
361 # disconnect the session from the ctrl if the highest resource has
362 # changed
363 if (obj.was_highest and not is_highest) or \
364 (not obj.was_highest and is_highest):
365 ctrl = self.msg_win_mgr.get_control(jid, account)
366 if ctrl:
367 ctrl.no_autonegotiation = False
368 ctrl.set_session(None)
369 ctrl.contact = highest
371 def handle_event_msgerror(self, obj):
372 #'MSGERROR' (account, (jid, error_code, error_msg, msg, time[session]))
373 account = obj.conn.name
374 jids = obj.fjid.split('/', 1)
375 jid = jids[0]
377 if obj.error_code == '503':
378 # If we get server-not-found error, stop sending chatstates
379 for contact in gajim.contacts.get_contacts(account, jid):
380 contact.composing_xep = False
382 session = obj.session
384 gc_control = self.msg_win_mgr.get_gc_control(jid, account)
385 if not gc_control and \
386 jid in self.minimized_controls[account]:
387 gc_control = self.minimized_controls[account][jid]
388 if gc_control and gc_control.type_id != message_control.TYPE_GC:
389 gc_control = None
390 if gc_control:
391 if len(jids) > 1: # it's a pm
392 nick = jids[1]
394 if session:
395 ctrl = session.control
396 else:
397 ctrl = self.msg_win_mgr.get_control(obj.fjid, account)
399 if not ctrl:
400 tv = gc_control.list_treeview
401 model = tv.get_model()
402 iter_ = gc_control.get_contact_iter(nick)
403 if iter_:
404 show = model[iter_][3]
405 else:
406 show = 'offline'
407 gc_c = gajim.contacts.create_gc_contact(room_jid=jid,
408 account=account, name=nick, show=show)
409 ctrl = self.new_private_chat(gc_c, account, session)
411 ctrl.print_conversation(_('Error %(code)s: %(msg)s') % {
412 'code': obj.error_code, 'msg': obj.error_msg}, 'status')
413 return
415 gc_control.print_conversation(_('Error %(code)s: %(msg)s') % {
416 'code': obj.error_code, 'msg': obj.error_msg}, 'status')
417 if gc_control.parent_win and \
418 gc_control.parent_win.get_active_jid() == jid:
419 gc_control.set_subject(gc_control.subject)
420 return
422 if gajim.jid_is_transport(jid):
423 jid = jid.replace('@', '')
424 msg = obj.error_msg
425 if obj.msg:
426 msg = _('error while sending %(message)s ( %(error)s )') % {
427 'message': obj.msg, 'error': msg}
428 if session:
429 session.roster_message(jid, msg, obj.time_, msg_type='error')
431 def handle_event_msgsent(self, obj):
432 #('MSGSENT', account, (jid, msg, keyID))
433 # do not play sound when standalone chatstate message (eg no msg)
434 if obj.message and gajim.config.get_per('soundevents', 'message_sent',
435 'enabled'):
436 helpers.play_sound('message_sent')
438 def handle_event_msgnotsent(self, obj):
439 #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session))
440 msg = _('error while sending %(message)s ( %(error)s )') % {
441 'message': obj.message, 'error': obj.error}
442 if not obj.session:
443 # No session. This can happen when sending a message from
444 # gajim-remote
445 log.warn(msg)
446 return
447 obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name,
448 msg_type='error')
450 def handle_event_subscribe_presence(self, obj):
451 #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
452 account = obj.conn.name
453 if helpers.allow_popup_window(account) or not self.systray_enabled:
454 if obj.jid in self.instances[account]['sub_request']:
455 self.instances[account]['sub_request'][obj.jid].window.destroy()
456 self.instances[account]['sub_request'][obj.jid] = \
457 dialogs.SubscriptionRequestWindow(obj.jid, obj.status, account,
458 obj.user_nick)
459 return
461 self.add_event(account, obj.jid, 'subscription_request', (obj.status,
462 obj.user_nick))
464 if helpers.allow_showing_notification(account):
465 path = gtkgui_helpers.get_icon_path('gajim-subscription_request',
467 event_type = _('Subscription request')
468 notify.popup(event_type, obj.jid, account, 'subscription_request',
469 path, event_type, obj.jid)
471 def handle_event_subscribed_presence(self, obj):
472 #('SUBSCRIBED', account, (jid, resource))
473 account = obj.conn.name
474 if obj.jid in gajim.contacts.get_jid_list(account):
475 c = gajim.contacts.get_first_contact_from_jid(account, obj.jid)
476 c.resource = obj.resource
477 self.roster.remove_contact_from_groups(c.jid, account,
478 [_('Not in Roster'), _('Observers')], update=False)
479 else:
480 keyID = ''
481 attached_keys = gajim.config.get_per('accounts', account,
482 'attached_gpg_keys').split()
483 if obj.jid in attached_keys:
484 keyID = attached_keys[attached_keys.index(obj.jid) + 1]
485 name = obj.jid.split('@', 1)[0]
486 name = name.split('%', 1)[0]
487 contact1 = gajim.contacts.create_contact(jid=obj.jid,
488 account=account, name=name, groups=[], show='online',
489 status='online', ask='to', resource=obj.resource, keyID=keyID)
490 gajim.contacts.add_contact(account, contact1)
491 self.roster.add_contact(obj.jid, account)
492 dialogs.InformationDialog(_('Authorization accepted'),
493 _('The contact "%s" has authorized you to see his or her status.')
494 % obj.jid)
496 def show_unsubscribed_dialog(self, account, contact):
497 def on_yes(is_checked, list_):
498 self.roster.on_req_usub(None, list_)
499 list_ = [(contact, account)]
500 dialogs.YesNoDialog(
501 _('Contact "%s" removed subscription from you') % contact.jid,
502 _('You will always see him or her as offline.\nDo you want to '
503 'remove him or her from your contact list?'),
504 on_response_yes=(on_yes, list_))
505 # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does
506 # not show deny
508 def handle_event_unsubscribed_presence(self, obj):
509 #('UNSUBSCRIBED', account, jid)
510 account = obj.conn.name
511 contact = gajim.contacts.get_first_contact_from_jid(account, obj.jid)
512 if not contact:
513 return
515 if helpers.allow_popup_window(account) or not self.systray_enabled:
516 self.show_unsubscribed_dialog(account, contact)
517 return
519 self.add_event(account, obj.jid, 'unsubscribed', contact)
521 if helpers.allow_showing_notification(account):
522 path = gtkgui_helpers.get_icon_path('gajim-unsubscribed', 48)
523 event_type = _('Unsubscribed')
524 notify.popup(event_type, obj.jid, account, 'unsubscribed', path,
525 event_type, obj.jid)
527 def handle_event_register_agent_info(self, obj):
528 # ('REGISTER_AGENT_INFO', account, (agent, infos, is_form))
529 # info in a dataform if is_form is True
530 if obj.is_form or 'instructions' in obj.config:
531 config.ServiceRegistrationWindow(obj.agent, obj.config,
532 obj.conn.name, obj.is_form)
533 else:
534 dialogs.ErrorDialog(_('Contact with "%s" cannot be established') % \
535 obj.agent, _('Check your connection or try again later.'))
537 def handle_event_vcard(self, obj):
538 # ('VCARD', account, data)
539 '''vcard holds the vcard data'''
540 our_jid = gajim.get_jid_from_account(obj.conn.name)
541 if obj.jid == our_jid:
542 if obj.nickname:
543 gajim.nicks[obj.conn.name] = obj.nickname
544 if obj.conn.name in self.show_vcard_when_connect:
545 self.show_vcard_when_connect.remove(obj.conn.name)
547 def handle_event_last_status_time(self, obj):
548 # ('LAST_STATUS_TIME', account, (jid, resource, seconds, status))
549 if obj.seconds < 0:
550 # Ann error occured
551 return
552 account = obj.conn.name
553 c = gajim.contacts.get_contact(account, obj.jid, obj.resource)
554 if c: # c can be none if it's a gc contact
555 if obj.status:
556 c.status = obj.status
557 self.roster.draw_contact(c.jid, account) # draw offline status
558 last_time = time.localtime(time.time() - obj.seconds)
559 if c.show == 'offline':
560 c.last_status_time = last_time
561 else:
562 c.last_activity_time = last_time
563 if self.roster.tooltip.id and self.roster.tooltip.win:
564 self.roster.tooltip.update_last_time(last_time)
566 def handle_event_gc_config(self, obj):
567 #('GC_CONFIG', account, (jid, form_node)) config is a dict
568 account = obj.conn.name
569 if obj.jid in gajim.automatic_rooms[account]:
570 if 'continue_tag' in gajim.automatic_rooms[account][obj.jid]:
571 # We're converting chat to muc. allow participants to invite
572 for f in obj.dataform.iter_fields():
573 if f.var == 'muc#roomconfig_allowinvites':
574 f.value = True
575 elif f.var == 'muc#roomconfig_publicroom':
576 f.value = False
577 elif f.var == 'muc#roomconfig_membersonly':
578 f.value = True
579 elif f.var == 'public_list':
580 f.value = False
581 obj.conn.send_gc_config(obj.jid, obj.dataform.get_purged())
582 else:
583 # use default configuration
584 obj.conn.send_gc_config(obj.jid, obj.form_node)
585 # invite contacts
586 # check if it is necessary to add <continue />
587 continue_tag = False
588 if 'continue_tag' in gajim.automatic_rooms[account][obj.jid]:
589 continue_tag = True
590 if 'invities' in gajim.automatic_rooms[account][obj.jid]:
591 for jid in gajim.automatic_rooms[account][obj.jid]['invities']:
592 obj.conn.send_invite(obj.jid, jid,
593 continue_tag=continue_tag)
594 del gajim.automatic_rooms[account][obj.jid]
595 elif obj.jid not in self.instances[account]['gc_config']:
596 self.instances[account]['gc_config'][obj.jid] = \
597 config.GroupchatConfigWindow(account, obj.jid, obj.dataform)
599 def handle_event_gc_affiliation(self, obj):
600 #('GC_AFFILIATION', account, (room_jid, users_dict))
601 account = obj.conn.name
602 if obj.jid in self.instances[account]['gc_config']:
603 self.instances[account]['gc_config'][obj.jid].\
604 affiliation_list_received(obj.users_dict)
606 def handle_event_gc_invitation(self, obj):
607 #('GC_INVITATION', (room_jid, jid_from, reason, password, is_continued))
608 jid = gajim.get_jid_without_resource(obj.jid_from)
609 account = obj.conn.name
610 if helpers.allow_popup_window(account) or not self.systray_enabled:
611 dialogs.InvitationReceivedDialog(account, obj.room_jid, jid,
612 obj.password, obj.reason, is_continued=obj.is_continued)
613 return
615 self.add_event(account, jid, 'gc-invitation', (obj.room_jid,
616 obj.reason, obj.password, obj.is_continued))
618 if helpers.allow_showing_notification(account):
619 path = gtkgui_helpers.get_icon_path('gajim-gc_invitation', 48)
620 event_type = _('Groupchat Invitation')
621 notify.popup(event_type, jid, account, 'gc-invitation', path,
622 event_type, obj.room_jid)
624 def forget_gpg_passphrase(self, keyid):
625 if keyid in self.gpg_passphrase:
626 del self.gpg_passphrase[keyid]
627 return False
629 def handle_event_bad_gpg_passphrase(self, obj):
630 #('BAD_PASSPHRASE', account, ())
631 if obj.use_gpg_agent:
632 sectext = _('You configured Gajim to use GPG agent, but there is no'
633 ' GPG agent running or it returned a wrong passphrase.\n')
634 sectext += _('You are currently connected without your OpenPGP '
635 'key.')
636 dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext)
637 else:
638 path = gtkgui_helpers.get_icon_path('gajim-warning', 48)
639 account = obj.conn.name
640 notify.popup('warning', account, account, 'warning', path,
641 _('OpenGPG Passphrase Incorrect'),
642 _('You are currently connected without your OpenPGP key.'))
643 self.forget_gpg_passphrase(obj.keyID)
645 def handle_event_client_cert_passphrase(self, obj):
646 def on_ok(passphrase, checked):
647 obj.conn.on_client_cert_passphrase(passphrase, obj.con, obj.port,
648 obj.secure_tuple)
650 def on_cancel():
651 obj.conn.on_client_cert_passphrase('', obj.con, obj.port,
652 obj.secure_tuple)
654 dialogs.PassphraseDialog(_('Certificate Passphrase Required'),
655 _('Enter the passphrase for the certificate for account %s') % \
656 obj.conn.name, ok_handler=on_ok, cancel_handler=on_cancel)
658 def handle_event_gpg_password_required(self, obj):
659 #('GPG_PASSWORD_REQUIRED', account, (callback,))
660 if obj.keyid in self.gpg_passphrase:
661 request = self.gpg_passphrase[obj.keyid]
662 else:
663 request = PassphraseRequest(obj.keyid)
664 self.gpg_passphrase[obj.keyid] = request
665 request.add_callback(obj.conn.name, obj.callback)
667 def handle_event_gpg_trust_key(self, obj):
668 #('GPG_ALWAYS_TRUST', account, callback)
669 def on_yes(checked):
670 if checked:
671 obj.conn.gpg.always_trust = True
672 obj.callback(True)
674 def on_no():
675 obj.callback(False)
677 dialogs.YesNoDialog(_('GPG key not trusted'), _('The GPG key used to '
678 'encrypt this chat is not trusted. Do you really want to encrypt '
679 'this message?'), checktext=_('_Do not ask me again'),
680 on_response_yes=on_yes, on_response_no=on_no)
682 def handle_event_password_required(self, obj):
683 #('PASSWORD_REQUIRED', account, None)
684 account = obj.conn.name
685 if account in self.pass_dialog:
686 return
687 text = _('Enter your password for account %s') % account
688 if passwords.USER_HAS_GNOMEKEYRING and \
689 not passwords.USER_USES_GNOMEKEYRING:
690 text += '\n' + _('Gnome Keyring is installed but not '
691 'correctly started (environment variable probably not '
692 'correctly set)')
694 def on_ok(passphrase, save):
695 if save:
696 gajim.config.set_per('accounts', account, 'savepass', True)
697 passwords.save_password(account, passphrase)
698 obj.conn.set_password(passphrase)
699 del self.pass_dialog[account]
701 def on_cancel():
702 self.roster.set_state(account, 'offline')
703 self.roster.update_status_combobox()
704 del self.pass_dialog[account]
706 self.pass_dialog[account] = dialogs.PassphraseDialog(
707 _('Password Required'), text, _('Save password'), ok_handler=on_ok,
708 cancel_handler=on_cancel)
710 def handle_event_roster_info(self, obj):
711 #('ROSTER_INFO', account, (jid, name, sub, ask, groups))
712 account = obj.conn.name
713 contacts = gajim.contacts.get_contacts(account, obj.jid)
714 if (not obj.sub or obj.sub == 'none') and \
715 (not obj.ask or obj.ask == 'none') and not obj.nickname and \
716 not obj.groups:
717 # contact removed us.
718 if contacts:
719 self.roster.remove_contact(obj.jid, account, backend=True)
720 return
721 elif not contacts:
722 if obj.sub == 'remove':
723 return
724 # Add new contact to roster
725 contact = gajim.contacts.create_contact(jid=obj.jid,
726 account=account, name=obj.nickname, groups=obj.groups,
727 show='offline', sub=obj.sub, ask=obj.ask)
728 gajim.contacts.add_contact(account, contact)
729 self.roster.add_contact(obj.jid, account)
730 else:
731 # If contact has changed (sub, ask or group) update roster
732 # Mind about observer status changes:
733 # According to xep 0162, a contact is not an observer anymore when
734 # we asked for auth, so also remove him if ask changed
735 old_groups = contacts[0].groups
736 if obj.sub == 'remove':
737 # another of our instance removed a contact. Remove it here too
738 self.roster.remove_contact(obj.jid, account, backend=True)
739 return
740 if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\
741 or old_groups != obj.groups:
742 # c.get_shown_groups() has changed. Reflect that in
743 # roster_window
744 self.roster.remove_contact(obj.jid, account, force=True)
745 for contact in contacts:
746 contact.name = obj.nickname or ''
747 contact.sub = obj.sub
748 contact.ask = obj.ask
749 contact.groups = obj.groups or []
750 self.roster.add_contact(obj.jid, account)
751 # Refilter and update old groups
752 for group in old_groups:
753 self.roster.draw_group(group, account)
754 self.roster.draw_contact(obj.jid, account)
755 if obj.jid in self.instances[account]['sub_request'] and obj.sub in (
756 'from', 'both'):
757 self.instances[account]['sub_request'][obj.jid].window.destroy()
759 def handle_event_bookmarks(self, obj):
760 # ('BOOKMARKS', account, [{name,jid,autojoin,password,nick}, {}])
761 # We received a bookmark item from the server (JEP48)
762 # Auto join GC windows if neccessary
764 self.roster.set_actions_menu_needs_rebuild()
765 invisible_show = gajim.SHOW_LIST.index('invisible')
766 # do not autojoin if we are invisible
767 if obj.conn.connected == invisible_show:
768 return
770 self.auto_join_bookmarks(obj.conn.name)
772 def handle_event_file_send_error(self, account, array):
773 jid = array[0]
774 file_props = array[1]
775 ft = self.instances['file_transfers']
776 ft.set_status(file_props['type'], file_props['sid'], 'stop')
778 if helpers.allow_popup_window(account):
779 ft.show_send_error(file_props)
780 return
782 self.add_event(account, jid, 'file-send-error', file_props)
784 if helpers.allow_showing_notification(account):
785 path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
786 event_type = _('File Transfer Error')
787 notify.popup(event_type, jid, account, 'file-send-error', path,
788 event_type, file_props['name'])
790 def handle_event_gmail_notify(self, obj):
791 jid = obj.jid
792 gmail_new_messages = int(obj.newmsgs)
793 gmail_messages_list = obj.gmail_messages_list
794 if not gajim.config.get('notify_on_new_gmail_email'):
795 return
796 path = gtkgui_helpers.get_icon_path('gajim-new_email_recv', 48)
797 title = _('New mail on %(gmail_mail_address)s') % \
798 {'gmail_mail_address': jid}
799 text = i18n.ngettext('You have %d new mail conversation',
800 'You have %d new mail conversations', gmail_new_messages,
801 gmail_new_messages, gmail_new_messages)
803 if gajim.config.get('notify_on_new_gmail_email_extra'):
804 cnt = 0
805 for gmessage in gmail_messages_list:
806 # FIXME: emulate Gtalk client popups. find out what they
807 # parse and how they decide what to show each message has a
808 # 'From', 'Subject' and 'Snippet' field
809 if cnt >= 5:
810 break
811 senders = ',\n '.join(reversed(gmessage['From']))
812 text += _('\n\nFrom: %(from_address)s\nSubject: '
813 '%(subject)s\n%(snippet)s') % {'from_address': senders,
814 'subject': gmessage['Subject'],
815 'snippet': gmessage['Snippet']}
816 cnt += 1
818 command = gajim.config.get('notify_on_new_gmail_email_command')
819 if command:
820 Popen(command, shell=True)
822 if gajim.config.get_per('soundevents', 'gmail_received', 'enabled'):
823 helpers.play_sound('gmail_received')
824 notify.popup(_('New E-mail'), jid, obj.conn.name, 'gmail',
825 path_to_image=path, title=title, text=text)
827 def handle_event_file_request_error(self, obj):
828 # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
829 ft = self.instances['file_transfers']
830 ft.set_status(obj.file_props['type'], obj.file_props['sid'], 'stop')
831 errno = obj.file_props['error']
833 if helpers.allow_popup_window(obj.conn.name):
834 if errno in (-4, -5):
835 ft.show_stopped(obj.jid, obj.file_props, obj.error_msg)
836 else:
837 ft.show_request_error(obj.file_props)
838 return
840 if errno in (-4, -5):
841 msg_type = 'file-error'
842 else:
843 msg_type = 'file-request-error'
845 self.add_event(obj.conn.name, obj.jid, msg_type, obj.file_props)
847 if helpers.allow_showing_notification(obj.conn.name):
848 # check if we should be notified
849 path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
850 event_type = _('File Transfer Error')
851 notify.popup(event_type, obj.jid, obj.conn.name, msg_type, path,
852 title=event_type, text=obj.file_props['name'])
854 def handle_event_file_request(self, obj):
855 account = obj.conn.name
856 if obj.jid not in gajim.contacts.get_jid_list(account):
857 keyID = ''
858 attached_keys = gajim.config.get_per('accounts', account,
859 'attached_gpg_keys').split()
860 if obj.jid in attached_keys:
861 keyID = attached_keys[attached_keys.index(obj.jid) + 1]
862 contact = gajim.contacts.create_not_in_roster_contact(jid=obj.jid,
863 account=account, keyID=keyID)
864 gajim.contacts.add_contact(account, contact)
865 self.roster.add_contact(obj.jid, account)
866 contact = gajim.contacts.get_first_contact_from_jid(account, obj.jid)
868 if helpers.allow_popup_window(account):
869 self.instances['file_transfers'].show_file_request(account, contact,
870 obj.file_props)
871 return
873 self.add_event(account, obj.jid, 'file-request', obj.file_props)
875 if helpers.allow_showing_notification(account):
876 path = gtkgui_helpers.get_icon_path('gajim-ft_request', 48)
877 txt = _('%s wants to send you a file.') % gajim.get_name_from_jid(
878 account, obj.jid)
879 event_type = _('File Transfer Request')
880 notify.popup(event_type, obj.jid, account, 'file-request',
881 path_to_image=path, title=event_type, text=txt)
883 def handle_event_file_error(self, title, message):
884 dialogs.ErrorDialog(title, message)
886 def handle_event_file_progress(self, account, file_props):
887 if time.time() - self.last_ftwindow_update > 0.5:
888 # update ft window every 500ms
889 self.last_ftwindow_update = time.time()
890 self.instances['file_transfers'].set_progress(file_props['type'],
891 file_props['sid'], file_props['received-len'])
893 def handle_event_file_rcv_completed(self, account, file_props):
894 ft = self.instances['file_transfers']
895 if file_props['error'] == 0:
896 ft.set_progress(file_props['type'], file_props['sid'],
897 file_props['received-len'])
898 else:
899 ft.set_status(file_props['type'], file_props['sid'], 'stop')
900 if 'stalled' in file_props and file_props['stalled'] or \
901 'paused' in file_props and file_props['paused']:
902 return
903 if file_props['type'] == 'r': # we receive a file
904 jid = unicode(file_props['sender'])
905 else: # we send a file
906 jid = unicode(file_props['receiver'])
908 if helpers.allow_popup_window(account):
909 if file_props['error'] == 0:
910 if gajim.config.get('notify_on_file_complete'):
911 ft.show_completed(jid, file_props)
912 elif file_props['error'] == -1:
913 ft.show_stopped(jid, file_props,
914 error_msg=_('Remote contact stopped transfer'))
915 elif file_props['error'] == -6:
916 ft.show_stopped(jid, file_props,
917 error_msg=_('Error opening file'))
918 return
920 msg_type = ''
921 event_type = ''
922 if file_props['error'] == 0 and gajim.config.get(
923 'notify_on_file_complete'):
924 msg_type = 'file-completed'
925 event_type = _('File Transfer Completed')
926 elif file_props['error'] in (-1, -6):
927 msg_type = 'file-stopped'
928 event_type = _('File Transfer Stopped')
930 if event_type == '':
931 # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
932 # this should never happen but it does. see process_result() in
933 # socks5.py
934 # who calls this func (sth is really wrong unless this func is also
935 # registered as progress_cb
936 return
938 if msg_type:
939 self.add_event(account, jid, msg_type, file_props)
941 if file_props is not None:
942 if file_props['type'] == 'r':
943 # get the name of the sender, as it is in the roster
944 sender = unicode(file_props['sender']).split('/')[0]
945 name = gajim.contacts.get_first_contact_from_jid(account,
946 sender).get_shown_name()
947 filename = os.path.basename(file_props['file-name'])
948 if event_type == _('File Transfer Completed'):
949 txt = _('You successfully received %(filename)s from '
950 '%(name)s.') % {'filename': filename, 'name': name}
951 img_name = 'gajim-ft_done'
952 else: # ft stopped
953 txt = _('File transfer of %(filename)s from %(name)s '
954 'stopped.') % {'filename': filename, 'name': name}
955 img_name = 'gajim-ft_stopped'
956 else:
957 receiver = file_props['receiver']
958 if hasattr(receiver, 'jid'):
959 receiver = receiver.jid
960 receiver = receiver.split('/')[0]
961 # get the name of the contact, as it is in the roster
962 name = gajim.contacts.get_first_contact_from_jid(account,
963 receiver).get_shown_name()
964 filename = os.path.basename(file_props['file-name'])
965 if event_type == _('File Transfer Completed'):
966 txt = _('You successfully sent %(filename)s to %(name)s.')\
967 % {'filename': filename, 'name': name}
968 img_name = 'gajim-ft_done'
969 else: # ft stopped
970 txt = _('File transfer of %(filename)s to %(name)s '
971 'stopped.') % {'filename': filename, 'name': name}
972 img_name = 'gajim-ft_stopped'
973 path = gtkgui_helpers.get_icon_path(img_name, 48)
974 else:
975 txt = ''
976 path = ''
978 if gajim.config.get('notify_on_file_complete') and \
979 (gajim.config.get('autopopupaway') or \
980 gajim.connections[account].connected in (2, 3)):
981 # we want to be notified and we are online/chat or we don't mind
982 # bugged when away/na/busy
983 notify.popup(event_type, jid, account, msg_type, path_to_image=path,
984 title=event_type, text=txt)
986 def ask_offline_status(self, account):
987 for contact in gajim.contacts.iter_contacts(account):
988 gajim.connections[account].request_last_status_time(contact.jid,
989 contact.resource)
991 def handle_event_signed_in(self, obj):
993 SIGNED_IN event is emitted when we sign in, so handle it
995 # ('SIGNED_IN', account, ())
996 # block signed in notifications for 30 seconds
997 account = obj.conn.name
998 gajim.block_signed_in_notifications[account] = True
999 state = self.sleeper.getState()
1000 connected = obj.conn.connected
1001 if gajim.config.get('ask_offline_status_on_connection'):
1002 # Ask offline status in 1 minute so w'are sure we got all online
1003 # presences
1004 gobject.timeout_add_seconds(60, self.ask_offline_status, account)
1005 if state != common.sleepy.STATE_UNKNOWN and connected in (2, 3):
1006 # we go online or free for chat, so we activate auto status
1007 gajim.sleeper_state[account] = 'online'
1008 elif not ((state == common.sleepy.STATE_AWAY and connected == 4) or \
1009 (state == common.sleepy.STATE_XA and connected == 5)):
1010 # If we are autoaway/xa and come back after a disconnection, do
1011 # nothing
1012 # Else disable autoaway
1013 gajim.sleeper_state[account] = 'off'
1015 if obj.conn.archiving_supported:
1016 # Start merging logs from server
1017 obj.conn.request_modifications_page(gajim.config.get_per('accounts',
1018 account, 'last_archiving_time'))
1019 gajim.config.set_per('accounts', account, 'last_archiving_time',
1020 time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
1022 invisible_show = gajim.SHOW_LIST.index('invisible')
1023 # We cannot join rooms if we are invisible
1024 if connected == invisible_show:
1025 return
1026 # send currently played music
1027 if obj.conn.pep_supported and dbus_support.supported and \
1028 gajim.config.get_per('accounts', account, 'publish_tune'):
1029 self.enable_music_listener()
1030 # enable location listener
1031 if obj.conn.pep_supported and dbus_support.supported and \
1032 gajim.config.get_per('accounts', account, 'publish_location'):
1033 location_listener.enable()
1036 def handle_event_metacontacts(self, obj):
1037 gajim.contacts.define_metacontacts(obj.conn.name, obj.meta_list)
1039 def handle_atom_entry(self, obj):
1040 AtomWindow.newAtomEntry(obj.atom_entry)
1042 def handle_event_failed_decrypt(self, obj):
1043 details = _('Unable to decrypt message from %s\nIt may have been '
1044 'tampered with.') % obj.fjid
1045 dialogs.WarningDialog(_('Unable to decrypt message'), details)
1047 def handle_event_zc_name_conflict(self, obj):
1048 def on_ok(new_name):
1049 gajim.config.set_per('accounts', obj.conn.name, 'name', new_name)
1050 show = obj.conn.old_show
1051 status = obj.conn.status
1052 obj.conn.username = new_name
1053 obj.conn.change_status(show, status)
1054 def on_cancel():
1055 obj.conn.change_status('offline', '')
1057 dlg = dialogs.InputDialog(_('Username Conflict'),
1058 _('Please type a new username for your local account'),
1059 input_str=obj.alt_name, is_modal=True, ok_handler=on_ok,
1060 cancel_handler=on_cancel)
1062 def handle_event_resource_conflict(self, obj):
1063 # ('RESOURCE_CONFLICT', account, ())
1064 # First we go offline, but we don't overwrite status message
1065 account = obj.conn.name
1066 conn = obj.conn
1067 self.roster.send_status(account, 'offline', conn.status)
1068 def on_ok(new_resource):
1069 gajim.config.set_per('accounts', account, 'resource', new_resource)
1070 self.roster.send_status(account, conn.old_show, conn.status)
1071 proposed_resource = conn.server_resource
1072 proposed_resource += gajim.config.get('gc_proposed_nick_char')
1073 dlg = dialogs.ResourceConflictDialog(_('Resource Conflict'),
1074 _('You are already connected to this account with the same '
1075 'resource. Please type a new one'), resource=proposed_resource,
1076 ok_handler=on_ok)
1078 def handle_event_jingle_incoming(self, obj):
1079 # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
1080 # data...))
1081 # TODO: conditional blocking if peer is not in roster
1083 account = obj.conn.name
1084 content_types = set(c[0] for c in obj.contents)
1086 # check type of jingle session
1087 if 'audio' in content_types or 'video' in content_types:
1088 # a voip session...
1089 # we now handle only voip, so the only thing we will do here is
1090 # not to return from function
1091 pass
1092 else:
1093 # unknown session type... it should be declined in common/jingle.py
1094 return
1096 ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
1097 or self.msg_win_mgr.get_control(obj.jid, account))
1098 if ctrl:
1099 if 'audio' in content_types:
1100 ctrl.set_audio_state('connection_received', obj.sid)
1101 if 'video' in content_types:
1102 ctrl.set_video_state('connection_received', obj.sid)
1104 dlg = dialogs.VoIPCallReceivedDialog.get_dialog(obj.fjid, obj.sid)
1105 if dlg:
1106 dlg.add_contents(content_types)
1107 return
1109 if helpers.allow_popup_window(account):
1110 dialogs.VoIPCallReceivedDialog(account, obj.fjid, obj.sid,
1111 content_types)
1112 return
1114 self.add_event(account, obj.jid, 'jingle-incoming', (obj.fjid, obj.sid,
1115 content_types))
1117 if helpers.allow_showing_notification(account):
1118 # TODO: we should use another pixmap ;-)
1119 txt = _('%s wants to start a voice chat.') % \
1120 gajim.get_name_from_jid(account, obj.fjid)
1121 path = gtkgui_helpers.get_icon_path('gajim-mic_active', 48)
1122 event_type = _('Voice Chat Request')
1123 notify.popup(event_type, obj.fjid, account, 'jingle-incoming',
1124 path_to_image=path, title=event_type, text=txt)
1126 def handle_event_jingle_connected(self, obj):
1127 # ('JINGLE_CONNECTED', account, (peerjid, sid, media))
1128 if obj.media in ('audio', 'video'):
1129 account = obj.conn.name
1130 ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
1131 or self.msg_win_mgr.get_control(obj.jid, account))
1132 if ctrl:
1133 if obj.media == 'audio':
1134 ctrl.set_audio_state('connected', obj.sid)
1135 else:
1136 ctrl.set_video_state('connected', obj.sid)
1138 def handle_event_jingle_disconnected(self, obj):
1139 # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason))
1140 account = obj.conn.name
1141 ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
1142 or self.msg_win_mgr.get_control(obj.jid, account))
1143 if ctrl:
1144 if obj.media is None:
1145 ctrl.stop_jingle(sid=obj.sid, reason=obj.reason)
1146 elif obj.media == 'audio':
1147 ctrl.set_audio_state('stop', sid=obj.sid, reason=obj.reason)
1148 elif obj.media == 'video':
1149 ctrl.set_video_state('stop', sid=obj.sid, reason=obj.reason)
1150 dialog = dialogs.VoIPCallReceivedDialog.get_dialog(obj.fjid, obj.sid)
1151 if dialog:
1152 if obj.media is None:
1153 dialog.dialog.destroy()
1154 else:
1155 dialog.remove_contents((obj.media, ))
1157 def handle_event_jingle_error(self, obj):
1158 # ('JINGLE_ERROR', account, (peerjid, sid, reason))
1159 account = obj.conn.name
1160 ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
1161 or self.msg_win_mgr.get_control(obj.jid, account))
1162 if ctrl:
1163 ctrl.set_audio_state('error', reason=obj.reason)
1165 def handle_event_roster_item_exchange(self, obj):
1166 # data = (action in [add, delete, modify], exchange_list, jid_from)
1167 dialogs.RosterItemExchangeWindow(obj.conn.name, obj.action,
1168 obj.exchange_items_list, obj.fjid)
1170 def handle_event_ssl_error(self, obj):
1171 # ('SSL_ERROR', account, (text, errnum, cert, sha1_fingerprint))
1172 account = obj.conn.name
1173 server = gajim.config.get_per('accounts', account, 'hostname')
1175 def on_ok(is_checked):
1176 del self.instances[account]['online_dialog']['ssl_error']
1177 if is_checked[0]:
1178 # Check if cert is already in file
1179 certs = ''
1180 if os.path.isfile(gajim.MY_CACERTS):
1181 f = open(gajim.MY_CACERTS)
1182 certs = f.read()
1183 f.close()
1184 if obj.cert in certs:
1185 dialogs.ErrorDialog(_('Certificate Already in File'),
1186 _('This certificate is already in file %s, so it\'s '
1187 'not added again.') % gajim.MY_CACERTS)
1188 else:
1189 f = open(gajim.MY_CACERTS, 'a')
1190 f.write(server + '\n')
1191 f.write(obj.cert + '\n\n')
1192 f.close()
1193 gajim.config.set_per('accounts', account,
1194 'ssl_fingerprint_sha1', obj.fingerprint)
1195 if is_checked[1]:
1196 ignore_ssl_errors = gajim.config.get_per('accounts', account,
1197 'ignore_ssl_errors').split()
1198 ignore_ssl_errors.append(str(obj.error_num))
1199 gajim.config.set_per('accounts', account, 'ignore_ssl_errors',
1200 ' '.join(ignore_ssl_errors))
1201 obj.conn.ssl_certificate_accepted()
1203 def on_cancel():
1204 del self.instances[account]['online_dialog']['ssl_error']
1205 obj.conn.disconnect(on_purpose=True)
1206 gajim.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
1207 show='offline'))
1209 pritext = _('Error verifying SSL certificate')
1210 sectext = _('There was an error verifying the SSL certificate of your '
1211 'jabber server: %(error)s\nDo you still want to connect to this '
1212 'server?') % {'error': obj.error_text}
1213 if obj.error_num in (18, 27):
1214 checktext1 = _('Add this certificate to the list of trusted '
1215 'certificates.\nSHA1 fingerprint of the certificate:\n%s') % \
1216 obj.fingerprint
1217 else:
1218 checktext1 = ''
1219 checktext2 = _('Ignore this error for this certificate.')
1220 if 'ssl_error' in self.instances[account]['online_dialog']:
1221 self.instances[account]['online_dialog']['ssl_error'].destroy()
1222 self.instances[account]['online_dialog']['ssl_error'] = \
1223 dialogs.SSLErrorDialog(obj.conn.name, obj.certificate, pritext,
1224 sectext, checktext1, checktext2, on_response_ok=on_ok,
1225 on_response_cancel=on_cancel)
1227 def handle_event_fingerprint_error(self, obj):
1228 # ('FINGERPRINT_ERROR', account, (new_fingerprint,))
1229 account = obj.conn.name
1230 def on_yes(is_checked):
1231 del self.instances[account]['online_dialog']['fingerprint_error']
1232 gajim.config.set_per('accounts', account, 'ssl_fingerprint_sha1',
1233 obj.new_fingerprint)
1234 # Reset the ignored ssl errors
1235 gajim.config.set_per('accounts', account, 'ignore_ssl_errors', '')
1236 obj.conn.ssl_certificate_accepted()
1238 def on_no():
1239 del self.instances[account]['online_dialog']['fingerprint_error']
1240 obj.conn.disconnect(on_purpose=True)
1241 gajim.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
1242 show='offline'))
1244 pritext = _('SSL certificate error')
1245 sectext = _('It seems the SSL certificate of account %(account)s has '
1246 'changed or your connection is being hacked.\nOld fingerprint: '
1247 '%(old)s\nNew fingerprint: %(new)s\n\nDo you still want to connect '
1248 'and update the fingerprint of the certificate?') % \
1249 {'account': account, 'old': gajim.config.get_per('accounts',
1250 account, 'ssl_fingerprint_sha1'), 'new': obj.new_fingerprint}
1251 if 'fingerprint_error' in self.instances[account]['online_dialog']:
1252 self.instances[account]['online_dialog']['fingerprint_error'].\
1253 destroy()
1254 self.instances[account]['online_dialog']['fingerprint_error'] = \
1255 dialogs.CheckFingerprintDialog(pritext, sectext, on_response_yes=on_yes,
1256 on_response_no=on_no, account=obj.conn.name,
1257 certificate=obj.certificate)
1259 def handle_event_plain_connection(self, obj):
1260 # ('PLAIN_CONNECTION', account, (connection))
1261 def on_ok(is_checked):
1262 if not is_checked[0]:
1263 on_cancel()
1264 return
1265 # On cancel call del self.instances, so don't call it another time
1266 # before
1267 del self.instances[obj.conn.name]['online_dialog']\
1268 ['plain_connection']
1269 if is_checked[1]:
1270 gajim.config.set_per('accounts', obj.conn.name,
1271 'warn_when_plaintext_connection', False)
1272 obj.conn.connection_accepted(obj.xmpp_client, 'plain')
1274 def on_cancel():
1275 del self.instances[obj.conn.name]['online_dialog']\
1276 ['plain_connection']
1277 obj.conn.disconnect(on_purpose=True)
1278 gajim.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
1279 show='offline'))
1281 pritext = _('Insecure connection')
1282 sectext = _('You are about to connect to the account %(account)s '
1283 '(%(server)s) with an insecure connection. This means all your '
1284 'conversations will be exchanged unencrypted. Are you sure you '
1285 'want to do that?') % {'account': obj.conn.name,
1286 'server': gajim.get_hostname_from_account(obj.conn.name)}
1287 checktext1 = _('Yes, I really want to connect insecurely')
1288 checktext2 = _('_Do not ask me again')
1289 if 'plain_connection' in self.instances[obj.conn.name]['online_dialog']:
1290 self.instances[obj.conn.name]['online_dialog']['plain_connection'].\
1291 destroy()
1292 self.instances[obj.conn.name]['online_dialog']['plain_connection'] = \
1293 dialogs.ConfirmationDialogDoubleCheck(pritext, sectext, checktext1,
1294 checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel,
1295 is_modal=False)
1297 def handle_event_insecure_ssl_connection(self, obj):
1298 # ('INSECURE_SSL_CONNECTION', account, (connection, connection_type))
1299 def on_ok(is_checked):
1300 if not is_checked[0]:
1301 on_cancel()
1302 return
1303 del self.instances[obj.conn.name]['online_dialog']['insecure_ssl']
1304 if is_checked[1]:
1305 gajim.config.set_per('accounts', obj.conn.name,
1306 'warn_when_insecure_ssl_connection', False)
1307 if obj.conn.connected == 0:
1308 # We have been disconnecting (too long time since window is
1309 # opened)
1310 # re-connect with auto-accept
1311 obj.conn.connection_auto_accepted = True
1312 show, msg = obj.conn.continue_connect_info[:2]
1313 self.roster.send_status(obj.conn.name, show, msg)
1314 return
1315 obj.conn.connection_accepted(obj.xmpp_client, obj.conn_type)
1317 def on_cancel():
1318 del self.instances[obj.conn.name]['online_dialog']['insecure_ssl']
1319 obj.conn.disconnect(on_purpose=True)
1320 gajim.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
1321 show='offline'))
1323 pritext = _('Insecure connection')
1324 sectext = _('You are about to send your password on an insecure '
1325 'connection. You should install PyOpenSSL to prevent that. Are you '
1326 'sure you want to do that?')
1327 checktext1 = _('Yes, I really want to connect insecurely')
1328 checktext2 = _('_Do not ask me again')
1329 if 'insecure_ssl' in self.instances[obj.conn.name]['online_dialog']:
1330 self.instances[obj.conn.name]['online_dialog']['insecure_ssl'].\
1331 destroy()
1332 self.instances[obj.conn.name]['online_dialog']['insecure_ssl'] = \
1333 dialogs.ConfirmationDialogDoubleCheck(pritext, sectext, checktext1,
1334 checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel,
1335 is_modal=False)
1337 def handle_event_insecure_password(self, obj):
1338 # ('INSECURE_PASSWORD', account, ())
1339 def on_ok(is_checked):
1340 if not is_checked[0]:
1341 on_cancel()
1342 return
1343 del self.instances[obj.conn.name]['online_dialog']\
1344 ['insecure_password']
1345 if is_checked[1]:
1346 gajim.config.set_per('accounts', obj.conn.name,
1347 'warn_when_insecure_password', False)
1348 if obj.conn.connected == 0:
1349 # We have been disconnecting (too long time since window is
1350 # opened)
1351 # re-connect with auto-accept
1352 obj.conn.connection_auto_accepted = True
1353 show, msg = obj.conn.continue_connect_info[:2]
1354 self.roster.send_status(obj.conn.name, show, msg)
1355 return
1356 obj.conn.accept_insecure_password()
1358 def on_cancel():
1359 del self.instances[obj.conn.name]['online_dialog']\
1360 ['insecure_password']
1361 obj.conn.disconnect(on_purpose=True)
1362 gajim.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn,
1363 show='offline'))
1365 pritext = _('Insecure connection')
1366 sectext = _('You are about to send your password unencrypted on an '
1367 'insecure connection. Are you sure you want to do that?')
1368 checktext1 = _('Yes, I really want to connect insecurely')
1369 checktext2 = _('_Do not ask me again')
1370 if 'insecure_password' in self.instances[obj.conn.name]\
1371 ['online_dialog']:
1372 self.instances[obj.conn.name]['online_dialog']\
1373 ['insecure_password'].destroy()
1374 self.instances[obj.conn.name]['online_dialog']['insecure_password'] = \
1375 dialogs.ConfirmationDialogDoubleCheck(pritext, sectext, checktext1,
1376 checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel,
1377 is_modal=False)
1379 def create_core_handlers_list(self):
1380 self.handlers = {
1381 'ERROR': [self.handle_event_error],
1382 'DB_ERROR': [self.handle_event_db_error],
1383 'INFORMATION': [self.handle_event_information],
1384 'FILE_SEND_ERROR': [self.handle_event_file_send_error],
1385 'atom-entry-received': [self.handle_atom_entry],
1386 'bad-gpg-passphrase': [self.handle_event_bad_gpg_passphrase],
1387 'bookmarks-received': [self.handle_event_bookmarks],
1388 'client-cert-passphrase': [
1389 self.handle_event_client_cert_passphrase],
1390 'connection-lost': [self.handle_event_connection_lost],
1391 'failed-decrypt': [(self.handle_event_failed_decrypt, ged.GUI2)],
1392 'file-request-error': [self.handle_event_file_request_error],
1393 'file-request-received': [self.handle_event_file_request],
1394 'fingerprint-error': [self.handle_event_fingerprint_error],
1395 'gc-invitation-received': [self.handle_event_gc_invitation],
1396 'gc-presence-received': [self.handle_event_gc_presence],
1397 'gmail-notify': [self.handle_event_gmail_notify],
1398 'gpg-password-required': [self.handle_event_gpg_password_required],
1399 'gpg-trust-key': [self.handle_event_gpg_trust_key],
1400 'http-auth-received': [self.handle_event_http_auth],
1401 'insecure-password': [self.handle_event_insecure_password],
1402 'insecure-ssl-connection': \
1403 [self.handle_event_insecure_ssl_connection],
1404 'iq-error-received': [self.handle_event_iq_error],
1405 'jingle-connected-received': [self.handle_event_jingle_connected],
1406 'jingle-disconnected-received': [
1407 self.handle_event_jingle_disconnected],
1408 'jingle-error-received': [self.handle_event_jingle_error],
1409 'jingle-request-received': [self.handle_event_jingle_incoming],
1410 'last-result-received': [self.handle_event_last_status_time],
1411 'message-error': [self.handle_event_msgerror],
1412 'message-not-sent': [self.handle_event_msgnotsent],
1413 'message-sent': [self.handle_event_msgsent],
1414 'metacontacts-received': [self.handle_event_metacontacts],
1415 'muc-admin-received': [self.handle_event_gc_affiliation],
1416 'muc-owner-received': [self.handle_event_gc_config],
1417 'our-show': [self.handle_event_status],
1418 'password-required': [self.handle_event_password_required],
1419 'plain-connection': [self.handle_event_plain_connection],
1420 'presence-received': [self.handle_event_presence],
1421 'register-agent-info-received': [self.handle_event_register_agent_info],
1422 'roster-info': [self.handle_event_roster_info],
1423 'roster-item-exchange-received': \
1424 [self.handle_event_roster_item_exchange],
1425 'signed-in': [self.handle_event_signed_in],
1426 'ssl-error': [self.handle_event_ssl_error],
1427 'stream-conflict-received': [self.handle_event_resource_conflict],
1428 'subscribe-presence-received': [
1429 self.handle_event_subscribe_presence],
1430 'subscribed-presence-received': [
1431 self.handle_event_subscribed_presence],
1432 'unsubscribed-presence-received': [
1433 self.handle_event_unsubscribed_presence],
1434 'vcard-received': [self.handle_event_vcard],
1435 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
1438 def register_core_handlers(self):
1440 Register core handlers in Global Events Dispatcher (GED).
1442 This is part of rewriting whole events handling system to use GED.
1444 for event_name, event_handlers in self.handlers.iteritems():
1445 for event_handler in event_handlers:
1446 prio = ged.GUI1
1447 if type(event_handler) == tuple:
1448 prio = event_handler[1]
1449 event_handler = event_handler[0]
1450 gajim.ged.register_event_handler(event_name, prio,
1451 event_handler)
1453 ################################################################################
1454 ### Methods dealing with gajim.events
1455 ################################################################################
1457 def add_event(self, account, jid, type_, event_args):
1459 Add an event to the gajim.events var
1461 # We add it to the gajim.events queue
1462 # Do we have a queue?
1463 jid = gajim.get_jid_without_resource(jid)
1464 no_queue = len(gajim.events.get_events(account, jid)) == 0
1465 # type_ can be gc-invitation file-send-error file-error
1466 # file-request-error file-request file-completed file-stopped
1467 # jingle-incoming
1468 # event_type can be in advancedNotificationWindow.events_list
1469 event_types = {'file-request': 'ft_request',
1470 'file-completed': 'ft_finished'}
1471 event_type = event_types.get(type_)
1472 show_in_roster = notify.get_show_in_roster(event_type, account, jid)
1473 show_in_systray = notify.get_show_in_systray(event_type, account, jid)
1474 event = gajim.events.create_event(type_, event_args,
1475 show_in_roster=show_in_roster,
1476 show_in_systray=show_in_systray)
1477 gajim.events.add_event(account, jid, event)
1479 self.roster.show_title()
1480 if no_queue: # We didn't have a queue: we change icons
1481 if not gajim.contacts.get_contact_with_highest_priority(account,
1482 jid):
1483 if type_ == 'gc-invitation':
1484 self.roster.add_groupchat(jid, account, status='offline')
1485 else:
1486 # add contact to roster ("Not In The Roster") if he is not
1487 self.roster.add_to_not_in_the_roster(account, jid)
1488 else:
1489 self.roster.draw_contact(jid, account)
1491 # Select the big brother contact in roster, it's visible because it has
1492 # events.
1493 family = gajim.contacts.get_metacontacts_family(account, jid)
1494 if family:
1495 nearby_family, bb_jid, bb_account = \
1496 gajim.contacts.get_nearby_family_and_big_brother(family,
1497 account)
1498 else:
1499 bb_jid, bb_account = jid, account
1500 self.roster.select_contact(bb_jid, bb_account)
1502 def handle_event(self, account, fjid, type_):
1503 w = None
1504 ctrl = None
1505 session = None
1507 resource = gajim.get_resource_from_jid(fjid)
1508 jid = gajim.get_jid_without_resource(fjid)
1510 if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'):
1511 w = self.msg_win_mgr.get_window(jid, account)
1512 if jid in self.minimized_controls[account]:
1513 self.roster.on_groupchat_maximized(None, jid, account)
1514 return
1515 else:
1516 ctrl = self.msg_win_mgr.get_gc_control(jid, account)
1518 elif type_ in ('printed_chat', 'chat', ''):
1519 # '' is for log in/out notifications
1521 if type_ != '':
1522 event = gajim.events.get_first_event(account, fjid, type_)
1523 if not event:
1524 event = gajim.events.get_first_event(account, jid, type_)
1525 if not event:
1526 return
1528 if type_ == 'printed_chat':
1529 ctrl = event.parameters[0]
1530 elif type_ == 'chat':
1531 session = event.parameters[8]
1532 ctrl = session.control
1533 elif type_ == '':
1534 ctrl = self.msg_win_mgr.get_control(fjid, account)
1536 if not ctrl:
1537 highest_contact = gajim.contacts.\
1538 get_contact_with_highest_priority(account, jid)
1539 # jid can have a window if this resource was lower when he sent
1540 # message and is now higher because the other one is offline
1541 if resource and highest_contact.resource == resource and \
1542 not self.msg_win_mgr.has_window(jid, account):
1543 # remove resource of events too
1544 gajim.events.change_jid(account, fjid, jid)
1545 resource = None
1546 fjid = jid
1547 contact = None
1548 if resource:
1549 contact = gajim.contacts.get_contact(account, jid, resource)
1550 if not contact:
1551 contact = highest_contact
1553 ctrl = self.new_chat(contact, account, resource=resource,
1554 session=session)
1556 gajim.last_message_time[account][jid] = 0 # long time ago
1558 w = ctrl.parent_win
1559 elif type_ in ('printed_pm', 'pm'):
1560 # assume that the most recently updated control we have for this
1561 # party is the one that this event was in
1562 event = gajim.events.get_first_event(account, fjid, type_)
1563 if not event:
1564 event = gajim.events.get_first_event(account, jid, type_)
1566 if type_ == 'printed_pm':
1567 ctrl = event.parameters[0]
1568 elif type_ == 'pm':
1569 session = event.parameters[8]
1571 if session and session.control:
1572 ctrl = session.control
1573 elif not ctrl:
1574 room_jid = jid
1575 nick = resource
1576 gc_contact = gajim.contacts.get_gc_contact(account, room_jid,
1577 nick)
1578 if gc_contact:
1579 show = gc_contact.show
1580 else:
1581 show = 'offline'
1582 gc_contact = gajim.contacts.create_gc_contact(
1583 room_jid=room_jid, account=account, name=nick,
1584 show=show)
1586 if not session:
1587 session = gajim.connections[account].make_new_session(
1588 fjid, None, type_='pm')
1590 self.new_private_chat(gc_contact, account, session=session)
1591 ctrl = session.control
1593 w = ctrl.parent_win
1594 elif type_ in ('normal', 'file-request', 'file-request-error',
1595 'file-send-error', 'file-error', 'file-stopped', 'file-completed',
1596 'jingle-incoming'):
1597 # Get the first single message event
1598 event = gajim.events.get_first_event(account, fjid, type_)
1599 if not event:
1600 # default to jid without resource
1601 event = gajim.events.get_first_event(account, jid, type_)
1602 if not event:
1603 return
1604 # Open the window
1605 self.roster.open_event(account, jid, event)
1606 else:
1607 # Open the window
1608 self.roster.open_event(account, fjid, event)
1609 elif type_ == 'gmail':
1610 url = gajim.connections[account].gmail_url
1611 if url:
1612 helpers.launch_browser_mailer('url', url)
1613 elif type_ == 'gc-invitation':
1614 event = gajim.events.get_first_event(account, jid, type_)
1615 data = event.parameters
1616 dialogs.InvitationReceivedDialog(account, data[0], jid, data[2],
1617 data[1], data[3])
1618 gajim.events.remove_events(account, jid, event)
1619 self.roster.draw_contact(jid, account)
1620 elif type_ == 'subscription_request':
1621 event = gajim.events.get_first_event(account, jid, type_)
1622 data = event.parameters
1623 dialogs.SubscriptionRequestWindow(jid, data[0], account, data[1])
1624 gajim.events.remove_events(account, jid, event)
1625 self.roster.draw_contact(jid, account)
1626 elif type_ == 'unsubscribed':
1627 event = gajim.events.get_first_event(account, jid, type_)
1628 contact = event.parameters
1629 self.show_unsubscribed_dialog(account, contact)
1630 gajim.events.remove_events(account, jid, event)
1631 self.roster.draw_contact(jid, account)
1632 if w:
1633 w.set_active_tab(ctrl)
1634 w.window.window.focus(gtk.get_current_event_time())
1635 # Using isinstance here because we want to catch all derived types
1636 if isinstance(ctrl, ChatControlBase):
1637 tv = ctrl.conv_textview
1638 tv.scroll_to_end()
1640 ################################################################################
1641 ### Methods dealing with emoticons
1642 ################################################################################
1644 def image_is_ok(self, image):
1645 if not os.path.exists(image):
1646 return False
1647 img = gtk.Image()
1648 try:
1649 img.set_from_file(image)
1650 except Exception:
1651 return False
1652 t = img.get_storage_type()
1653 if t != gtk.IMAGE_PIXBUF and t != gtk.IMAGE_ANIMATION:
1654 return False
1655 return True
1657 @property
1658 def basic_pattern_re(self):
1659 if not self._basic_pattern_re:
1660 self._basic_pattern_re = re.compile(self.basic_pattern,
1661 re.IGNORECASE)
1662 return self._basic_pattern_re
1664 @property
1665 def emot_and_basic_re(self):
1666 if not self._emot_and_basic_re:
1667 self._emot_and_basic_re = re.compile(self.emot_and_basic,
1668 re.IGNORECASE + re.UNICODE)
1669 return self._emot_and_basic_re
1671 @property
1672 def sth_at_sth_dot_sth_re(self):
1673 if not self._sth_at_sth_dot_sth_re:
1674 self._sth_at_sth_dot_sth_re = re.compile(self.sth_at_sth_dot_sth)
1675 return self._sth_at_sth_dot_sth_re
1677 @property
1678 def invalid_XML_chars_re(self):
1679 if not self._invalid_XML_chars_re:
1680 self._invalid_XML_chars_re = re.compile(self.invalid_XML_chars)
1681 return self._invalid_XML_chars_re
1683 def make_regexps(self):
1684 # regexp meta characters are: . ^ $ * + ? { } [ ] \ | ( )
1685 # one escapes the metachars with \
1686 # \S matches anything but ' ' '\t' '\n' '\r' '\f' and '\v'
1687 # \s matches any whitespace character
1688 # \w any alphanumeric character
1689 # \W any non-alphanumeric character
1690 # \b means word boundary. This is a zero-width assertion that
1691 # matches only at the beginning or end of a word.
1692 # ^ matches at the beginning of lines
1694 # * means 0 or more times
1695 # + means 1 or more times
1696 # ? means 0 or 1 time
1697 # | means or
1698 # [^*] anything but '*' (inside [] you don't have to escape metachars)
1699 # [^\s*] anything but whitespaces and '*'
1700 # (?<!\S) is a one char lookbehind assertion and asks for any leading
1701 # whitespace
1702 # and mathces beginning of lines so we have correct formatting detection
1703 # even if the the text is just '*foo*'
1704 # (?!\S) is the same thing but it's a lookahead assertion
1705 # \S*[^\s\W] --> in the matching string don't match ? or ) etc.. if at
1706 # the end
1707 # so http://be) will match http://be and http://be)be) will match
1708 # http://be)be
1710 legacy_prefixes = r"((?<=\()(www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$"\
1711 r"&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+(?=\)))"\
1712 r"|((www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]"\
1713 r"|%[A-Fa-f0-9]{2})+"\
1714 r"\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)"
1715 # NOTE: it's ok to catch www.gr such stuff exist!
1717 # FIXME: recognize xmpp: and treat it specially
1718 links = r"((?<=\()[A-Za-z][A-Za-z0-9\+\.\-]*:"\
1719 r"([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\
1720 r"(?=\)))|(\w[\w\+\.\-]*:([^<>\s]|%[A-Fa-f0-9]{2})+)"
1722 # 2nd one: at_least_one_char@at_least_one_char.at_least_one_char
1723 mail = r'\bmailto:\S*[^\s\W]|' r'\b\S+@\S+\.\S*[^\s\W]'
1725 # detects eg. *b* *bold* *bold bold* test *bold* *bold*! (*bold*)
1726 # doesn't detect (it's a feature :P) * bold* *bold * * bold * test*bold*
1727 formatting = r'|(?<!\w)' r'\*[^\s*]' r'([^*]*[^\s*])?' r'\*(?!\w)|'\
1728 r'(?<!\S)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\S)|'\
1729 r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
1731 latex = r'|\$\$[^$\\]*?([\]\[0-9A-Za-z()|+*/-]|'\
1732 r'[\\][\]\[0-9A-Za-z()|{}$])(.*?[^\\])?\$\$'
1734 basic_pattern = links + '|' + mail + '|' + legacy_prefixes
1736 link_pattern = basic_pattern
1737 self.link_pattern_re = re.compile(link_pattern, re.I | re.U)
1739 if gajim.config.get('use_latex'):
1740 basic_pattern += latex
1742 if gajim.config.get('ascii_formatting'):
1743 basic_pattern += formatting
1744 self.basic_pattern = basic_pattern
1746 emoticons_pattern = ''
1747 if gajim.config.get('emoticons_theme'):
1748 # When an emoticon is bordered by an alpha-numeric character it is
1749 # NOT expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc
1750 # We still allow multiple emoticons side-by-side like :P:P:P
1751 # sort keys by length so :qwe emot is checked before :q
1752 keys = sorted(self.emoticons, key=len, reverse=True)
1753 emoticons_pattern_prematch = ''
1754 emoticons_pattern_postmatch = ''
1755 emoticon_length = 0
1756 for emoticon in keys: # travel thru emoticons list
1757 emoticon = emoticon.decode('utf-8')
1758 emoticon_escaped = re.escape(emoticon) # espace regexp metachars
1759 # | means or in regexp
1760 emoticons_pattern += emoticon_escaped + '|'
1761 if (emoticon_length != len(emoticon)):
1762 # Build up expressions to match emoticons next to others
1763 emoticons_pattern_prematch = \
1764 emoticons_pattern_prematch[:-1] + ')|(?<='
1765 emoticons_pattern_postmatch = \
1766 emoticons_pattern_postmatch[:-1] + ')|(?='
1767 emoticon_length = len(emoticon)
1768 emoticons_pattern_prematch += emoticon_escaped + '|'
1769 emoticons_pattern_postmatch += emoticon_escaped + '|'
1770 # We match from our list of emoticons, but they must either have
1771 # whitespace, or another emoticon next to it to match successfully
1772 # [\w.] alphanumeric and dot (for not matching 8) in (2.8))
1773 emoticons_pattern = '|' + '(?:(?<![\w.]' + \
1774 emoticons_pattern_prematch[:-1] + '))' + '(?:' + \
1775 emoticons_pattern[:-1] + ')' + '(?:(?![\w]' + \
1776 emoticons_pattern_postmatch[:-1] + '))'
1778 # because emoticons match later (in the string) they need to be after
1779 # basic matches that may occur earlier
1780 self.emot_and_basic = basic_pattern + emoticons_pattern
1782 # needed for xhtml display
1783 self.emot_only = emoticons_pattern
1785 # at least one character in 3 parts (before @, after @, after .)
1786 self.sth_at_sth_dot_sth = r'\S+@\S+\.\S*[^\s)?]'
1788 # Invalid XML chars
1789 self.invalid_XML_chars = u'[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x1f]|'\
1790 u'[\ud800-\udfff]|[\ufffe-\uffff]'
1792 def popup_emoticons_under_button(self, button, parent_win):
1794 Popup the emoticons menu under button, located in parent_win
1796 gtkgui_helpers.popup_emoticons_under_button(self.emoticons_menu,
1797 button, parent_win)
1799 def prepare_emoticons_menu(self):
1800 menu = gtk.Menu()
1801 def emoticon_clicked(w, str_):
1802 if self.emoticon_menuitem_clicked:
1803 self.emoticon_menuitem_clicked(str_)
1804 # don't keep reference to CB of object
1805 # this will prevent making it uncollectable
1806 self.emoticon_menuitem_clicked = None
1807 def selection_done(widget):
1808 # remove reference to CB of object, which will
1809 # make it uncollectable
1810 self.emoticon_menuitem_clicked = None
1811 counter = 0
1812 # Calculate the side lenght of the popup to make it a square
1813 size = int(round(math.sqrt(len(self.emoticons_images))))
1814 for image in self.emoticons_images:
1815 item = gtk.MenuItem()
1816 img = gtk.Image()
1817 if isinstance(image[1], gtk.gdk.PixbufAnimation):
1818 img.set_from_animation(image[1])
1819 else:
1820 img.set_from_pixbuf(image[1])
1821 item.add(img)
1822 item.connect('activate', emoticon_clicked, image[0])
1823 # add tooltip with ascii
1824 item.set_tooltip_text(image[0])
1825 menu.attach(item, counter % size, counter % size + 1,
1826 counter / size, counter / size + 1)
1827 counter += 1
1828 menu.connect('selection-done', selection_done)
1829 menu.show_all()
1830 return menu
1832 def _init_emoticons(self, path, need_reload = False):
1833 #initialize emoticons dictionary and unique images list
1834 self.emoticons_images = list()
1835 self.emoticons = dict()
1836 self.emoticons_animations = dict()
1838 sys.path.append(path)
1839 import emoticons
1840 if need_reload:
1841 # we need to reload else that doesn't work when changing emoticon
1842 # set
1843 reload(emoticons)
1844 emots = emoticons.emoticons
1845 for emot_filename in emots:
1846 emot_file = os.path.join(path, emot_filename)
1847 if not self.image_is_ok(emot_file):
1848 continue
1849 for emot in emots[emot_filename]:
1850 emot = emot.decode('utf-8')
1851 # This avoids duplicated emoticons with the same image eg. :)
1852 # and :-)
1853 if not emot_file in self.emoticons.values():
1854 if emot_file.endswith('.gif'):
1855 pix = gtk.gdk.PixbufAnimation(emot_file)
1856 else:
1857 pix = gtk.gdk.pixbuf_new_from_file_at_size(emot_file,
1858 16, 16)
1859 self.emoticons_images.append((emot, pix))
1860 self.emoticons[emot.upper()] = emot_file
1861 del emoticons
1862 sys.path.remove(path)
1864 def init_emoticons(self, need_reload = False):
1865 emot_theme = gajim.config.get('emoticons_theme')
1866 if not emot_theme:
1867 return
1869 path = os.path.join(gajim.DATA_DIR, 'emoticons', emot_theme)
1870 if not os.path.exists(path):
1871 # It's maybe a user theme
1872 path = os.path.join(gajim.MY_EMOTS_PATH, emot_theme)
1873 if not os.path.exists(path):
1874 # theme doesn't exist, disable emoticons
1875 dialogs.WarningDialog(_('Emoticons disabled'),
1876 _('Your configured emoticons theme has not been found, so '
1877 'emoticons have been disabled.'))
1878 gajim.config.set('emoticons_theme', '')
1879 return
1880 self._init_emoticons(path, need_reload)
1881 if len(self.emoticons) == 0:
1882 # maybe old format of emoticons file, try to convert it
1883 try:
1884 import pprint
1885 import emoticons
1886 emots = emoticons.emoticons
1887 fd = open(os.path.join(path, 'emoticons.py'), 'w')
1888 fd.write('emoticons = ')
1889 pprint.pprint( dict([
1890 (file_, [i for i in emots.keys() if emots[i] == file_])
1891 for file_ in set(emots.values())]), fd)
1892 fd.close()
1893 del emoticons
1894 self._init_emoticons(path, need_reload=True)
1895 except Exception:
1896 pass
1897 if len(self.emoticons) == 0:
1898 dialogs.WarningDialog(_('Emoticons disabled'),
1899 _('Your configured emoticons theme cannot been loaded. You '
1900 'maybe need to update the format of emoticons.py file. See '
1901 'http://trac.gajim.org/wiki/Emoticons for more details.'))
1902 if self.emoticons_menu:
1903 self.emoticons_menu.destroy()
1904 self.emoticons_menu = self.prepare_emoticons_menu()
1906 ################################################################################
1907 ### Methods for opening new messages controls
1908 ################################################################################
1910 def join_gc_room(self, account, room_jid, nick, password, minimize=False,
1911 is_continued=False):
1913 Join the room immediately
1916 if gajim.contacts.get_contact(account, room_jid) and \
1917 not gajim.contacts.get_contact(account, room_jid).is_groupchat():
1918 dialogs.ErrorDialog(_('This is not a group chat'),
1919 _('%s is not the name of a group chat.') % room_jid)
1920 return
1922 if not nick:
1923 nick = gajim.nicks[account]
1925 minimized_control = gajim.interface.minimized_controls[account].get(
1926 room_jid, None)
1928 if (self.msg_win_mgr.has_window(room_jid, account) or \
1929 minimized_control) and gajim.gc_connected[account][room_jid]:
1930 if self.msg_win_mgr.has_window(room_jid, account):
1931 gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
1932 win = gc_ctrl.parent_win
1933 win.set_active_tab(gc_ctrl)
1934 else:
1935 self.roster.on_groupchat_maximized(None, room_jid, account)
1936 dialogs.ErrorDialog(_('You are already in group chat %s') % \
1937 room_jid)
1938 return
1940 invisible_show = gajim.SHOW_LIST.index('invisible')
1941 if gajim.connections[account].connected == invisible_show:
1942 dialogs.ErrorDialog(
1943 _('You cannot join a group chat while you are invisible'))
1944 return
1946 if minimized_control is None and not self.msg_win_mgr.has_window(
1947 room_jid, account):
1948 # Join new groupchat
1949 if minimize:
1950 # GCMIN
1951 contact = gajim.contacts.create_contact(jid=room_jid,
1952 account=account, name=nick)
1953 gc_control = GroupchatControl(None, contact, account)
1954 gajim.interface.minimized_controls[account][room_jid] = \
1955 gc_control
1956 self.roster.add_groupchat(room_jid, account)
1957 else:
1958 self.new_room(room_jid, nick, account,
1959 is_continued=is_continued)
1960 elif minimized_control is None:
1961 # We are already in that groupchat
1962 gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
1963 gc_control.nick = nick
1964 gc_control.parent_win.set_active_tab(gc_control)
1966 # Connect
1967 gajim.connections[account].join_gc(nick, room_jid, password)
1968 if password:
1969 gajim.gc_passwords[room_jid] = password
1971 def new_room(self, room_jid, nick, account, is_continued=False):
1972 # Get target window, create a control, and associate it with the window
1973 # GCMIN
1974 contact = gajim.contacts.create_contact(jid=room_jid, account=account,
1975 name=nick)
1976 mw = self.msg_win_mgr.get_window(contact.jid, account)
1977 if not mw:
1978 mw = self.msg_win_mgr.create_window(contact, account,
1979 GroupchatControl.TYPE_ID)
1980 gc_control = GroupchatControl(mw, contact, account,
1981 is_continued=is_continued)
1982 mw.new_tab(gc_control)
1984 def new_private_chat(self, gc_contact, account, session=None):
1985 conn = gajim.connections[account]
1986 if not session and gc_contact.get_full_jid() in conn.sessions:
1987 sessions = [s for s in conn.sessions[gc_contact.get_full_jid()].\
1988 values() if isinstance(s, ChatControlSession)]
1990 # look for an existing session with a chat control
1991 for s in sessions:
1992 if s.control:
1993 session = s
1994 break
1995 if not session and not len(sessions) == 0:
1996 # there are no sessions with chat controls, just take the first
1997 # one
1998 session = sessions[0]
1999 if not session:
2000 # couldn't find an existing ChatControlSession, just make a new one
2001 session = conn.make_new_session(gc_contact.get_full_jid(), None,
2002 'pm')
2004 contact = gc_contact.as_contact()
2005 if not session.control:
2006 message_window = self.msg_win_mgr.get_window(
2007 gc_contact.get_full_jid(), account)
2008 if not message_window:
2009 message_window = self.msg_win_mgr.create_window(contact,
2010 account, message_control.TYPE_PM)
2012 session.control = PrivateChatControl(message_window, gc_contact,
2013 contact, account, session)
2014 message_window.new_tab(session.control)
2016 if gajim.events.get_events(account, gc_contact.get_full_jid()):
2017 # We call this here to avoid race conditions with widget validation
2018 session.control.read_queue()
2020 return session.control
2022 def new_chat(self, contact, account, resource=None, session=None):
2023 # Get target window, create a control, and associate it with the window
2024 type_ = message_control.TYPE_CHAT
2026 fjid = contact.jid
2027 if resource:
2028 fjid += '/' + resource
2030 mw = self.msg_win_mgr.get_window(fjid, account)
2031 if not mw:
2032 mw = self.msg_win_mgr.create_window(contact, account, type_,
2033 resource)
2035 chat_control = ChatControl(mw, contact, account, session, resource)
2037 mw.new_tab(chat_control)
2039 if len(gajim.events.get_events(account, fjid)):
2040 # We call this here to avoid race conditions with widget validation
2041 chat_control.read_queue()
2043 return chat_control
2045 def new_chat_from_jid(self, account, fjid, message=None):
2046 jid, resource = gajim.get_room_and_nick_from_fjid(fjid)
2047 contact = gajim.contacts.get_contact(account, jid, resource)
2048 added_to_roster = False
2049 if not contact:
2050 added_to_roster = True
2051 contact = self.roster.add_to_not_in_the_roster(account, jid,
2052 resource=resource)
2054 ctrl = self.msg_win_mgr.get_control(fjid, account)
2056 if not ctrl:
2057 ctrl = self.new_chat(contact, account,
2058 resource=resource)
2059 if len(gajim.events.get_events(account, fjid)):
2060 ctrl.read_queue()
2062 if message:
2063 buffer_ = ctrl.msg_textview.get_buffer()
2064 buffer_.set_text(message)
2065 mw = ctrl.parent_win
2066 mw.set_active_tab(ctrl)
2067 # For JEP-0172
2068 if added_to_roster:
2069 ctrl.user_nick = gajim.nicks[account]
2070 gobject.idle_add(mw.window.grab_focus)
2072 return ctrl
2074 def on_open_chat_window(self, widget, contact, account, resource=None,
2075 session=None):
2076 # Get the window containing the chat
2077 fjid = contact.jid
2079 if resource:
2080 fjid += '/' + resource
2082 ctrl = None
2084 if session:
2085 ctrl = session.control
2086 if not ctrl:
2087 win = self.msg_win_mgr.get_window(fjid, account)
2089 if win:
2090 ctrl = win.get_control(fjid, account)
2092 if not ctrl:
2093 ctrl = self.new_chat(contact, account, resource=resource,
2094 session=session)
2095 # last message is long time ago
2096 gajim.last_message_time[account][ctrl.get_full_jid()] = 0
2098 win = ctrl.parent_win
2100 win.set_active_tab(ctrl)
2102 if gajim.connections[account].is_zeroconf and \
2103 gajim.connections[account].status in ('offline', 'invisible'):
2104 ctrl = win.get_control(fjid, account)
2105 if ctrl:
2106 ctrl.got_disconnected()
2108 ################################################################################
2109 ### Other Methods
2110 ################################################################################
2112 def change_awn_icon_status(self, status):
2113 if not dbus_support.supported:
2114 # do nothing if user doesn't have D-Bus bindings
2115 return
2116 try:
2117 bus = dbus.SessionBus()
2118 if not 'com.google.code.Awn' in bus.list_names():
2119 # Awn is not installed
2120 return
2121 except Exception:
2122 return
2123 iconset = gajim.config.get('iconset')
2124 prefix = os.path.join(helpers.get_iconset_path(iconset), '32x32')
2125 if status in ('chat', 'away', 'xa', 'dnd', 'invisible', 'offline'):
2126 status = status + '.png'
2127 elif status == 'online':
2128 prefix = ''
2129 status = gtkgui_helpers.get_icon_path('gajim', 32)
2130 path = os.path.join(prefix, status)
2131 try:
2132 obj = bus.get_object('com.google.code.Awn', '/com/google/code/Awn')
2133 awn = dbus.Interface(obj, 'com.google.code.Awn')
2134 awn.SetTaskIconByName('Gajim', os.path.abspath(path))
2135 except Exception:
2136 pass
2138 def enable_music_listener(self):
2139 listener = MusicTrackListener.get()
2140 if not self.music_track_changed_signal:
2141 self.music_track_changed_signal = listener.connect(
2142 'music-track-changed', self.music_track_changed)
2143 track = listener.get_playing_track()
2144 self.music_track_changed(listener, track)
2146 def disable_music_listener(self):
2147 listener = MusicTrackListener.get()
2148 listener.disconnect(self.music_track_changed_signal)
2149 self.music_track_changed_signal = None
2151 def music_track_changed(self, unused_listener, music_track_info,
2152 account=None):
2153 if not account:
2154 accounts = gajim.connections.keys()
2155 else:
2156 accounts = [account]
2158 is_paused = hasattr(music_track_info, 'paused') and \
2159 music_track_info.paused == 0
2160 if not music_track_info or is_paused:
2161 artist = title = source = ''
2162 else:
2163 artist = music_track_info.artist
2164 title = music_track_info.title
2165 source = music_track_info.album
2166 for acct in accounts:
2167 if not gajim.account_is_connected(acct):
2168 continue
2169 if not gajim.connections[acct].pep_supported:
2170 continue
2171 if not gajim.config.get_per('accounts', acct, 'publish_tune'):
2172 continue
2173 if gajim.connections[acct].music_track_info == music_track_info:
2174 continue
2175 gajim.connections[acct].send_tune(artist, title, source)
2176 gajim.connections[acct].music_track_info = music_track_info
2178 def get_bg_fg_colors(self):
2179 def gdkcolor_to_rgb (gdkcolor):
2180 return [c / 65535. for c in (gdkcolor.red, gdkcolor.green,
2181 gdkcolor.blue)]
2183 def format_rgb (r, g, b):
2184 return ' '.join([str(c) for c in ('rgb', r, g, b)])
2186 def format_gdkcolor (gdkcolor):
2187 return format_rgb (*gdkcolor_to_rgb (gdkcolor))
2189 # get style colors and create string for dvipng
2190 dummy = gtk.Invisible()
2191 dummy.ensure_style()
2192 style = dummy.get_style()
2193 bg_str = format_gdkcolor(style.base[gtk.STATE_NORMAL])
2194 fg_str = format_gdkcolor(style.text[gtk.STATE_NORMAL])
2195 return (bg_str, fg_str)
2197 def get_fg_color(self, fmt='hex'):
2198 def format_gdkcolor (c):
2199 if fmt == 'tex':
2200 return ' '.join([str(s) for s in
2201 ('rgb', c.red_float, c.green_float, c.blue_float)])
2202 elif fmt == 'hex':
2203 return str(c)
2205 # get foreground style color and create string
2206 dummy = gtk.Invisible()
2207 dummy.ensure_style()
2208 return format_gdkcolor(dummy.get_style().text[gtk.STATE_NORMAL])
2210 def read_sleepy(self):
2212 Check idle status and change that status if needed
2214 if not self.sleeper.poll():
2215 # idle detection is not supported in that OS
2216 return False # stop looping in vain
2217 state = self.sleeper.getState()
2218 for account in gajim.connections:
2219 if account not in gajim.sleeper_state or \
2220 not gajim.sleeper_state[account]:
2221 continue
2222 if state == common.sleepy.STATE_AWAKE and \
2223 gajim.sleeper_state[account] in ('autoaway', 'autoxa'):
2224 # we go online
2225 self.roster.send_status(account, 'online',
2226 gajim.status_before_autoaway[account])
2227 gajim.status_before_autoaway[account] = ''
2228 gajim.sleeper_state[account] = 'online'
2229 elif state == common.sleepy.STATE_AWAY and \
2230 gajim.sleeper_state[account] == 'online' and \
2231 gajim.config.get('autoaway'):
2232 # we save out online status
2233 gajim.status_before_autoaway[account] = \
2234 gajim.connections[account].status
2235 # we go away (no auto status) [we pass True to auto param]
2236 auto_message = gajim.config.get('autoaway_message')
2237 if not auto_message:
2238 auto_message = gajim.connections[account].status
2239 else:
2240 auto_message = auto_message.replace('$S', '%(status)s')
2241 auto_message = auto_message.replace('$T', '%(time)s')
2242 auto_message = auto_message % {
2243 'status': gajim.status_before_autoaway[account],
2244 'time': gajim.config.get('autoawaytime')
2246 self.roster.send_status(account, 'away', auto_message,
2247 auto=True)
2248 gajim.sleeper_state[account] = 'autoaway'
2249 elif state == common.sleepy.STATE_XA and \
2250 gajim.sleeper_state[account] in ('online', 'autoaway',
2251 'autoaway-forced') and gajim.config.get('autoxa'):
2252 # we go extended away [we pass True to auto param]
2253 auto_message = gajim.config.get('autoxa_message')
2254 if not auto_message:
2255 auto_message = gajim.connections[account].status
2256 else:
2257 auto_message = auto_message.replace('$S', '%(status)s')
2258 auto_message = auto_message.replace('$T', '%(time)s')
2259 auto_message = auto_message % {
2260 'status': gajim.status_before_autoaway[account],
2261 'time': gajim.config.get('autoxatime')
2263 self.roster.send_status(account, 'xa', auto_message, auto=True)
2264 gajim.sleeper_state[account] = 'autoxa'
2265 return True # renew timeout (loop for ever)
2267 def autoconnect(self):
2269 Auto connect at startup
2271 # dict of account that want to connect sorted by status
2272 shows = {}
2273 for a in gajim.connections:
2274 if gajim.config.get_per('accounts', a, 'autoconnect'):
2275 if gajim.config.get_per('accounts', a, 'restore_last_status'):
2276 self.roster.send_status(a, gajim.config.get_per('accounts',
2277 a, 'last_status'), helpers.from_one_line(
2278 gajim.config.get_per('accounts', a, 'last_status_msg')))
2279 continue
2280 show = gajim.config.get_per('accounts', a, 'autoconnect_as')
2281 if not show in gajim.SHOW_LIST:
2282 continue
2283 if not show in shows:
2284 shows[show] = [a]
2285 else:
2286 shows[show].append(a)
2287 def on_message(message, pep_dict):
2288 if message is None:
2289 return
2290 for a in shows[show]:
2291 self.roster.send_status(a, show, message)
2292 self.roster.send_pep(a, pep_dict)
2293 for show in shows:
2294 message = self.roster.get_status_message(show, on_message)
2295 return False
2297 def show_systray(self):
2298 self.systray_enabled = True
2299 self.systray.show_icon()
2301 def hide_systray(self):
2302 self.systray_enabled = False
2303 self.systray.hide_icon()
2305 def on_launch_browser_mailer(self, widget, url, kind):
2306 helpers.launch_browser_mailer(kind, url)
2308 def process_connections(self):
2310 Called each foo (200) miliseconds. Check for idlequeue timeouts
2312 try:
2313 gajim.idlequeue.process()
2314 except Exception:
2315 # Otherwise, an exception will stop our loop
2316 timeout, in_seconds = gajim.idlequeue.PROCESS_TIMEOUT
2317 if in_seconds:
2318 gobject.timeout_add_seconds(timeout, self.process_connections)
2319 else:
2320 gobject.timeout_add(timeout, self.process_connections)
2321 raise
2322 return True # renew timeout (loop for ever)
2324 def save_config(self):
2325 err_str = parser.write()
2326 if err_str is not None:
2327 print >> sys.stderr, err_str
2328 # it is good to notify the user
2329 # in case he or she cannot see the output of the console
2330 dialogs.ErrorDialog(_('Could not save your settings and '
2331 'preferences'), err_str)
2332 sys.exit()
2334 def save_avatar_files(self, jid, photo, puny_nick = None, local = False):
2336 Save an avatar to a separate file, and generate files for dbus
2337 notifications. An avatar can be given as a pixmap directly or as an
2338 decoded image
2340 puny_jid = helpers.sanitize_filename(jid)
2341 path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid)
2342 if puny_nick:
2343 path_to_file = os.path.join(path_to_file, puny_nick)
2344 # remove old avatars
2345 for typ in ('jpeg', 'png'):
2346 if local:
2347 path_to_original_file = path_to_file + '_local'+ '.' + typ
2348 else:
2349 path_to_original_file = path_to_file + '.' + typ
2350 if os.path.isfile(path_to_original_file):
2351 os.remove(path_to_original_file)
2352 if local and photo:
2353 pixbuf = photo
2354 typ = 'png'
2355 extension = '_local.png' # save local avatars as png file
2356 else:
2357 pixbuf, typ = gtkgui_helpers.get_pixbuf_from_data(photo,
2358 want_type=True)
2359 if pixbuf is None:
2360 return
2361 extension = '.' + typ
2362 if typ not in ('jpeg', 'png'):
2363 gajim.log.debug('gtkpixbuf cannot save other than jpeg and '\
2364 'png formats. saving %s\'avatar as png file (originaly %s)'\
2365 % (jid, typ))
2366 typ = 'png'
2367 extension = '.png'
2368 path_to_original_file = path_to_file + extension
2369 try:
2370 pixbuf.save(path_to_original_file, typ)
2371 except Exception, e:
2372 log.error('Error writing avatar file %s: %s' % (
2373 path_to_original_file, str(e)))
2374 # Generate and save the resized, color avatar
2375 pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'notification')
2376 if pixbuf:
2377 path_to_normal_file = path_to_file + '_notif_size_colored' + \
2378 extension
2379 try:
2380 pixbuf.save(path_to_normal_file, 'png')
2381 except Exception, e:
2382 log.error('Error writing avatar file %s: %s' % \
2383 (path_to_original_file, str(e)))
2384 # Generate and save the resized, black and white avatar
2385 bwbuf = gtkgui_helpers.get_scaled_pixbuf(
2386 gtkgui_helpers.make_pixbuf_grayscale(pixbuf), 'notification')
2387 if bwbuf:
2388 path_to_bw_file = path_to_file + '_notif_size_bw' + extension
2389 try:
2390 bwbuf.save(path_to_bw_file, 'png')
2391 except Exception, e:
2392 log.error('Error writing avatar file %s: %s' % \
2393 (path_to_original_file, str(e)))
2395 def remove_avatar_files(self, jid, puny_nick = None, local = False):
2397 Remove avatar files of a jid
2399 puny_jid = helpers.sanitize_filename(jid)
2400 path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid)
2401 if puny_nick:
2402 path_to_file = os.path.join(path_to_file, puny_nick)
2403 for ext in ('.jpeg', '.png'):
2404 if local:
2405 ext = '_local' + ext
2406 path_to_original_file = path_to_file + ext
2407 if os.path.isfile(path_to_file + ext):
2408 os.remove(path_to_file + ext)
2409 if os.path.isfile(path_to_file + '_notif_size_colored' + ext):
2410 os.remove(path_to_file + '_notif_size_colored' + ext)
2411 if os.path.isfile(path_to_file + '_notif_size_bw' + ext):
2412 os.remove(path_to_file + '_notif_size_bw' + ext)
2414 def auto_join_bookmarks(self, account):
2416 Autojoin bookmarked GCs that have 'auto join' on for this account
2418 for bm in gajim.connections[account].bookmarks:
2419 if bm['autojoin'] in ('1', 'true'):
2420 jid = bm['jid']
2421 # Only join non-opened groupchats. Opened one are already
2422 # auto-joined on re-connection
2423 if not jid in gajim.gc_connected[account]:
2424 # we are not already connected
2425 minimize = bm['minimize'] in ('1', 'true')
2426 self.join_gc_room(account, jid, bm['nick'],
2427 bm['password'], minimize = minimize)
2428 elif jid in self.minimized_controls[account]:
2429 # more or less a hack:
2430 # On disconnect the minimized gc contact instances
2431 # were set to offline. Reconnect them to show up in the
2432 # roster.
2433 self.roster.add_groupchat(jid, account)
2435 def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password,
2436 nick):
2438 Add a bookmark for this account, sorted in bookmark list
2440 bm = {
2441 'name': name,
2442 'jid': jid,
2443 'autojoin': autojoin,
2444 'minimize': minimize,
2445 'password': password,
2446 'nick': nick
2448 place_found = False
2449 index = 0
2450 # check for duplicate entry and respect alpha order
2451 for bookmark in gajim.connections[account].bookmarks:
2452 if bookmark['jid'] == bm['jid']:
2453 dialogs.ErrorDialog(
2454 _('Bookmark already set'),
2455 _('Group Chat "%s" is already in your bookmarks.') % \
2456 bm['jid'])
2457 return
2458 if bookmark['name'] > bm['name']:
2459 place_found = True
2460 break
2461 index += 1
2462 if place_found:
2463 gajim.connections[account].bookmarks.insert(index, bm)
2464 else:
2465 gajim.connections[account].bookmarks.append(bm)
2466 gajim.connections[account].store_bookmarks()
2467 self.roster.set_actions_menu_needs_rebuild()
2468 dialogs.InformationDialog(
2469 _('Bookmark has been added successfully'),
2470 _('You can manage your bookmarks via Actions menu in your roster.'))
2473 # does JID exist only within a groupchat?
2474 def is_pm_contact(self, fjid, account):
2475 bare_jid = gajim.get_jid_without_resource(fjid)
2477 gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account)
2479 if not gc_ctrl and \
2480 bare_jid in self.minimized_controls[account]:
2481 gc_ctrl = self.minimized_controls[account][bare_jid]
2483 return gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC
2485 def create_ipython_window(self):
2486 try:
2487 from ipython_view import IPythonView
2488 except ImportError:
2489 print 'ipython_view not found'
2490 return
2491 import pango
2493 if os.name == 'nt':
2494 font = 'Lucida Console 9'
2495 else:
2496 font = 'Luxi Mono 10'
2498 window = gtk.Window()
2499 window.set_size_request(750, 550)
2500 window.set_resizable(True)
2501 sw = gtk.ScrolledWindow()
2502 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
2503 view = IPythonView()
2504 view.modify_font(pango.FontDescription(font))
2505 view.set_wrap_mode(gtk.WRAP_CHAR)
2506 sw.add(view)
2507 window.add(sw)
2508 window.show_all()
2509 def on_delete(win, event):
2510 win.hide()
2511 return True
2512 window.connect('delete_event', on_delete)
2513 view.updateNamespace({'gajim': gajim})
2514 gajim.ipython_window = window
2516 def run(self):
2517 if gajim.config.get('trayicon') != 'never':
2518 self.show_systray()
2520 self.roster = roster_window.RosterWindow()
2521 # Creating plugin manager
2522 import plugins
2523 gajim.plugin_manager = plugins.PluginManager()
2525 self.roster._before_fill()
2526 for account in gajim.connections:
2527 gajim.connections[account].load_roster_from_db()
2528 self.roster._after_fill()
2530 # get instances for windows/dialogs that will show_all()/hide()
2531 self.instances['file_transfers'] = dialogs.FileTransfersWindow()
2533 gobject.timeout_add(100, self.autoconnect)
2534 timeout, in_seconds = gajim.idlequeue.PROCESS_TIMEOUT
2535 if in_seconds:
2536 gobject.timeout_add_seconds(timeout, self.process_connections)
2537 else:
2538 gobject.timeout_add(timeout, self.process_connections)
2539 gobject.timeout_add_seconds(gajim.config.get(
2540 'check_idle_every_foo_seconds'), self.read_sleepy)
2542 # when using libasyncns we need to process resolver in regular intervals
2543 if resolver.USE_LIBASYNCNS:
2544 gobject.timeout_add(200, gajim.resolver.process)
2546 def remote_init():
2547 if gajim.config.get('remote_control'):
2548 try:
2549 import remote_control
2550 self.remote_ctrl = remote_control.Remote()
2551 except Exception:
2552 pass
2553 gobject.timeout_add_seconds(5, remote_init)
2555 def __init__(self):
2556 gajim.interface = self
2557 gajim.thread_interface = ThreadInterface
2558 # This is the manager and factory of message windows set by the module
2559 self.msg_win_mgr = None
2560 self.jabber_state_images = {'16': {}, '32': {}, 'opened': {},
2561 'closed': {}}
2562 self.emoticons_menu = None
2563 # handler when an emoticon is clicked in emoticons_menu
2564 self.emoticon_menuitem_clicked = None
2565 self.minimized_controls = {}
2566 self.status_sent_to_users = {}
2567 self.status_sent_to_groups = {}
2568 self.gpg_passphrase = {}
2569 self.pass_dialog = {}
2570 self.db_error_dialog = None
2571 self.default_colors = {
2572 'inmsgcolor': gajim.config.get('inmsgcolor'),
2573 'outmsgcolor': gajim.config.get('outmsgcolor'),
2574 'inmsgtxtcolor': gajim.config.get('inmsgtxtcolor'),
2575 'outmsgtxtcolor': gajim.config.get('outmsgtxtcolor'),
2576 'statusmsgcolor': gajim.config.get('statusmsgcolor'),
2577 'urlmsgcolor': gajim.config.get('urlmsgcolor'),
2580 self.handlers = {}
2581 self.roster = None
2582 self._invalid_XML_chars_re = None
2583 self._basic_pattern_re = None
2584 self._emot_and_basic_re = None
2585 self._sth_at_sth_dot_sth_re = None
2586 self.link_pattern_re = None
2587 self.invalid_XML_chars = None
2588 self.basic_pattern = None
2589 self.emot_and_basic = None
2590 self.sth_at_sth_dot_sth = None
2591 self.emot_only = None
2592 self.emoticons = []
2593 self.emoticons_animations = {}
2594 self.emoticons_images = {}
2596 cfg_was_read = parser.read()
2598 from common import latex
2599 gajim.HAVE_LATEX = gajim.config.get('use_latex') and \
2600 latex.check_for_latex_support()
2602 gajim.logger.reset_shown_unread_messages()
2603 # override logging settings from config (don't take care of '-q' option)
2604 if gajim.config.get('verbose'):
2605 logging_helpers.set_verbose()
2607 # Is Gajim default app?
2608 if os.name != 'nt' and gajim.config.get('check_if_gajim_is_default'):
2609 gtkgui_helpers.possibly_set_gajim_as_xmpp_handler()
2611 for account in gajim.config.get_per('accounts'):
2612 if gajim.config.get_per('accounts', account, 'is_zeroconf'):
2613 gajim.ZEROCONF_ACC_NAME = account
2614 break
2615 # Is gnome configured to activate row on single click ?
2616 try:
2617 import gconf
2618 client = gconf.client_get_default()
2619 click_policy = client.get_string(
2620 '/apps/nautilus/preferences/click_policy')
2621 if click_policy == 'single':
2622 gajim.single_click = True
2623 except Exception:
2624 pass
2625 # add default status messages if there is not in the config file
2626 if len(gajim.config.get_per('statusmsg')) == 0:
2627 default = gajim.config.statusmsg_default
2628 for msg in default:
2629 gajim.config.add_per('statusmsg', msg)
2630 gajim.config.set_per('statusmsg', msg, 'message',
2631 default[msg][0])
2632 gajim.config.set_per('statusmsg', msg, 'activity',
2633 default[msg][1])
2634 gajim.config.set_per('statusmsg', msg, 'subactivity',
2635 default[msg][2])
2636 gajim.config.set_per('statusmsg', msg, 'activity_text',
2637 default[msg][3])
2638 gajim.config.set_per('statusmsg', msg, 'mood',
2639 default[msg][4])
2640 gajim.config.set_per('statusmsg', msg, 'mood_text',
2641 default[msg][5])
2642 #add default themes if there is not in the config file
2643 theme = gajim.config.get('roster_theme')
2644 if not theme in gajim.config.get_per('themes'):
2645 gajim.config.set('roster_theme', _('default'))
2646 if len(gajim.config.get_per('themes')) == 0:
2647 d = ['accounttextcolor', 'accountbgcolor', 'accountfont',
2648 'accountfontattrs', 'grouptextcolor', 'groupbgcolor',
2649 'groupfont', 'groupfontattrs', 'contacttextcolor',
2650 'contactbgcolor', 'contactfont', 'contactfontattrs',
2651 'bannertextcolor', 'bannerbgcolor']
2653 default = gajim.config.themes_default
2654 for theme_name in default:
2655 gajim.config.add_per('themes', theme_name)
2656 theme = default[theme_name]
2657 for o in d:
2658 gajim.config.set_per('themes', theme_name, o,
2659 theme[d.index(o)])
2661 gajim.idlequeue = idlequeue.get_idlequeue()
2662 # resolve and keep current record of resolved hosts
2663 gajim.resolver = resolver.get_resolver(gajim.idlequeue)
2664 gajim.socks5queue = socks5.SocksQueue(gajim.idlequeue,
2665 self.handle_event_file_rcv_completed,
2666 self.handle_event_file_progress,
2667 self.handle_event_file_error)
2668 gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue)
2669 gajim.default_session_type = ChatControlSession
2671 # Creating Network Events Controller
2672 from common import nec
2673 gajim.nec = nec.NetworkEventsController()
2674 gajim.notification = notify.Notification()
2676 self.create_core_handlers_list()
2677 self.register_core_handlers()
2679 if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'active') \
2680 and gajim.HAVE_ZEROCONF:
2681 gajim.connections[gajim.ZEROCONF_ACC_NAME] = \
2682 connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME)
2683 for account in gajim.config.get_per('accounts'):
2684 if not gajim.config.get_per('accounts', account, 'is_zeroconf') and\
2685 gajim.config.get_per('accounts', account, 'active'):
2686 gajim.connections[account] = Connection(account)
2688 # gtk hooks
2689 gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail')
2690 gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url')
2691 gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url')
2693 self.instances = {}
2695 for a in gajim.connections:
2696 self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {},
2697 'search': {}, 'online_dialog': {}, 'sub_request': {}}
2698 # online_dialog contains all dialogs that have a meaning only when
2699 # we are not disconnected
2700 self.minimized_controls[a] = {}
2701 gajim.contacts.add_account(a)
2702 gajim.groups[a] = {}
2703 gajim.gc_connected[a] = {}
2704 gajim.automatic_rooms[a] = {}
2705 gajim.newly_added[a] = []
2706 gajim.to_be_removed[a] = []
2707 gajim.nicks[a] = gajim.config.get_per('accounts', a, 'name')
2708 gajim.block_signed_in_notifications[a] = True
2709 gajim.sleeper_state[a] = 0
2710 gajim.encrypted_chats[a] = []
2711 gajim.last_message_time[a] = {}
2712 gajim.status_before_autoaway[a] = ''
2713 gajim.transport_avatar[a] = {}
2714 gajim.gajim_optional_features[a] = []
2715 gajim.caps_hash[a] = ''
2717 helpers.update_optional_features()
2718 # prepopulate data which we are sure of; note: we do not log these info
2719 for account in gajim.connections:
2720 gajimcaps = caps_cache.capscache[('sha-1',
2721 gajim.caps_hash[account])]
2722 gajimcaps.identities = [gajim.gajim_identity]
2723 gajimcaps.features = gajim.gajim_common_features + \
2724 gajim.gajim_optional_features[account]
2726 self.remote_ctrl = None
2728 if gajim.config.get('networkmanager_support') and \
2729 dbus_support.supported:
2730 import network_manager_listener
2732 # Handle gnome screensaver
2733 if dbus_support.supported:
2734 def gnome_screensaver_ActiveChanged_cb(active):
2735 if not active:
2736 for account in gajim.connections:
2737 if gajim.sleeper_state[account] == 'autoaway-forced':
2738 # We came back online ofter gnome-screensaver
2739 # autoaway
2740 self.roster.send_status(account, 'online',
2741 gajim.status_before_autoaway[account])
2742 gajim.status_before_autoaway[account] = ''
2743 gajim.sleeper_state[account] = 'online'
2744 return
2745 if not gajim.config.get('autoaway'):
2746 # Don't go auto away if user disabled the option
2747 return
2748 for account in gajim.connections:
2749 if account not in gajim.sleeper_state or \
2750 not gajim.sleeper_state[account]:
2751 continue
2752 if gajim.sleeper_state[account] == 'online':
2753 # we save out online status
2754 gajim.status_before_autoaway[account] = \
2755 gajim.connections[account].status
2756 # we go away (no auto status) [we pass True to auto
2757 # param]
2758 auto_message = gajim.config.get('autoaway_message')
2759 if not auto_message:
2760 auto_message = gajim.connections[account].status
2761 else:
2762 auto_message = auto_message.replace('$S',
2763 '%(status)s')
2764 auto_message = auto_message.replace('$T',
2765 '%(time)s')
2766 auto_message = auto_message % {
2767 'status': gajim.status_before_autoaway[account],
2768 'time': gajim.config.get('autoxatime')}
2769 self.roster.send_status(account, 'away', auto_message,
2770 auto=True)
2771 gajim.sleeper_state[account] = 'autoaway-forced'
2773 try:
2774 bus = dbus.SessionBus()
2775 bus.add_signal_receiver(gnome_screensaver_ActiveChanged_cb,
2776 'ActiveChanged', 'org.gnome.ScreenSaver')
2777 except Exception:
2778 pass
2780 self.show_vcard_when_connect = []
2782 self.sleeper = common.sleepy.Sleepy(
2783 gajim.config.get('autoawaytime') * 60, # make minutes to seconds
2784 gajim.config.get('autoxatime') * 60)
2786 gtkgui_helpers.make_jabber_state_images()
2788 self.systray_enabled = False
2790 import statusicon
2791 self.systray = statusicon.StatusIcon()
2793 pixs = []
2794 for size in (16, 32, 48, 64, 128):
2795 pix = gtkgui_helpers.get_icon_pixmap('gajim', size)
2796 if pix:
2797 pixs.append(pix)
2798 if pixs:
2799 # set the icon to all windows
2800 gtk.window_set_default_icon_list(*pixs)
2802 self.init_emoticons()
2803 self.make_regexps()
2805 # get transports type from DB
2806 gajim.transport_type = gajim.logger.get_transports_type()
2808 # test is dictionnary is present for speller
2809 if gajim.config.get('use_speller'):
2810 lang = gajim.config.get('speller_language')
2811 if not lang:
2812 lang = gajim.LANG
2813 tv = gtk.TextView()
2814 try:
2815 import gtkspell
2816 spell = gtkspell.Spell(tv, lang)
2817 except (ImportError, TypeError, RuntimeError, OSError):
2818 dialogs.AspellDictError(lang)
2820 if gajim.config.get('soundplayer') == '':
2821 # only on first time Gajim starts
2822 commands = ('aplay', 'play', 'esdplay', 'artsplay', 'ossplay')
2823 for command in commands:
2824 if helpers.is_in_path(command):
2825 if command == 'aplay':
2826 command += ' -q'
2827 gajim.config.set('soundplayer', command)
2828 break
2830 self.last_ftwindow_update = 0
2832 self.music_track_changed_signal = None
2835 class PassphraseRequest:
2836 def __init__(self, keyid):
2837 self.keyid = keyid
2838 self.callbacks = []
2839 self.dialog_created = False
2840 self.dialog = None
2841 self.passphrase = None
2842 self.completed = False
2844 def interrupt(self, account=None):
2845 if account:
2846 for (acct, cb) in self.callbacks:
2847 if acct == account:
2848 self.callbacks.remove((acct, cb))
2849 else:
2850 self.callbacks = []
2851 if not len(self.callbacks):
2852 self.dialog.window.destroy()
2854 def run_callback(self, account, callback):
2855 gajim.connections[account].gpg_passphrase(self.passphrase)
2856 callback()
2858 def add_callback(self, account, cb):
2859 if self.completed:
2860 self.run_callback(account, cb)
2861 else:
2862 self.callbacks.append((account, cb))
2863 if not self.dialog_created:
2864 self.create_dialog(account)
2866 def complete(self, passphrase):
2867 self.passphrase = passphrase
2868 self.completed = True
2869 if passphrase is not None:
2870 gobject.timeout_add_seconds(30,
2871 gajim.interface.forget_gpg_passphrase, self.keyid)
2872 for (account, cb) in self.callbacks:
2873 self.run_callback(account, cb)
2874 self.callbacks = []
2876 def create_dialog(self, account):
2877 title = _('Passphrase Required')
2878 second = _('Enter GPG key passphrase for key %(keyid)s (account '
2879 '%(account)s).') % {'keyid': self.keyid, 'account': account}
2881 def _cancel():
2882 # user cancelled, continue without GPG
2883 self.complete(None)
2885 def _ok(passphrase, checked, count):
2886 result = gajim.connections[account].test_gpg_passphrase(passphrase)
2887 if result == 'ok':
2888 # passphrase is good
2889 self.complete(passphrase)
2890 return
2891 elif result == 'expired':
2892 dialogs.ErrorDialog(_('GPG key expired'),
2893 _('Your GPG key has expired, you will be connected to %s '
2894 'without OpenPGP.') % account)
2895 # Don't try to connect with GPG
2896 gajim.connections[account].continue_connect_info[2] = False
2897 self.complete(None)
2898 return
2900 if count < 3:
2901 # ask again
2902 dialogs.PassphraseDialog(_('Wrong Passphrase'),
2903 _('Please retype your GPG passphrase or press Cancel.'),
2904 ok_handler=(_ok, count + 1), cancel_handler=_cancel)
2905 else:
2906 # user failed 3 times, continue without GPG
2907 self.complete(None)
2909 self.dialog = dialogs.PassphraseDialog(title, second, ok_handler=(_ok,
2910 1), cancel_handler=_cancel)
2911 self.dialog_created = True
2914 class ThreadInterface:
2915 def __init__(self, func, func_args=(), callback=None, callback_args=()):
2917 Call a function in a thread
2919 def thread_function(func, func_args, callback, callback_args):
2920 output = func(*func_args)
2921 if callback:
2922 gobject.idle_add(callback, output, *callback_args)
2924 Thread(target=thread_function, args=(func, func_args, callback,
2925 callback_args)).start()