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