Philipp Hörist pushed to branch master at gajim / gajim
Commits: e548a892 by Philipp Hörist at 2017-07-25T21:00:20+02:00 Add pylint hints
- - - - - 85d220c8 by Philipp Hörist at 2017-07-25T21:00:20+02:00 Add Synchronise History Dialog
- On first contact with the MAM Archive only request 7 days - To sync the rest of the archive the new Dialog can be used.
- - - - - 06d890ee by Philipp Hörist at 2017-07-25T21:00:20+02:00 Add more convenient get_own_jid() method
- - - - - 89367a83 by Philipp Hörist at 2017-07-25T21:00:20+02:00 Add simple parse_delay() method
The XEP is pretty strict on Delay tags so we can make a simple method.
- - - - - aefb5711 by Philipp Hörist at 2017-07-25T21:00:20+02:00 Add new NetworkEvents method
This lets us attach all attributes of the base event to the new event. Often Events trigger other Events. When that happens we often want to keep all attr from the previous Event, and just continue under a new Event. Until now all attr had to be pulled out of `self.base_event` again.
- - - - - 33a51f31 by Philipp Hörist at 2017-07-25T21:01:18+02:00 Preparation for MUC Archive querys
- Refactor and clean up code around MamMessageReceivedEvent - Goal is to add a GcMamMessageReceivedEvent later on - For that cause added a raw-mam-message-received base event
- - - - - ef6e7c7c by Philipp Hörist at 2017-07-25T23:44:33+02:00 Merge branch 'mamsync' into 'master'
Add Synchronise History Dialog and prepare for MUC MAM
See merge request !109 - - - - -
13 changed files:
- data/style/gajim.css - gajim/app_actions.py - gajim/common/config.py - gajim/common/connection.py - gajim/common/connection_handlers_events.py - gajim/common/gajim.py - gajim/common/helpers.py - gajim/common/message_archiving.py - gajim/common/nec.py - gajim/gajim.py - gajim/gui_interface.py - gajim/gui_menu_builder.py - + gajim/history_sync.py
Changes:
===================================== data/style/gajim.css ===================================== --- 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 }
===================================== gajim/app_actions.py ===================================== --- 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]:
===================================== gajim/common/config.py ===================================== --- 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, '' ],
===================================== gajim/common/connection.py ===================================== --- 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
===================================== gajim/common/connection_handlers_events.py ===================================== --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -18,6 +18,9 @@ ## along with Gajim. If not, see http://www.gnu.org/licenses/. ##
+# 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 = [] - - def init(self): + base_network_events = ['raw-mam-message-received'] + + 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 + 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') + 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 - 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')) - self.msgtxt = self.msg_.getTagData('body') - if to_ == gajim.get_jid_from_account(account): - self.with_ = frm_ + + 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
===================================== gajim/common/gajim.py ===================================== --- 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():
===================================== gajim/common/helpers.py ===================================== --- 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:
===================================== gajim/common/message_archiving.py ===================================== --- 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
===================================== gajim/common/nec.py ===================================== --- 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 = []
===================================== gajim/gajim.py ===================================== --- 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'),
===================================== gajim/gui_interface.py ===================================== --- 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
===================================== gajim/gui_menu_builder.py ===================================== --- 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')) ]),
===================================== gajim/history_sync.py ===================================== --- /dev/null +++ b/gajim/history_sync.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com> +# +# 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 http://www.gnu.org/licenses/. + +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
View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/78addad06785beac2b797c700d81bfba2e...