diff --git a/data/gui/advanced_menuitem_menu.ui b/data/gui/advanced_menuitem_menu.ui index f9ac23da2..1c1869400 100644 --- a/data/gui/advanced_menuitem_menu.ui +++ b/data/gui/advanced_menuitem_menu.ui @@ -9,6 +9,13 @@ True + + + True + Edit Archi_ving Preferences + True + + Edit _Privacy Lists... diff --git a/data/gui/archiving_preferences_window.ui b/data/gui/archiving_preferences_window.ui new file mode 100644 index 000000000..0c8db71e1 --- /dev/null +++ b/data/gui/archiving_preferences_window.ui @@ -0,0 +1,312 @@ + + + + + + + + + + + + No + + + Yes + + + + + + + + + + + Prefer + + + Concede + + + Forbid + + + + + + + + + + + Prefer + + + Concede + + + Forbid + + + + + + + + + + + Prefer + + + Concede + + + Forbid + + + + + 12 + + + + True + vertical + + + True + 4 + 2 + + + True + <i>Method Manual</i> + True + + + 3 + 4 + + + + + True + <i>Method Local</i> + True + + + 2 + 3 + + + + + True + <i>Method Auto</i> + True + + + 1 + 2 + + + + + True + liststore4 + + + + + 0 + + + + + 1 + 2 + 3 + 4 + + + + + True + liststore3 + + + + + 0 + + + + + 1 + 2 + 2 + 3 + + + + + True + liststore2 + + + + + 0 + + + + + 1 + 2 + 1 + 2 + + + + + True + Auto + + + + + True + liststore1 + + + + + 0 + + + + + 1 + 2 + + + + + False + 0 + + + + + True + vertical + + + True + True + automatic + automatic + + + True + True + + + + + + 0 + + + + + True + spread + + + gtk-add + True + True + True + True + + + + False + False + 0 + + + + + gtk-remove + True + True + True + True + + + + False + False + 1 + + + + + gtk-edit + True + True + True + True + + + + False + False + 2 + + + + + False + 1 + + + + + 1 + + + + + True + end + + + gtk-close + True + True + True + False + True + + + + False + False + 0 + + + + + False + 2 + + + + + + diff --git a/data/gui/item_archiving_preferences_window.ui b/data/gui/item_archiving_preferences_window.ui new file mode 100644 index 000000000..3e46b37a6 --- /dev/null +++ b/data/gui/item_archiving_preferences_window.ui @@ -0,0 +1,227 @@ + + + + + + 12 + + + + True + vertical + 12 + + + True + 4 + 2 + + + True + expire + + + 1 + 2 + + + + + True + True + + + + 1 + 2 + 1 + 2 + + + + + True + liststore2 + + + + + 0 + + + + + 1 + 2 + 2 + 3 + + + + + True + liststore1 + + + + 0 + + + + + 1 + 2 + 3 + 4 + + + + + True + save + + + 3 + 4 + + + + + True + otr + + + 2 + 3 + + + + + True + jid + + + + + True + True + + + + 1 + 2 + + + + + False + False + 0 + + + + + True + + + + False + False + 0 + + + + + True + end + + + gtk-cancel + True + True + True + True + + + + False + False + 0 + + + + + gtk-ok + True + True + True + True + + + + False + False + 1 + + + + + 1 + + + + + False + 1 + + + + + + + + + + + + + body + + + false + + + message + + + stream + + + + + + + + + + + approve + + + concede + + + forbid + + + oppose + + + prefer + + + require + + + + diff --git a/src/chat_control.py b/src/chat_control.py index 370a9730a..3fd462a00 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -46,6 +46,7 @@ from common import exceptions from message_control import MessageControl from conversation_textview import ConversationTextview from message_textview import MessageTextView +from common.stanza_session import EncryptedStanzaSession, ArchivingStanzaSession from common.contacts import GC_Contact from common.logger import constants from common.pep import MOODS, ACTIVITIES @@ -2202,6 +2203,18 @@ class ChatControl(ChatControlBase): msg = _('Session negotiation cancelled') ChatControlBase.print_conversation_line(self, msg, 'status', '', None) + def print_archiving_session_details(self): + """ + Print esession settings to textview + """ + archiving = bool(self.session) and isinstance(self.session, + ArchivingStanzaSession) and self.session.archiving + if archiving: + msg = _('This session WILL be archived on server') + else: + msg = _('This session WILL NOT be archived on server') + ChatControlBase.print_conversation_line(self, msg, 'status', '', None) + def print_esession_details(self): """ Print esession settings to textview @@ -2226,6 +2239,12 @@ class ChatControl(ChatControlBase): self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \ self.session.is_loggable(), self.session and self.session.verified_identity) + def print_session_details(self): + if isinstance(self.session, EncryptedStanzaSession): + self.print_esession_details() + elif isinstance(self.session, ArchivingStanzaSession): + self.print_archiving_session_details() + def print_conversation(self, text, frm='', tim=None, encrypted=False, subject=None, xhtml=None, simple=False, xep0184_id=None, displaymarking=None): @@ -2658,6 +2677,8 @@ class ChatControl(ChatControlBase): if want_e2e and not self.no_autonegotiation \ and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION): self.begin_e2e_negotiation() + elif not self.session or not self.session.status: + self.begin_archiving_negotiation() else: self.send_chatstate('active', self.contact) @@ -2901,7 +2922,7 @@ class ChatControl(ChatControlBase): else: self.begin_e2e_negotiation() - def begin_e2e_negotiation(self): + def begin_negotiation(self): self.no_autonegotiation = True if not self.session: @@ -2909,8 +2930,14 @@ class ChatControl(ChatControlBase): new_sess = gajim.connections[self.account].make_new_session(fjid, type_=self.type_id) self.set_session(new_sess) + def begin_e2e_negotiation(self): + self.begin_negotiation() self.session.negotiate_e2e(False) + def begin_archiving_negotiation(self): + self.begin_negotiation() + self.session.negotiate_archiving() + def got_connected(self): ChatControlBase.got_connected(self) # Refreshing contact diff --git a/src/common/config.py b/src/common/config.py index 3473d6f7e..c9cb20121 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -371,6 +371,7 @@ class Config: 'send_idle_time': [ opt_bool, True ], 'roster_version': [opt_str, ''], 'subscription_request_msg': [opt_str, '', _('Message that is sent to contacts you want to add')], + 'last_archiving_time': [opt_str, '1970-01-01T00:00:00Z', _('Last time we syncronized with logs from server.')], }, {}), 'statusmsg': ({ 'message': [ opt_str, '' ], diff --git a/src/common/connection.py b/src/common/connection.py index 0e281d5a7..55593c9e3 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -153,6 +153,7 @@ class CommonConnection: self.privacy_rules_supported = False self.vcard_supported = False self.private_storage_supported = False + self.archive_pref_supported = False self.muc_jid = {} # jid of muc server for each transport type self._stun_servers = [] # STUN servers of our jabber server @@ -1541,6 +1542,9 @@ class Connection(CommonConnection, ConnectionHandlers): self.connection.set_send_timeout(self.keepalives, self.send_keepalive) self.connection.set_send_timeout2(self.pingalives, self.sendPing) self.connection.onreceive(None) + + self.request_message_archiving_preferences() + self.discoverInfo(gajim.config.get_per('accounts', self.name, 'hostname'), id_prefix='Gajim_') self.privacy_rules_requested = False diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 15063324d..9ab712818 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -51,6 +51,11 @@ from common.pubsub import ConnectionPubSub from common.pep import ConnectionPEP from common.protocol.caps import ConnectionCaps from common.protocol.bytestream import ConnectionSocks5Bytestream +from common.message_archiving import ConnectionArchive +from common.message_archiving import ARCHIVING_COLLECTIONS_ARRIVED +from common.message_archiving import ARCHIVING_COLLECTION_ARRIVED +from common.message_archiving import ARCHIVING_MODIFICATIONS_ARRIVED + from common import ged from common import nec from common.nec import NetworkEvent @@ -363,6 +368,14 @@ class ConnectionDisco: our_jid = gajim.get_jid_from_account(self.name) self.send_pb_purge(our_jid, 'storage:bookmarks') self.send_pb_delete(our_jid, 'storage:bookmarks') + if features.__contains__(common.xmpp.NS_ARCHIVE_AUTO): + self.archive_auto_supported = True + if features.__contains__(common.xmpp.NS_ARCHIVE_MANAGE): + self.archive_manage_supported = True + if features.__contains__(common.xmpp.NS_ARCHIVE_MANUAL): + self.archive_manual_supported = True + if features.__contains__(common.xmpp.NS_ARCHIVE_PREF): + self.archive_pref_supported = True if features.__contains__(common.xmpp.NS_BYTESTREAM): our_jid = helpers.parse_jid(gajim.get_jid_from_account(self.name) +\ '/' + self.server_resource) @@ -701,6 +714,71 @@ class ConnectionVcard: form = common.dataforms.ExtendForm(node=form_tag) self.dispatch('PEP_CONFIG', (node, form)) + elif self.awaiting_answers[id_][0] == ARCHIVING_COLLECTIONS_ARRIVED: + # TODO + print 'ARCHIVING_COLLECTIONS_ARRIVED' + pass + + elif self.awaiting_answers[id_][0] == ARCHIVING_COLLECTION_ARRIVED: + def save_if_not_exists(with_, nick, direction, tim, payload): + assert len(payload) == 1, 'got several archiving messages in' +\ + ' the same time %s' % ''.join(payload) + if payload[0].getName() == 'body': + gajim.logger.save_if_not_exists(with_, direction, tim, + msg=payload[0].getData(), nick=nick) + elif payload[0].getName() == 'message': + print 'Not implemented' + chat = iq_obj.getTag('chat') + if chat: + with_ = chat.getAttr('with') + start_ = chat.getAttr('start') + tim = helpers.datetime_tuple(start_) + tim = timegm(tim) + nb = 0 + for element in chat.getChildren(): + try: + secs = int(element.getAttr('secs')) + except TypeError: + secs = 0 + if secs: + tim += secs + nick = element.getAttr('name') + if element.getName() == 'from': + save_if_not_exists(with_, nick, 'from', localtime(tim), + element.getPayload()) + nb += 1 + if element.getName() == 'to': + save_if_not_exists(with_, nick, 'to', localtime(tim), + element.getPayload()) + nb += 1 + set_ = chat.getTag('set') + first = set_.getTag('first') + if first: + try: + index = int(first.getAttr('index')) + except TypeError: + index = 0 + try: + count = int(set_.getTagData('count')) + except TypeError: + count = 0 + if count > index + nb: + # Request the next page + after = element.getTagData('last') + self.request_collection_page(with_, start_, after=after) + + elif self.awaiting_answers[id_][0] == ARCHIVING_MODIFICATIONS_ARRIVED: + modified = iq_obj.getTag('modified') + if modified: + for element in modified.getChildren(): + if element.getName() == 'changed': + with_ = element.getAttr('with') + start_ = element.getAttr('start') + self.request_collection_page(with_, start_) + elif element.getName() == 'removed': + # do nothing + pass + del self.awaiting_answers[id_] def _vCardCB(self, con, vc): @@ -955,11 +1033,13 @@ class ConnectionHandlersBase: return sess -class ConnectionHandlers(ConnectionVcard, ConnectionSocks5Bytestream, -ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionPEP, -ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): +class ConnectionHandlers(ConnectionArchive, ConnectionVcard, +ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCommands, +ConnectionPubSub, ConnectionPEP, ConnectionCaps, ConnectionHandlersBase, +ConnectionJingle): def __init__(self): global HAS_IDLE + ConnectionArchive.__init__(self) ConnectionVcard.__init__(self) ConnectionSocks5Bytestream.__init__(self) ConnectionCommands.__init__(self) @@ -2303,6 +2383,7 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): common.xmpp.NS_SEARCH) con.RegisterHandler('iq', self._PrivacySetCB, 'set', common.xmpp.NS_PRIVACY) + con.RegisterHandler('iq', self._ArchiveCB, ns=common.xmpp.NS_ARCHIVE) con.RegisterHandler('iq', self._PubSubCB, 'result') con.RegisterHandler('iq', self._PubSubErrorCB, 'error') con.RegisterHandler('iq', self._JingleCB, 'result') diff --git a/src/common/logger.py b/src/common/logger.py index 2a3ffbf86..4998eae2a 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -45,6 +45,9 @@ LOG_DB_PATH = configpaths.gajimpaths['LOG_DB'] LOG_DB_FOLDER, LOG_DB_FILE = os.path.split(LOG_DB_PATH) CACHE_DB_PATH = configpaths.gajimpaths['CACHE_DB'] +import logging +log = logging.getLogger('gajim.c.logger') + class Constants: def __init__(self): ( @@ -142,7 +145,7 @@ class Logger: try: self.cur.execute("ATTACH DATABASE '%s' AS cache" % CACHE_DB_PATH) except sqlite.Error, e: - gajim.log.debug("Failed to attach cache database: %s" % str(e)) + log.debug("Failed to attach cache database: %s" % str(e)) def set_synchronous(self, sync): try: @@ -151,7 +154,7 @@ class Logger: else: self.cur.execute("PRAGMA synchronous = OFF") except sqlite.Error, e: - gajim.log.debug("Failed to set_synchronous(%s): %s" % (sync, str(e))) + log.debug("Failed to set_synchronous(%s): %s" % (sync, str(e))) def init_vars(self): self.open_db() @@ -1053,3 +1056,48 @@ class Logger: self.cur.execute('DELETE FROM roster_group WHERE account_jid_id=?', (account_jid_id,)) self.con.commit() + + def save_if_not_exists(self, with_, direction, tim, msg='', nick=None): + if tim: + time_col = int(float(time.mktime(tim))) + else: + time_col = int(float(time.time())) + if msg: + if self.jid_is_from_pm(with_) or nick: + # It's a groupchat message + if nick: + # It's a message from a groupchat occupent + type_ = 'gc_msg' + with_ = with_ + '/' + nick + else: + # It's a server message message, we don't log them + return + else: + if direction == 'from': + type_ = 'chat_msg_recv' + elif direction == 'to': + type_ = 'chat_msg_sent' + jid_id = self.get_jid_id(with_) + where_sql = 'jid_id = %s AND message=?' % jid_id + if type_ == 'gc_msg': + # We cannot differentiate gc message and pm messages, so look in + # both logs + with_2 = gajim.get_jid_without_resource(with_) + if with_ != with_2: + jid_id2 = self.get_jid_id(with_2) + where_sql = 'jid_id in (%s, %s) AND message=?' % (jid_id, + jid_id2) + start_time = time_col - 300 # 5 minutes arrount given time + end_time = time_col + 300 # 5 minutes arrount given time + self.cur.execute(''' + SELECT log_line_id FROM logs + WHERE (%s) + AND time BETWEEN %d AND %d + ORDER BY time + ''' % (where_sql, start_time, end_time), (msg,)) + results = self.cur.fetchall() + if results: + log.debug('Log already in DB, ignoring it') + return + log.debug('New log received from server archives, storing it') + self.write(type_, with_, message=msg, tim=tim) diff --git a/src/common/message_archiving.py b/src/common/message_archiving.py new file mode 100644 index 000000000..548240459 --- /dev/null +++ b/src/common/message_archiving.py @@ -0,0 +1,258 @@ +# -*- coding:utf-8 -*- +## src/common/message_archiving.py +## +## Copyright (C) 2009 Anaƫl Verrier +## +## This file is part of Gajim. +## +## Gajim is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## Gajim is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +import common.xmpp + +import logging +log = logging.getLogger('gajim.c.message_archiving') + +ARCHIVING_COLLECTIONS_ARRIVED = 'archiving_collections_arrived' +ARCHIVING_COLLECTION_ARRIVED = 'archiving_collection_arrived' +ARCHIVING_MODIFICATIONS_ARRIVED = 'archiving_modifications_arrived' + +class ConnectionArchive: + def __init__(self): + self.archive_auto_supported = False + self.archive_manage_supported = False + self.archive_manual_supported = False + self.archive_pref_supported = False + self.auto = None + self.method_auto = None + self.method_local = None + self.method_manual = None + self.default = None + self.items = {} + + def request_message_archiving_preferences(self): + iq_ = common.xmpp.Iq('get') + iq_.setTag('pref', namespace=common.xmpp.NS_ARCHIVE) + self.connection.send(iq_) + + def set_pref(self, name, **data): + ''' + data contains names and values of pref name attributes. + ''' + iq_ = common.xmpp.Iq('set') + pref = iq_.setTag('pref', namespace=common.xmpp.NS_ARCHIVE) + tag = pref.setTag(name) + for key, value in data.items(): + if value is not None: + tag.setAttr(key, value) + self.connection.send(iq_) + + def set_auto(self, save): + self.set_pref('auto', save=save) + + def set_method(self, type, use): + self.set_pref('method', type=type, use=use) + + def set_default(self, otr, save, expire=None): + self.set_pref('default', otr=otr, save=save, expire=expire) + + def append_or_update_item(self, jid, otr, save, expire): + self.set_pref('item', jid=jid, otr=otr, save=save) + + def remove_item(self, jid): + iq_ = common.xmpp.Iq('set') + itemremove = iq_.setTag('itemremove', namespace=common.xmpp.NS_ARCHIVE) + item = itemremove.setTag('item') + item.setAttr('jid', jid) + self.connection.send(iq_) + + def stop_archiving_session(self, thread_id): + iq_ = common.xmpp.Iq('set') + pref = iq_.setTag('pref', namespace=common.xmpp.NS_ARCHIVE) + session = pref.setTag('session', attrs={'thread': thread_id, + 'save': 'false', 'otr': 'concede'}) + self.connection.send(iq_) + + def get_item_pref(self, jid): + jid = common.xmpp.JID(jid) + if unicode(jid) in self.items: + return self.items[jid] + + if jid.getStripped() in self.items: + return self.items[jid.getStripped()] + + if jid.getDomain() in self.items: + return self.items[jid.getDomain()] + + return self.default + + def logging_preference(self, jid, initiator_options=None): + otr = self.get_item_pref(jid)['otr'] + if initiator_options: + if ((initiator_options == ['mustnot'] and otr == 'forbid') or + (initiator_options == ['may'] and otr == 'require')): + return None + + if (initiator_options == ['mustnot'] or + (initiator_options[0] == 'mustnot' and + otr not in ('opppose', 'forbid')) or + (initiator_options == ['may', 'mustnot'] and + otr in ('require', 'prefer'))): + return 'mustnot' + + return 'may' + + if otr == 'require': + return ['mustnot'] + + if otr in ('prefer', 'approve'): + return ['mustnot', 'may'] + + if otr in ('concede', 'oppose'): + return ['may', 'mustnot'] + + # otr == 'forbid' + return ['may'] + + def _ArchiveCB(self, con, iq_obj): + log.debug('_ArchiveCB %s' % iq_obj.getType()) + if iq_obj.getType() == 'error': + self.dispatch('ARCHIVING_ERROR', iq_obj.getErrorMsg()) + return + elif iq_obj.getType() not in ('result', 'set'): + return + + if iq_obj.getTag('pref'): + pref = iq_obj.getTag('pref') + + if pref.getTag('auto'): + self.auto = pref.getTagAttr('auto', 'save') + log.debug('archiving preference: auto: %s' % self.auto) + self.dispatch('ARCHIVING_CHANGED', ('auto', + self.auto)) + + method_auto = pref.getTag('method', attrs={'type': 'auto'}) + if method_auto: + self.method_auto = method_auto.getAttr('use') + self.dispatch('ARCHIVING_CHANGED', ('method_auto', + self.method_auto)) + + method_local = pref.getTag('method', attrs={'type': 'local'}) + if method_local: + self.method_local = method_local.getAttr('use') + self.dispatch('ARCHIVING_CHANGED', ('method_local', + self.method_local)) + + method_manual = pref.getTag('method', attrs={'type': 'manual'}) + if method_manual: + self.method_manual = method_manual.getAttr('use') + self.dispatch('ARCHIVING_CHANGED', ('method_manual', + self.method_manual)) + + log.debug('archiving preferences: method auto: %s, local: %s, ' + 'manual: %s' % (self.method_auto, self.method_local, + self.method_manual)) + + if pref.getTag('default'): + default = pref.getTag('default') + log.debug('archiving preferences: default otr: %s, save: %s, ' + 'expire: %s, unset: %s' % (default.getAttr('otr'), + default.getAttr('save'), default.getAttr('expire'), + default.getAttr('unset'))) + self.default = { + 'expire': default.getAttr('expire'), + 'otr': default.getAttr('otr'), + 'save': default.getAttr('save'), + 'unset': default.getAttr('unset')} + self.dispatch('ARCHIVING_CHANGED', ('default', + self.default)) + for item in pref.getTags('item'): + log.debug('archiving preferences for jid %s: otr: %s, save: %s, ' + 'expire: %s' % (item.getAttr('jid'), item.getAttr('otr'), + item.getAttr('save'), item.getAttr('expire'))) + self.items[item.getAttr('jid')] = { + 'expire': item.getAttr('expire'), + 'otr': item.getAttr('otr'), 'save': item.getAttr('save')} + self.dispatch('ARCHIVING_CHANGED', ('item', + item.getAttr('jid'), self.items[item.getAttr('jid')])) + elif iq_obj.getTag('itemremove'): + for item in pref.getTags('item'): + del self.items[item.getAttr('jid')] + self.dispatch('ARCHIVING_CHANGED', ('itemremove', + item.getAttr('jid'))) + + raise common.xmpp.NodeProcessed + + def request_collections_list_page(self, with_='', start=None, end=None, + after=None, max=30, exact_match=False): + iq_ = common.xmpp.Iq('get') + list_ = iq_.setTag('list', namespace=common.xmpp.NS_ARCHIVE) + if with_: + list_.setAttr('with', with_) + if exact_match: + list_.setAttr('exactmatch', 'true') + if start: + list_.setAttr('start', start) + if end: + list_.setAttr('end', end) + set_ = list_.setTag('set', namespace=common.xmpp.NS_RSM) + set_.setTagData('max', max) + if after: + set_.setTagData('after', after) + id_ = self.connection.getAnID() + iq_.setID(id_) + self.awaiting_answers[id_] = (ARCHIVING_COLLECTIONS_ARRIVED, ) + self.connection.send(iq_) + + def request_collection_page(self, with_, start, end=None, after=None, + max=30, exact_match=False): + iq_ = common.xmpp.Iq('get') + retrieve = iq_.setTag('retrieve', namespace=common.xmpp.NS_ARCHIVE, + attrs={'with': with_, 'start': start}) + if exact_match: + retrieve.setAttr('exactmatch', 'true') + set_ = retrieve.setTag('set', namespace=common.xmpp.NS_RSM) + set_.setTagData('max', max) + if after: + set_.setTagData('after', after) + id_ = self.connection.getAnID() + iq_.setID(id_) + self.awaiting_answers[id_] = (ARCHIVING_COLLECTION_ARRIVED, ) + self.connection.send(iq_) + + def remove_collection(self, with_='', start=None, end=None, + exact_match=False, open=False): + iq_ = common.xmpp.Iq('set') + remove = iq_.setTag('remove', namespace=common.xmpp.NS_ARCHIVE) + if with_: + remove.setAttr('with', with_) + if exact_match: + remove.setAttr('exactmatch', 'true') + if start: + remove.setAttr('start', start) + if end: + remove.setAttr('end', end) + if open: + remove.setAttr('open', 'true') + self.connection.send(iq_) + + def request_modifications_page(self, start, max=30): + iq_ = common.xmpp.Iq('get') + moified = iq_.setTag('modified', namespace=common.xmpp.NS_ARCHIVE, + attrs={'start': start}) + set_ = moified.setTag('set', namespace=common.xmpp.NS_RSM) + set_.setTagData('max', max) + id_ = self.connection.getAnID() + iq_.setID(id_) + self.awaiting_answers[id_] = (ARCHIVING_MODIFICATIONS_ARRIVED, ) + self.connection.send(iq_) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 028bee596..c5bcf09b5 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -175,7 +175,123 @@ class StanzaSession(object): self.status = None -class EncryptedStanzaSession(StanzaSession): +class ArchivingStanzaSession(StanzaSession): + def __init__(self, conn, jid, thread_id, type_='chat'): + StanzaSession.__init__(self, conn, jid, thread_id, type_='chat') + self.archiving = False + + def archiving_logging_preference(self, initiator_options=None): + return self.conn.logging_preference(self.jid, initiator_options) + + def negotiate_archiving(self): + self.negotiated = {} + + request = xmpp.Message() + feature = request.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + x = xmpp.DataForm(typ='form') + + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', + typ='hidden')) + x.addChild(node=xmpp.DataField(name='accept', value='1', typ='boolean', + required=True)) + + x.addChild(node=xmpp.DataField(name='logging', typ='list-single', + options=self.archiving_logging_preference(), required=True)) + + x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', + options=['never'], required=True)) + x.addChild(node=xmpp.DataField(name='security', typ='list-single', + options=['none'], required=True)) + + feature.addChild(node=x) + + self.status = 'requested-archiving' + + self.send(request) + + def respond_archiving(self, form): + field = form.getField('logging') + options = [x[1] for x in field.getOptions()] + values = field.getValues() + + logging = self.archiving_logging_preference(options) + self.negotiated['logging'] = logging + + response = xmpp.Message() + feature = response.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + x = xmpp.DataForm(typ='submit') + + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='accept', value='true')) + + x.addChild(node=xmpp.DataField(name='logging', value=logging)) + + self.status = 'responded-archiving' + + feature.addChild(node=x) + + if not logging: + response = xmpp.Error(response, xmpp.ERR_NOT_ACCEPTABLE) + + feature = xmpp.Node(xmpp.NS_FEATURE + ' feature') + + n = xmpp.Node('field') + n['var'] = 'logging' + feature.addChild(node=n) + + response.T.error.addChild(node=feature) + + self.send(response) + + def we_accept_archiving(self, form): + if self.negotiated['logging'] == 'mustnot': + self.loggable = False + log.debug('archiving session accepted: %s' % self.loggable) + self.status = 'active' + self.archiving = True + if self.control: + self.control.print_archiving_session_details() + + def archiving_accepted(self, form): + negotiated = {} + ask_user = {} + not_acceptable = [] + + if form['logging'] not in self.archiving_logging_preference(): + raise + + self.negotiated['logging'] = form['logging'] + + accept = xmpp.Message() + feature = accept.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) + + result = xmpp.DataForm(typ='result') + + result.addChild(node=xmpp.DataField(name='FORM_TYPE', + value='urn:xmpp:ssn')) + result.addChild(node=xmpp.DataField(name='accept', value='1')) + + feature.addChild(node=result) + + self.send(accept) + if self.negotiated['logging'] == 'mustnot': + self.loggable = False + log.debug('archiving session accepted: %s' % self.loggable) + self.status = 'active' + self.archiving = True + if self.control: + self.control.print_archiving_session_details() + + def stop_archiving_for_session(self): + self.conn.stop_archiving_session(self.thread_id) + + +class EncryptedStanzaSession(ArchivingStanzaSession): """ An encrypted stanza negotiation has several states. They arerepresented as the following values in the 'status' attribute of the session object: @@ -202,7 +318,8 @@ class EncryptedStanzaSession(StanzaSession): """ def __init__(self, conn, jid, thread_id, type_='chat'): - StanzaSession.__init__(self, conn, jid, thread_id, type_='chat') + ArchivingStanzaSession.__init__(self, conn, jid, thread_id, + type_='chat') self.xes = {} self.es = {} @@ -921,6 +1038,8 @@ class EncryptedStanzaSession(StanzaSession): if self.control: self.control.print_esession_details() + self.stop_archiving_for_session() + def final_steps_alice(self, form): srs = '' srses = secrets.secrets().retained_secrets(self.conn.name, @@ -961,6 +1080,8 @@ class EncryptedStanzaSession(StanzaSession): if self.control: self.control.print_esession_details() + self.stop_archiving_for_session() + def do_retained_secret(self, k, old_srs): """ Calculate the new retained secret. determine if the user needs to check diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index eb06c6a28..c218514b5 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -28,6 +28,11 @@ NS_ADDRESS ='http://jabber.org/protocol/address' NS_AGENTS ='jabber:iq:agents' NS_AMP ='http://jabber.org/protocol/amp' NS_AMP_ERRORS =NS_AMP+'#errors' +NS_ARCHIVE ='urn:xmpp:archive' #XEP-0136 +NS_ARCHIVE_AUTO =NS_ARCHIVE+':auto' #XEP-0136 +NS_ARCHIVE_MANAGE =NS_ARCHIVE+':manage' #XEP-0136 +NS_ARCHIVE_MANUAL =NS_ARCHIVE+':manual' #XEP-0136 +NS_ARCHIVE_PREF =NS_ARCHIVE+':pref' NS_ATOM ='http://www.w3.org/2005/Atom' NS_AUTH ='jabber:iq:auth' NS_AVATAR ='http://www.xmpp.org/extensions/xep-0084.html#ns-metadata' @@ -102,6 +107,7 @@ NS_ROSTER ='jabber:iq:roster' NS_ROSTERX ='http://jabber.org/protocol/rosterx' # XEP-0144 NS_ROSTER_VER ='urn:xmpp:features:rosterver' # XEP-0273 NS_RPC ='jabber:iq:rpc' # XEP-0009 +NS_RSM ='http://jabber.org/protocol/rsm' NS_SASL ='urn:ietf:params:xml:ns:xmpp-sasl' NS_SECLABEL ='urn:xmpp:sec-label:0' NS_SECLABEL_CATALOG ='urn:xmpp:sec-label:catalog:0' diff --git a/src/dialogs.py b/src/dialogs.py index a1adc58bb..2fff8c743 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -3381,6 +3381,356 @@ class RosterItemExchangeWindow: self.window.destroy() +class ItemArchivingPreferencesWindow: + otr_name = ('approve', 'concede', 'forbid', 'oppose', 'prefer', 'require') + otr_index = dict([(j, i) for i, j in enumerate(otr_name)]) + save_name = ('body', 'false', 'message', 'stream') + save_index = dict([(j, i) for i, j in enumerate(save_name)]) + + def __init__(self, account, item): + self.account = account + self.item = item + if self.item and self.item != 'Default': + self.item_config = gajim.connections[self.account].items[self.item] + else: + self.item_config = gajim.connections[self.account].default + self.waiting = None + + # Connect to gtk builder + self.xml = gtkgui_helpers.get_gtk_builder( + 'item_archiving_preferences_window.ui') + self.window = self.xml.get_object('item_archiving_preferences_window') + + # Add Widgets + for widget_to_add in ('jid_entry', 'expire_entry', 'otr_combobox', + 'save_combobox', 'cancel_button', 'ok_button', 'progressbar'): + self.__dict__[widget_to_add] = self.xml.get_object(widget_to_add) + + if self.item: + self.jid_entry.set_text(self.item) + expire_value = self.item_config['expire'] or '' + self.otr_combobox.set_active(self.otr_index[self.item_config['otr']]) + self.save_combobox.set_active( + self.save_index[self.item_config['save']]) + self.expire_entry.set_text(expire_value) + + self.window.set_title(_('Archiving Preferences for %s') % self.account) + + self.window.show_all() + self.progressbar.hide() + self.xml.connect_signals(self) + + def update_progressbar(self): + if self.waiting: + self.progressbar.pulse() + return True + return False + + def on_otr_combobox_changed(self, widget): + otr = self.otr_name[self.otr_combobox.get_active()] + if otr == 'require': + self.save_combobox.set_active(self.save_index['false']) + + def on_ok_button_clicked(self, widget): + # Return directly if operation in progress + if self.waiting: + return + + item = self.jid_entry.get_text() + otr = self.otr_name[self.otr_combobox.get_active()] + save = self.save_name[self.save_combobox.get_active()] + expire = self.expire_entry.get_text() + + if self.item != 'Default': + try: + item = helpers.parse_jid(item) + except helpers.InvalidFormat, s: + pritext = _('Invalid User ID') + ErrorDialog(pritext, str(s)) + return + + if expire: + try: + if int(expire) < 0 or str(int(expire)) != expire: + raise ValueError + except ValueError: + pritext = _('Invalid expire value') + sectext = _('Expire must be a valid positive integer.') + ErrorDialog(pritext, sectext) + return + + if not (item == self.item and expire == self.item_config['expire'] and + otr == self.item_config['otr'] and save == self.item_config['save']): + if not self.item or self.item == item: + if self.item == 'Default': + self.waiting = 'default' + gajim.connections[self.account].set_default( + otr, save, expire) + else: + self.waiting = 'item' + gajim.connections[self.account].append_or_update_item( + item, otr, save, expire) + else: + self.waiting = 'item' + gajim.connections[self.account].append_or_update_item( + item, otr, save, expire) + gajim.connections[self.account].remove_item(self.item) + self.launch_progressbar() + #self.window.destroy() + + def on_cancel_button_clicked(self, widget): + self.window.destroy() + + def on_item_archiving_preferences_window_destroy(self, widget): + if self.item: + key_name = 'edit_item_archiving_preferences_%s' % self.item + else: + key_name = 'new_item_archiving_preferences' + if key_name in gajim.interface.instances[self.account]: + del gajim.interface.instances[self.account][key_name] + + def launch_progressbar(self): + self.progressbar.show() + self.update_progressbar_timeout_id = gobject.timeout_add( + 100, self.update_progressbar) + + def response_arrived(self, data): + if self.waiting: + self.window.destroy() + + def error_arrived(self, error): + if self.waiting: + self.waiting = None + self.progressbar.hide() + pritext = _('There is an error with the form') + sectext = error + ErrorDialog(pritext, sectext) + + +class ArchivingPreferencesWindow: + auto_name = ('false', 'true') + auto_index = dict([(j, i) for i, j in enumerate(auto_name)]) + method_foo_name = ('prefer', 'concede', 'forbid') + method_foo_index = dict([(j, i) for i, j in enumerate(method_foo_name)]) + + def __init__(self, account): + self.account = account + self.waiting = [] + + # Connect to glade + self.xml = gtkgui_helpers.get_gtk_builder( + 'archiving_preferences_window.ui') + self.window = self.xml.get_object('archiving_preferences_window') + + # Add Widgets + for widget_to_add in ('auto_combobox', 'method_auto_combobox', + 'method_local_combobox', 'method_manual_combobox', 'close_button', + 'item_treeview', 'item_notebook', 'otr_combobox', 'save_combobox', + 'expire_entry', 'remove_button', 'edit_button'): + self.__dict__[widget_to_add] = self.xml.get_object(widget_to_add) + + self.auto_combobox.set_active( + self.auto_index[gajim.connections[self.account].auto]) + self.method_auto_combobox.set_active( + self.method_foo_index[gajim.connections[self.account].method_auto]) + self.method_local_combobox.set_active( + self.method_foo_index[gajim.connections[self.account].method_local]) + self.method_manual_combobox.set_active( + self.method_foo_index[gajim.connections[self.account].\ + method_manual]) + + model = gtk.ListStore(str, str, str, str) + self.item_treeview.set_model(model) + col = gtk.TreeViewColumn('jid') + self.item_treeview.append_column(col) + renderer = gtk.CellRendererText() + col.pack_start(renderer, True) + col.set_attributes(renderer, text=0) + + col = gtk.TreeViewColumn('expire') + col.pack_start(renderer, True) + col.set_attributes(renderer, text=1) + self.item_treeview.append_column(col) + + col = gtk.TreeViewColumn('otr') + col.pack_start(renderer, True) + col.set_attributes(renderer, text=2) + self.item_treeview.append_column(col) + + col = gtk.TreeViewColumn('save') + col.pack_start(renderer, True) + col.set_attributes(renderer, text=3) + self.item_treeview.append_column(col) + + self.fill_items() + + self.current_item = None + + def sort_items(model, iter1, iter2): + item1 = model.get_value(iter1, 0) + item2 = model.get_value(iter2, 0) + if item1 == 'Default': + return -1 + if item2 == 'Default': + return 1 + if '@' in item1: + if '@' not in item2: + return 1 + elif '@' in item2: + return -1 + if item1 < item2: + return -1 + if item1 > item2: + return 1 + # item1 == item2 ? WTF? + return 0 + + model.set_sort_column_id(0, gtk.SORT_ASCENDING) + model.set_sort_func(0, sort_items) + + self.remove_button.set_sensitive(False) + self.edit_button.set_sensitive(False) + + self.window.set_title(_('Archiving Preferences for %s') % self.account) + + self.window.show_all() + + self.xml.connect_signals(self) + + def on_add_item_button_clicked(self, widget): + key_name = 'new_item_archiving_preferences' + if key_name in gajim.interface.instances[self.account]: + gajim.interface.instances[self.account][key_name].window.present() + else: + gajim.interface.instances[self.account][key_name] = \ + ItemArchivingPreferencesWindow(self.account, '') + + def on_remove_item_button_clicked(self, widget): + if not self.current_item: + return + + self.waiting.append('itemremove') + sel = self.item_treeview.get_selection() + (model, iter_) = sel.get_selected() + gajim.connections[self.account].remove_item(model[iter_][0]) + model.remove(iter_) + self.remove_button.set_sensitive(False) + self.edit_button.set_sensitive(False) + + def on_edit_item_button_clicked(self, widget): + if not self.current_item: + return + + key_name = 'edit_item_archiving_preferences_%s' % self.current_item + if key_name in gajim.interface.instances[self.account]: + gajim.interface.instances[self.account][key_name].window.present() + else: + gajim.interface.instances[self.account][key_name] = \ + ItemArchivingPreferencesWindow(self.account, self.current_item) + + def on_item_treeview_cursor_changed(self, widget): + sel = self.item_treeview.get_selection() + (model, iter_) = sel.get_selected() + item = None + if iter_: + item = model[iter_][0] + if self.current_item and self.current_item == item: + return + + self.current_item = item + if self.current_item == 'Default': + self.remove_button.set_sensitive(False) + self.edit_button.set_sensitive(True) + elif self.current_item: + self.remove_button.set_sensitive(True) + self.edit_button.set_sensitive(True) + else: + self.remove_button.set_sensitive(False) + self.edit_button.set_sensitive(False) + + def on_auto_combobox_changed(self, widget): + save = self.auto_name[widget.get_active()] + gajim.connections[self.account].set_auto(save) + + def on_method_foo_combobox_changed(self, widget): + # We retrieve method type from widget name + # ('foo' in 'method_foo_combobox') + method_type = widget.name.split('_')[1] + use = self.method_foo_name[widget.get_active()] + self.waiting.append('method_%s' % method_type) + gajim.connections[self.account].set_method(method_type, use) + + def get_child_window(self): + edit_key_name = 'edit_item_archiving_preferences_%s' % self.current_item + new_key_name = 'new_item_archiving_preferences' + + if edit_key_name in gajim.interface.instances[self.account]: + return gajim.interface.instances[self.account][edit_key_name] + + if new_key_name in gajim.interface.instances[self.account]: + return gajim.interface.instances[self.account][new_key_name] + + def archiving_changed(self, data): + if data[0] in ('auto', 'method_auto', 'method_local', 'method_manual'): + if data[0] in self.waiting: + self.waiting.remove(data[0]) + elif data[0] == 'default': + key_name = 'edit_item_archiving_preferences_%s' % \ + self.current_item + if key_name in gajim.interface.instances[self.account]: + gajim.interface.instances[self.account][key_name].\ + response_arrived(data[1:]) + self.fill_items(True) + elif data[0] == 'item': + child = self.get_child_window() + if child: + is_new = not child.item + child.response_arrived(data[1:]) + if is_new: + model = self.item_treeview.get_model() + model.append((data[1], data[2]['expire'], data[2]['otr'], + data[2]['save'])) + return + self.fill_items(True) + elif data[0] == 'itemremove' == self.waiting: + if data[0] in self.waiting: + self.waiting.remove(data[0]) + self.fill_items(True) + + def fill_items(self, clear=False): + model = self.item_treeview.get_model() + if clear: + model.clear() + default_config = gajim.connections[self.account].default + expire_value = default_config['expire'] or '' + model.append(('Default', expire_value, + default_config['otr'], default_config['save'])) + for item, item_config in \ + gajim.connections[self.account].items.items(): + expire_value = item_config['expire'] or '' + model.append((item, expire_value, item_config['otr'], + item_config['save'])) + + def archiving_error(self, error): + if self.waiting: + pritext = _('There is an error') + sectext = error + ErrorDialog(pritext, sectext) + self.waiting.pop() + else: + child = self.get_child_window() + if child: + child.error_arrived(error) + print error + + def on_close_button_clicked(self, widget): + self.window.destroy() + + def on_archiving_preferences_window_destroy(self, widget): + if 'archiving_preferences' in gajim.interface.instances[self.account]: + del gajim.interface.instances[self.account]['archiving_preferences'] + + class PrivacyListWindow: """ Window that is used for creating NEW or EDITING already there privacy lists diff --git a/src/gui_interface.py b/src/gui_interface.py index 4059d25b4..ac235fa0a 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1573,6 +1573,11 @@ class Interface: if gajim.connections[account].pep_supported and dbus_support.supported \ and gajim.config.get_per('accounts', account, 'publish_location'): location_listener.enable() + # Start merging logs from server + gajim.connections[account].request_modifications_page( + gajim.config.get_per('accounts', account, 'last_archiving_time')) + gajim.config.set_per('accounts', account, 'last_archiving_time', + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())) def handle_event_metacontacts(self, account, tags_list): gajim.contacts.define_metacontacts(account, tags_list) @@ -2064,6 +2069,18 @@ class Interface: if pm_ctrl and hasattr(pm_ctrl, "update_contact"): pm_ctrl.update_contact() + def handle_event_archiving_changed(self, account, data): + # ('ARCHIVING_CHANGED', account, (type, value) + if 'archiving_preferences' in self.instances[account]: + self.instances[account]['archiving_preferences'].archiving_changed( + data) + + def handle_event_archiving_error(self, account, data): + # ('ARCHIVING_CHANGED', account, (error_msg,)) + if 'archiving_preferences' in self.instances[account]: + self.instances[account]['archiving_preferences'].archiving_error( + data) + def create_core_handlers_list(self): self.handlers = { 'ROSTER': [self.handle_event_roster], @@ -2147,6 +2164,8 @@ class Interface: 'JINGLE_ERROR': [self.handle_event_jingle_error], 'PEP_RECEIVED': [self.handle_event_pep_received], 'CAPS_RECEIVED': [self.handle_event_caps_received], + 'ARCHIVING_CHANGED': [self.handle_event_archiving_changed], + 'ARCHIVING_ERROR': [self.handle_event_archiving_error], 'gmail-notify': [self.handle_event_gmail_notify], 'http-auth-received': [self.handle_event_http_auth], 'last-result-received': [self.handle_event_last_status_time], diff --git a/src/message_control.py b/src/message_control.py index 700ac9485..e74de2ef1 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -30,6 +30,7 @@ import gtkgui_helpers from common import gajim from common import helpers +from common.stanza_session import EncryptedStanzaSession, ArchivingStanzaSession # Derived types MUST register their type IDs here if custom behavor is required TYPE_CHAT = 'chat' @@ -200,11 +201,18 @@ class MessageControl(object): if self.resource: jid += '/' + self.resource - crypto_changed = bool(session and session.enable_encryption) != \ - bool(oldsession and oldsession.enable_encryption) + crypto_changed = bool(session and isinstance(session, + EncryptedStanzaSession) and session.enable_encryption) != \ + bool(oldsession and isinstance(oldsession, + EncryptedStanzaSession) and oldsession.enable_encryption) - if crypto_changed: - self.print_esession_details() + archiving_changed = bool(session and isinstance(session, + ArchivingStanzaSession) and session.archiving) != \ + bool(oldsession and isinstance(oldsession, + ArchivingStanzaSession) and oldsession.archiving) + + if crypto_changed or archiving_changed: + self.print_session_details() def send_message(self, message, keyID='', type_='chat', chatstate=None, msg_id=None, composing_xep=None, resource=None, user_nick=None, diff --git a/src/roster_window.py b/src/roster_window.py index b4848ec70..2a81bf507 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2447,6 +2447,14 @@ class RosterWindow: gajim.interface.instances[account]['xml_console'] = \ dialogs.XMLConsoleWindow(account) + def on_archiving_preferences_menuitem_activate(self, widget, account): + if 'archiving_preferences' in gajim.interface.instances[account]: + gajim.interface.instances[account]['archiving_preferences'].window.\ + present() + else: + gajim.interface.instances[account]['archiving_preferences'] = \ + dialogs.ArchivingPreferencesWindow(account) + def on_privacy_lists_menuitem_activate(self, widget, account): if 'privacy_lists' in gajim.interface.instances[account]: gajim.interface.instances[account]['privacy_lists'].window.present() @@ -5710,6 +5718,8 @@ class RosterWindow: advanced_menuitem_menu = xml.get_object('advanced_menuitem_menu') xml_console_menuitem = xml.get_object('xml_console_menuitem') + archiving_preferences_menuitem = xml.get_object( + 'archiving_preferences_menuitem') privacy_lists_menuitem = xml.get_object('privacy_lists_menuitem') administrator_menuitem = xml.get_object('administrator_menuitem') send_server_message_menuitem = xml.get_object( @@ -5721,12 +5731,17 @@ class RosterWindow: xml_console_menuitem.connect('activate', self.on_xml_console_menuitem_activate, account) - if gajim.connections[account] and gajim.connections[account].\ - privacy_rules_supported: - privacy_lists_menuitem.connect('activate', - self.on_privacy_lists_menuitem_activate, account) - else: - privacy_lists_menuitem.set_sensitive(False) + if gajim.connections[account]: + if gajim.connections[account].privacy_rules_supported: + privacy_lists_menuitem.connect('activate', + self.on_privacy_lists_menuitem_activate, account) + else: + privacy_lists_menuitem.set_sensitive(False) + if gajim.connections[account].archive_pref_supported: + archiving_preferences_menuitem.connect('activate', + self.on_archiving_preferences_menuitem_activate, account) + else: + archiving_preferences_menuitem.set_sensitive(False) if gajim.connections[account].is_zeroconf: administrator_menuitem.set_sensitive(False) diff --git a/src/session.py b/src/session.py index 2396f9883..30693ed1d 100644 --- a/src/session.py +++ b/src/session.py @@ -413,31 +413,40 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): # encrypted session states. these are described in stanza_session.py try: - # bob responds if form.getType() == 'form' and 'security' in form.asDict(): - # we don't support 3-message negotiation as the responder - if 'dhkeys' in form.asDict(): - self.fail_bad_negotiation('3 message negotiation not supported ' - 'when responding', ('dhkeys',)) - return + security_options = [x[1] for x in form.getField('security').\ + getOptions()] + if security_options == ['none']: + self.respond_archiving(form) + else: + # bob responds - negotiated, not_acceptable, ask_user = self.verify_options_bob(form) + # we don't support 3-message negotiation as the responder + if 'dhkeys' in form.asDict(): + self.fail_bad_negotiation('3 message negotiation not ' + 'supported when responding', ('dhkeys',)) + return - if ask_user: - def accept_nondefault_options(is_checked): - self.dialog.destroy() - negotiated.update(ask_user) - self.respond_e2e_bob(form, negotiated, not_acceptable) + negotiated, not_acceptable, ask_user = \ + self.verify_options_bob(form) - def reject_nondefault_options(): - self.dialog.destroy() - for key in ask_user.keys(): - not_acceptable.append(key) - self.respond_e2e_bob(form, negotiated, not_acceptable) + if ask_user: + def accept_nondefault_options(is_checked): + self.dialog.destroy() + negotiated.update(ask_user) + self.respond_e2e_bob(form, negotiated, + not_acceptable) - self.dialog = dialogs.YesNoDialog(_('Confirm these session ' - 'options'), - _('''The remote client wants to negotiate a session with these features: + def reject_nondefault_options(): + self.dialog.destroy() + for key in ask_user.keys(): + not_acceptable.append(key) + self.respond_e2e_bob(form, negotiated, + not_acceptable) + + self.dialog = dialogs.YesNoDialog(_('Confirm these ' + 'session options'), _('''The remote client wants ' + 'to negotiate an session with these features: %s @@ -445,8 +454,17 @@ Are these options acceptable?''') % (negotiation.describe_features( ask_user)), on_response_yes=accept_nondefault_options, on_response_no=reject_nondefault_options) - else: - self.respond_e2e_bob(form, negotiated, not_acceptable) + else: + self.respond_e2e_bob(form, negotiated, not_acceptable) + + return + + elif self.status == 'requested-archiving' and form.getType() == \ + 'submit': + try: + self.archiving_accepted(form) + except exceptions.NegotiationError, details: + self.fail_bad_negotiation(details) return @@ -482,6 +500,14 @@ Are these options acceptable?''') % (negotiation.describe_features( except exceptions.NegotiationError, details: self.fail_bad_negotiation(details) + return + elif self.status == 'responded-archiving' and form.getType() == \ + 'result': + try: + self.we_accept_archiving(form) + except exceptions.NegotiationError, details: + self.fail_bad_negotiation(details) + return elif self.status == 'responded-e2e' and form.getType() == 'result': try: