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