diff --git a/src/chat_control.py b/src/chat_control.py index 870648a6e..a6575aeb4 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -6,7 +6,7 @@ ## Dimitur Kirov ## Copyright (C) 2007 Lukas Petrovicky ## Julien Pivotto -## Stephan Erb +## Stephan Erb ## ## This file is part of Gajim. ## @@ -104,8 +104,8 @@ class ChatControlBase(MessageControl): type_])) def draw_banner(self): - '''Draw the fat line at the top of the window that - houses the icon, jid, ... + '''Draw the fat line at the top of the window that + houses the icon, jid, ... ''' self.draw_banner_text() self._update_banner_state_image() @@ -133,7 +133,7 @@ class ChatControlBase(MessageControl): def status_url_clicked(self, widget, url): helpers.launch_browser_mailer('url', url) - def __init__(self, type_id, parent_win, widget_name, contact, acct, + def __init__(self, type_id, parent_win, widget_name, contact, acct, resource = None): MessageControl.__init__(self, type_id, parent_win, widget_name, contact, acct, resource = resource); @@ -238,7 +238,7 @@ class ChatControlBase(MessageControl): self.msg_textview.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT, self.dnd_list, gtk.gdk.ACTION_COPY) - + self.update_font() # Hook up send button @@ -266,7 +266,7 @@ class ChatControlBase(MessageControl): # loop removing non-existant dictionaries # iterating on a copy for lang in dict(langs): - try: + try: spell.set_language(langs[lang]) except: del langs[lang] @@ -330,7 +330,7 @@ class ChatControlBase(MessageControl): menu.show_all() - # moved from ChatControl + # moved from ChatControl def _on_banner_eventbox_button_press_event(self, widget, event): '''If right-clicked, show popup''' if event.button == 3: # right click @@ -362,7 +362,7 @@ class ChatControlBase(MessageControl): self.disconnect_style_event(banner_name_label) self.disconnect_style_event(self.banner_status_label) if bgcolor: - banner_eventbox.modify_bg(gtk.STATE_NORMAL, + banner_eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(bgcolor)) default_bg = False else: @@ -391,14 +391,14 @@ class ChatControlBase(MessageControl): if found: widget.disconnect(id) del self.handlers[id] - + def connect_style_event(self, widget, set_fg = False, set_bg = False): self.disconnect_style_event(widget) id = widget.connect('style-set', self._on_style_set_event, set_fg, set_bg) self.handlers[id] = widget def _on_style_set_event(self, widget, style, *opts): - '''set style of widget from style class *.Frame.Eventbox + '''set style of widget from style class *.Frame.Eventbox opts[0] == True -> set fg color opts[1] == True -> set bg color''' banner_eventbox = self.xml.get_widget('banner_eventbox') @@ -564,7 +564,7 @@ class ChatControlBase(MessageControl): send_message = False else: # ENTER send_message = True - + if gajim.connections[self.account].connected < 2: # we are not connected dialogs.ErrorDialog(_('A connection is not available'), _('Your message can not be sent until you are connected.')) @@ -644,7 +644,7 @@ class ChatControlBase(MessageControl): # we don't want size of the buffer to grow indefinately max_size = gajim.config.get('key_up_lines') if size >= max_size: - for i in xrange(0, size - 1): + for i in xrange(0, size - 1): self.sent_history[i] = self.sent_history[i + 1] self.sent_history[max_size - 1] = message # self.sent_history_pos has changed if we browsed sent_history, @@ -693,19 +693,21 @@ class ChatControlBase(MessageControl): kind in ('incoming', 'incoming_queue'): # we want to have save this message in events list # other_tags_for_text == ['marked'] --> highlighted gc message - type_ = 'printed_' + self.type_id - event = 'message_received' if gc_message: if 'marked' in other_tags_for_text: type_ = 'printed_marked_gc_msg' else: type_ = 'printed_gc_msg' event = 'gc_message_received' + else: + type_ = 'printed_' + self.type_id + event = 'message_received' show_in_roster = notify.get_show_in_roster(event, - self.account, self.contact) + self.account, self.contact, self.session) show_in_systray = notify.get_show_in_systray(event, self.account, self.contact, type_) - event = gajim.events.create_event(type_, None, + + event = gajim.events.create_event(type_, (self.session,), show_in_roster = show_in_roster, show_in_systray = show_in_systray) gajim.events.add_event(self.account, full_jid, event) @@ -722,11 +724,10 @@ class ChatControlBase(MessageControl): not self.parent_win.is_active() or not end) and \ kind in ('incoming', 'incoming_queue'): self.parent_win.redraw_tab(self) - ctrl = gajim.interface.msg_win_mgr.get_control(full_jid, self.account) if not self.parent_win.is_active(): - self.parent_win.show_title(True, ctrl) # Enabled Urgent hint + self.parent_win.show_title(True, self) # Enabled Urgent hint else: - self.parent_win.show_title(False, ctrl) # Disabled Urgent hint + self.parent_win.show_title(False, self) # Disabled Urgent hint def toggle_emoticons(self): '''hide show emoticons_button and make sure emoticons_menu is always there @@ -856,7 +857,7 @@ class ChatControlBase(MessageControl): # we don't want to always resize in height the message_textview # so we have minimum on conversation_textview's scrolled window - # but we also want to avoid window resizing so if we reach that + # but we also want to avoid window resizing so if we reach that # minimum for conversation_textview and maximum for message_textview # we set to automatic the scrollbar policy diff_y = message_height - requisition.height @@ -867,13 +868,13 @@ class ChatControlBase(MessageControl): 'vscrollbar-policy') # scroll only when scrollbar appear if policy != gtk.POLICY_AUTOMATIC: - self.msg_scrolledwindow.set_property('vscrollbar-policy', + self.msg_scrolledwindow.set_property('vscrollbar-policy', gtk.POLICY_AUTOMATIC) - self.msg_scrolledwindow.set_property('height-request', + self.msg_scrolledwindow.set_property('height-request', message_height + conversation_height - min_height) self.bring_scroll_to_end(msg_textview) else: - self.msg_scrolledwindow.set_property('vscrollbar-policy', + self.msg_scrolledwindow.set_property('vscrollbar-policy', gtk.POLICY_NEVER) self.msg_scrolledwindow.set_property('height-request', -1) self.conv_textview.bring_scroll_to_end(diff_y - 18, False) @@ -883,10 +884,10 @@ class ChatControlBase(MessageControl): # enable scrollbar automatic policy for horizontal scrollbar # if message we have in message_textview is too big if requisition.width > message_width: - self.msg_scrolledwindow.set_property('hscrollbar-policy', + self.msg_scrolledwindow.set_property('hscrollbar-policy', gtk.POLICY_AUTOMATIC) else: - self.msg_scrolledwindow.set_property('hscrollbar-policy', + self.msg_scrolledwindow.set_property('hscrollbar-policy', gtk.POLICY_NEVER) return True @@ -916,27 +917,26 @@ class ChatControlBase(MessageControl): types_list = ['printed_' + type_, type_] if not len(gajim.events.get_events(self.account, jid, types_list)): - return + return if not self.parent_win: return if self.conv_textview.at_the_end() and \ self.parent_win.get_active_control() == self and \ self.parent_win.window.is_active(): # we are at the end - if not gajim.events.remove_events(self.account, self.get_full_jid(), - types = types_list): + if not self.session.remove_events(types_list): # There were events to remove self.redraw_after_event_removed(jid) def redraw_after_event_removed(self, jid): - ''' We just removed a 'printed_*' event, redraw contact in roster or + ''' We just removed a 'printed_*' event, redraw contact in roster or gc_roster and titles in roster and msg_win ''' self.parent_win.redraw_tab(self) self.parent_win.show_title() # TODO : get the contact and check notify.get_show_in_roster() if self.type_id == message_control.TYPE_PM: room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - groupchat_control = gajim.interface.msg_win_mgr.get_control( + groupchat_control = gajim.interface.msg_win_mgr.get_gc_control( room_jid, self.account) if room_jid in gajim.interface.minimized_controls[self.account]: groupchat_control = \ @@ -947,15 +947,14 @@ class ChatControlBase(MessageControl): if contact: gajim.interface.roster.draw_contact(room_jid, self.account) groupchat_control.draw_contact(nick) - mw = gajim.interface.msg_win_mgr.get_window(room_jid, self.account) - if mw: - mw.redraw_tab(groupchat_control) + if groupchat_control.parent_win: + groupchat_control.parent_win.redraw_tab(groupchat_control) else: gajim.interface.roster.draw_contact(jid, self.account) gajim.interface.roster.show_title() def sent_messages_scroll(self, direction, conv_buf): - size = len(self.sent_history) + size = len(self.sent_history) if self.orig_msg is None: # user was typing something and then went into history, so save # whatever is already typed @@ -1053,7 +1052,7 @@ class ChatControl(ChatControlBase): self.chat_buttons_set_visible(compact_view) self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_chat_banner')) - + # Add lock image to show chat encryption self.lock_image = self.xml.get_widget('lock_image') self.lock_tooltip = gtk.Tooltips() @@ -1080,7 +1079,7 @@ class ChatControl(ChatControlBase): if gajim.get_transport_name_from_jid(self.contact.jid) or \ gajim.connections[self.account].is_zeroconf: convert_to_gc_button.set_sensitive(False) - + # keep timeout id and window obj for possible big avatar # it is on enter-notify and leave-notify so no need to be per jid self.show_bigger_avatar_timeout_id = None @@ -1126,12 +1125,12 @@ class ChatControl(ChatControlBase): gajim.encrypted_chats[self.account].append(contact.jid) msg = _('GPG encryption enabled') ChatControlBase.print_conversation_line(self, msg, 'status', '', None) - + if self.session: self.session.loggable = gajim.config.get('log_encrypted_sessions') self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active, self.session and \ self.session.is_loggable()) - + self.status_tooltip = gtk.Tooltips() if gajim.otr_module: @@ -1154,17 +1153,17 @@ class ChatControl(ChatControlBase): return avatar_w = avatar_pixbuf.get_width() avatar_h = avatar_pixbuf.get_height() - + scaled_buf = self.xml.get_widget('avatar_image').get_pixbuf() scaled_buf_w = scaled_buf.get_width() scaled_buf_h = scaled_buf.get_height() - + # do we have something bigger to show? if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h: # wait for 0.5 sec in case we leave earlier self.show_bigger_avatar_timeout_id = gobject.timeout_add(500, self.show_bigger_avatar, widget) - + def on_avatar_eventbox_leave_notify_event(self, widget, event): '''we left the eventbox area that holds the avatar img''' # did we add a timeout? if yes remove it @@ -1176,14 +1175,14 @@ class ChatControl(ChatControlBase): if event.button == 3: # right click menu = gtk.Menu() menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) - id = menuitem.connect('activate', + id = menuitem.connect('activate', gtkgui_helpers.on_avatar_save_as_menuitem_activate, self.contact.jid, self.account, self.contact.get_shown_name() + \ '.jpeg') self.handlers[id] = menuitem menu.append(menuitem) menu.show_all() - menu.connect('selection-done', lambda w:w.destroy()) + menu.connect('selection-done', lambda w:w.destroy()) # show the menu menu.show_all() menu.popup(None, None, None, event.button, event.time) @@ -1268,15 +1267,15 @@ class ChatControl(ChatControlBase): banner_status_img.set_from_pixbuf(scaled_pix) def draw_banner_text(self): - '''Draw the text in the fat line at the top of the window that - houses the name, jid. + '''Draw the text in the fat line at the top of the window that + houses the name, jid. ''' contact = self.contact jid = contact.jid banner_name_label = self.xml.get_widget('banner_name_label') banner_eventbox = self.xml.get_widget('banner_eventbox') - + name = contact.get_shown_name() if self.resource: name += '/' + self.resource @@ -1363,7 +1362,7 @@ class ChatControl(ChatControlBase): ec.remove(self.contact.jid) self.gpg_is_active = False msg = _('GPG encryption disabled') - ChatControlBase.print_conversation_line(self, msg, 'status', '', None) + ChatControlBase.print_conversation_line(self, msg, 'status', '', None) if self.session: self.session.loggable = True @@ -1388,12 +1387,12 @@ class ChatControl(ChatControlBase): gajim.config.add_per('contacts', self.contact.jid) gajim.config.set_per('contacts', self.contact.jid, 'gpg_enabled', self.gpg_is_active) - + self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active, self.session and \ self.session.is_loggable()) - + def _show_lock_image(self, visible, enc_type = '', enc_enabled = False, chat_logged = False): - '''Set lock icon visibiity and create tooltip''' + '''Set lock icon visibility and create tooltip''' status_string = enc_enabled and 'is' or 'is NOT' logged_string = chat_logged and 'will' or 'will NOT' tooltip = '%s Encryption %s active. \nYour chat session %s be logged.' %\ @@ -1549,7 +1548,7 @@ class ChatControl(ChatControlBase): # assume no activity and let the motion-notify or 'insert-text' make them # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! self.reset_kbd_mouse_timeout_vars() - return True # loop forever + return True # loop forever def check_for_possible_inactive_chatstate(self, arg): ''' did we move mouse over that window or wrote something in message @@ -1601,7 +1600,7 @@ class ChatControl(ChatControlBase): msg = _('E2E encryption disabled') ChatControlBase.print_conversation_line(self, msg, 'status', '', None) self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \ - self.session.is_loggable()) + self.session.is_loggable()) def print_conversation(self, text, frm='', tim=None, encrypted=False, subject=None, xhtml=None, simple=False): @@ -1625,13 +1624,13 @@ class ChatControl(ChatControlBase): if self.session and self.session.enable_encryption: if not encrypted: msg = _('The following message was NOT encrypted') - ChatControlBase.print_conversation_line(self, msg, + ChatControlBase.print_conversation_line(self, msg, 'status', '', tim) else: # GPG encryption if encrypted and not self.gpg_is_active: msg = _('The following message was encrypted') - ChatControlBase.print_conversation_line(self, msg, + ChatControlBase.print_conversation_line(self, msg, 'status', '', tim) self._toggle_gpg() elif not encrypted and self.gpg_is_active: @@ -1672,7 +1671,7 @@ class ChatControl(ChatControlBase): elif num_unread > 1: unread = '[' + unicode(num_unread) + ']' - # Draw tab label using chatstate + # Draw tab label using chatstate theme = gajim.config.get('roster_theme') color = None if not chatstate: @@ -1700,7 +1699,7 @@ class ChatControl(ChatControlBase): color = self.lighten_color(color) else: # active or not chatstate, get color from gtk color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE] - + name = self.contact.get_shown_name() if self.resource: @@ -1719,7 +1718,7 @@ class ChatControl(ChatControlBase): ['printed_' + self.type_id, self.type_id])) # Set tab image (always 16x16); unread messages show the 'event' image tab_img = None - + if num_unread and gajim.config.get('show_unread_tab_icon'): img_16 = gajim.interface.roster.get_appropriate_state_images( self.contact.jid, icon_name = 'event') @@ -1760,7 +1759,7 @@ class ChatControl(ChatControlBase): convert_to_gc_menuitem = xml.get_widget('convert_to_groupchat') muc_icon = gtkgui_helpers.load_icon('muc_active') if muc_icon: - convert_to_gc_menuitem.set_image(muc_icon) + convert_to_gc_menuitem.set_image(muc_icon) ag = gtk.accel_groups_from_object(self.parent_win.window)[0] send_file_menuitem.add_accelerator('activate', ag, gtk.keysyms.f, gtk.gdk.CONTROL_MASK, @@ -1774,7 +1773,7 @@ class ChatControl(ChatControlBase): contact = self.parent_win.get_active_contact() jid = contact.jid - + # check if we support and use gpg if not gajim.config.get_per('accounts', self.account, 'keyid') or\ not gajim.connections[self.account].USE_GPG or\ @@ -1814,22 +1813,22 @@ class ChatControl(ChatControlBase): convert_to_gc_menuitem.set_sensitive(False) # connect signals - id = history_menuitem.connect('activate', + id = history_menuitem.connect('activate', self._on_history_menuitem_activate) self.handlers[id] = history_menuitem - id = send_file_menuitem.connect('activate', + id = send_file_menuitem.connect('activate', self._on_send_file_menuitem_activate) - self.handlers[id] = send_file_menuitem + self.handlers[id] = send_file_menuitem id = add_to_roster_menuitem.connect('activate', self._on_add_to_roster_menuitem_activate) self.handlers[id] = add_to_roster_menuitem id = toggle_gpg_menuitem.connect('activate', self._on_toggle_gpg_menuitem_activate) self.handlers[id] = toggle_gpg_menuitem - id = toggle_e2e_menuitem.connect('activate', + id = toggle_e2e_menuitem.connect('activate', self._on_toggle_e2e_menuitem_activate) - self.handlers[id] = toggle_e2e_menuitem - id = information_menuitem.connect('activate', + self.handlers[id] = toggle_e2e_menuitem + id = information_menuitem.connect('activate', self._on_contact_information_menuitem_activate) self.handlers[id] = information_menuitem id = convert_to_gc_menuitem.connect('activate', @@ -1885,7 +1884,7 @@ class ChatControl(ChatControlBase): # JEP 85 does not allow resending the same chatstate # this function checks for that and just returns so it's safe to call it # with same state. - + # This functions also checks for violation in state transitions # and raises RuntimeException with appropriate message # more on that http://www.jabber.org/jeps/jep-0085.html#statechart @@ -1914,7 +1913,7 @@ class ChatControl(ChatControlBase): if contact.composing_xep is False: # jid cannot do xep85 nor xep22 return - # if the new state we wanna send (state) equals + # if the new state we wanna send (state) equals # the current state (contact.our_chatstate) then return if contact.our_chatstate == state: return @@ -1926,7 +1925,7 @@ class ChatControl(ChatControlBase): # in self.send_message() because we need REAL message (with ) # for that procedure so return to make sure we send only once # 'active' until we know peer supports jep85 - return + return if contact.our_chatstate == 'ask': return @@ -1946,7 +1945,7 @@ class ChatControl(ChatControlBase): MessageControl.send_message(self, None, chatstate = 'active') contact.our_chatstate = 'active' self.reset_kbd_mouse_timeout_vars() - + # if we're inactive prevent composing (JEP violation) elif contact.our_chatstate == 'inactive' and state == 'composing': # go active before @@ -1963,10 +1962,15 @@ class ChatControl(ChatControlBase): def shutdown(self): # destroy banner tooltip - bug #pygtk for that! self.status_tooltip.destroy() + # Send 'gone' chatstate self.send_chatstate('gone', self.contact) self.contact.chatstate = None self.contact.our_chatstate = None + + # disconnect self from session + self.session.control = None + # Disconnect timer callbacks gobject.source_remove(self.possible_paused_timeout_id) gobject.source_remove(self.possible_inactive_timeout_id) @@ -2173,12 +2177,6 @@ class ChatControl(ChatControlBase): if hasattr(self, 'session') and self.session and self.session.enable_encryption: self.print_esession_details() - # Is it a pm ? - is_pm = False - room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - control = gajim.interface.msg_win_mgr.get_control(room_jid, self.account) - if control and control.type_id == message_control.TYPE_GC: - is_pm = True # list of message ids which should be marked as read message_ids = [] for event in events: @@ -2203,8 +2201,13 @@ class ChatControl(ChatControlBase): types = [self.type_id]) typ = 'chat' # Is it a normal chat or a pm ? + # reset to status image in gc if it is a pm - if is_pm: + # Is it a pm ? + room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) + control = gajim.interface.msg_win_mgr.get_gc_control(room_jid, + self.account) + if control and control.type_id == message_control.TYPE_GC: control.update_ui() control.parent_win.show_title() typ = 'pm' @@ -2265,18 +2268,18 @@ class ChatControl(ChatControlBase): window.set_app_paintable(True) if gtk.gtk_version >= (2, 10, 0) and gtk.pygtk_version >= (2, 10, 0): window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLTIP) - + window.realize() window.window.set_back_pixmap(pixmap, False) # make it transparent window.window.shape_combine_mask(mask, 0, 0) - # make the bigger avatar window show up centered + # make the bigger avatar window show up centered x0, y0 = small_avatar.window.get_origin() x0 += small_avatar.allocation.x y0 += small_avatar.allocation.y center_x= x0 + (small_avatar.allocation.width / 2) center_y = y0 + (small_avatar.allocation.height / 2) - pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2) + pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2) window.move(pos_x, pos_y) # make the cursor invisible so we can see the image invisible_cursor = gtkgui_helpers.get_invisible_cursor() @@ -2307,7 +2310,7 @@ class ChatControl(ChatControlBase): c = self.gc_contact else: c = self.contact - gajim.interface.instances['file_transfers'].show_file_send_request( + gajim.interface.instances['file_transfers'].show_file_send_request( self.account, c) def _on_add_to_roster_menuitem_activate(self, widget): @@ -2317,7 +2320,7 @@ class ChatControl(ChatControlBase): gajim.interface.roster.on_info(widget, self.contact, self.account) def _on_toggle_gpg_menuitem_activate(self, widget): - self._toggle_gpg() + self._toggle_gpg() def _on_convert_to_gc_menuitem_activate(self, widget): '''user want to invite some friends to chat''' diff --git a/src/common/commands.py b/src/common/commands.py index 2e4ad6e25..28c6f3a3c 100644 --- a/src/common/commands.py +++ b/src/common/commands.py @@ -204,7 +204,7 @@ class LeaveGroupchatsCommand(AdHocCommand): # for next invocation self.execute = self.leavegroupchats - + return True # keep the session def leavegroupchats(self, request): @@ -225,7 +225,7 @@ class LeaveGroupchatsCommand(AdHocCommand): account = self.connection.name try: for room_jid in gc: - gc_control = gajim.interface.msg_win_mgr.get_control(room_jid, + gc_control = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) gc_control.parent_win.remove_tab(gc_control, None, force = True) except: # KeyError if there's no presence-type field in form or diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index dd4d9ce8a..ad256e91f 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -49,7 +49,8 @@ if dbus_support.supported: import dbus from music_track_listener import MusicTrackListener -from common.stanza_session import EncryptedStanzaSession +from session import ChatControlSession +import tictactoe STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -994,7 +995,7 @@ class ConnectionVcard: gajim.interface.remove_avatar_files(our_jid) self.awaiting_answers[id] = (VCARD_PUBLISHED, iq2) - + def _IqCB(self, con, iq_obj): id = iq_obj.getID() @@ -1046,6 +1047,7 @@ class ConnectionVcard: our_jid = gajim.get_jid_from_account(self.name) if iq_obj.getType() == 'error' and jid == our_jid: # our server doesn't support vcard + gajim.log.debug('xxx error xxx') self.vcard_supported = False if not iq_obj.getTag('vCard') or iq_obj.getType() == 'error': if frm and frm != our_jid: @@ -1261,7 +1263,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, reply.addChild(node=common.xmpp.ErrorNode('service-unavailable', typ='cancel')) con.send(reply) - + raise common.xmpp.NodeProcessed def _InitE2ECB(self, con, stanza, session): @@ -1291,7 +1293,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, errmsg = iq_obj.getErrorMsg() errcode = iq_obj.getErrorCode() self.dispatch('ERROR_ANSWER', (id, jid_from, errmsg, errcode)) - + def _PrivateCB(self, con, iq_obj): ''' Private Data (XEP 048 and 049) @@ -1499,6 +1501,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _messageCB(self, con, msg): '''Called when we receive a message''' + gajim.log.debug('MessageCB') + frm = helpers.get_full_jid_from_iq(msg) mtype = msg.getType() thread_id = msg.getThread() @@ -1506,8 +1510,23 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if not mtype: mtype = 'normal' - if not mtype == 'groupchat': - session = self.get_session(frm, thread_id, mtype) + game_invite = msg.getTag('invite', namespace='http://jabber.org/protocol/games') + if game_invite: + game = game_invite.getTag('game') + + if game.getAttr('var') == \ + 'http://jabber.org/protocol/games/tictactoe': + klass = tictactoe.TicTacToeSession + + # this assumes that the invitation came with a thread_id we haven't + # seen + session = self.make_new_session(frm, thread_id, klass=klass) + + session.invited(msg) + + return + elif mtype != 'groupchat': + session = self.get_or_create_session(frm, thread_id) if thread_id and not session.received_thread_id: session.received_thread_id = True @@ -1516,66 +1535,57 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if msg.getTag('event') is not None: self._pubsubEventCB(con, msg) return - # check if the message is a xep70-confirmation-request - if msg.getTag('confirm') and msg.getTag('confirm').namespace == \ - common.xmpp.NS_HTTP_AUTH: + + # check if the message is a XEP-0070 confirmation request + if msg.getTag('confirm', namespace=common.xmpp.NS_HTTP_AUTH): self._HttpAuthCB(con, msg) return - if msg.getTag('feature') and msg.getTag('feature').namespace == \ - common.xmpp.NS_FEATURE: + + # check if the message is a XEP-0020 feature negotiation request + if msg.getTag('feature', namespace=common.xmpp.NS_FEATURE): if gajim.HAVE_PYCRYPTO: self._FeatureNegCB(con, msg, session) return - if msg.getTag('init') and msg.getTag('init').namespace == \ - common.xmpp.NS_ESESSION_INIT: + if msg.getTag('init', namespace=common.xmpp.NS_ESESSION_INIT): self._InitE2ECB(con, msg, session) - + encrypted = False tim = msg.getTimestamp() tim = helpers.datetime_tuple(tim) tim = localtime(timegm(tim)) - e2e_tag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) - if e2e_tag: + if msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO): encrypted = True try: msg = session.decrypt_stanza(msg) except: - self.dispatch('FAILED_DECRYPT', (frm, tim)) + self.dispatch('FAILED_DECRYPT', (frm, tim, session)) msgtxt = msg.getBody() - msghtml = msg.getXHTML() subject = msg.getSubject() # if not there, it's None - tim = msg.getTimestamp() - tim = helpers.datetime_tuple(tim) - tim = localtime(timegm(tim)) + frm = helpers.get_full_jid_from_iq(msg) jid = helpers.get_jid_from_iq(msg) + addressTag = msg.getTag('addresses', namespace = common.xmpp.NS_ADDRESS) + # Be sure it comes from one of our resource, else ignore address element if addressTag and jid == gajim.get_jid_from_account(self.name): address = addressTag.getTag('address', attrs={'type': 'ofrom'}) if address: frm = address.getAttr('jid') jid = gajim.get_jid_without_resource(frm) - no_log_for = gajim.config.get_per('accounts', self.name, - 'no_log_for') - if not no_log_for: - no_log_for = '' - no_log_for = no_log_for.split() - chatstate = None - encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED) - decmsg = '' + # invitations invite = None + encTag = msg.getTag('x', namespace=common.xmpp.NS_ENCRYPTED) + if not encTag: invite = msg.getTag('x', namespace = common.xmpp.NS_MUC_USER) if invite and not invite.getTag('invite'): invite = None - delayed = msg.getTag('x', namespace = common.xmpp.NS_DELAY) is not None - msg_id = None - composing_xep = None + # FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) do NOT RECOMMENDED # invitation # stanza (MUC XEP) remove in 2007, as we do not do NOT RECOMMENDED @@ -1589,218 +1599,185 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('GC_INVITATION', (room_jid, frm, '', None, is_continued)) return - form_node = None - for xtag in xtags: - if xtag.getNamespace() == common.xmpp.NS_DATA: - form_node = xtag - break - # chatstates - look for chatstate tags in a message if not delayed - if not delayed: - composing_xep = False - children = msg.getChildren() - for child in children: - if child.getNamespace() == 'http://jabber.org/protocol/chatstates': - chatstate = child.getName() - composing_xep = 'XEP-0085' - break - # No XEP-0085 support, fallback to XEP-0022 - if not chatstate: - chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT) - if chatstate_child: - chatstate = 'active' - composing_xep = 'XEP-0022' - if not msgtxt and chatstate_child.getTag('composing'): - chatstate = 'composing' - # XEP-0172 User Nickname - user_nick = msg.getTagData('nick') - if not user_nick: - user_nick = '' if encTag and self.USE_GPG: - #decrypt encmsg = encTag.getData() keyID = gajim.config.get_per('accounts', self.name, 'keyid') if keyID: decmsg = self.gpg.decrypt(encmsg, keyID) # \x00 chars are not allowed in C (so in GTK) - decmsg = decmsg.replace('\x00', '') - if decmsg: - msgtxt = decmsg - encrypted = True + msgtxt = decmsg.replace('\x00', '') + encrypted = True if mtype == 'error': - error_msg = msg.getErrorMsg() - if not error_msg: - error_msg = msgtxt - msgtxt = None - if session.is_loggable(): - try: - gajim.logger.write('error', frm, error_msg, tim = tim, - subject = subject) - except exceptions.PysqliteOperationalError, e: - self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt, - tim)) - return + self.dispatch_error_message(msg, msgtxt, session, frm, tim, subject) elif mtype == 'groupchat': - has_timestamp = False - if msg.timestamp: - has_timestamp = True - if subject is not None: - self.dispatch('GC_SUBJECT', (frm, subject, msgtxt, has_timestamp)) + self.dispatch_gc_message(msg, subject, frm, msgtxt, jid, tim) + elif invite is not None: + self.dispatch_invite_message(invite, frm) + else: + # XXX this shouldn't be hardcoded + if isinstance(session, ChatControlSession): + if gajim.otr_module and isinstance(msgtxt, unicode): + otr_msg_tuple = gajim.otr_module.otrl_message_receiving( + gajim.connections[self.name].otr_userstates, + (gajim.otr_ui_ops, {'account':self.name}), + gajim.get_jid_from_account(self.name).encode(), + gajim.OTR_PROTO, frm.encode(), msgtxt.encode(), + (gajim.otr_add_appdata, self.name)) + msgtxt = unicode(otr_msg_tuple[1]) + # OTR messages are unformatted, or rather contain the same + # text in and + msghtml = msgtxt + + + if gajim.otr_module.otrl_tlv_find(otr_msg_tuple[2], + gajim.otr_module.OTRL_TLV_DISCONNECTED) != None: + gajim.otr_ui_ops.gajim_log("%s has ended his/her private conversation" + " with you; you should do the same."%frm, self.name, + frm) + if session.control: + session.control.update_ui() + + ctx = gajim.otr_module.otrl_context_find(gajim.connections[self.name].otr_userstates, frm.encode(), + gajim.get_jid_from_account(self.name).encode(), gajim.OTR_PROTO, 1, + (gajim.otr_add_appdata, self.name))[0] + + tlvs = otr_msg_tuple[2] + ctx.app_data.handle_tlv(tlvs) + + session.received(frm, msgtxt, tim, encrypted, subject, msg) else: - statusCode = msg.getStatusCode() - if not msg.getTag('body'): #no - # It could be a config change. See - # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify - if msg.getTag('x'): - if statusCode != []: - self.dispatch('GC_CONFIG_CHANGE', (jid, statusCode)) - return - # Ignore message from room in which we are not - if not self.last_history_time.has_key(jid): - return - self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msghtml, - statusCode)) - tim_int = int(float(mktime(tim))) - if self.name not in no_log_for and jid not in no_log_for and not \ - tim_int <= self.last_history_time[jid] and msgtxt \ - and frm.find('/') >= 0: - # if frm.find('/') < 0, it means message comes from room itself - # usually it hold description and can be send at each connection - # so don't store it in logs - try: - gajim.logger.write('gc_msg', frm, msgtxt, tim = tim) - # save the time we log to avoid duplicate logs - self.last_history_time[jid] = tim_int - except exceptions.PysqliteOperationalError, e: - self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - return - elif mtype == 'chat': # it's type 'chat' - - if gajim.otr_module and isinstance(msgtxt, unicode): - otr_msg_tuple = gajim.otr_module.otrl_message_receiving( - gajim.connections[self.name].otr_userstates, - (gajim.otr_ui_ops, {'account':self.name}), - gajim.get_jid_from_account(self.name).encode(), - gajim.OTR_PROTO, frm.encode(), msgtxt.encode(), - (gajim.otr_add_appdata, self.name)) - msgtxt = unicode(otr_msg_tuple[1]) - # OTR messages are unformatted, or rather contain the same - # text in and - msghtml = msgtxt - - if gajim.otr_module.otrl_tlv_find(otr_msg_tuple[2], - gajim.otr_module.OTRL_TLV_DISCONNECTED) != None: - gajim.otr_ui_ops.gajim_log("%s has ended his/her private conversation" - " with you; you should do the same."%frm, self.name, - frm) - ctrl = gajim.interface.msg_win_mgr.get_control(frm, self.name) - if ctrl: - ctrl.update_ui() - - ctx = gajim.otr_module.otrl_context_find(gajim.connections[self.name].otr_userstates, frm.encode(), - gajim.get_jid_from_account(self.name).encode(), gajim.OTR_PROTO, 1, - (gajim.otr_add_appdata, self.name))[0] - tlvs = otr_msg_tuple[2] - ctx.app_data.handle_tlv(tlvs) - - if not msg.getTag('body') and chatstate is None: #no - return - if msg.getTag('body') and session.is_loggable() and msgtxt: - try: - msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, - tim = tim, subject = subject) - except exceptions.PysqliteOperationalError, e: - self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - else: # it's single message - if invite is not None: - item = invite.getTag('invite') - jid_from = item.getAttr('from') - reason = item.getTagData('reason') - item = invite.getTag('password') - password = invite.getTagData('password') - is_continued = False - if invite.getTag('invite').getTag('continue'): - is_continued = True - self.dispatch('GC_INVITATION',(frm, jid_from, reason, password, - is_continued)) - return - if session.is_loggable()and msgtxt: - try: - gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, - subject = subject) - except exceptions.PysqliteOperationalError, e: - self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - mtype = 'normal' - treat_as = gajim.config.get('treat_incoming_messages') - if treat_as: - mtype = treat_as - self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, - subject, chatstate, msg_id, composing_xep, user_nick, msghtml, - session, form_node)) + session.received(msg) # END messageCB - def get_session(self, jid, thread_id, type): + # process and dispatch an error message + def dispatch_error_message(self, msg, msgtxt, session, frm, tim, subject): + error_msg = msg.getErrorMsg() + + if not error_msg: + error_msg = msgtxt + msgtxt = None + + if session.is_loggable(): + try: + gajim.logger.write('error', frm, error_msg, tim=tim, + subject=subject) + except exceptions.PysqliteOperationalError, e: + self.dispatch('ERROR', (_('Disk Write Error'), str(e))) + self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt, + tim, session)) + + # process and dispatch a groupchat message + def dispatch_gc_message(self, msg, subject, frm, msgtxt, jid, tim): + has_timestamp = bool(msg.timestamp) + + if subject is not None: + self.dispatch('GC_SUBJECT', (frm, subject, msgtxt, has_timestamp)) + return + + statusCode = msg.getStatusCode() + + if not msg.getTag('body'): # no + # It could be a config change. See + # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify + if msg.getTag('x'): + if statusCode != []: + self.dispatch('GC_CONFIG_CHANGE', (jid, statusCode)) + return + + # Ignore message from room in which we are not + if not self.last_history_time.has_key(jid): + return + + self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msg.getXHTML(), + statusCode)) + + no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') + + if not no_log_for: + no_log_for = '' + + no_log_for = no_log_for.split() + + tim_int = int(float(mktime(tim))) + + if self.name not in no_log_for and jid not in no_log_for and not \ + tim_int <= self.last_history_time[jid] and msgtxt and frm.find('/') >= 0: + # if frm.find('/') < 0, it means message comes from room itself + # usually it hold description and can be send at each connection + # so don't store it in logs + try: + gajim.logger.write('gc_msg', frm, msgtxt, tim=tim) + # save the time we log to avoid duplicate logs + self.last_history_time[jid] = tim_int + except exceptions.PysqliteOperationalError, e: + self.dispatch('ERROR', (_('Disk Write Error'), str(e))) + + def dispatch_invite_message(self, invite, frm): + item = invite.getTag('invite') + jid_from = item.getAttr('from') + reason = item.getTagData('reason') + item = invite.getTag('password') + password = invite.getTagData('password') + + is_continued = False + if invite.getTag('invite').getTag('continue'): + is_continued = True + self.dispatch('GC_INVITATION',(frm, jid_from, reason, password, + is_continued)) + + def get_or_create_session(self, jid, thread_id): '''returns an existing session between this connection and 'jid', returns a new one if none exist.''' - session = self.find_session(jid, thread_id, type) + + pm = True + if not gajim.interface.is_pm_contact(jid, self.name): + pm = False + jid = gajim.get_jid_without_resource(jid) + + session = self.find_session(jid, thread_id) if session: return session + + if pm: + return self.make_new_session(jid, thread_id, type = 'pm') else: - # it's possible we initiated a session with a bare JID and this is the - # first time we've seen a resource - bare_jid = gajim.get_jid_without_resource(jid) - if bare_jid != jid: - session = self.find_session(bare_jid, thread_id, type) - if session: - if not session.received_thread_id: - thread_id = session.thread_id + return self.make_new_session(jid, thread_id) - self.move_session(bare_jid, thread_id, jid.split("/")[1]) - return session - - return self.make_new_session(jid, thread_id, type) - - def find_session(self, jid, thread_id, type): + def find_session(self, jid, thread_id): try: - if type == 'chat' and not thread_id: + if not thread_id: return self.find_null_session(jid) else: return self.sessions[jid][thread_id] except KeyError: return None + def terminate_sessions(self): + '''send termination messages and delete all active sessions''' + for jid in self.sessions: + for thread_id in self.sessions[jid]: + self.sessions[jid][thread_id].terminate() + + self.sessions = {} + def delete_session(self, jid, thread_id): try: del self.sessions[jid][thread_id] - + if not self.sessions[jid]: del self.sessions[jid] except KeyError: pass - def move_session(self, original_jid, thread_id, to_resource): - '''moves a session to another resource.''' - session = self.sessions[original_jid][thread_id] - - del self.sessions[original_jid][thread_id] - - new_jid = gajim.get_jid_without_resource(original_jid) + '/' + to_resource - session.jid = common.xmpp.JID(new_jid) - - if not new_jid in self.sessions: - self.sessions[new_jid] = {} - - self.sessions[new_jid][thread_id] = session - def find_null_session(self, jid): - '''finds all of the sessions between us and jid that jid hasn't sent a thread_id in yet. + '''finds all of the sessions between us and a remote jid in which we +haven't received a thread_id yet and returns the session that we last +sent a message to.''' -returns the session that we last sent a message to.''' - - sessions_with_jid = self.sessions[jid].values() - no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions_with_jid) + sessions = self.sessions[jid].values() + no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions) if no_threadid_sessions: no_threadid_sessions.sort(key=lambda s: s.last_send) @@ -1808,8 +1785,16 @@ returns the session that we last sent a message to.''' else: return None - def make_new_session(self, jid, thread_id = None, type = 'chat'): - sess = EncryptedStanzaSession(self, common.xmpp.JID(jid), thread_id, type) + def make_new_session(self, jid, thread_id=None, type='chat', klass=None): + if not klass: + klass = ChatControlSession + + # determine if this session is a pm session + # if not, discard the resource + if not type == 'pm': + jid = gajim.get_jid_without_resource(jid) + + sess = klass(self, common.xmpp.JID(jid), thread_id, type) if not jid in self.sessions: self.sessions[jid] = {} @@ -2303,7 +2288,7 @@ returns the session that we last sent a message to.''' self.dispatch('RESOURCE_CONFLICT', ()) def _register_handlers(self, con, con_type): - # try to find another way to register handlers in each class + # try to find another way to register handlers in each class # that defines handlers con.RegisterHandler('message', self._messageCB) con.RegisterHandler('presence', self._presenceCB) diff --git a/src/common/events.py b/src/common/events.py index f676ec7fc..ac9625783 100644 --- a/src/common/events.py +++ b/src/common/events.py @@ -116,7 +116,7 @@ class Events: def remove_events(self, account, jid, event = None, types = []): '''if event is not specified, remove all events from this jid, - optionnaly only from given type + optionally only from given type return True if no such event found''' if not self._events.has_key(account): return True @@ -168,7 +168,7 @@ class Events: '''returns all events from the given account of the form {jid1: [], jid2: []} if jid is given, returns all events from the given jid in a list: [] - optionnaly only from given type''' + optionally only from given type''' if not self._events.has_key(account): return [] if not jid: diff --git a/src/common/gajim.py b/src/common/gajim.py index 747e9e8b6..3b0c60472 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -178,7 +178,7 @@ gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_PRIVACY, xmpp.NS_PRIVATE, xmpp.NS_REGISTER, xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED, 'msglog', 'sslc2s', 'stringprep', xmpp.NS_PING, - xmpp.NS_TIME_REVISED] + xmpp.NS_TIME_REVISED, xmpp.NS_GAMING] # Optional features gajim supports gajim_optional_features = [] @@ -218,7 +218,7 @@ def get_real_jid_from_fjid(account, fjid): if not nick: # It's not a fake_jid, it is a real jid return fjid # we return the real jid real_jid = fjid - if interface.msg_win_mgr.get_control(room_jid, account): + if interface.msg_win_mgr.get_gc_control(room_jid, account): # It's a pm, so if we have real jid it's in contact.jid gc_contact = contacts.get_gc_contact(account, room_jid, nick) if not gc_contact: diff --git a/src/common/pep.py b/src/common/pep.py index 457875fc6..29d3be090 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -253,10 +253,9 @@ def user_nickname(items, name, jid): if nick is not None: contact.contact_name = nick gajim.interface.roster.draw_contact(user, name) - ctrl = gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(user, name): ctrl.update_ui() - win = gajim.interface.msg_win_mgr.get_window(user, name) + win = ctrl.parent_win win.redraw_tab(ctrl) win.show_title() elif retract: diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 43faf6a5b..e45f10715 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -32,10 +32,23 @@ class StanzaSession(object): else: self.thread_id = self.generate_thread_id() + self.loggable = True + self.last_send = 0 self.status = None self.negotiated = {} + def is_loggable(self): + account = self.conn.name + no_log_for = gajim.config.get_per('accounts', account, 'no_log_for') + + if not no_log_for: + no_log_for = '' + + no_log_for = no_log_for.split() + + return self.loggable and account not in no_log_for and self.jid not in no_log_for + def generate_thread_id(self): return "".join([random.choice(string.ascii_letters) for x in xrange(0,32)]) @@ -70,7 +83,7 @@ class StanzaSession(object): def cancelled_negotiation(self): '''A negotiation has been cancelled, so reset this session to its default state.''' - if hasattr(self, 'control'): + if self.control: self.control.on_cancel_session_negotiation() self.status = None @@ -132,8 +145,6 @@ class EncryptedStanzaSession(StanzaSession): def __init__(self, conn, jid, thread_id, type = 'chat'): StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') - self.loggable = True - self.xes = {} self.es = {} @@ -767,7 +778,7 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'active' self.enable_encryption = True - if hasattr(self, 'control'): + if self.control: self.control.print_esession_details() def final_steps_alice(self, form): @@ -787,15 +798,14 @@ class EncryptedStanzaSession(StanzaSession): self.do_retained_secret(k, srs) - # don't need to calculate ks_s here - + # ks_s doesn't need to be calculated here self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(k) self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(k) # 4.6.2 Verifying Bob's Identity self.verify_identity(form, self.d, False, 'b') -# Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. + # Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza. if self.negotiated['logging'] == 'mustnot': self.loggable = False @@ -803,7 +813,7 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'active' self.enable_encryption = True - if hasattr(self, 'control'): + if self.control: self.control.print_esession_details() # calculate and store the new retained secret @@ -891,17 +901,6 @@ otherwise, list the fields we haven't implemented''' # preventing falsified messages from going through. self.km_o = '' - def is_loggable(self): - account = self.conn.name - no_log_for = gajim.config.get_per('accounts', account, 'no_log_for') - - if not no_log_for: - no_log_for = '' - - no_log_for = no_log_for.split() - - return self.loggable and account not in no_log_for and self.jid not in no_log_for - def cancelled_negotiation(self): StanzaSession.cancelled_negotiation(self) self.enable_encryption = False diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index e83556416..2aa945686 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -658,7 +658,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream): frm = unicode(frm) jid = frm - session = self.get_session(frm, thread_id, mtype) + session = self.get_or_create_session(frm, thread_id, mtype) if thread_id and not session.received_thread_id: session.received_thread_id = True @@ -794,7 +794,12 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream): raise common.xmpp.NodeProcessed - def get_session(self, jid, thread_id, type): + def terminate_sessions(self): + '''send termination messages and delete all active sessions''' + # XXX + pass + + def get_or_create_session(self, jid, thread_id, type): '''returns an existing session between this connection and 'jid', returns a new one if none exist.''' session = self.find_session(jid, thread_id, type) diff --git a/src/dialogs.py b/src/dialogs.py index f06bf6932..76cfa47d0 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -1717,7 +1717,7 @@ class JoinGroupchatWindow: return if gajim.interface.msg_win_mgr.has_window(room_jid, self.account): - ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, self.account) + ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, self.account) if ctrl.type_id != message_control.TYPE_GC: ErrorDialog(_('This is not a group chat'), _('%s is not the name of a group chat.') % room_jid) diff --git a/src/gajim.py b/src/gajim.py index 172c58fc1..74aa00caf 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -241,6 +241,7 @@ from chat_control import ChatControl from groupchat_control import GroupchatControl from groupchat_control import PrivateChatControl from atom_window import AtomWindow +from session import ChatControlSession import common.sleepy @@ -257,7 +258,7 @@ from common.xmpp import Message as XmppMessage try: import otr, otr_windows - + gajim.otr_module = otr gajim.otr_windows = otr_windows except ImportError: @@ -295,20 +296,20 @@ class OtrlMessageAppOps: # chat) if id: gajim.logger.set_read_messages([id]) - + def get_control(self, fjid, account): # first try to get the window with the full jid - ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) - if ctrl: + ctrls = gajim.interface.msg_win_mgr.get_chat_controls(fjid, account) + if ctrls: # got one, be happy - return ctrl - + return ctrls[0] + # otherwise try without the resource - ctrl = gajim.interface.msg_win_mgr.get_control( + ctrls = gajim.interface.msg_win_mgr.get_chat_controls( gajim.get_jid_without_resource(fjid), account) # but only use it when it is not a GC window - if ctrl and ctrl.TYPE_ID == message_control.TYPE_CHAT: - return ctrl + if ctrls and ctrls[0].TYPE_ID == message_control.TYPE_CHAT: + return ctrls[0] def policy(self, opdata=None, context=None): policy = gajim.config.get_per("contacts", context.username, @@ -378,7 +379,7 @@ class OtrlMessageAppOps: common.xmpp.Message(to = recipient, body = message, typ = 'chat')) return - + gajim.connections[opdata['account']].send_message(recipient, message, **opdata['kwargs']) @@ -656,7 +657,7 @@ class Interface: title = data[1] prompt = data[2] proposed_nick = data[3] - gc_control = self.msg_win_mgr.get_control(room_jid, account) + gc_control = self.msg_win_mgr.get_gc_control(room_jid, account) if not gc_control and \ room_jid in self.minimized_controls[account]: gc_control = self.minimized_controls[account][room_jid] @@ -710,9 +711,10 @@ class Interface: (jid_from, file_props)) conn.disconnect_transfer(file_props) return - ctrl = self.msg_win_mgr.get_control(jid_from, account) - if ctrl and ctrl.type_id == message_control.TYPE_GC: - ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) + + for ctrl in self.msg_win_mgr.get_chat_controls(jid_from, account): + if ctrl.type_id == message_control.TYPE_GC: + ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) def handle_event_con_type(self, account, con_type): # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp' @@ -942,146 +944,12 @@ class Interface: self.handle_event_gc_notify(account, (jid, array[1], status_message, array[3], None, None, None, None, None, [], None, None)) - def handle_event_msg(self, account, array): - # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject, - # chatstate, msg_id, composing_xep, user_nick, xhtml, session, form_node)) - # user_nick is JEP-0172 - - full_jid_with_resource = array[0] - jid = gajim.get_jid_without_resource(full_jid_with_resource) - resource = gajim.get_resource_from_jid(full_jid_with_resource) - - message = array[1] - encrypted = array[3] - msg_type = array[4] - subject = array[5] - chatstate = array[6] - msg_id = array[7] - composing_xep = array[8] - xhtml = array[10] - session = array[11] - if gajim.config.get('ignore_incoming_xhtml'): - xhtml = None - if gajim.jid_is_transport(jid): - jid = jid.replace('@', '') - - groupchat_control = self.msg_win_mgr.get_control(jid, account) - if not groupchat_control and \ - jid in self.minimized_controls[account]: - groupchat_control = self.minimized_controls[account][jid] - pm = False - if groupchat_control and groupchat_control.type_id == \ - message_control.TYPE_GC: - # It's a Private message - pm = True - msg_type = 'pm' - - chat_control = None - jid_of_control = full_jid_with_resource - highest_contact = gajim.contacts.get_contact_with_highest_priority( - account, jid) - # Look for a chat control that has the given resource, or default to one - # without resource - ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account) - if ctrl: - chat_control = ctrl - elif not pm and (not highest_contact or not highest_contact.resource): - # unknow contact or offline message - jid_of_control = jid - chat_control = self.msg_win_mgr.get_control(jid, account) - elif highest_contact and resource != highest_contact.resource and \ - highest_contact.show != 'offline': - jid_of_control = full_jid_with_resource - chat_control = None - elif not pm: - jid_of_control = jid - chat_control = self.msg_win_mgr.get_control(jid, account) - - # Handle chat states - contact = gajim.contacts.get_contact(account, jid, resource) - if contact: - if contact.composing_xep != 'XEP-0085': # We cache xep85 support - contact.composing_xep = composing_xep - if chat_control and chat_control.type_id == message_control.TYPE_CHAT: - if chatstate is not None: - # other peer sent us reply, so he supports jep85 or jep22 - contact.chatstate = chatstate - if contact.our_chatstate == 'ask': # we were jep85 disco? - contact.our_chatstate = 'active' # no more - chat_control.handle_incoming_chatstate() - elif contact.chatstate != 'active': - # got no valid jep85 answer, peer does not support it - contact.chatstate = False - elif chatstate == 'active': - # Brand new message, incoming. - contact.our_chatstate = chatstate - contact.chatstate = chatstate - if msg_id: # Do not overwrite an existing msg_id with None - contact.msg_id = msg_id - - # THIS MUST BE AFTER chatstates handling - # AND BEFORE playsound (else we ear sounding on chatstates!) - if not message: # empty message text - return - - if gajim.config.get('ignore_unknown_contacts') and \ - not gajim.contacts.get_contacts(account, jid) and not pm: - return - if not contact: - # contact is not in the roster, create a fake one to display - # notification - contact = common.contacts.Contact(jid = jid, resource = resource) - advanced_notif_num = notify.get_advanced_notification('message_received', - account, contact) - - # Is it a first or next message received ? - first = False - if msg_type == 'normal': - if not gajim.events.get_events(account, jid, ['normal']): - first = True - elif not chat_control and not gajim.events.get_events(account, - jid_of_control, [msg_type]): # msg_type can be chat or pm - first = True - - if pm: - nickname = resource - groupchat_control.on_private_message(nickname, message, array[2], - xhtml, session, msg_id) - else: - # array: (jid, msg, time, encrypted, msg_type, subject) - if encrypted: - self.roster.on_message(jid, message, array[2], account, array[3], - msg_type, subject, resource, msg_id, array[9], - advanced_notif_num, session=session, form_node=array[12]) - else: - # xhtml in last element - self.roster.on_message(jid, message, array[2], account, array[3], - msg_type, subject, resource, msg_id, array[9], - advanced_notif_num, xhtml=xhtml, session=session, - form_node=array[12]) - nickname = gajim.get_name_from_jid(account, jid) - # Check and do wanted notifications - msg = message - if subject: - msg = _('Subject: %s') % subject + '\n' + msg - focused = False - if chat_control: - parent_win = chat_control.parent_win - if chat_control == parent_win.get_active_control() and \ - parent_win.window.has_focus: - focused = True - notify.notify('new_message', jid_of_control, account, [msg_type, - first, nickname, msg, focused], advanced_notif_num) - - if self.remote_ctrl: - self.remote_ctrl.raise_signal('NewMessage', (account, array)) - def handle_event_msgerror(self, account, array): - #'MSGERROR' (account, (jid, error_code, error_msg, msg, time)) + #'MSGERROR' (account, (jid, error_code, error_msg, msg, time[, session])) full_jid_with_resource = array[0] jids = full_jid_with_resource.split('/', 1) jid = jids[0] - gc_control = self.msg_win_mgr.get_control(jid, account) + gc_control = self.msg_win_mgr.get_gc_control(jid, account) if not gc_control and \ jid in self.minimized_controls[account]: gc_control = self.minimized_controls[account][jid] @@ -1117,8 +985,7 @@ class Interface: msg = array[2] if array[3]: msg = _('error while sending %s ( %s )') % (array[3], msg) - self.roster.on_message(jid, msg, array[4], account, \ - msg_type='error') + array[5].roster_message(jid, msg, array[4], msg_type='error') def handle_event_msgsent(self, account, array): #('MSGSENT', account, (jid, msg, keyID)) @@ -1311,21 +1178,22 @@ class Interface: win.set_values(vcard) # show avatar in chat - win = None - ctrl = None + ctrls = [] if resource and self.msg_win_mgr.has_window( jid + '/' + resource, account): win = self.msg_win_mgr.get_window(jid + '/' + resource, account) - ctrl = win.get_control(jid + '/' + resource, account) + ctrls = win.get_controls(jid + '/' + resource, account) elif self.msg_win_mgr.has_window(jid, account): win = self.msg_win_mgr.get_window(jid, account) - ctrl = win.get_control(jid, account) - if win and ctrl.type_id != message_control.TYPE_GC: - ctrl.show_avatar() + ctrls = win.get_controls(jid, account) + + for ctrl in ctrls: + if ctrl.type_id != message_control.TYPE_GC: + ctrl.show_avatar() # Show avatar in roster or gc_roster - gc_ctrl = self.msg_win_mgr.get_control(jid, account) + gc_ctrl = self.msg_win_mgr.get_gc_control(jid, account) if not gc_ctrl and \ jid in self.minimized_controls[account]: gc_ctrl = self.minimized_controls[account][jid] @@ -1382,21 +1250,25 @@ class Interface: # Get the window and control for the updated status, this may be a # PrivateChatControl - control = self.msg_win_mgr.get_control(room_jid, account) + control = self.msg_win_mgr.get_gc_control(room_jid, account) + if not control and \ room_jid in self.minimized_controls[account]: control = self.minimized_controls[account][room_jid] - if control and control.type_id != message_control.TYPE_GC: + if not control or (control and control.type_id != message_control.TYPE_GC): return - if control: - control.chg_contact_status(nick, show, status, array[4], array[5], - array[6], array[7], array[8], array[9], array[10], array[11]) - ctrl = self.msg_win_mgr.get_control(fjid, account) + control.chg_contact_status(nick, show, status, array[4], array[5], + array[6], array[7], array[8], array[9], array[10], array[11]) - # print status in chat window and update status/GPG image - if ctrl: + contact = gajim.contacts.\ + get_contact_with_highest_priority(account, room_jid) + if contact: + self.roster.draw_contact(room_jid, account) + + # print status in chat windows and update status/GPG image + for ctrl in self.msg_win_mgr.get_chat_controls(fjid, account): statusCode = array[9] if '303' in statusCode: new_nick = array[10] @@ -1432,7 +1304,7 @@ class Interface: jids = array[0].split('/', 1) room_jid = jids[0] - gc_control = self.msg_win_mgr.get_control(room_jid, account) + gc_control = self.msg_win_mgr.get_gc_control(room_jid, account) if not gc_control and \ room_jid in self.minimized_controls[account]: gc_control = self.minimized_controls[account][room_jid] @@ -1460,7 +1332,7 @@ class Interface: jids = array[0].split('/', 1) jid = jids[0] - gc_control = self.msg_win_mgr.get_control(jid, account) + gc_control = self.msg_win_mgr.get_gc_control(jid, account) if not gc_control and \ jid in self.minimized_controls[account]: @@ -1525,7 +1397,7 @@ class Interface: jid = array[0] statusCode = array[1] - gc_control = self.msg_win_mgr.get_control(jid, account) + gc_control = self.msg_win_mgr.get_gc_control(jid, account) if not gc_control and \ jid in self.minimized_controls[account]: gc_control = self.minimized_controls[account][jid] @@ -1584,7 +1456,7 @@ class Interface: self.roster.on_disconnect(None, room_jid, account) else: win = self.msg_win_mgr.get_window(room_jid, account) - ctrl = win.get_control(room_jid, account) + ctrl = win.get_gc_control(room_jid, account) win.remove_tab(ctrl, 3) dlg = dialogs.InputDialog(_('Password Required'), @@ -2012,11 +1884,11 @@ class Interface: AtomWindow.newAtomEntry(atom_entry) def handle_event_failed_decrypt(self, account, data): - jid, tim = data + jid, tim, session = data - ctrl = self.msg_win_mgr.get_control(jid, account) - if ctrl: - ctrl.print_conversation_line('Unable to decrypt message from %s\nIt may have been tampered with.' % (jid), 'status', '', tim) + if session.control: + session.control.print_conversation_line('Unable to decrypt message from '+ + '%s\nIt may have been tampered with.' % (jid), 'status', '', tim) else: print 'failed decrypt, unable to find a control to notify you in.' @@ -2153,6 +2025,8 @@ class Interface: if ctrl: new_sess = gajim.connections[account].make_new_session(str(jid)) ctrl.set_session(new_sess) + gajim.connections[account].delete_session(str(jid), + session.thread_id) if was_encrypted: ctrl.print_esession_details() @@ -2162,19 +2036,14 @@ class Interface: # non-esession negotiation. this isn't very useful, but i'm keeping it around # to test my test suite. if form.getType() == 'form': - ctrl = self.msg_win_mgr.get_control(str(jid), account) - if not ctrl: + if not session.control: resource = jid.getResource() contact = gajim.contacts.get_contact(account, str(jid), resource) if not contact: connection = gajim.connections[account] contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) - self.new_chat(contact, account, resource = resource) - - ctrl = self.msg_win_mgr.get_control(str(jid), account) - - ctrl.set_session(session) + self.new_chat(session, contact, account, resource = resource) negotiation.FeatureNegotiationWindow(account, jid, session, form) @@ -2245,25 +2114,23 @@ class Interface: gajim.connections[account].change_status('offline','') def handle_event_ping_sent(self, account, contact): - ctrl = self.msg_win_mgr.get_control(contact.get_full_jid(), account) - if ctrl is None: - ctrl = self.msg_win_mgr.get_control(contact.jid, account) - ctrl.print_conversation(_('Ping?'), 'status') + for ctrl in self.msg_win_mgr.get_chat_controls(contact.jid, account): + ctrl.print_conversation(_('Ping?'), 'status') + for ctrl in self.msg_win_mgr.get_chat_controls(contact.get_full_jid(), account): + ctrl.print_conversation(_('Ping?'), 'status') def handle_event_ping_reply(self, account, data): contact = data[0] seconds = data[1] - ctrl = self.msg_win_mgr.get_control(contact.get_full_jid(), account) - if ctrl is None: - ctrl = self.msg_win_mgr.get_control(contact.jid, account) - if ctrl: + for ctrl in self.msg_win_mgr.get_chat_controls(contact.jid, account): + ctrl.print_conversation(_('Pong! (%s s.)') % seconds, 'status') + for ctrl in self.msg_win_mgr.get_chat_controls(contact.get_full_jid(), account): ctrl.print_conversation(_('Pong! (%s s.)') % seconds, 'status') def handle_event_ping_error(self, account, contact): - ctrl = self.msg_win_mgr.get_control(contact.get_full_jid(), account) - if ctrl is None: - ctrl = self.msg_win_mgr.get_control(contact.jid, account) - if ctrl: + for ctrl in self.msg_win_mgr.get_chat_controls(contact.jid, account): + ctrl.print_conversation(_('Error.'), 'status') + for ctrl in self.msg_win_mgr.get_chat_controls(contact.get_full_jid(), account): ctrl.print_conversation(_('Error.'), 'status') def handle_event_search_form(self, account, data): @@ -2412,7 +2279,6 @@ class Interface: 'ERROR_ANSWER': self.handle_event_error_answer, 'STATUS': self.handle_event_status, 'NOTIFY': self.handle_event_notify, - 'MSG': self.handle_event_msg, 'MSGERROR': self.handle_event_msgerror, 'MSGSENT': self.handle_event_msgsent, 'MSGNOTSENT': self.handle_event_msgnotsent, @@ -2551,16 +2417,37 @@ class Interface: jid = gajim.get_jid_without_resource(fjid) if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'): w = self.msg_win_mgr.get_window(jid, account) + ctrl = None if self.minimized_controls[account].has_key(jid): if not w: ctrl = self.minimized_controls[account][jid] w = self.msg_win_mgr.create_window(ctrl.contact, \ ctrl.account, ctrl.type_id) self.roster.on_groupchat_maximized(None, jid, account) + + if not ctrl: + ctrl = self.msg_win_mgr.get_gc_control(jid, account) + elif type_ in ('printed_chat', 'chat', ''): # '' is for log in/out notifications - if self.msg_win_mgr.has_window(fjid, account): - w = self.msg_win_mgr.get_window(fjid, account) + + event = gajim.events.get_first_event(account, fjid, type_) + if not event: + event = gajim.events.get_first_event(account, jid, type_) + + session = None + + if type_ == 'printed_chat': + session = event.parameters[0] + elif type_ == 'chat': + session = event.parameters[8] + + if session and session.control: + ctrl = session.control + elif type_ == '' and self.msg_win_mgr.has_window(fjid, account): + # assume that the most recently updated control we have for this party + # is the one that this event was in + ctrl = self.msg_win_mgr.get_chat_controls(fjid, account)[0] else: highest_contact = gajim.contacts.get_contact_with_highest_priority( account, jid) @@ -2577,12 +2464,32 @@ class Interface: contact = gajim.contacts.get_contact(account, jid, resource) if not contact: contact = highest_contact - self.new_chat(contact, account, resource = resource) - w = self.msg_win_mgr.get_window(fjid, account) + + if not session: + session = gajim.connections[account].get_or_create_session(fjid, None) + + self.new_chat(session, contact, account, resource = resource) + ctrl = session.control + gajim.last_message_time[account][jid] = 0 # long time ago + + w = ctrl.parent_win elif type_ in ('printed_pm', 'pm'): - if self.msg_win_mgr.has_window(fjid, account): - w = self.msg_win_mgr.get_window(fjid, account) + # assume that the most recently updated control we have for this party + # is the one that this event was in + event = gajim.events.get_first_event(account, fjid, type_) + if not event: + event = gajim.events.get_first_event(account, jid, type_) + + session = None + + if type_ == 'printed_pm': + session = event.parameters[0] + elif type_ == 'pm': + session = event.parameters[8] + + if session and session.control: + ctrl = session.control else: room_jid = jid nick = resource @@ -2594,8 +2501,14 @@ class Interface: show = 'offline' gc_contact = gajim.contacts.create_gc_contact( room_jid = room_jid, name = nick, show = show) - self.new_private_chat(gc_contact, account) - w = self.msg_win_mgr.get_window(fjid, account) + + if not session: + session = gajim.connections[account].make_new_session(fjid, None, type='pm') + + self.new_private_chat(gc_contact, account, session=session) + ctrl = session.control + + w = ctrl.parent_win elif type_ in ('normal', 'file-request', 'file-request-error', 'file-send-error', 'file-error', 'file-stopped', 'file-completed'): # Get the first single message event @@ -2622,10 +2535,9 @@ class Interface: gajim.events.remove_events(account, jid, event) self.roster.draw_contact(jid, account) if w: - w.set_active_tab(fjid, account) + w.set_active_tab(ctrl) w.window.present() w.window.window.focus() - ctrl = w.get_control(fjid, account) # Using isinstance here because we want to catch all derived types if isinstance(ctrl, ChatControlBase): tv = ctrl.conv_textview @@ -2862,9 +2774,10 @@ class Interface: '''joins the room immediately''' if self.msg_win_mgr.has_window(room_jid, account) and \ gajim.gc_connected[account][room_jid]: - win = self.msg_win_mgr.get_window(room_jid, account) + gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account) + win = gc_ctrl.parent_win win.window.present() - win.set_active_tab(room_jid, account) + win.set_active_tab(gc_ctrl) dialogs.ErrorDialog(_('You are already in group chat %s') % room_jid) return minimized_control_exists = False @@ -2889,9 +2802,9 @@ class Interface: not self.msg_win_mgr.has_window(room_jid, account): self.new_room(room_jid, nick, account, is_continued=is_continued) if not minimized_control_exists: - gc_win = self.msg_win_mgr.get_window(room_jid, account) - gc_win.set_active_tab(room_jid, account) - gc_win.window.present() + gc_control = self.msg_win_mgr.get_gc_control(room_jid, account) + gc_control.parent_win.set_active_tab(gc_control) + gc_control.parent_win.window.present() gajim.connections[account].join_gc(nick, room_jid, password) if password: gajim.gc_passwords[room_jid] = password @@ -2915,18 +2828,42 @@ class Interface: contact = gajim.contacts.contact_from_gc_contact(gc_contact) type_ = message_control.TYPE_PM fjid = gc_contact.room_jid + '/' + gc_contact.name - mw = self.msg_win_mgr.get_window(fjid, account) - if not mw: - mw = self.msg_win_mgr.create_window(contact, account, type_) - chat_control = \ - PrivateChatControl(mw, gc_contact, contact, account, session) - mw.new_tab(chat_control) + conn = gajim.connections[account] + + if not session and fjid in conn.sessions: + sessions = filter(lambda s: isinstance(s, ChatControlSession), + conn.sessions[fjid].values()) + + # look for an existing session with a chat control + for s in sessions: + if s.control: + session = s + break + + if not session and not len(sessions) == 0: + # there are no sessions with chat controls, just take the first one + session = sessions[0] + + if not session: + # couldn't find an existing ChatControlSession, just make a new one + + session = conn.make_new_session(fjid, None, 'pm') + + if not session.control: + mw = self.msg_win_mgr.get_window(fjid, account) + if not mw: + mw = self.msg_win_mgr.create_window(contact, account, type_) + + session.control = PrivateChatControl(mw, gc_contact, contact, account, + session) + mw.new_tab(session.control) + if len(gajim.events.get_events(account, fjid)): # We call this here to avoid race conditions with widget validation - chat_control.read_queue() + session.control.read_queue() - def new_chat(self, contact, account, resource = None, session = None): + def new_chat(self, session, contact, account, resource = None): # Get target window, create a control, and associate it with the window type_ = message_control.TYPE_CHAT @@ -2946,6 +2883,8 @@ class Interface: # We call this here to avoid race conditions with widget validation chat_control.read_queue() + return chat_control + def new_chat_from_jid(self, account, fjid): jid, resource = gajim.get_room_and_nick_from_fjid(fjid) contact = gajim.contacts.get_contact(account, jid, resource) @@ -2953,35 +2892,70 @@ class Interface: if not contact: added_to_roster = True contact = self.roster.add_to_not_in_the_roster(account, jid, - resource = resource) + resource=resource) + + session = gajim.connections[account].get_or_create_session(fjid, None) if not self.msg_win_mgr.has_window(fjid, account): - self.new_chat(contact, account, resource = resource) - mw = self.msg_win_mgr.get_window(fjid, account) - mw.set_active_tab(fjid, account) + session.control = self.new_chat(session, contact, account, + resource=resource) + if len(gajim.events.get_events(account, fjid)): + session.control.read_queue() + + mw = session.control.parent_win + mw.set_active_tab(session.control) mw.window.present() # For JEP-0172 if added_to_roster: - mc = mw.get_control(fjid, account) - mc.user_nick = gajim.nicks[account] + session.control.user_nick = gajim.nicks[account] + + def on_open_chat_window(self, widget, contact, account, resource=None, + session=None): - def on_open_chat_window(self, widget, contact, account, resource = None, - session = None): # Get the window containing the chat fjid = contact.jid + if resource: fjid += '/' + resource - win = self.msg_win_mgr.get_window(fjid, account) - if not win: - self.new_chat(contact, account, resource = resource, session = session) - win = self.msg_win_mgr.get_window(fjid, account) - ctrl = win.get_control(fjid, account) + + conn = gajim.connections[account] + + if not session and contact.jid in conn.sessions: + sessions = filter(lambda s: isinstance(s, ChatControlSession), + conn.sessions[contact.jid].values()) + + # look for an existing session with a chat control + for s in sessions: + if s.control: + session = s + break + + if not session and not len(sessions) == 0: + # there are no sessions with chat controls, just take the first one + session = sessions[0] + + if not session: + # couldn't find an existing ChatControlSession, just make a new one + session = conn.make_new_session(fjid, None, 'chat') + + if not session.control: + # open a new chat control + session.control = self.new_chat(session, contact, account, + resource=resource) + + if len(gajim.events.get_events(account, fjid)): + session.control.read_queue() + # last message is long time ago - gajim.last_message_time[account][ctrl.get_full_jid()] = 0 - win.set_active_tab(fjid, account) - if gajim.connections[account].is_zeroconf and \ - gajim.connections[account].status in ('offline', 'invisible'): - win.get_control(fjid, account).got_disconnected() + gajim.last_message_time[account][session.control.get_full_jid()] = 0 + + win = session.control.parent_win + win.set_active_tab(session.control) + + if conn.is_zeroconf and conn.status in ('offline', 'invisible'): + for ctrl in win.get_controls(fjid, account): + ctrl.got_disconnected() + win.window.present() ################################################################################ @@ -3172,7 +3146,7 @@ class Interface: 'password': password, 'nick': nick } - place_found = False + place_found = False index = 0 # check for duplicate entry and respect alpha order for bookmark in gajim.connections[account].bookmarks: @@ -3196,6 +3170,18 @@ class Interface: _('You can manage your bookmarks via Actions menu in your roster.')) + # does JID exist only within a groupchat? + def is_pm_contact(self, fjid, account): + bare_jid = gajim.get_jid_without_resource(fjid) + + gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account) + + if not gc_ctrl and \ + bare_jid in self.minimized_controls[account]: + gc_ctrl = self.minimized_controls[account][bare_jid] + + return gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC + def create_ipython_window(self): try: from ipython_view import IPythonView diff --git a/src/groupchat_control.py b/src/groupchat_control.py index ddc33aeee..9c7666c80 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -43,6 +43,7 @@ import cell_renderer_image from common import gajim from common import helpers +from common.stanza_session import StanzaSession from chat_control import ChatControl from chat_control import ChatControlBase @@ -57,7 +58,7 @@ C_TYPE, # type of the row ('contact' or 'role') C_TEXT, # text shown in the cellrenderer C_AVATAR, # avatar of the contact ) = range(5) - + def set_renderer_color(treeview, renderer, set_background = True): '''set style for group row, using PRELIGHT system color''' if set_background: @@ -120,7 +121,7 @@ class PrivateChatControl(ChatControl): def __init__(self, parent_win, gc_contact, contact, account, session): room_jid = contact.jid.split('/')[0] - room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, account) + room_ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) if gajim.interface.minimized_controls[account].has_key(room_jid): room_ctrl = gajim.interface.minimized_controls[account][room_jid] self.room_name = room_ctrl.name @@ -222,6 +223,9 @@ class GroupchatControl(ChatControlBase): self.new_nick = '' self.name = self.room_jid.split('@')[0] + self.session = StanzaSession(gajim.connections[self.account], + self.room_jid, 'gc', 'gc') + compact_view = gajim.config.get('compact_view') self.chat_buttons_set_visible(compact_view) self.widget_set_visible(self.xml.get_widget('banner_eventbox'), @@ -456,9 +460,9 @@ class GroupchatControl(ChatControlBase): 'state_muc_msg_color') if color_name: color = gtk.gdk.colormap_get_system().alloc_color(color_name) - + label_str = self.name - + # count waiting highlighted messages unread = '' num_unread = self.get_nb_unread() @@ -619,9 +623,8 @@ class GroupchatControl(ChatControlBase): no_queue = len(gajim.events.get_events(self.account, fjid)) == 0 # We print if window is opened - pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account) - if pm_control: - pm_control.print_conversation(msg, tim = tim, xhtml = xhtml) + if session.control: + session.control.print_conversation(msg, tim = tim, xhtml = xhtml) return event = gajim.events.create_event('pm', (msg, '', 'incoming', tim, @@ -645,7 +648,7 @@ class GroupchatControl(ChatControlBase): self.parent_win.show_title() self.parent_win.redraw_tab(self) else: - self._start_private_message(nick, session) + self._start_private_message(nick) # Scroll to line self.list_treeview.expand_row(path[0:1], False) self.list_treeview.scroll_to_cell(path) @@ -870,10 +873,9 @@ class GroupchatControl(ChatControlBase): for nick in nick_list: # Update pm chat window fjid = self.room_jid + '/' + nick - ctrl = gajim.interface.msg_win_mgr.get_control(fjid, self.account) gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) - if ctrl: + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(fjid, self.account): gc_contact.show = 'offline' gc_contact.status = '' ctrl.update_ui() @@ -898,18 +900,17 @@ class GroupchatControl(ChatControlBase): # Recalculate column width for ellipsizin self.list_treeview.columns_autosize() - def on_send_pm(self, widget = None, model = None, iter = None, nick = None, - msg = None): - '''opens a chat window and msg is not None sends private message to a + def on_send_pm(self, widget=None, model=None, iter=None, nick=None, + msg=None): + '''opens a chat window and if msg is not None sends private message to a contact in a room''' if nick is None: nick = model[iter][C_NICK].decode('utf-8') fjid = gajim.construct_fjid(self.room_jid, nick) # 'fake' jid - self._start_private_message(nick) + ctrl = self._start_private_message(nick) if msg: - gajim.interface.msg_win_mgr.get_control(fjid, self.account).\ - send_message(msg) + ctrl.send_message(msg) def on_send_file(self, widget, gc_contact): '''sends a file to a contact in the room''' @@ -1585,7 +1586,7 @@ class GroupchatControl(ChatControlBase): # Minimize it win = gajim.interface.msg_win_mgr.get_window(self.contact.jid, self.account) - ctrl = win.get_control(self.contact.jid, self.account) + ctrl = win.get_gc_control(self.contact.jid, self.account) ctrl_page = win.notebook.page_num(ctrl.widget) control = win.notebook.get_nth_page(ctrl_page) @@ -1615,7 +1616,7 @@ class GroupchatControl(ChatControlBase): for nick in nick_list: # Update pm chat window fjid = self.room_jid + '/' + nick - ctrl = gajim.interface.msg_win_mgr.get_control(fjid, self.account) + ctrl = gajim.interface.msg_win_mgr.get_gc_control(fjid, self.account) if ctrl: contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) contact.show = 'offline' @@ -2028,7 +2029,7 @@ class GroupchatControl(ChatControlBase): menu.show_all() menu.popup(None, None, None, event.button, event.time) - def _start_private_message(self, nick, session = None): + def _start_private_message(self, nick): gc_c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) nick_jid = gc_c.get_full_jid() @@ -2036,9 +2037,14 @@ class GroupchatControl(ChatControlBase): if not win: gajim.interface.new_private_chat(gc_c, self.account) win = gajim.interface.msg_win_mgr.get_window(nick_jid, self.account) - win.set_active_tab(nick_jid, self.account) + + ctrl = win.get_controls(nick_jid, self.account)[0] + + win.set_active_tab(ctrl) win.window.present() + return ctrl + def on_row_activated(self, widget, path): '''When an iter is activated (dubblick or single click if gnome is set this way''' @@ -2097,7 +2103,7 @@ class GroupchatControl(ChatControlBase): return if gajim.single_click and not event.state & gtk.gdk.SHIFT_MASK: - self.on_row_activated(widget, path) + self.on_row_activated(widget, path) return True else: model = widget.get_model() diff --git a/src/message_control.py b/src/message_control.py index 22a6e51fb..d12f4ef4d 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -32,7 +32,7 @@ class MessageControl: '''An abstract base widget that can embed in the gtk.Notebook of a MessageWindow''' def __init__(self, type_id, parent_win, widget_name, contact, account, resource = None): - # dict { cb id : widget} + # dict { cb id : widget} # keep all registered callbacks of widgets, created by self.xml self.handlers = {} self.type_id = type_id @@ -117,25 +117,24 @@ class MessageControl: return len(gajim.events.get_events(self.account, self.contact.jid)) def set_session(self, session): - if hasattr(self, 'session') and session == self.session: + oldsession = None + if hasattr(self, 'session'): + oldsession = self.session + + if oldsession and session == oldsession: return - was_encrypted = False - - if hasattr(self, 'session') and self.session: - if self.session.enable_encryption: - was_encrypted = True - - gajim.connections[self.account].delete_session(self.session.jid, - self.session.thread_id) - self.session = session if session: session.control = self - if was_encrypted: - self.print_esession_details() + if oldsession: + self.parent_win.change_thread_key(self.contact.jid, self.account, + oldsession.thread_id, session.thread_id) + + if oldsession.enable_encryption: + self.print_esession_details() def send_message(self, message, keyID = '', type = 'chat', chatstate = None, msg_id = None, composing_xep = None, resource = None, @@ -145,12 +144,6 @@ class MessageControl: jid = self.contact.jid original_message = message - if not self.session: - fjid = self.contact.get_full_jid() - new_session = gajim.connections[self.account].make_new_session(fjid) - - self.set_session(new_session) - if gajim.otr_module: if type == 'chat' and isinstance(message, unicode): d = {'kwargs':{'keyID':keyID, 'type':type, @@ -158,7 +151,7 @@ class MessageControl: 'composing_xep':composing_xep, 'resource':self.resource, 'user_nick':user_nick, 'session':self.session, 'original_message':original_message}, 'account':self.account} - + new_msg = gajim.otr_module.otrl_message_sending( gajim.connections[self.account].otr_userstates, (gajim.otr_ui_ops, d), diff --git a/src/message_window.py b/src/message_window.py index 7d98e81d0..d7053d299 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -154,8 +154,9 @@ class MessageWindow(object): def get_num_controls(self): n = 0 - for dict in self._controls.values(): - n += len(dict) + for jid_dict in self._controls.values(): + for dict in jid_dict.values(): + n += len(dict) return n def resize(self, width, height): @@ -208,7 +209,11 @@ class MessageWindow(object): if not self._controls.has_key(control.account): self._controls[control.account] = {} fjid = control.get_full_jid() - self._controls[control.account][fjid] = control + + if not self._controls[control.account].has_key(fjid): + self._controls[control.account][fjid] = {} + + self._controls[control.account][fjid][control.session.thread_id] = control if self.get_num_controls() == 2: # is first conversation_textview scrolled down ? @@ -412,8 +417,7 @@ class MessageWindow(object): else: gtkgui_helpers.set_unset_urgency_hint(self.window, False) - def set_active_tab(self, jid, acct): - ctrl = self._controls[acct][jid] + def set_active_tab(self, ctrl): ctrl_page = self.notebook.page_num(ctrl.widget) self.notebook.set_current_page(ctrl_page) @@ -445,7 +449,13 @@ class MessageWindow(object): self.notebook.remove_page(self.notebook.page_num(ctrl.widget)) fjid = ctrl.get_full_jid() - del self._controls[ctrl.account][fjid] + thread_id = ctrl.session.thread_id + + del self._controls[ctrl.account][fjid][thread_id] + + if len(self._controls[ctrl.account][fjid]) == 0: + del self._controls[ctrl.account][fjid] + if len(self._controls[ctrl.account]) == 0: del self._controls[ctrl.account] @@ -549,16 +559,16 @@ class MessageWindow(object): for ctrl in self.controls(): ctrl.update_tags() - def get_control(self, key, acct): + def get_control(self, key, acct, thread_id): '''Return the MessageControl for jid or n, where n is a notebook page index. - When key is an int index acct may be None''' + When key is an int index acct and thread_id may be None''' if isinstance(key, str): key = unicode(key, 'utf-8') if isinstance(key, unicode): jid = key try: - return self._controls[acct][jid] + return self._controls[acct][jid][thread_id] except: return None else: @@ -569,24 +579,45 @@ class MessageWindow(object): nth_child = notebook.get_nth_page(page_num) return self._widget_to_control(nth_child) - def change_key(self, old_jid, new_jid, acct): - '''Change the key of a control''' + def get_gc_control(self, jid, acct): + return self.get_control(jid, acct, 'gc') + + def get_controls(self, jid, acct): try: - # Check if control exists - ctrl = self._controls[acct][old_jid] - except: + return self._controls[acct][jid].values() + except KeyError: + return [] + + def change_key(self, old_jid, new_jid, acct): + '''Change the JID key of a control''' + try: + # Check if controls exists + ctrls = self._controls[acct][old_jid] + except KeyError: return - self._controls[acct][new_jid] = self._controls[acct][old_jid] + self._controls[acct][new_jid] = ctrls del self._controls[acct][old_jid] if old_jid in gajim.last_message_time[acct]: gajim.last_message_time[acct][new_jid] = \ gajim.last_message_time[acct][old_jid] del gajim.last_message_time[acct][old_jid] + def change_thread_key(self, jid, acct, old_thread_id, new_thread_id): + '''Change the thread_id key of a control''' + try: + # Check if control exists + ctrl = self._controls[acct][jid][old_thread_id] + except KeyError: + return + + self._controls[acct][jid][new_thread_id] = ctrl + del self._controls[acct][jid][old_thread_id] + def controls(self): - for ctrl_dict in self._controls.values(): - for ctrl in ctrl_dict.values(): - yield ctrl + for jid_dict in self._controls.values(): + for ctrl_dict in jid_dict.values(): + for ctrl in ctrl_dict.values(): + yield ctrl def move_to_next_unread_tab(self, forward): ind = self.notebook.get_current_page() @@ -604,7 +635,7 @@ class MessageWindow(object): ind = ind - 1 if ind < 0: ind = self.notebook.get_n_pages() - 1 - ctrl = self.get_control(ind, None) + ctrl = self.get_control(ind, None, None) if ctrl.get_nb_unread() > 0: found = True break # found @@ -796,8 +827,19 @@ class MessageWindowMgr(gobject.GObject): def get_window(self, jid, acct): for win in self.windows(): - if win.get_control(jid, acct): - return win + try: + if win._controls[acct][jid]: + return win + except KeyError: + pass + return None + + def get_gc_control(self, jid, acct): + win = self.get_window(jid, acct) + + if win: + return win.get_gc_control(jid, acct) + return None def has_window(self, jid, acct): @@ -930,11 +972,11 @@ class MessageWindowMgr(gobject.GObject): del self._windows[k] return - def get_control(self, jid, acct): + def get_control(self, jid, acct, session): '''Amongst all windows, return the MessageControl for jid''' win = self.get_window(jid, acct) if win: - return win.get_control(jid, acct) + return win.get_control(jid, acct, session) return None def get_controls(self, type = None, acct = None): @@ -946,6 +988,14 @@ class MessageWindowMgr(gobject.GObject): ctrls.append(c) return ctrls + def get_chat_controls(self, jid, acct): + win = self.get_window(jid, acct) + + if win: + return win.get_controls(jid, acct) + else: + return [] + def windows(self): for w in self._windows.values(): yield w diff --git a/src/notify.py b/src/notify.py index cc4f60d01..249b11f4a 100644 --- a/src/notify.py +++ b/src/notify.py @@ -53,8 +53,7 @@ try: except: USER_HAS_GROWL = False - -def get_show_in_roster(event, account, contact): +def get_show_in_roster(event, account, contact, session = None): '''Return True if this event must be shown in roster, else False''' if event == 'gc_message_received': return True @@ -65,8 +64,7 @@ def get_show_in_roster(event, account, contact): if gajim.config.get_per('notifications', str(num), 'roster') == 'no': return False if event == 'message_received': - chat_control = helpers.get_chat_control(account, contact) - if chat_control: + if session and session.control: return False return True diff --git a/src/otr_windows.py b/src/otr_windows.py index 17d73eff5..61aad50af 100644 --- a/src/otr_windows.py +++ b/src/otr_windows.py @@ -92,8 +92,7 @@ class ContactOtrSMPWindow: self.gw("progressbar").set_fraction(1) gajim.otr_ui_ops.gajim_log(text, self.account, self.contact.get_full_jid()) self.gw("desc_label").set_markup(text) - ctrl = gajim.interface.msg_win_mgr.get_control(self.contact.jid, self.account) - if ctrl: + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(self.contact.jid, self.account): ctrl.update_ui() gajim.otr_ui_ops.write_fingerprints({'account':self.account}) diff --git a/src/roster_window.py b/src/roster_window.py index f3169c759..32699ca0e 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -53,6 +53,8 @@ from common import i18n from message_window import MessageWindowMgr +from session import ChatControlSession + from common import dbus_support if dbus_support.supported: from music_track_listener import MusicTrackListener @@ -757,8 +759,8 @@ class RosterWindow: # Close chat window msg_win = gajim.interface.msg_win_mgr.get_window(contact.jid, account) - ctrl = gajim.interface.msg_win_mgr.get_control(contact.jid, account) - msg_win.remove_tab(ctrl, msg_win.CLOSE_CLOSE_BUTTON) + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(contact.jid, account): + msg_win.remove_tab(ctrl, msg_win.CLOSE_CLOSE_BUTTON) else: need_readd = True if need_readd: @@ -1133,6 +1135,51 @@ class RosterWindow: self.model[child_iter][C_AVATAR_PIXBUF] = scaled_pixbuf return False + def join_gc_room(self, account, room_jid, nick, password, minimize=False, + is_continued=False): + '''joins the room immediately''' + if gajim.interface.msg_win_mgr.has_window(room_jid, account) and \ + gajim.gc_connected[account][room_jid]: + win = gajim.interface.msg_win_mgr.get_window(room_jid, account) + ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) + win.window.present() + win.set_active_tab(ctrl) + dialogs.ErrorDialog(_('You are already in group chat %s') % room_jid) + return + minimized_control_exists = False + if room_jid in gajim.interface.minimized_controls[account]: + minimized_control_exists = True + invisible_show = gajim.SHOW_LIST.index('invisible') + if gajim.connections[account].connected == invisible_show: + dialogs.ErrorDialog( + _('You cannot join a group chat while you are invisible')) + return + if minimize and not minimized_control_exists and \ + not gajim.interface.msg_win_mgr.has_window(room_jid, account): + contact = gajim.contacts.create_contact(jid = room_jid, name = nick) + gc_control = GroupchatControl(None, contact, account) + gajim.interface.minimized_controls[account][room_jid] = gc_control + gajim.connections[account].join_gc(nick, room_jid, password) + if password: + gajim.gc_passwords[room_jid] = password + self.add_groupchat_to_roster(account, room_jid) + return + if not minimized_control_exists and \ + not gajim.interface.msg_win_mgr.has_window(room_jid, account): + self.new_room(room_jid, nick, account, is_continued=is_continued) + if not minimized_control_exists: + gc_win = gajim.interface.msg_win_mgr.get_window(room_jid, account) + gc_control = gc_win.get_gc_control(room_jid, account) + gc_win.set_active_tab(gc_control) + gc_win.window.present() + gajim.connections[account].join_gc(nick, room_jid, password) + if password: + gajim.gc_passwords[room_jid] = password + contact = gajim.contacts.get_contact_with_highest_priority(account, \ + room_jid) + if contact or minimized_control_exists: + self.add_groupchat_to_roster(account, room_jid) + def draw_completely_and_show_if_needed(self, jid, account): '''Draw contact, account and groups of given jid Show contact if it has pending events @@ -1434,9 +1481,9 @@ class RosterWindow: session = gajim.connections[account].make_new_session(jid) tim = time.localtime(float(result[2])) - self.on_message(jid, result[1], tim, account, msg_type = 'chat', - msg_id = result[0], session = session) - + session.roster_message(jid, result[1], tim, msg_type='chat', + msg_id=result[0]) + elif (time.time() - result[2]) > 2592000: # ok, here we see that we have a message in unread messages table # that is older than a month. It is probably from someone not in our @@ -1496,10 +1543,10 @@ class RosterWindow: gajim.transport_avatar[account][host] = [contact1.jid] else: gajim.transport_avatar[account][host].append(contact1.jid) - # If we already have a chat window opened, update it with new contact + + # If we already have chat windows opened, update them with new contact # instance - chat_control = gajim.interface.msg_win_mgr.get_control(ji, account) - if chat_control: + for chat_control in gajim.interface.msg_win_mgr.get_chat_controls(ji, account): chat_control.contact = contact1 def enable_syncing_status_msg_from_current_music_track(self, enabled): @@ -1830,9 +1877,10 @@ class RosterWindow: account): win = gajim.interface.msg_win_mgr.get_window(jid_with_resource, account) - ctrl = win.get_control(jid_with_resource, account) - ctrl.update_ui() - win.redraw_tab(ctrl) + for ctrl in win.get_controls(jid_with_resource, account): + ctrl.update_ui() + win.redraw_tab(ctrl) + gajim.contacts.remove_contact(account, contact) elif contact.jid == gajim.get_jid_from_account(account) and show == 'offline': # Our SelfContact went offline. Remove him @@ -1841,19 +1889,20 @@ class RosterWindow: # print status in chat window and update status/GPG image if gajim.interface.msg_win_mgr.has_window(contact.jid, account): win = gajim.interface.msg_win_mgr.get_window(contact.jid, account) - ctrl = win.get_control(contact.jid, account) - ctrl.contact = gajim.contacts.get_contact_with_highest_priority( - account, contact.jid) - ctrl.update_ui() - win.redraw_tab(ctrl) - uf_show = helpers.get_uf_show(show) - ctrl.print_conversation(_('%s is now %s') % (name, uf_show), - 'status') - if status: - ctrl.print_conversation(' (', 'status', simple=True) - ctrl.print_conversation('%s' % (status), 'status', simple=True) - ctrl.print_conversation(')', 'status', simple=True) + + for ctrl in win.get_controls(contact.jid, account): + ctrl.contact = gajim.contacts.get_contact_with_highest_priority( + account, contact.jid) + ctrl.update_ui() + win.redraw_tab(ctrl) + + ctrl.print_conversation(_('%s is now %s') % (name, uf_show), + 'status') + if status: + ctrl.print_conversation(' (', 'status', simple=True) + ctrl.print_conversation('%s' % (status), 'status', simple=True) + ctrl.print_conversation(')', 'status', simple=True) # unset custom status if gajim.interface.status_sent_to_users.has_key(account) and \ @@ -1973,119 +2022,6 @@ class RosterWindow: gajim.config.get('roster_width'), gajim.config.get('roster_height')) - def on_message(self, jid, msg, tim, account, encrypted=False, msg_type='', - subject=None, resource='', msg_id=None, user_nick='', - advanced_notif_num=None, xhtml=None, session=None, form_node=None): - '''when we receive a message''' - contact = None - # if chat window will be for specific resource - resource_for_chat = resource - fjid = jid - # Try to catch the contact with correct resource - if resource: - fjid = jid + '/' + resource - contact = gajim.contacts.get_contact(account, jid, resource) - highest_contact = gajim.contacts.get_contact_with_highest_priority( - account, jid) - if not contact: - # If there is another resource, it may be a message from an invisible - # resource - lcontact = gajim.contacts.get_contacts(account, jid) - if (len(lcontact) > 1 or (lcontact and lcontact[0].resource and \ - lcontact[0].show != 'offline')) and jid.find('@') > 0: - contact = gajim.contacts.copy_contact(highest_contact) - contact.resource = resource - if resource: - fjid = jid + '/' + resource - contact.priority = 0 - contact.show = 'offline' - contact.status = '' - gajim.contacts.add_contact(account, contact) - - else: - # Default to highest prio - fjid = jid - resource_for_chat = None - contact = highest_contact - if not contact: - # contact is not in roster - contact = self.add_to_not_in_the_roster(account, jid, user_nick) - - - # If visible, try to get first line of contact in roster - path = None - iters = self._get_contact_iter(jid, account, contact = contact) - if iters: - path = self.modelfilter.get_path(iters[0]) - - # Look for a chat control that has the given resource - ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account) - if not ctrl: - # if not, if message comes from highest prio, get control or open one - # without resource - if highest_contact and contact.resource == highest_contact.resource \ - and not jid == gajim.get_jid_from_account(account): - ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) - fjid = jid - resource_for_chat = None - - # Do we have a queue? - no_queue = len(gajim.events.get_events(account, fjid)) == 0 - - popup = helpers.allow_popup_window(account, advanced_notif_num) - - if msg_type == 'normal' and popup: # it's single message to be autopopuped - dialogs.SingleMessageWindow(account, contact.jid, action='receive', - from_whom=jid, subject=subject, message=msg, resource=resource, - session=session, form_node=form_node) - return - - # We print if window is opened and it's not a single message - if ctrl and msg_type != 'normal': - typ = '' - if msg_type == 'error': - typ = 'status' - if session: - ctrl.set_session(session) - ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, - subject = subject, xhtml = xhtml) - if msg_id: - gajim.logger.set_read_messages([msg_id]) - return - - # We save it in a queue - type_ = 'chat' - event_type = 'message_received' - if msg_type == 'normal': - type_ = 'normal' - event_type = 'single_message_received' - show_in_roster = notify.get_show_in_roster(event_type, account, contact) - show_in_systray = notify.get_show_in_systray(event_type, account, contact) - event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, - encrypted, resource, msg_id, xhtml, session, form_node), - show_in_roster=show_in_roster, show_in_systray=show_in_systray) - gajim.events.add_event(account, fjid, event) - if popup: - if not ctrl: - gajim.interface.new_chat(contact, account, \ - resource=resource_for_chat) - if path and not self.dragging and gajim.config.get( - 'scroll_roster_to_last_message'): - # we curently see contact in our roster - # show and select his line in roster - # do not change selection while DND'ing - self.tree.expand_row(path[0:1], False) - self.tree.expand_row(path[0:2], False) - self.tree.scroll_to_cell(path) - self.tree.set_cursor(path) - else: - if no_queue: # We didn't have a queue: we change icons - self.draw_contact(jid, account) - self.show_title() # we show the * or [n] - # Show contact in roster (if he is invisible for example) and select - # line - self.show_and_select_contact_if_having_events(jid, account) - def close_all_from_dict(self, dic): '''close all the windows in the given dictionary''' for w in dic.values(): @@ -2151,8 +2087,8 @@ class RosterWindow: def on_quit_request(self, widget = None): ''' user want to quit. Check if he should be warned about messages - pending. Send offline to all connected account. We do NOT really quit - gajim here ''' + pending. Terminate all sessions and send offline to all connected + account. We do NOT really quit gajim here ''' accounts = gajim.connections.keys() get_msg = False for acct in accounts: @@ -2169,7 +2105,7 @@ class RosterWindow: unread = gajim.events.get_nb_events() if not gajim.config.get('notify_on_all_muc_messages'): unread_not_to_notify = gajim.events.get_nb_events(['printed_gc_msg']) - unread -= unread_not_to_notify + unread -= unread_not_to_notify # check if we have recent messages recent = False @@ -2192,13 +2128,15 @@ class RosterWindow: self.quit_on_next_offline = 0 for acct in accounts: + gajim.connections[acct].terminate_sessions() + if gajim.connections[acct].connected: self.quit_on_next_offline += 1 self.send_status(acct, 'offline', message) if not self.quit_on_next_offline: self.quit_gtkgui_interface() - + ################################################################################ ### Menu and GUI callbacks ### FIXME: order callbacks in itself... @@ -2630,9 +2568,8 @@ class RosterWindow: u.name = new_text gajim.connections[account].update_contact(jid, new_text, u.groups) self.draw_contact(jid, account) - # Update opened chat - ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) - if ctrl: + # Update opened chats + for ctrl in gajim.interface.msg_win_mgr.get_controls(jid, account): ctrl.update_ui() win = gajim.interface.msg_win_mgr.get_window(jid, account) win.redraw_tab(ctrl) @@ -2719,8 +2656,7 @@ class RosterWindow: keyID = keyID[0] keys[contact.jid] = keyID - if gajim.interface.msg_win_mgr.has_window(contact.jid, account): - ctrl = gajim.interface.msg_win_mgr.get_control(contact.jid, account) + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(contact.jid, account): ctrl.update_ui() keys_str = '' for jid in keys: @@ -2866,7 +2802,7 @@ class RosterWindow: ctrl.account, ctrl.type_id) ctrl.parent_win = mw mw.new_tab(ctrl) - mw.set_active_tab(jid, account) + mw.set_active_tab(ctrl) mw.window.present() del gajim.interface.minimized_controls[account][jid] @@ -3029,7 +2965,7 @@ class RosterWindow: x_min = 0 if gajim.single_click and not event.state & gtk.gdk.SHIFT_MASK and \ not event.state & gtk.gdk.CONTROL_MASK: - # Don't handle dubble click if we press icon of a metacontact + # Don't handle double click if we press icon of a metacontact titer = model.get_iter(path) if x > x_min and x < x_min + 27 and type_ == 'contact' and \ model.iter_has_child(titer): @@ -3296,7 +3232,19 @@ class RosterWindow: def on_profile_avatar_menuitem_activate(self, widget, account): gajim.interface.edit_own_details(account) - + + def play_tictactoe(self, widget, contact, account, resource=None): + jid = contact.jid + + if resource is not None: + jid = jid + u'/' + resource + + import tictactoe + + sess = gajim.connections[account].make_new_session(jid, + klass=tictactoe.TicTacToeSession) + sess.begin() + def on_execute_command(self, widget, contact, account, resource=None): '''Execute command. Full JID needed; if it is other contact, resource is necessary. Widget is unnecessary, only to be @@ -3347,8 +3295,8 @@ class RosterWindow: self.show_treeview_menu(event) def on_row_activated(self, widget, path): - '''When an iter is activated (dubblick or single click if gnome is set - this way''' + '''When an iter is activated (double-click or single click if gnome is + set this way''' model = self.modelfilter account = model[path][C_ACCOUNT].decode('utf-8') type_ = model[path][C_TYPE] @@ -3398,6 +3346,7 @@ class RosterWindow: gajim.contacts.get_contact_with_highest_priority(account, jid) if jid == gajim.get_jid_from_account(account): resource = contact.resource + gajim.interface.on_open_chat_window(None, contact, account, \ resource = resource, session = session) @@ -3417,7 +3366,7 @@ class RosterWindow: accounts = gajim.connections.keys() else: accounts = [model[titer][C_ACCOUNT].decode('utf-8')] - + type_ = model[titer][C_TYPE] if type_ == 'group': child_model[child_iter][C_IMG] = gajim.interface.jabber_state_images[ @@ -4012,10 +3961,11 @@ class RosterWindow: start = '[' + str(nb_unread) + '] ' elif nb_unread == 1: start = '* ' + self.window.set_title(start + 'Gajim') gtkgui_helpers.set_unset_urgency_hint(self.window, nb_unread) - + def _change_style(self, model, path, titer, option): if option is None or model[titer][C_TYPE] == option: # We changed style for this type of row @@ -4035,17 +3985,17 @@ class RosterWindow: gajim.interface.instances[account]['disco'][addr].paint_banner() for ctrl in gajim.interface.minimized_controls[account].values(): ctrl.repaint_themed_widgets() - + def update_avatar_in_gui(self, jid, account): # Update roster self.draw_avatar(jid, account) # Update chat window if gajim.interface.msg_win_mgr.has_window(jid, account): win = gajim.interface.msg_win_mgr.get_window(jid, account) - ctrl = win.get_control(jid, account) - if win and ctrl.type_id != message_control.TYPE_GC: - ctrl.show_avatar() - + for ctrl in win.get_chat_controls(jid, account): + if win and ctrl.type_id != message_control.TYPE_GC: + ctrl.show_avatar() + def on_roster_treeview_style_set(self, treeview, style): '''When style (theme) changes, redraw all contacts''' for contact in self._iter_contact_rows(): @@ -4864,7 +4814,7 @@ class RosterWindow: menu.connect('selection-done', gtkgui_helpers.destroy_widget) menu.show_all() menu.popup(None, None, None, event_button, event.time) - + def make_contact_menu(self, event, titer): '''Make contact\'s popup menu''' model = self.modelfilter @@ -5009,6 +4959,10 @@ class RosterWindow: execute_command_menuitem = xml.get_widget( 'execute_command_menuitem') + tictactoe_menuitem = xml.get_widget('tictactoe_menuitem') + tictactoe_menuitem.connect('activate', self.play_tictactoe, contact, + account, contact.resource) + # send custom status icon blocked = False if jid in gajim.connections[account].blocked_contacts: diff --git a/src/session.py b/src/session.py new file mode 100644 index 000000000..88690e188 --- /dev/null +++ b/src/session.py @@ -0,0 +1,333 @@ +from common import helpers + +from common import exceptions +from common import gajim +from common import stanza_session +from common import contacts + +import common.xmpp + +import dialogs + +import message_control + +import notify + +class ChatControlSession(stanza_session.EncryptedStanzaSession): + def __init__(self, conn, jid, thread_id, type = 'chat'): + stanza_session.EncryptedStanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') + + self.control = None + + def acknowledge_termination(self): + # the other party terminated the session. we'll keep the control around, though. + stanza_session.EncryptedStanzaSession.acknowledge_termination(self) + + if self.control: + self.control.session = None + + # remove events associated with this session from the queue + def remove_events(self, types): + any_removed = False + + for event in gajim.events.get_events(self.conn, self.jid, types=types): + if event.parameters[8] != self: + continue + + r = gajim.events.remove_events(self.conn, self.jid, event) + + if not_any_removed: + any_removed = r + + return any_removed + + # extracts chatstate from a stanza + def get_chatstate(self, msg, msgtxt): + composing_xep = None + chatstate = None + + # chatstates - look for chatstate tags in a message if not delayed + delayed = msg.getTag('x', namespace=common.xmpp.NS_DELAY) != None + if not delayed: + composing_xep = False + children = msg.getChildren() + for child in children: + if child.getNamespace() == 'http://jabber.org/protocol/chatstates': + chatstate = child.getName() + composing_xep = 'XEP-0085' + break + # No XEP-0085 support, fallback to XEP-0022 + if not chatstate: + chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT) + if chatstate_child: + chatstate = 'active' + composing_xep = 'XEP-0022' + if not msgtxt and chatstate_child.getTag('composing'): + chatstate = 'composing' + + return (composing_xep, chatstate) + + # dispatch a received stanza + def received(self, full_jid_with_resource, msgtxt, tim, encrypted, subject, msg): + msg_type = msg.getType() + msg_id = None + + # XEP-0172 User Nickname + user_nick = msg.getTagData('nick') + if not user_nick: + user_nick ='' + + form_node = None + for xtag in msg.getTags('x'): + if xtag.getNamespace() == common.xmpp.NS_DATA: + form_node = xtag + break + + composing_xep, chatstate = self.get_chatstate(msg, msgtxt) + + xhtml = msg.getXHTML() + + if msg_type == 'chat': + if not msg.getTag('body') and chatstate is None: + return + + log_type = 'chat_msg_recv' + else: + log_type = 'single_msg_recv' + + if self.is_loggable() and msgtxt: + try: + msg_id = gajim.logger.write(log_type, full_jid_with_resource, msgtxt, + tim=tim, subject=subject) + except exceptions.PysqliteOperationalError, e: + gajim.dispatch('ERROR', (_('Disk WriteError'), str(e))) + + treat_as = gajim.config.get('treat_incoming_messages') + if treat_as: + msg_type = treat_as + + jid = gajim.get_jid_without_resource(full_jid_with_resource) + resource = gajim.get_resource_from_jid(full_jid_with_resource) + + if gajim.config.get('ignore_incoming_xhtml'): + xhtml = None + if gajim.jid_is_transport(jid): + jid = jid.replace('@', '') + + groupchat_control = gajim.interface.msg_win_mgr.get_gc_control(jid, self.conn.name) + + if not groupchat_control and \ + jid in gajim.interface.minimized_controls[self.conn.name]: + groupchat_control = gajim.interface.minimized_controls[self.conn.name][jid] + + pm = False + if groupchat_control and groupchat_control.type_id == \ + message_control.TYPE_GC: + # It's a Private message + pm = True + msg_type = 'pm' + + jid_of_control = full_jid_with_resource + + # Handle chat states + contact = gajim.contacts.get_contact(self.conn.name, jid, resource) + if contact: + if contact.composing_xep != 'XEP-0085': # We cache xep85 support + contact.composing_xep = composing_xep + if self.control and self.control.type_id == message_control.TYPE_CHAT: + if chatstate is not None: + # other peer sent us reply, so he supports jep85 or jep22 + contact.chatstate = chatstate + if contact.our_chatstate == 'ask': # we were jep85 disco? + contact.our_chatstate = 'active' # no more + self.control.handle_incoming_chatstate() + elif contact.chatstate != 'active': + # got no valid jep85 answer, peer does not support it + contact.chatstate = False + elif chatstate == 'active': + # Brand new message, incoming. + contact.our_chatstate = chatstate + contact.chatstate = chatstate + if msg_id: # Do not overwrite an existing msg_id with None + contact.msg_id = msg_id + + # THIS MUST BE AFTER chatstates handling + # AND BEFORE playsound (else we ear sounding on chatstates!) + if not msgtxt: # empty message text + return + + if gajim.config.get('ignore_unknown_contacts') and \ + not gajim.contacts.get_contacts(self.conn.name, jid) and not pm: + return + + if not contact: + # contact is not in the roster, create a fake one to display + # notification + contact = contacts.Contact(jid = jid, resource = resource) + advanced_notif_num = notify.get_advanced_notification('message_received', + self.conn.name, contact) + + # Is it a first or next message received ? + first = False + if not self.control and not gajim.events.get_events(self.conn.name, + jid_of_control, [msg_type]): + first = True + + if pm: + nickname = resource + groupchat_control.on_private_message(nickname, msgtxt, tim, + xhtml, self, msg_id) + else: + self.roster_message(jid, msgtxt, tim, encrypted, msg_type, + subject, resource, msg_id, user_nick, advanced_notif_num, + xhtml=xhtml, form_node=form_node) + + nickname = gajim.get_name_from_jid(self.conn.name, jid) + # Check and do wanted notifications + msg = msgtxt + if subject: + msg = _('Subject: %s') % subject + '\n' + msg + focused = False + + if self.control: + parent_win = self.control.parent_win + if self.control == parent_win.get_active_control() and \ + parent_win.window.has_focus: + focused = True + + notify.notify('new_message', jid_of_control, self.conn.name, [msg_type, + first, nickname, msg, focused], advanced_notif_num) + + if gajim.interface.remote_ctrl: + gajim.interface.remote_ctrl.raise_signal('NewMessage', + (self.conn.name, [full_jid_with_resource, msgtxt, tim, + encrypted, msg_type, subject, chatstate, msg_id, + composing_xep, user_nick, xhtml, form_node])) + + # display the message or show notification in the roster + def roster_message(self, jid, msg, tim, encrypted=False, msg_type='', + subject=None, resource='', msg_id=None, user_nick='', + advanced_notif_num=None, xhtml=None, form_node=None): + + contact = None + # if chat window will be for specific resource + resource_for_chat = resource + + fjid = jid + + # Try to catch the contact with correct resource + if resource: + fjid = jid + '/' + resource + contact = gajim.contacts.get_contact(self.conn.name, jid, resource) + + highest_contact = gajim.contacts.get_contact_with_highest_priority( + self.conn.name, jid) + if not contact: + # If there is another resource, it may be a message from an invisible + # resource + lcontact = gajim.contacts.get_contacts(self.conn.name, jid) + if (len(lcontact) > 1 or (lcontact and lcontact[0].resource and \ + lcontact[0].show != 'offline')) and jid.find('@') > 0: + contact = gajim.contacts.copy_contact(highest_contact) + contact.resource = resource + if resource: + fjid = jid + '/' + resource + contact.priority = 0 + contact.show = 'offline' + contact.status = '' + gajim.contacts.add_contact(self.conn.name, contact) + + else: + # Default to highest prio + fjid = jid + resource_for_chat = None + contact = highest_contact + + if not contact: + # contact is not in roster + contact = gajim.interface.roster.add_to_not_in_the_roster( + self.conn.name, jid, user_nick) + + # If visible, try to get first line of contact in roster + path = None + iters = gajim.interface.roster._get_contact_iter(jid, self.conn.name, + contact=contact) + if iters: + path = gajim.interface.roster.modelfilter.get_path(iters[0]) + + if not self.control: + # if no control exists and message comes from highest prio, the new + # control shouldn't have a resource + if highest_contact and contact.resource == highest_contact.resource \ + and not jid == gajim.get_jid_from_account(self.conn.name): + fjid = jid + resource_for_chat = None + + # Do we have a queue? + no_queue = len(gajim.events.get_events(self.conn.name, fjid)) == 0 + + popup = helpers.allow_popup_window(self.conn.name, advanced_notif_num) + + if msg_type == 'normal' and popup: # it's single message to be autopopuped + dialogs.SingleMessageWindow(self.conn.name, contact.jid, + action='receive', from_whom=jid, subject=subject, message=msg, + resource=resource, session=self, form_node=form_node) + return + + # We print if window is opened and it's not a single message + if self.control and msg_type != 'normal': + typ = '' + + if msg_type == 'error': + typ = 'status' + + self.control.print_conversation(msg, typ, tim=tim, encrypted=encrypted, + subject=subject, xhtml=xhtml) + + if msg_id: + gajim.logger.set_read_messages([msg_id]) + + return + + # We save it in a queue + type_ = 'chat' + event_type = 'message_received' + + if msg_type == 'normal': + type_ = 'normal' + event_type = 'single_message_received' + + show_in_roster = notify.get_show_in_roster(event_type, self.conn.name, contact, self) + show_in_systray = notify.get_show_in_systray(event_type, self.conn.name, contact) + + event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, + encrypted, resource, msg_id, xhtml, self, form_node), + show_in_roster=show_in_roster, show_in_systray=show_in_systray) + + gajim.events.add_event(self.conn.name, fjid, event) + + if popup: + if not self.control: + self.control = gajim.interface.roster.new_chat(self, contact, self.conn.name, resource=resource_for_chat) + + if len(gajim.events.get_events(self.conn.name, fjid)): + self.control.read_queue() + + if path and not gajim.interface.dragging and gajim.config.get( + 'scroll_roster_to_last_message'): + # we curently see contact in our roster + # show and select his line in roster + # do not change selection while DND'ing + self.tree.expand_row(path[0:1], False) + self.tree.expand_row(path[0:2], False) + self.tree.scroll_to_cell(path) + self.tree.set_cursor(path) + else: + if no_queue: # We didn't have a queue: we change icons + gajim.interface.roster.draw_contact(jid, self.conn.name) + + gajim.interface.roster.show_title() # we show the * or [n] + # Show contact in roster (if he is invisible for example) and select + # line + gajim.interface.roster.show_and_select_contact_if_having_events(jid, + self.conn.name) diff --git a/src/tictactoe.py b/src/tictactoe.py new file mode 100644 index 000000000..716f5dcd2 --- /dev/null +++ b/src/tictactoe.py @@ -0,0 +1,427 @@ +from common import stanza_session +from common import xmpp + +import pygtk +pygtk.require('2.0') +import gtk +from gtk import gdk +import cairo + +# implements + +games_ns = 'http://jabber.org/protocol/games' + +class InvalidMove(Exception): + pass + +class TicTacToeSession(stanza_session.StanzaSession): + # initiate a session + def begin(self, rows = 3, cols = 3, role_s = 'x'): + self.rows = rows + self.cols = cols + + self.role_s = role_s + + self.strike = 3 + + if self.role_s == 'x': + self.role_o = 'o' + else: + self.role_o = 'x' + + self.send_invitation() + + self.next_move_id = 1 + self.received = self.wait_for_invite_response + + def send_invitation(self): + msg = xmpp.Message() + + invite = msg.NT.invite + invite.setNamespace(games_ns) + + game = invite.NT.game + game.setAttr('var', games_ns + '/tictactoe') + + x = xmpp.DataForm(typ='submit') + + game.addChild(node=x) + + self.send(msg) + + def read_invitation(self, msg): + invite = msg.getTag('invite', namespace=games_ns) + game = invite.getTag('game') + x = game.getTag('x', namespace='jabber:x:data') + + form = xmpp.DataForm(node=x) + + if form.getField('role'): + self.role_o = form.getField('role').getValues()[0] + else: + self.role_o = 'x' + + if form.getField('rows'): + self.rows = int(form.getField('rows').getValues()[0]) + else: + self.rows = 3 + + if form.getField('cols'): + self.cols = int(form.getField('cols').getValues()[0]) + else: + self.cols = 3 + + # number in a row needed to win + if form.getField('strike'): + self.strike = int(form.getField('strike').getValues()[0]) + else: + self.strike = 3 + + # received an invitation + def invited(self, msg): + self.read_invitation(msg) + + # XXX prompt user + # "accept, reject, ignore" + + # the number of the move about to be made + self.next_move_id = 1 + + # display the board + self.board = TicTacToeBoard(self, self.rows, self.cols) + + # accept the invitation, join the game + response = xmpp.Message() + + join = response.NT.join + join.setNamespace(games_ns) + + self.send(response) + + if self.role_o == 'x': + self.role_s = 'o' + + self.their_turn() + else: + self.role_s = 'x' + self.role_o = 'o' + + self.our_turn() + + # just sent an invitation, expecting a reply + def wait_for_invite_response(self, msg): + if msg.getTag('join', namespace=games_ns): + self.board = TicTacToeBoard(self, self.rows, self.cols) + + if self.role_s == 'x': + self.our_turn() + else: + self.their_turn() + + elif msg.getTag('decline', namespace=games_ns): + # XXX notify the user + + # XXX end session + pass + + # silently ignores any received messages + def ignore(self, msg): + pass + + def game_over(self, msg): + invite = msg.getTag('invite', namespace=games_ns) + + # ignore messages unless they're renewing the game + if invite and invite.getAttr('type') == 'renew': + self.invited(msg) + + def wait_for_move(self, msg): + turn = msg.getTag('turn', namespace=games_ns) + move = turn.getTag('move', namespace='http://jabber.org/protocol/games/tictactoe') + + row = int(move.getAttr('row')) + col = int(move.getAttr('col')) + id = int(move.getAttr('id')) + + if id != self.next_move_id: + print 'unexpected move id, lost a move somewhere?' + return + + try: + self.board.mark(row, col, self.role_o) + except InvalidMove, e: + # received an invalid move, end the game. + + # XXX notify the user + self.end_game('cheating') + return + + # check win conditions + if self.board.check_for_strike(self.role_o, row, col, self.strike): + self.lost() + elif self.board.full(): + self.drawn() + else: + self.next_move_id += 1 + + self.our_turn() + + def is_my_turn(self): + # XXX not great semantics + return self.received == self.ignore + + def our_turn(self): + # ignore messages until we've made our move + self.received = self.ignore + self.board.set_title('your turn') + + def their_turn(self): + self.received = self.wait_for_move + self.board.set_title('their turn') + + # called when the board receives input + def move(self, row, col): + try: + self.board.mark(row, col, self.role_s) + except InvalidMove, e: + print 'you made an invalid move' + return + + self.send_move(row, col) + + # check win conditions + if self.board.check_for_strike(self.role_s, row, col, self.strike): + self.won() + elif self.board.full(): + self.drawn() + else: + self.next_move_id += 1 + + self.their_turn() + + # sends a move message + def send_move(self, row, column): + msg = xmpp.Message() + msg.setType('chat') + + turn = msg.NT.turn + turn.setNamespace(games_ns) + + move = turn.NT.move + move.setNamespace(games_ns+'/tictactoe') + + move.setAttr('row', str(row)) + move.setAttr('col', str(column)) + move.setAttr('id', str(self.next_move_id)) + + self.send(msg) + + # sends a termination message and ends the game + def end_game(self, reason): + msg = xmpp.Message() + + terminate = msg.NT.terminate + terminate.setNamespace(games_ns) + terminate.setAttr('reason', reason) + + self.send(msg) + + self.received = self.game_over + + def won(self): + self.end_game('won') + self.board.won() + + def lost(self): + self.end_game('lost') + self.board.lost() + + def drawn(self): + self.end_game('draw') + self.board.drawn() + +class TicTacToeBoard: + def __init__(self, session, rows, cols): + self.session = session + + self.state = 'None' + + self.rows = rows + self.cols = cols + + self.board = [ [None] * self.cols for r in xrange(self.rows) ] + + self.setup_window() + + # check if the last move (at row r and column c) won the game + def check_for_strike(self, p, r, c, strike): + # number in a row: up and down, left and right + tallyI = 0 + tally_ = 0 + + # number in a row: diagonal + # (imagine L or F as two sides of a right triangle: L\ or F/) + tallyL = 0 + tallyF = 0 + + # convert real columns to internal columns + r -= 1 + c -= 1 + + for d in xrange(-strike, strike): + r_in_range = 0 <= r+d < self.rows + c_in_range = 0 <= c+d < self.cols + + # vertical check + if r_in_range: + tallyI = tallyI + 1 + if self.board[r+d][c] != p: + tallyI = 0 + + # horizontal check + if c_in_range: + tally_ = tally_ + 1 + if self.board[r][c+d] != p: + tally_ = 0 + + # diagonal checks + if r_in_range and c_in_range: + tallyL = tallyL + 1 + if self.board[r+d][c+d] != p: + tallyL = 0 + + if r_in_range and 0 <= c-d < self.cols: + tallyF = tallyF + 1 + if self.board[r+d][c-d] != p: + tallyF = 0 + + if any([t == strike for t in (tallyL, tallyF, tallyI, tally_)]): + return True + + return False + + # is the board full? + def full(self): + for r in xrange(self.rows): + for c in xrange(self.cols): + if self.board[r][c] == None: + return False + + return True + + def setup_window(self): + self.win = gtk.Window() + + self.title_prefix = 'tic-tac-toe with %s' % self.session.jid + self.set_title() + + self.win.set_app_paintable(True) + + self.win.add_events(gdk.BUTTON_PRESS_MASK) + self.win.connect('button-press-event', self.clicked) + self.win.connect('expose-event', self.expose) + + self.win.show_all() + + def clicked(self, widget, event): + if not self.session.is_my_turn(): + return + + (height, width) = widget.get_size() + + # convert click co-ordinates to row and column + + row_height = height // self.rows + col_width = width // self.cols + + row = int(event.y // row_height) + 1 + column = int(event.x // col_width) + 1 + + self.session.move(row, column) + + # this actually draws the board + def expose(self, widget, event): + win = widget.window + + cr = win.cairo_create() + + cr.set_source_rgb(1.0, 1.0, 1.0) + + cr.set_operator(cairo.OPERATOR_SOURCE) + cr.paint() + + (width, height) = widget.get_size() + + row_height = height // self.rows + col_width = width // self.cols + + for i in xrange(self.rows): + for j in xrange(self.cols): + if self.board[i][j] == 'x': + self.draw_x(cr, i, j, row_height, col_width) + elif self.board[i][j] == 'o': + self.draw_o(cr, i, j, row_height, col_width) + + # XXX draw 'won', 'lost', 'draw' + + def draw_x(self, cr, row, col, row_height, col_width): + cr.set_source_rgb(0, 0, 0) + + top = row_height * (row + 0.2) + bottom = row_height * (row + 0.8) + + left = col_width * (col + 0.2) + right = col_width * (col + 0.8) + + cr.set_line_width(row_height / 5) + + cr.move_to(left, top) + cr.line_to(right, bottom) + + cr.move_to(right, top) + cr.line_to(left, bottom) + + cr.stroke() + + def draw_o(self, cr, row, col, row_height, col_width): + cr.set_source_rgb(0, 0, 0) + + x = col_width * (col + 0.5) + y = row_height * (row + 0.5) + + cr.arc(x, y, row_height/4, 0, 2.0*3.2) # slightly further than 2*pi + + cr.set_line_width(row_height / 5) + cr.stroke() + + # mark a move on the board + def mark(self, row, column, player): + if self.board[row-1][column-1]: + raise InvalidMove + else: + self.board[row-1][column-1] = player + + self.win.queue_draw() + + def set_title(self, suffix = None): + str = self.title_prefix + + if suffix: + str += ': ' + suffix + + self.win.set_title(str) + + def won(self): + self.state = 'won' + self.set_title('you won!') + self.win.queue_draw() + + def lost(self): + self.state = 'lost' + self.set_title('you lost.') + self.win.queue_draw() + + def drawn(self): + self.state = 'drawn' + self.win.set_title(self.title_prefix + ': a draw.') + self.win.queue_draw() diff --git a/src/vcard.py b/src/vcard.py index 99b9db47f..eab39dcbf 100644 --- a/src/vcard.py +++ b/src/vcard.py @@ -122,11 +122,9 @@ class VcardWindow: jid = self.contact.jid # Update roster gajim.interface.roster.draw_avatar(jid, self.account) - # Update chat window - if gajim.interface.msg_win_mgr.has_window(jid, self.account): - win = gajim.interface.msg_win_mgr.get_window(jid, self.account) - ctrl = win.get_control(jid, self.account) - if win and ctrl.type_id != message_control.TYPE_GC: + # Update chat windows + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(jid, self.account): + if ctrl.type_id != message_control.TYPE_GC: ctrl.show_avatar() def on_vcard_information_window_destroy(self, widget):