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