1# This file is part of Gajim. 2# 3# Gajim is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published 5# by the Free Software Foundation; version 3 only. 6# 7# Gajim is distributed in the hope that it will be useful, 8# but WITHOUT ANY WARRANTY; without even the implied warranty of 9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10# GNU General Public License for more details. 11# 12# You should have received a copy of the GNU General Public License 13# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 14 15import logging 16from enum import IntEnum 17from datetime import datetime, timedelta 18 19from gi.repository import Gtk 20from gi.repository import GLib 21 22from nbxmpp.errors import StanzaError 23from nbxmpp.errors import MalformedStanzaError 24 25from gajim.common import app 26from gajim.common import ged 27from gajim.common.i18n import _ 28from gajim.common.const import ArchiveState 29from gajim.common.helpers import event_filter 30 31from .util import load_icon 32from .util import EventHelper 33 34log = logging.getLogger('gajim.gui.history_sync') 35 36 37class Pages(IntEnum): 38 TIME = 0 39 SYNC = 1 40 SUMMARY = 2 41 42 43class HistorySyncAssistant(Gtk.Assistant, EventHelper): 44 def __init__(self, account, parent): 45 Gtk.Assistant.__init__(self) 46 EventHelper.__init__(self) 47 self.set_application(app.app) 48 self.set_position(Gtk.WindowPosition.CENTER) 49 self.set_name('HistorySyncAssistant') 50 self.set_default_size(300, -1) 51 self.set_resizable(False) 52 self.set_transient_for(parent) 53 54 self.account = account 55 self.con = app.connections[self.account] 56 self.timedelta = None 57 self.now = datetime.utcnow() 58 self.query_id = None 59 self.start = None 60 self.end = None 61 self.next = None 62 63 self._hide_buttons() 64 65 own_jid = self.con.get_own_jid().bare 66 67 mam_start = ArchiveState.NEVER 68 archive = app.storage.archive.get_archive_infos(own_jid) 69 if archive is not None and archive.oldest_mam_timestamp is not None: 70 mam_start = int(float(archive.oldest_mam_timestamp)) 71 72 if mam_start == ArchiveState.NEVER: 73 self.current_start = self.now 74 elif mam_start == ArchiveState.ALL: 75 self.current_start = datetime.utcfromtimestamp(0) 76 else: 77 self.current_start = datetime.fromtimestamp(mam_start) 78 79 self.select_time = SelectTimePage(self) 80 self.append_page(self.select_time) 81 self.set_page_type(self.select_time, Gtk.AssistantPageType.INTRO) 82 83 self.download_history = DownloadHistoryPage(self) 84 self.append_page(self.download_history) 85 self.set_page_type(self.download_history, 86 Gtk.AssistantPageType.PROGRESS) 87 self.set_page_complete(self.download_history, True) 88 89 self.summary = SummaryPage(self) 90 self.append_page(self.summary) 91 self.set_page_type(self.summary, Gtk.AssistantPageType.SUMMARY) 92 self.set_page_complete(self.summary, True) 93 94 # pylint: disable=line-too-long 95 self.register_events([ 96 ('archiving-count-received', ged.GUI1, self._received_count), 97 ('archiving-interval-finished', ged.GUI1, self._received_finished), 98 ('mam-message-received', ged.PRECORE, self._nec_mam_message_received), 99 ]) 100 # pylint: enable=line-too-long 101 102 self.connect('prepare', self._on_page_change) 103 self.connect('cancel', self._on_close_clicked) 104 self.connect('close', self._on_close_clicked) 105 106 if mam_start == ArchiveState.ALL: 107 self.set_current_page(Pages.SUMMARY) 108 self.summary.nothing_to_do() 109 110 self.show_all() 111 self.set_title(_('Synchronise History')) 112 113 def _hide_buttons(self): 114 ''' 115 Hide some of the standard buttons that are included in Gtk.Assistant 116 ''' 117 if self.get_property('use-header-bar'): 118 action_area = self.get_children()[1] 119 else: 120 box = self.get_children()[0] 121 content_box = box.get_children()[1] 122 action_area = content_box.get_children()[1] 123 for button in action_area.get_children(): 124 button_name = Gtk.Buildable.get_name(button) 125 if button_name == 'back': 126 button.connect('show', self._on_show_button) 127 elif button_name == 'forward': 128 self.next = button 129 button.connect('show', self._on_show_button) 130 131 @staticmethod 132 def _on_show_button(button): 133 button.hide() 134 135 def _prepare_query(self): 136 if self.timedelta: 137 self.start = self.now - self.timedelta 138 self.end = self.current_start 139 140 log.info('Get mam_start_date: %s', self.current_start) 141 log.info('Now: %s', self.now) 142 log.info('Start: %s', self.start) 143 log.info('End: %s', self.end) 144 145 jid = self.con.get_own_jid().bare 146 147 self.con.get_module('MAM').make_query(jid, 148 start=self.start, 149 end=self.end, 150 max_=0, 151 callback=self._received_count) 152 153 def _received_count(self, task): 154 try: 155 result = task.finish() 156 except (StanzaError, MalformedStanzaError): 157 return 158 159 if result.rsm.count is not None: 160 self.download_history.count = int(result.rsm.count) 161 self.query_id = self.con.get_module('MAM').request_archive_interval( 162 self.start, self.end) 163 164 @event_filter(['account']) 165 def _received_finished(self, event): 166 if event.query_id != self.query_id: 167 return 168 self.query_id = None 169 log.info('Query finished') 170 GLib.idle_add(self.download_history.finished) 171 self.set_current_page(Pages.SUMMARY) 172 self.summary.finished() 173 174 @event_filter(['account']) 175 def _nec_mam_message_received(self, event): 176 if self.query_id != event.properties.mam.query_id: 177 return 178 179 log.debug('Received message') 180 GLib.idle_add(self.download_history.set_fraction) 181 182 def on_row_selected(self, _listbox, row): 183 self.timedelta = row.get_child().get_delta() 184 if row: 185 self.set_page_complete(self.select_time, True) 186 else: 187 self.set_page_complete(self.select_time, False) 188 189 def _on_page_change(self, _assistant, page): 190 if page == self.download_history: 191 self.next.hide() 192 self._prepare_query() 193 self.set_title(_('Synchronise History')) 194 195 def _on_close_clicked(self, *args): 196 self.destroy() 197 198 199class SelectTimePage(Gtk.Box): 200 def __init__(self, assistant): 201 super().__init__(orientation=Gtk.Orientation.VERTICAL) 202 self.set_spacing(18) 203 self.assistant = assistant 204 label = Gtk.Label( 205 label=_('How far back should the chat history be synchronised?')) 206 207 listbox = Gtk.ListBox() 208 listbox.set_hexpand(False) 209 listbox.set_halign(Gtk.Align.CENTER) 210 listbox.add(TimeOption(_('One Month'), 1)) 211 listbox.add(TimeOption(_('Three Months'), 3)) 212 listbox.add(TimeOption(_('One Year'), 12)) 213 listbox.add(TimeOption(_('Everything'))) 214 listbox.connect('row-selected', assistant.on_row_selected) 215 216 for row in listbox.get_children(): 217 option = row.get_child() 218 if not option.get_delta(): 219 continue 220 if assistant.now - option.get_delta() > assistant.current_start: 221 row.set_activatable(False) 222 row.set_selectable(False) 223 224 self.pack_start(label, True, True, 0) 225 self.pack_start(listbox, False, False, 0) 226 227 228class DownloadHistoryPage(Gtk.Box): 229 def __init__(self, assistant): 230 super().__init__(orientation=Gtk.Orientation.VERTICAL) 231 self.set_spacing(18) 232 self.assistant = assistant 233 self.count = 0 234 self.received = 0 235 236 surface = load_icon('folder-download-symbolic', self, size=64) 237 image = Gtk.Image.new_from_surface(surface) 238 239 self.progress = Gtk.ProgressBar() 240 self.progress.set_show_text(True) 241 self.progress.set_text(_('Connecting...')) 242 self.progress.set_pulse_step(0.1) 243 self.progress.set_vexpand(True) 244 self.progress.set_valign(Gtk.Align.CENTER) 245 246 self.pack_start(image, False, False, 0) 247 self.pack_start(self.progress, False, False, 0) 248 249 def set_fraction(self): 250 self.received += 1 251 if self.count: 252 self.progress.set_fraction(self.received / self.count) 253 self.progress.set_text(_('%(received)s of %(max)s') % { 254 'received': self.received, 'max': self.count}) 255 else: 256 self.progress.pulse() 257 self.progress.set_text(_('Downloaded %s messages') % self.received) 258 259 def finished(self): 260 self.progress.set_fraction(1) 261 262 263class SummaryPage(Gtk.Box): 264 def __init__(self, assistant): 265 super().__init__(orientation=Gtk.Orientation.VERTICAL) 266 self.set_spacing(18) 267 self.assistant = assistant 268 269 self.label = Gtk.Label() 270 self.label.set_name('FinishedLabel') 271 self.label.set_valign(Gtk.Align.CENTER) 272 273 self.pack_start(self.label, True, True, 0) 274 275 def finished(self): 276 received = self.assistant.download_history.received 277 self.label.set_text(_('Finished synchronising chat history:\n' 278 '%s messages downloaded') % received) 279 280 def nothing_to_do(self): 281 self.label.set_text(_('Gajim is fully synchronised with the archive.')) 282 283 def query_already_running(self): 284 self.label.set_text(_('There is already a synchronisation in ' 285 'progress. Please try again later.')) 286 287 288class TimeOption(Gtk.Label): 289 def __init__(self, label, months=None): 290 super().__init__(label=label) 291 self.date = months 292 if months: 293 self.date = timedelta(days=30 * months) 294 295 def get_delta(self): 296 return self.date 297