From e548a89269b1937f32c1f8a01d22739b24aadcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Mon, 24 Jul 2017 14:39:32 +0200 Subject: [PATCH 1/6] Add pylint hints --- gajim/common/connection_handlers_events.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index 15e31c74e..b133b143a 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 From 85d220c80e0f3f69be79534794ab1f310d64d957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Fri, 21 Jul 2017 16:53:58 +0200 Subject: [PATCH 2/6] 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. --- data/style/gajim.css | 9 + gajim/app_actions.py | 9 + gajim/common/config.py | 1 + gajim/common/connection_handlers_events.py | 10 +- gajim/common/message_archiving.py | 87 ++++-- gajim/gajim.py | 1 + gajim/gui_interface.py | 6 +- gajim/gui_menu_builder.py | 1 + gajim/history_sync.py | 336 +++++++++++++++++++++ 9 files changed, 422 insertions(+), 38 deletions(-) create mode 100644 gajim/history_sync.py 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_handlers_events.py b/gajim/common/connection_handlers_events.py index b133b143a..f7ae73a0c 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -1220,7 +1220,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): nbxmpp.NS_MAM_2): forwarded = result.getTag('forwarded', namespace=nbxmpp.NS_FORWARD) gajim.nec.push_incoming_event(MamMessageReceivedEvent(None, - conn=self.conn, stanza=forwarded)) + conn=self.conn, stanza=forwarded, query_id=result.getAttr('queryid'))) return # Mediated invitation? @@ -1807,8 +1807,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 @@ -1825,8 +1825,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/message_archiving.py b/gajim/common/message_archiving.py index 45c619207..4b7203ac5 100644 --- a/gajim/common/message_archiving.py +++ b/gajim/common/message_archiving.py @@ -26,6 +26,7 @@ from common.connection_handlers_events import ArchivingReceivedEvent 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,8 @@ 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.ged.register_event_handler('archiving-finished-legacy', ged.CORE, self._nec_result_finished) gajim.ged.register_event_handler('archiving-finished', ged.CORE, @@ -108,19 +110,24 @@ 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: @@ -128,28 +135,53 @@ class ConnectionArchive313(ConnectionArchive): gajim.logger.save_if_not_exists(obj.with_, obj.direction, obj.tim, 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,7 +399,6 @@ 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, stanza=iq_obj)) raise nbxmpp.NodeProcessed 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 From 06d890eea7d3e177c6dac9024dea90daea875df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sun, 23 Jul 2017 09:55:13 +0200 Subject: [PATCH 3/6] Add more convenient get_own_jid() method --- gajim/common/connection.py | 15 +++++++++++++++ gajim/common/connection_handlers_events.py | 3 +-- gajim/common/gajim.py | 5 +---- 3 files changed, 17 insertions(+), 6 deletions(-) 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 f7ae73a0c..6e467fc44 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -1131,8 +1131,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 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(): From 89367a83ec8f224289d7f8ddd5f4d69673143756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Mon, 24 Jul 2017 02:14:25 +0200 Subject: [PATCH 4/6] Add simple parse_delay() method The XEP is pretty strict on Delay tags so we can make a simple method. --- gajim/common/helpers.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) 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: From aefb5711685b3a354146950aca0dc7c56df14b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Mon, 24 Jul 2017 23:24:39 +0200 Subject: [PATCH 5/6] 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. --- gajim/common/nec.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 = [] From 33a51f31809f13b610aa259975495d5fcf434c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Mon, 24 Jul 2017 03:41:57 +0200 Subject: [PATCH 6/6] 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 --- gajim/common/connection_handlers_events.py | 106 ++++++++++++++------- gajim/common/message_archiving.py | 7 +- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index 6e467fc44..785e4c408 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -44,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 @@ -1037,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): @@ -1087,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') @@ -1105,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 @@ -1218,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, query_id=result.getAttr('queryid'))) + 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? diff --git a/gajim/common/message_archiving.py b/gajim/common/message_archiving.py index 4b7203ac5..a3cfaeef8 100644 --- a/gajim/common/message_archiving.py +++ b/gajim/common/message_archiving.py @@ -22,7 +22,7 @@ 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 @@ -48,6 +48,7 @@ class ConnectionArchive313(ConnectionArchive): 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, @@ -132,7 +133,7 @@ class ConnectionArchive313(ConnectionArchive): 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 get_query_id(self): @@ -399,7 +400,7 @@ class ConnectionArchive136(ConnectionArchive): return ['may'] def _ArchiveCB(self, con, iq_obj): - 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