diff --git a/data/style/gajim.css b/data/style/gajim.css index d0b564ab9..625b4db57 100644 --- a/data/style/gajim.css +++ b/data/style/gajim.css @@ -12,3 +12,12 @@ popover#EmoticonPopover flowboxchild > label { font-size: 24px; } popover#EmoticonPopover notebook label { font-size: 24px; } popover#EmoticonPopover flowbox { padding-left: 5px; padding-right: 6px; } popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; } + +/* HistorySyncAssistant */ +#HistorySyncAssistant list { border: 1px solid; border-color: @borders; } +#HistorySyncAssistant progressbar text { color: #000; font-size: 18px; padding: 10px;} +#HistorySyncAssistant list > row { padding: 10px 30px 10px 30px; } +#HistorySyncAssistant list > row > label { color: @insensitive_fg_color } +#HistorySyncAssistant list > row.activatable > label { color: @theme_text_color; } +#HistorySyncAssistant list > row.activatable:selected > label { color: @theme_selected_fg_color; } +#FinishedLabel { font-size: 14px; font-weight: bold } diff --git a/gajim/app_actions.py b/gajim/app_actions.py index 9095b99f6..f1f8a1fa5 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -31,6 +31,7 @@ import shortcuts_window import plugins.gui import history_window import disco +from history_sync import HistorySyncAssistant class AppActions(): @@ -150,6 +151,14 @@ class AppActions(): gajim.interface.instances[account]['archiving_preferences'] = \ dialogs.ArchivingPreferencesWindow(account) + def on_history_sync(self, action, param): + account = param.get_string() + if 'history_sync' in gajim.interface.instances[account]: + gajim.interface.instances[account]['history_sync'].present() + else: + gajim.interface.instances[account]['history_sync'] = \ + HistorySyncAssistant(account, gajim.interface.roster.window) + def on_privacy_lists(self, action, param): account = param.get_string() if 'privacy_lists' in gajim.interface.instances[account]: diff --git a/gajim/common/config.py b/gajim/common/config.py index be3ee7926..25320b4dd 100644 --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -419,6 +419,7 @@ class Config: 'oauth2_redirect_url': [ opt_str, 'https%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for OAuth 2.0 authentication.')], 'opened_chat_controls': [opt_str, '', _('Space separated list of JIDs for which we want to re-open a chat window on next startup.')], 'last_mam_id': [opt_str, '', _('Last MAM id we are syncronized with')], + 'mam_start_date': [opt_int, 0, _('The earliest date we requested MAM history for')], }, {}), 'statusmsg': ({ 'message': [ opt_str, '' ], diff --git a/gajim/common/connection.py b/gajim/common/connection.py index 2e0cde5bf..af2a8288b 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -753,6 +753,21 @@ class Connection(CommonConnection, ConnectionHandlers): def check_jid(self, jid): return helpers.parse_jid(jid) + def get_own_jid(self, full=False): + """ + Return our own jid as JID + If full = True, this raises an exception if we cant provide + the full JID + """ + if self.connection: + full_jid = self.connection._registered_name + return nbxmpp.JID(full_jid) + else: + if full: + raise exceptions.GajimGeneralException( + 'We are not connected, full JID unknown.') + return nbxmpp.JID(gajim.get_jid_from_account(self.name)) + def reconnect(self): # Do not try to reco while we are already trying self.time_to_reconnect = None diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index 15e31c74e..785e4c408 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -18,6 +18,9 @@ ## along with Gajim. If not, see . ## +# pylint: disable=no-init +# pylint: disable=attribute-defined-outside-init + from calendar import timegm import datetime import hashlib @@ -41,6 +44,7 @@ from common.logger import LOG_DB_PATH from common.pep import SUPPORTED_PERSONAL_USER_EVENTS from common.jingle_transport import JingleTransportSocks5 from common.file_props import FilesProp +from common.nec import NetworkEvent if gajim.HAVE_PYOPENSSL: import OpenSSL.crypto @@ -1034,43 +1038,70 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent): class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): name = 'mam-message-received' - base_network_events = [] + base_network_events = ['raw-mam-message-received'] - def init(self): + def __init__(self, name, base_event): + ''' + Pre-Generated attributes on self: + + :conn: Connection instance + :stanza: Complete stanza Node + :forwarded: Forwarded Node + :result: Result Node + ''' + self._set_base_event_vars_as_attributes(base_event) self.additional_data = {} self.encrypted = False - + self.groupchat = False + def generate(self): - if not self.stanza: - return - account = self.conn.name - self.msg_ = self.stanza.getTag('message') - # use timestamp of archived message, if available and archive timestamp otherwise - delay = self.stanza.getTag('delay', namespace=nbxmpp.NS_DELAY2) - delay2 = self.msg_.getTag('delay', namespace=nbxmpp.NS_DELAY2) - if delay2: - delay = delay2 - if not delay: - return - tim = delay.getAttr('stamp') - tim = helpers.datetime_tuple(tim) - self.tim = timegm(tim) - to_ = self.msg_.getAttr('to') - if to_: - to_ = gajim.get_jid_without_resource(to_) - else: - to_ = gajim.get_jid_from_account(account) - frm_ = gajim.get_jid_without_resource(self.msg_.getAttr('from')) + archive_jid = self.stanza.getFrom() + own_jid = self.conn.get_own_jid() + if archive_jid and not archive_jid.bareMatch(own_jid): + # MAM Message not from our Archive + log.info('MAM message not from our user archive') + return False + + self.msg_ = self.forwarded.getTag('message') + + if self.msg_.getType() == 'groupchat': + log.info('Received groupchat message from user archive') + return False + self.msgtxt = self.msg_.getTagData('body') - if to_ == gajim.get_jid_from_account(account): - self.with_ = frm_ + self.stanza_id = self.msg_.getID() + self.mam_id = self.result.getID() + self.query_id = self.result.getAttr('queryid') + + # Use timestamp provided by archive, + # Fallback: Use timestamp provided by user and issue a warning + delay = self.forwarded.getTag('delay', namespace=nbxmpp.NS_DELAY2) + if not delay: + log.warning('No timestamp on archive Message, try fallback') + delay = self.msg_.getTag('delay', namespace=nbxmpp.NS_DELAY2) + if not delay: + log.error('Received MAM message without timestamp') + return + + self.timestamp = helpers.parse_delay(delay) + + frm = self.msg_.getFrom() + to = self.msg_.getTo() + + if not to or to.bareMatch(own_jid): + self.with_ = str(frm) self.direction = 'from' - self.resource = gajim.get_resource_from_jid( - self.msg_.getAttr('from')) + self.resource = frm.getResource() else: - self.with_ = to_ + self.with_ = str(to) self.direction = 'to' - self.resource = gajim.get_resource_from_jid(self.msg_.getAttr('to')) + self.resource = to.getResource() + + log_message = \ + 'received: mam-message: ' \ + 'stanza id: {:15} - mam id: {:15} - query id: {}'.format( + self.stanza_id, self.mam_id, self.query_id) + log.debug(log_message) return True class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): @@ -1084,14 +1115,14 @@ class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): self.additional_data = self.msg_obj.additional_data self.with_ = self.msg_obj.with_ self.direction = self.msg_obj.direction - self.tim = self.msg_obj.tim + self.timestamp = self.msg_obj.timestamp res = self.msg_obj.resource self.msgtxt = self.msg_obj.msgtxt is_pm = gajim.logger.jid_is_room_jid(self.with_) if msg_.getAttr('type') == 'groupchat': if is_pm == False: log.warn('JID %s is marked as normal contact in database ' - 'but we got a groupchat message from it.') + 'but we got a groupchat message from it.', self.with_) return if is_pm == None: gajim.logger.get_jid_id(self.with_, 'ROOM') @@ -1102,12 +1133,12 @@ class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): server = gajim.get_server_from_jid(self.with_) if server not in self.conn.mam_awaiting_disco_result: self.conn.mam_awaiting_disco_result[server] = [ - [self.with_, self.direction, self.tim, self.msgtxt, + [self.with_, self.direction, self.timestamp, self.msgtxt, res]] self.conn.discoverInfo(server) else: self.conn.mam_awaiting_disco_result[server].append( - [self.with_, self.direction, self.tim, self.msgtxt, + [self.with_, self.direction, self.timestamp, self.msgtxt, res]) return return True @@ -1128,8 +1159,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): self.encrypted = False account = self.conn.name - our_full_jid = gajim.get_jid_from_account(account, full=True) - if self.stanza.getFrom() == our_full_jid: + if self.stanza.getFrom() == self.conn.get_own_jid(full=True): # Drop messages sent from our own full jid # It can happen that when we sent message to our own bare jid # that the server routes that message back to us @@ -1216,8 +1246,16 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): nbxmpp.NS_MAM_1, nbxmpp.NS_MAM_2): forwarded = result.getTag('forwarded', namespace=nbxmpp.NS_FORWARD) - gajim.nec.push_incoming_event(MamMessageReceivedEvent(None, - conn=self.conn, stanza=forwarded)) + if not forwarded: + log.warning('Invalid MAM Message: no forwarded child') + return + + gajim.nec.push_incoming_event( + NetworkEvent('raw-mam-message-received', + conn=self.conn, + stanza=self.stanza, + forwarded=forwarded, + result=result)) return # Mediated invitation? @@ -1804,8 +1842,8 @@ class ArchivingFinishedReceivedEvent(nec.NetworkIncomingEvent): if self.type_ != 'result' or not self.fin: return - self.queryid = self.fin.getAttr('queryid') - if not self.queryid: + self.query_id = self.fin.getAttr('queryid') + if not self.query_id: return return True @@ -1822,8 +1860,8 @@ class ArchivingFinishedLegacyReceivedEvent(nec.NetworkIncomingEvent): if not self.fin: return - self.queryid = self.fin.getAttr('queryid') - if not self.queryid: + self.query_id = self.fin.getAttr('queryid') + if not self.query_id: return return True diff --git a/gajim/common/gajim.py b/gajim/common/gajim.py index bbe71a15a..970fefa47 100644 --- a/gajim/common/gajim.py +++ b/gajim/common/gajim.py @@ -420,16 +420,13 @@ def jid_is_transport(jid): return True return False -def get_jid_from_account(account_name, full=False): +def get_jid_from_account(account_name): """ Return the jid we use in the given account """ name = config.get_per('accounts', account_name, 'name') hostname = config.get_per('accounts', account_name, 'hostname') jid = name + '@' + hostname - if full: - resource = connections[account_name].server_resource - jid += '/' + resource return jid def get_our_jids(): diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index b1bbbee72..00a4e9dd5 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -43,11 +43,13 @@ import shlex from common import caps_cache import socket import time -import datetime +from datetime import datetime, timedelta from encodings.punycode import punycode_encode from string import Template +import nbxmpp + from common.i18n import Q_ from common.i18n import ngettext @@ -589,16 +591,45 @@ def datetime_tuple(timestamp): tim = time.strptime(date + 'T' + tim, '%Y%m%dT%H:%M:%S') if zone: zone = zone.replace(':', '') - tim = datetime.datetime.fromtimestamp(time.mktime(tim)) + tim = datetime.fromtimestamp(time.mktime(tim)) if len(zone) > 2: zone = time.strptime(zone, '%H%M') else: zone = time.strptime(zone, '%H') - zone = datetime.timedelta(hours=zone.tm_hour, minutes=zone.tm_min) + zone = timedelta(hours=zone.tm_hour, minutes=zone.tm_min) tim += zone * sign tim = tim.timetuple() return tim +def parse_delay(timestamp): + ''' + Parse a timestamp + https://xmpp.org/extensions/xep-0203.html + Note: Not all delay tags should be parsed with this method + see https://xmpp.org/extensions/xep-0082.html for more information + + :param timestamp: a XEP-0203 fomated timestring string or a delay Node + + Examples: + '2017-11-05T01:41:20Z' + '2017-11-05T01:41:20.123Z' + + return epoch UTC timestamp + ''' + if isinstance(timestamp, nbxmpp.protocol.Node): + timestamp = timestamp.getAttr('stamp') + timestamp += '+0000' + try: + datetime_ = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ%z') + except ValueError: + try: + datetime_ = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ%z') + except ValueError: + log.error('Could not parse delay timestamp: %s', timestamp) + raise + return datetime_.timestamp() + + from common import gajim if gajim.HAVE_PYCURL: import pycurl @@ -1280,7 +1311,6 @@ def prepare_and_validate_gpg_keyID(account, jid, keyID): return keyID def update_optional_features(account = None): - import nbxmpp if account: accounts = [account] else: diff --git a/gajim/common/message_archiving.py b/gajim/common/message_archiving.py index 45c619207..a3cfaeef8 100644 --- a/gajim/common/message_archiving.py +++ b/gajim/common/message_archiving.py @@ -22,10 +22,11 @@ import nbxmpp from common import gajim from common import ged from common import helpers -from common.connection_handlers_events import ArchivingReceivedEvent +import common.connection_handlers_events as ev from calendar import timegm from time import localtime +from datetime import datetime, timedelta import logging log = logging.getLogger('gajim.c.message_archiving') @@ -33,7 +34,6 @@ 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' -MAM_RESULTS_ARRIVED = 'mam_results_arrived' class ConnectionArchive: def __init__(self): @@ -46,6 +46,9 @@ class ConnectionArchive313(ConnectionArchive): self.archiving_313_supported = False self.mam_awaiting_disco_result = {} self.iq_answer = [] + self.mam_query_date = None + self.mam_query_id = None + gajim.nec.register_incoming_event(ev.MamMessageReceivedEvent) gajim.ged.register_event_handler('archiving-finished-legacy', ged.CORE, self._nec_result_finished) gajim.ged.register_event_handler('archiving-finished', ged.CORE, @@ -108,48 +111,78 @@ class ConnectionArchive313(ConnectionArchive): if obj.conn.name != self.name: return - if obj.queryid not in self.awaiting_answers: + if obj.query_id != self.mam_query_id: return - if self.awaiting_answers[obj.queryid][0] == MAM_RESULTS_ARRIVED: - set_ = obj.fin.getTag('set', namespace=nbxmpp.NS_RSM) - if set_: - last = set_.getTagData('last') - if last: - gajim.config.set_per('accounts', self.name, 'last_mam_id', last) - complete = obj.fin.getAttr('complete') - if complete != 'true': - self.request_archive(after=last) - del self.awaiting_answers[obj.queryid] + set_ = obj.fin.getTag('set', namespace=nbxmpp.NS_RSM) + if set_: + last = set_.getTagData('last') + complete = obj.fin.getAttr('complete') + if last: + gajim.config.set_per('accounts', self.name, 'last_mam_id', last) + if complete != 'true': + self.request_archive(self.get_query_id(), after=last) + if complete == 'true': + self.mam_query_id = None + if self.mam_query_date: + gajim.config.set_per( + 'accounts', self.name, + 'mam_start_date', self.mam_query_date.timestamp()) + self.mam_query_date = None def _nec_mam_decrypted_message_received(self, obj): if obj.conn.name != self.name: return - gajim.logger.save_if_not_exists(obj.with_, obj.direction, obj.tim, + gajim.logger.save_if_not_exists(obj.with_, obj.direction, obj.timestamp, msg=obj.msgtxt, nick=obj.nick, additional_data=obj.additional_data) - def request_archive(self, start=None, end=None, with_=None, after=None, - max=30): - iq_ = nbxmpp.Iq('set') - query = iq_.addChild('query', namespace=self.archiving_namespace) - x = query.addChild(node=nbxmpp.DataForm(typ='submit')) - x.addChild(node=nbxmpp.DataField(typ='hidden', name='FORM_TYPE', value=self.archiving_namespace)) + def get_query_id(self): + self.mam_query_id = self.connection.getAnID() + return self.mam_query_id + + def request_archive_on_signin(self): + mam_id = gajim.config.get_per('accounts', self.name, 'last_mam_id') + query_id = self.get_query_id() + if mam_id: + self.request_archive(query_id, after=mam_id) + else: + # First Start, we request the last week + self.mam_query_date = datetime.utcnow() - timedelta(days=7) + log.info('First start: query archive start: %s', self.mam_query_date) + self.request_archive(query_id, start=self.mam_query_date) + + def request_archive(self, query_id, start=None, end=None, with_=None, + after=None, max_=30): + namespace = self.archiving_namespace + iq = nbxmpp.Iq('set') + query = iq.addChild('query', namespace=namespace) + form = query.addChild(node=nbxmpp.DataForm(typ='submit')) + field = nbxmpp.DataField(typ='hidden', + name='FORM_TYPE', + value=namespace) + form.addChild(node=field) if start: - x.addChild(node=nbxmpp.DataField(typ='text-single', name='start', value=start)) + field = nbxmpp.DataField(typ='text-single', + name='start', + value=start.strftime('%Y-%m-%dT%H:%M:%SZ')) + form.addChild(node=field) if end: - x.addChild(node=nbxmpp.DataField(typ='text-single', name='end', value=end)) + field = nbxmpp.DataField(typ='text-single', + name='end', + value=end.strftime('%Y-%m-%dT%H:%M:%SZ')) + form.addChild(node=field) if with_: - x.addChild(node=nbxmpp.DataField(typ='jid-single', name='with', value=with_)) + field = nbxmpp.DataField(typ='jid-single', name='with', value=with_) + form.addChild(node=field) + set_ = query.setTag('set', namespace=nbxmpp.NS_RSM) - set_.setTagData('max', max) + set_.setTagData('max', max_) if after: set_.setTagData('after', after) - queryid_ = self.connection.getAnID() - query.setAttr('queryid', queryid_) + query.setAttr('queryid', query_id) id_ = self.connection.getAnID() - iq_.setID(id_) - self.awaiting_answers[queryid_] = (MAM_RESULTS_ARRIVED, ) - self.connection.send(iq_) + iq.setID(id_) + self.connection.send(iq) def request_archive_preferences(self): if not gajim.account_is_connected(self.name): @@ -367,8 +400,7 @@ class ConnectionArchive136(ConnectionArchive): return ['may'] def _ArchiveCB(self, con, iq_obj): - log.debug('_ArchiveCB %s' % iq_obj.getType()) - gajim.nec.push_incoming_event(ArchivingReceivedEvent(None, conn=self, + gajim.nec.push_incoming_event(ev.ArchivingReceivedEvent(None, conn=self, stanza=iq_obj)) raise nbxmpp.NodeProcessed diff --git a/gajim/common/nec.py b/gajim/common/nec.py index b699085c1..af8465870 100644 --- a/gajim/common/nec.py +++ b/gajim/common/nec.py @@ -154,8 +154,13 @@ class NetworkEvent(object): def _set_kwargs_as_attributes(self, **kwargs): for k, v in kwargs.items(): - setattr(self, k, v) + if k not in ('name', 'base_network_events'): + setattr(self, k, v) + def _set_base_event_vars_as_attributes(self, event): + for k, v in vars(event).items(): + if k not in ('name', 'base_network_events'): + setattr(self, k, v) class NetworkIncomingEvent(NetworkEvent): base_network_events = [] diff --git a/gajim/gajim.py b/gajim/gajim.py index fb6916c99..0b1ed2e3b 100644 --- a/gajim/gajim.py +++ b/gajim/gajim.py @@ -326,6 +326,7 @@ class GajimApplication(Gtk.Application): ('-profile', action.on_profile, 'feature', 's'), ('-xml-console', action.on_xml_console, 'always', 's'), ('-archive', action.on_archiving_preferences, 'feature', 's'), + ('-sync-history', action.on_history_sync, 'online', 's'), ('-privacylists', action.on_privacy_lists, 'feature', 's'), ('-send-server-message', action.on_send_server_message, 'online', 's'), diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index 8dbfa900e..6d56d92b4 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -1187,11 +1187,7 @@ class Interface: time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())) if obj.conn.archiving_313_supported and gajim.config.get_per('accounts', account, 'sync_logs_with_server'): - mam_id = gajim.config.get_per('accounts', account, 'last_mam_id') - if mam_id: - obj.conn.request_archive(after=mam_id) - else: - obj.conn.request_archive(start='2013-02-24T03:51:42Z') + obj.conn.request_archive_on_signin() invisible_show = gajim.SHOW_LIST.index('invisible') # We cannot join rooms if we are invisible diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py index d96a6ad18..d56781099 100644 --- a/gajim/gui_menu_builder.py +++ b/gajim/gui_menu_builder.py @@ -671,6 +671,7 @@ def get_account_menu(account): ('-start-single-chat', _('Send Single Message...')), ('Advanced', [ ('-archive', _('Archiving Preferences')), + ('-sync-history', _('Synchronise History')), ('-privacylists', _('Privacy Lists')), ('-xml-console', _('XML Console')) ]), diff --git a/gajim/history_sync.py b/gajim/history_sync.py new file mode 100644 index 000000000..43fd001ec --- /dev/null +++ b/gajim/history_sync.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Philipp Hörist +# +# 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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 logging +from enum import IntEnum +from datetime import datetime, timedelta, timezone + +import nbxmpp +from gi.repository import Gtk, GLib + +from common import gajim +from common import ged +from gtkgui_helpers import get_icon_pixmap + +log = logging.getLogger('gajim.c.message_archiving') + +class Pages(IntEnum): + TIME = 0 + SYNC = 1 + SUMMARY = 2 + +class ArchiveState(IntEnum): + NEVER = 0 + ALL = 1 + +class HistorySyncAssistant(Gtk.Assistant): + def __init__(self, account, parent): + Gtk.Assistant.__init__(self) + self.set_title(_('Synchronise History')) + self.set_resizable(False) + self.set_default_size(300, -1) + self.set_name('HistorySyncAssistant') + self.set_transient_for(parent) + self.account = account + self.con = gajim.connections[self.account] + self.timedelta = None + self.now = datetime.utcnow() + self.query_id = None + self.count_query_id = None + self.start = None + self.end = None + self.next = None + self.hide_buttons() + + mam_start = gajim.config.get_per('accounts', account, 'mam_start_date') + if not mam_start or mam_start == ArchiveState.NEVER: + self.current_start = self.now + elif mam_start == ArchiveState.ALL: + self.current_start = datetime.utcfromtimestamp(0) + else: + self.current_start = datetime.fromtimestamp(mam_start) + + self.select_time = SelectTimePage(self) + self.append_page(self.select_time) + self.set_page_type(self.select_time, Gtk.AssistantPageType.INTRO) + + self.download_history = DownloadHistoryPage(self) + self.append_page(self.download_history) + self.set_page_type(self.download_history, Gtk.AssistantPageType.PROGRESS) + self.set_page_complete(self.download_history, True) + + self.summary = SummaryPage(self) + self.append_page(self.summary) + self.set_page_type(self.summary, Gtk.AssistantPageType.SUMMARY) + self.set_page_complete(self.summary, True) + + gajim.ged.register_event_handler('archiving-finished', + ged.PRECORE, + self._nec_archiving_finished) + gajim.ged.register_event_handler('mam-decrypted-message-received', + ged.PRECORE, + self._nec_mam_message_received) + + self.connect('prepare', self.on_page_change) + self.connect('destroy', self.on_destroy) + self.connect("cancel", self.on_close_clicked) + self.connect("close", self.on_close_clicked) + + if mam_start == ArchiveState.ALL: + self.set_current_page(Pages.SUMMARY) + self.summary.nothing_to_do() + + if self.con.mam_query_id: + self.set_current_page(Pages.SUMMARY) + self.summary.query_already_running() + + self.show_all() + + def hide_buttons(self): + ''' + Hide some of the standard buttons that are included in Gtk.Assistant + ''' + if self.get_property('use-header-bar'): + action_area = self.get_children()[1] + else: + box = self.get_children()[0] + content_box = box.get_children()[1] + action_area = content_box.get_children()[1] + for button in action_area.get_children(): + button_name = Gtk.Buildable.get_name(button) + if button_name == 'back': + button.connect('show', self._on_show_button) + elif button_name == 'forward': + self.next = button + button.connect('show', self._on_show_button) + + @staticmethod + def _on_show_button(button): + button.hide() + + def prepare_query(self): + if self.timedelta: + self.start = self.now - self.timedelta + self.end = self.current_start + + log.info('config: get mam_start_date: %s', self.current_start) + log.info('now: %s', self.now) + log.info('start: %s', self.start) + log.info('end: %s', self.end) + + self.query_count() + + def query_count(self): + self.count_query_id = self.con.connection.getAnID() + self.con.request_archive(self.count_query_id, + start=self.start, + end=self.end, + max_=0) + + def query_messages(self, last=None): + self.query_id = self.con.connection.getAnID() + self.con.request_archive(self.query_id, + start=self.start, + end=self.end, + after=last, + max_=30) + + def on_row_selected(self, listbox, row): + self.timedelta = row.get_child().get_delta() + if row: + self.set_page_complete(self.select_time, True) + else: + self.set_page_complete(self.select_time, False) + + def on_page_change(self, assistant, page): + if page == self.download_history: + self.next.hide() + self.prepare_query() + + def on_destroy(self, *args): + gajim.ged.remove_event_handler('archiving-finished', + ged.PRECORE, + self._nec_archiving_finished) + gajim.ged.remove_event_handler('mam-decrypted-message-received', + ged.PRECORE, + self._nec_mam_message_received) + del gajim.interface.instances[self.account]['history_sync'] + + def on_close_clicked(self, *args): + self.destroy() + + def _nec_mam_message_received(self, obj): + if obj.conn.name != self.account: + return + + if obj.msg_obj.query_id != self.query_id: + return + + log.debug('received message') + GLib.idle_add(self.download_history.set_fraction) + + def _nec_archiving_finished(self, obj): + if obj.conn.name != self.account: + return + + if obj.query_id not in (self.query_id, self.count_query_id): + return + + set_ = obj.fin.getTag('set', namespace=nbxmpp.NS_RSM) + if not set_: + log.error('invalid result') + log.error(obj.fin) + return + + if obj.query_id == self.count_query_id: + count = set_.getTagData('count') + log.info('message count received: %s', count) + if count: + self.download_history.count = int(count) + self.query_messages() + return + + if obj.query_id == self.query_id: + last = set_.getTagData('last') + complete = obj.fin.getAttr('complete') + if not last and complete != 'true': + log.error('invalid result') + log.error(obj.fin) + return + + if complete != 'true': + self.query_messages(last) + else: + log.info('query finished') + GLib.idle_add(self.download_history.finished) + if self.start: + timestamp = self.start.timestamp() + else: + timestamp = ArchiveState.ALL + gajim.config.set_per('accounts', self.account, + 'mam_start_date', timestamp) + log.debug('config: set mam_start_date: %s', timestamp) + self.set_current_page(Pages.SUMMARY) + self.summary.finished() + + +class SelectTimePage(Gtk.Box): + def __init__(self, assistant): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(18) + self.assistant = assistant + label = Gtk.Label(label=_('How far back do you want to go?')) + + listbox = Gtk.ListBox() + listbox.set_hexpand(False) + listbox.set_halign(Gtk.Align.CENTER) + listbox.add(TimeOption(_('One Month'), 1)) + listbox.add(TimeOption(_('Three Months'), 3)) + listbox.add(TimeOption(_('One Year'), 12)) + listbox.add(TimeOption(_('Everything'))) + listbox.connect('row-selected', assistant.on_row_selected) + + for row in listbox.get_children(): + option = row.get_child() + if not option.get_delta(): + continue + if assistant.now - option.get_delta() > assistant.current_start: + row.set_activatable(False) + row.set_selectable(False) + + self.pack_start(label, True, True, 0) + self.pack_start(listbox, False, False, 0) + +class DownloadHistoryPage(Gtk.Box): + def __init__(self, assistant): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(18) + self.assistant = assistant + self.count = 0 + self.received = 0 + + pix = get_icon_pixmap('folder-download-symbolic', size=64) + image = Gtk.Image() + image.set_from_pixbuf(pix) + + self.progress = Gtk.ProgressBar() + self.progress.set_show_text(True) + self.progress.set_text(_('Connecting...')) + self.progress.set_pulse_step(0.1) + self.progress.set_vexpand(True) + self.progress.set_valign(Gtk.Align.CENTER) + + self.pack_start(image, False, False, 0) + self.pack_start(self.progress, False, False, 0) + + def set_fraction(self): + self.received += 1 + if self.count: + self.progress.set_fraction(self.received / self.count) + self.progress.set_text(_('%s of %s' % (self.received, self.count))) + else: + self.progress.pulse() + self.progress.set_text(_('Downloaded %s Messages' % self.received)) + + def finished(self): + self.progress.set_fraction(1) + +class SummaryPage(Gtk.Box): + def __init__(self, assistant): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(18) + self.assistant = assistant + + self.label = Gtk.Label() + self.label.set_name('FinishedLabel') + self.label.set_valign(Gtk.Align.CENTER) + + self.pack_start(self.label, True, True, 0) + + def finished(self): + received = self.assistant.download_history.received + finished = _(''' + Finshed synchronising your History. + {received} Messages downloaded. + '''.format(received=received)) + self.label.set_text(finished) + + def nothing_to_do(self): + nothing_to_do = _(''' + Gajim is fully synchronised + with the Archive. + ''') + self.label.set_text(nothing_to_do) + + def query_already_running(self): + already_running = _(''' + There is already a synchronisation in + progress. Please try later. + ''') + self.label.set_text(already_running) + +class TimeOption(Gtk.Label): + def __init__(self, label, months=None): + super().__init__(label=label) + self.date = months + if months: + self.date = timedelta(days=30*months) + + def get_delta(self): + return self.date