history_manager.py 26.3 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
#                         Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2018 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; version 3 only.
#
# 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/>.
22

Philipp Hörist's avatar
Philipp Hörist committed
23 24 25 26
# NOTE: some method names may match those of logger.py but that's it
# someday (TM) should have common class
# that abstracts db connections and helpers on it
# the same can be said for history.py
27 28

import os
29
import sys
30 31 32 33 34
import time
import getopt
import sqlite3
from enum import IntEnum, unique

35 36 37 38 39 40 41 42
import gi

try:
    gi.require_versions({'Gtk': '3.0'})
except ValueError as error:
    sys.exit('Missing dependency: %s' % error)

# pylint: disable=C0413
43
from gi.repository import Gtk
Dicson's avatar
Dicson committed
44
from gi.repository import Gdk
Yann Leboulanger's avatar
Yann Leboulanger committed
45
from gi.repository import GLib
46
from gi.repository import Gio
Daniel Brötzmann's avatar
Daniel Brötzmann committed
47
from gi.repository import Pango
48

49
from gajim.common import app
50
from gajim.common import configpaths
Philipp Hörist's avatar
Philipp Hörist committed
51
from gajim.common.i18n import _
52
from gajim.common.i18n import ngettext
Philipp Hörist's avatar
Philipp Hörist committed
53
from gajim.common.const import StyleAttr
54 55
from gajim.common.const import JIDConstant
from gajim.common.const import KindConstant
56
from gajim.common.const import ShowConstant
57 58 59 60 61 62 63 64 65 66 67

def is_standalone():
    # Determine if we are in standalone mode
    if Gio.Application.get_default() is None:
        return True
    if __name__ == '__main__':
        return True
    return False


if is_standalone():
68
    try:
69 70
        shortargs = 'hvsc:l:p:'
        longargs = 'help verbose separate config-path= loglevel= profile='
71
        opts = getopt.getopt(sys.argv[1:], shortargs, longargs.split())[0]
Yann Leboulanger's avatar
Yann Leboulanger committed
72
    except getopt.error as msg:
73 74
        print(str(msg))
        print('for help use --help')
75 76 77
        sys.exit(2)
    for o, a in opts:
        if o in ('-h', '--help'):
78 79 80 81 82
            print(_('Usage:') + \
                '\n  gajim-history-manager [options] filename\n\n' + \
                _('Options:') + \
                '\n  -h, --help         ' + \
                    _('Show this help message and exit') + \
83
                '\n  -c, --config-path  ' + _('Choose folder for logfile') + '\n')
84 85
            sys.exit()
        elif o in ('-c', '--config-path'):
86
            configpaths.set_config_root(a)
87

88
    configpaths.init()
89
    app.load_css_config()
90

André's avatar
André committed
91
from gajim.common import helpers
92
from gajim.gtk.dialogs import ErrorDialog
93 94
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.dialogs import DialogButton
95
from gajim.gtk.filechoosers import FileSaveDialog
96
from gajim.gtk.util import convert_rgb_to_hex
97 98
from gajim.gtk.util import get_builder
from gajim.gtk.util import get_app_icon_list
99
# pylint: enable=C0413
100

101
@unique
102 103 104 105 106
class Column(IntEnum):
    UNIXTIME = 2
    MESSAGE = 3
    SUBJECT = 4
    NICKNAME = 5
107

108

109
class HistoryManager:
110
    def __init__(self):
111 112
        log_db_path = configpaths.get('LOG_DB')
        if not os.path.exists(log_db_path):
113
            ErrorDialog(_('Cannot find history logs database'),
114
                        _('%s does not exist.') % log_db_path)
115 116
            sys.exit()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
117 118 119
        self._ui = get_builder('history_manager.ui')
        Gtk.Window.set_default_icon_list(get_app_icon_list(
            self._ui.history_manager_window))
120

Dicson's avatar
Dicson committed
121
        self.jids_already_in = []  # holds jids that we already have in DB
122 123
        self.AT_LEAST_ONE_DELETION_DONE = False

124 125
        self.con = sqlite3.connect(
            log_db_path, timeout=20.0, isolation_level='IMMEDIATE')
126
        self.con.execute("PRAGMA secure_delete=1")
127 128 129 130 131 132 133 134
        self.cur = self.con.cursor()

        self._init_jids_listview()
        self._init_logs_listview()
        self._init_search_results_listview()

        self._fill_jids_listview()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
135
        self._ui.search_entry.grab_focus()
136

Daniel Brötzmann's avatar
Daniel Brötzmann committed
137
        self._ui.history_manager_window.show_all()
138

Daniel Brötzmann's avatar
Daniel Brötzmann committed
139
        self._ui.connect_signals(self)
140 141

    def _init_jids_listview(self):
142
        self.jids_liststore = Gtk.ListStore(str, str)  # jid, jid_id
Daniel Brötzmann's avatar
Daniel Brötzmann committed
143 144
        self._ui.jids_listview.set_model(self.jids_liststore)
        self._ui.jids_listview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
145

146
        renderer_text = Gtk.CellRendererText()  # holds jid
147
        col = Gtk.TreeViewColumn(_('XMPP Address'), renderer_text, text=0)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
148
        self._ui.jids_listview.append_column(col)
149

Daniel Brötzmann's avatar
Daniel Brötzmann committed
150
        self._ui.jids_listview.get_selection().connect('changed',
151 152 153
                self.on_jids_listview_selection_changed)

    def _init_logs_listview(self):
Dicson's avatar
Dicson committed
154
        # log_line_id(HIDDEN), jid_id(HIDDEN), time, message, subject, nickname
155
        self.logs_liststore = Gtk.ListStore(str, str, str, str, str, str)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
156 157
        self._ui.logs_listview.set_model(self.logs_liststore)
        self._ui.logs_listview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
158

159
        renderer_text = Gtk.CellRendererText()  # holds time
160
        col = Gtk.TreeViewColumn(_('Date'), renderer_text, text=Column.UNIXTIME)
Dicson's avatar
Dicson committed
161
        # user can click this header and sort
162
        col.set_sort_column_id(Column.UNIXTIME)
163
        col.set_resizable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
164
        self._ui.logs_listview.append_column(col)
165

166
        renderer_text = Gtk.CellRendererText()  # holds nickname
167
        col = Gtk.TreeViewColumn(_('Nickname'), renderer_text, text=Column.NICKNAME)
Dicson's avatar
Dicson committed
168
        # user can click this header and sort
169
        col.set_sort_column_id(Column.NICKNAME)
170 171 172
        col.set_resizable(True)
        col.set_visible(False)
        self.nickname_col_for_logs = col
Daniel Brötzmann's avatar
Daniel Brötzmann committed
173
        self._ui.logs_listview.append_column(col)
174

175
        renderer_text = Gtk.CellRendererText()  # holds message
Daniel Brötzmann's avatar
Daniel Brötzmann committed
176 177
        renderer_text.set_property('width_chars', 60)
        renderer_text.set_property('ellipsize', Pango.EllipsizeMode.END)
178
        col = Gtk.TreeViewColumn(_('Message'), renderer_text, markup=Column.MESSAGE)
Dicson's avatar
Dicson committed
179
        # user can click this header and sort
180
        col.set_sort_column_id(Column.MESSAGE)
181 182
        col.set_resizable(True)
        self.message_col_for_logs = col
Daniel Brötzmann's avatar
Daniel Brötzmann committed
183
        self._ui.logs_listview.append_column(col)
184

185
        renderer_text = Gtk.CellRendererText()  # holds subject
186
        col = Gtk.TreeViewColumn(_('Subject'), renderer_text, text=Column.SUBJECT)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
187 188
        # user can click this header and sort
        col.set_sort_column_id(Column.SUBJECT)
189 190 191
        col.set_resizable(True)
        col.set_visible(False)
        self.subject_col_for_logs = col
Daniel Brötzmann's avatar
Daniel Brötzmann committed
192
        self._ui.logs_listview.append_column(col)
193 194 195

    def _init_search_results_listview(self):
        # log_line_id (HIDDEN), jid, time, message, subject, nickname
Dicson's avatar
Dicson committed
196
        self.search_results_liststore = Gtk.ListStore(int, str, str, str, str,
Dicson's avatar
Dicson committed
197
            str)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
198
        self._ui.search_results_listview.set_model(self.search_results_liststore)
199

200
        renderer_text = Gtk.CellRendererText()  # holds JID (who said this)
201
        col = Gtk.TreeViewColumn(_('XMPP Address'), renderer_text, text=1)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
202 203
        # user can click this header and sort
        col.set_sort_column_id(1)
204
        col.set_resizable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
205
        self._ui.search_results_listview.append_column(col)
206

207
        renderer_text = Gtk.CellRendererText()  # holds time
208
        col = Gtk.TreeViewColumn(_('Date'), renderer_text, text=Column.UNIXTIME)
Dicson's avatar
Dicson committed
209
        # user can click this header and sort
210
        col.set_sort_column_id(Column.UNIXTIME)
211
        col.set_resizable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
212
        self._ui.search_results_listview.append_column(col)
213

214
        renderer_text = Gtk.CellRendererText()  # holds message
Daniel Brötzmann's avatar
Daniel Brötzmann committed
215 216
        renderer_text.set_property('width_chars', 60)
        renderer_text.set_property('ellipsize', Pango.EllipsizeMode.END)
217
        col = Gtk.TreeViewColumn(_('Message'), renderer_text, text=Column.MESSAGE)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
218 219
        # user can click this header and sort
        col.set_sort_column_id(Column.MESSAGE)
220
        col.set_resizable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
221
        self._ui.search_results_listview.append_column(col)
222

223
        renderer_text = Gtk.CellRendererText()  # holds subject
224
        col = Gtk.TreeViewColumn(_('Subject'), renderer_text, text=Column.SUBJECT)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
225 226
        # user can click this header and sort
        col.set_sort_column_id(Column.SUBJECT)
227
        col.set_resizable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
228
        self._ui.search_results_listview.append_column(col)
229

230
        renderer_text = Gtk.CellRendererText()  # holds nickname
231
        col = Gtk.TreeViewColumn(_('Nickname'), renderer_text, text=Column.NICKNAME)
Dicson's avatar
Dicson committed
232
        # user can click this header and sort
233
        col.set_sort_column_id(Column.NICKNAME)
234
        col.set_resizable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
235
        self._ui.search_results_listview.append_column(col)
236 237

    def on_history_manager_window_delete_event(self, widget, event):
Dicson's avatar
Dicson committed
238
        if not self.AT_LEAST_ONE_DELETION_DONE:
239
            if is_standalone():
Andrey Gursky's avatar
Andrey Gursky committed
240
                Gtk.main_quit()
241 242
            return

243
        def _on_yes():
Dicson's avatar
Dicson committed
244 245
            self.cur.execute('VACUUM')
            self.con.commit()
246
            if is_standalone():
Andrey Gursky's avatar
Andrey Gursky committed
247
                Gtk.main_quit()
Dicson's avatar
Dicson committed
248

249
        def _on_no():
250
            if is_standalone():
Andrey Gursky's avatar
Andrey Gursky committed
251
                Gtk.main_quit()
Dicson's avatar
Dicson committed
252

253 254
        NewConfirmationDialog(
            _('Database Cleanup'),
255 256 257
            _('Clean up the database?'),
            _('This is STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING.\n'
              'Normally, the allocated database size will not be freed, it '
258 259 260 261 262 263
              'will just become reusable. This operation may take a while.'),
            [DialogButton.make('Cancel',
                               callback=_on_no),
             DialogButton.make('Remove',
                               text=_('_Cleanup'),
                               callback=_on_yes)]).show()
264 265 266

    def _fill_jids_listview(self):
        # get those jids that have at least one entry in logs
Dicson's avatar
Dicson committed
267 268
        self.cur.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN ('
                'SELECT distinct logs.jid_id FROM logs) ORDER BY jid')
269
        # list of tuples: [('aaa@bbb',), ('cc@dd',)]
Dicson's avatar
Dicson committed
270
        rows = self.cur.fetchall()
271
        for row in rows:
Dicson's avatar
Dicson committed
272
            self.jids_already_in.append(row[0])  # jid
Dicson's avatar
Dicson committed
273
            self.jids_liststore.append([row[0], str(row[1])])  # jid, jid_id
274

Dicson's avatar
Dicson committed
275
    def on_jids_listview_selection_changed(self, widget, data=None):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
276
        liststore, list_of_paths = self._ui.jids_listview.get_selection()\
277 278
                .get_selected_rows()

279 280 281
        self.logs_liststore.clear()
        if not list_of_paths:
            return
282

Daniel Brötzmann's avatar
Daniel Brötzmann committed
283 284 285
        self._ui.welcome_box.hide()
        self._ui.search_results_scrolledwindow.hide()
        self._ui.logs_scrolledwindow.show()
286 287

        list_of_rowrefs = []
Dicson's avatar
Dicson committed
288
        for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
289
            list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
290

Dicson's avatar
Dicson committed
291
        for rowref in list_of_rowrefs:  # FILL THE STORE, for all rows selected
292 293 294
            path = rowref.get_path()
            if path is None:
                continue
295
            jid = liststore[path][0]  # jid
296 297 298 299 300 301 302 303 304 305
            self._fill_logs_listview(jid)

    def _get_jid_id(self, jid):
        """
        jids table has jid and jid_id
        logs table has log_id, jid_id, contact_name, time, kind, show, message

        So to ask logs we need jid_id that matches our jid in jids table this
        method wants jid and returns the jid_id for later sql-ing on logs
        """
Dicson's avatar
Dicson committed
306
        if jid.find('/') != -1:  # if it has a /
307
            jid_is_from_pm = self._jid_is_from_pm(jid)
Dicson's avatar
Dicson committed
308 309
            if not jid_is_from_pm:  # it's normal jid with resource
                jid = jid.split('/', 1)[0]  # remove the resource
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
        self.cur.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid,))
        jid_id = self.cur.fetchone()[0]
        return str(jid_id)

    def _get_jid_from_jid_id(self, jid_id):
        """
        jids table has jid and jid_id

        This method accepts jid_id and returns the jid for later sql-ing on logs
        """
        self.cur.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id,))
        jid = self.cur.fetchone()[0]
        return jid

    def _jid_is_from_pm(self, jid):
        """
        If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf
        is not a normal guy and nkour is not his resource? We ask if gajim@conf
        is already in jids (with type room jid). This fails if user disables
Emmanuel Gil Peyrot's avatar
Emmanuel Gil Peyrot committed
329
        logging for room and only enables for pm (so highly unlikely) and if we
330 331 332 333 334 335
        fail we do not go chaos (user will see the first pm as if it was message
        in room's public chat) and after that everything is ok
        """
        possible_room_jid = jid.split('/', 1)[0]

        self.cur.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
336
                (possible_room_jid, JIDConstant.ROOM_TYPE))
337 338 339
        row = self.cur.fetchone()
        if row is None:
            return False
340
        return True
341 342 343 344 345 346 347

    def _jid_is_room_type(self, jid):
        """
        Return True/False if given id is room type or not eg. if it is room
        """
        self.cur.execute('SELECT type FROM jids WHERE jid = ?', (jid,))
        row = self.cur.fetchone()
Philipp Hörist's avatar
Philipp Hörist committed
348
        return row[0] == JIDConstant.ROOM_TYPE
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366

    def _fill_logs_listview(self, jid):
        """
        Fill the listview with all messages that user sent to or received from
        JID
        """
        # no need to lower jid in this context as jid is already lowered
        # as we use those jids from db
        jid_id = self._get_jid_id(jid)
        self.cur.execute('''
                SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
                FROM logs
                WHERE jid_id = ?
                ORDER BY time
                ''', (jid_id,))

        results = self.cur.fetchall()

Dicson's avatar
Dicson committed
367
        if self._jid_is_room_type(jid):  # is it room?
368 369 370 371 372 373
            self.nickname_col_for_logs.set_visible(True)
            self.subject_col_for_logs.set_visible(False)
        else:
            self.nickname_col_for_logs.set_visible(False)
            self.subject_col_for_logs.set_visible(True)

374
        format_ = helpers.from_one_line(app.config.get('time_stamp'))
375 376 377 378 379
        for row in results:
            # exposed in UI (TreeViewColumns) are only
            # time, message, subject, nickname
            # but store in liststore
            # log_line_id, jid_id, time, message, subject, nickname
Dicson's avatar
Dicson committed
380 381
            log_line_id, jid_id, time_, kind, message, subject, nickname, \
                show = row
382
            try:
383
                time_ = time.strftime(format_, time.localtime(float(time_)))
384 385 386 387
            except ValueError:
                pass
            else:
                color = None
388
                if kind in (KindConstant.SINGLE_MSG_RECV,
389 390 391 392
                            KindConstant.CHAT_MSG_RECV,
                            KindConstant.GC_MSG):
                    color = app.css_config.get_value(
                        '.gajim-incoming-nickname', StyleAttr.COLOR)
393
                elif kind in (KindConstant.SINGLE_MSG_SENT,
394 395 396
                              KindConstant.CHAT_MSG_SENT):
                    color = app.css_config.get_value(
                        '.gajim-outgoing-nickname', StyleAttr.COLOR)
397
                elif kind in (KindConstant.STATUS,
398 399 400
                              KindConstant.GCSTATUS):
                    color = app.css_config.get_value(
                        '.gajim-status-message', StyleAttr.COLOR)
401 402 403 404 405
                    # include status into (status) message
                    if message is None:
                        message = ''
                    else:
                        message = ' : ' + message
406 407

                    message = helpers.get_uf_show(ShowConstant(show)) + message
408 409 410

                message_ = '<span'
                if color:
411
                    message_ += ' foreground="%s"' % convert_rgb_to_hex(color)
Yann Leboulanger's avatar
Yann Leboulanger committed
412
                message_ += '>%s</span>' % GLib.markup_escape_text(message)
413 414 415
                self.logs_liststore.append(
                    (str(log_line_id), str(jid_id),
                     time_, message_, subject, nickname))
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430

    def _fill_search_results_listview(self, text):
        """
        Ask db and fill listview with results that match text
        """
        self.search_results_liststore.clear()
        like_sql = '%' + text + '%'
        self.cur.execute('''
                SELECT log_line_id, jid_id, time, message, subject, contact_name
                FROM logs
                WHERE message LIKE ? OR subject LIKE ?
                ORDER BY time
                ''', (like_sql, like_sql))

        results = self.cur.fetchall()
431
        format_ = helpers.from_one_line(app.config.get('time_stamp'))
432 433 434 435 436 437 438
        for row in results:
            # exposed in UI (TreeViewColumns) are only
            # JID, time, message, subject, nickname
            # but store in liststore
            # log_line_id, jid (from jid_id), time, message, subject, nickname
            log_line_id, jid_id, time_, message, subject, nickname = row
            try:
439
                time_ = time.strftime(format_, time.localtime(float(time_)))
440 441 442 443 444 445 446 447 448
            except ValueError:
                pass
            else:
                jid = self._get_jid_from_jid_id(jid_id)

                self.search_results_liststore.append((log_line_id, jid, time_,
                        message, subject, nickname))

    def on_logs_listview_key_press_event(self, widget, event):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
449
        liststore, list_of_paths = self._ui.logs_listview.get_selection()\
450
                .get_selected_rows()
451
        if event.keyval == Gdk.KEY_Delete:
452 453 454
            self._delete_logs(liststore, list_of_paths)

    def on_listview_button_press_event(self, widget, event):
Dicson's avatar
Dicson committed
455
        if event.button == 3:  # right click
Daniel Brötzmann's avatar
Daniel Brötzmann committed
456
            _ui = get_builder('history_manager.ui', ['context_menu'])
Dicson's avatar
Dicson committed
457
            if Gtk.Buildable.get_name(widget) != 'jids_listview':
Daniel Brötzmann's avatar
Daniel Brötzmann committed
458 459
                _ui.export_menuitem.hide()
            _ui.delete_menuitem.connect('activate',
460 461
                    self.on_delete_menuitem_activate, widget)

Daniel Brötzmann's avatar
Daniel Brötzmann committed
462 463
            _ui.connect_signals(self)
            _ui.context_menu.popup(None, None, None, None,
464 465 466 467
                    event.button, event.time)
            return True

    def on_export_menuitem_activate(self, widget):
Philipp Hörist's avatar
Philipp Hörist committed
468
        FileSaveDialog(self._on_export,
Daniel Brötzmann's avatar
Daniel Brötzmann committed
469
                       transient_for=self._ui.history_manager_window,
Philipp Hörist's avatar
Philipp Hörist committed
470
                       modal=True)
471

Philipp Hörist's avatar
Philipp Hörist committed
472
    def _on_export(self, filename):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
473
        liststore, list_of_paths = self._ui.jids_listview.get_selection()\
Philipp Hörist's avatar
Philipp Hörist committed
474 475
                .get_selected_rows()
        self._export_jids_logs_to_file(liststore, list_of_paths, filename)
476 477

    def on_delete_menuitem_activate(self, widget, listview):
478
        widget_name = Gtk.Buildable.get_name(listview)
479
        liststore, list_of_paths = listview.get_selection().get_selected_rows()
480
        if widget_name == 'jids_listview':
481
            self._delete_jid_logs(liststore, list_of_paths)
482
        elif widget_name in ('logs_listview', 'search_results_listview'):
483
            self._delete_logs(liststore, list_of_paths)
Dicson's avatar
Dicson committed
484
        else:  # Huh ? We don't know this widget
485 486 487
            return

    def on_jids_listview_key_press_event(self, widget, event):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
488
        liststore, list_of_paths = self._ui.jids_listview.get_selection()\
489
                .get_selected_rows()
490
        if event.keyval == Gdk.KEY_Delete:
491 492 493 494
            self._delete_jid_logs(liststore, list_of_paths)

    def _export_jids_logs_to_file(self, liststore, list_of_paths, path_to_file):
        paths_len = len(list_of_paths)
Dicson's avatar
Dicson committed
495
        if paths_len == 0:  # nothing is selected
496 497 498
            return

        list_of_rowrefs = []
Dicson's avatar
Dicson committed
499
        for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
500
            list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516

        for rowref in list_of_rowrefs:
            path = rowref.get_path()
            if path is None:
                continue
            jid_id = liststore[path][1]
            self.cur.execute('''
                    SELECT time, kind, message, contact_name FROM logs
                    WHERE jid_id = ?
                    ORDER BY time
                    ''', (jid_id,))

        # FIXME: we may have two contacts selected to export. fix that
        # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
        results = self.cur.fetchall()
        #print results[0]
517
        file_ = open(path_to_file, 'w', encoding='utf-8')
518 519 520 521
        for row in results:
            # in store: time, kind, message, contact_name FROM logs
            # in text: JID or You or nickname (if it's gc_msg), time, message
            time_, kind, message, nickname = row
522 523
            if kind in (KindConstant.SINGLE_MSG_RECV,
                    KindConstant.CHAT_MSG_RECV):
524
                who = self._get_jid_from_jid_id(jid_id)
525 526
            elif kind in (KindConstant.SINGLE_MSG_SENT,
                    KindConstant.CHAT_MSG_SENT):
527
                who = _('You')
528
            elif kind == KindConstant.GC_MSG:
529
                who = nickname
Dicson's avatar
Dicson committed
530
            else:  # status or gc_status. do not save
531 532 533 534
                #print kind
                continue

            try:
Dicson's avatar
Dicson committed
535
                time_ = time.strftime('%c', time.localtime(float(time_)))
536 537 538
            except ValueError:
                pass

Dicson's avatar
Dicson committed
539 540
            file_.write(_('%(who)s on %(time)s said: %(message)s\n') % {
                'who': who, 'time': time_, 'message': message})
541 542 543

    def _delete_jid_logs(self, liststore, list_of_paths):
        paths_len = len(list_of_paths)
Dicson's avatar
Dicson committed
544
        if paths_len == 0:  # nothing is selected
545 546
            return

547
        def on_ok():
548 549
            # delete all rows from db that match jid_id
            list_of_rowrefs = []
Dicson's avatar
Dicson committed
550
            for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
551
                list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
552 553 554 555 556 557

            for rowref in list_of_rowrefs:
                path = rowref.get_path()
                if path is None:
                    continue
                jid_id = liststore[path][1]
Dicson's avatar
Dicson committed
558
                del liststore[path]  # remove from UI
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
                # remove from db
                self.cur.execute('''
                        DELETE FROM logs
                        WHERE jid_id = ?
                        ''', (jid_id,))

                # now delete "jid, jid_id" row from jids table
                self.cur.execute('''
                                DELETE FROM jids
                                WHERE jid_id = ?
                                ''', (jid_id,))

            self.con.commit()

            self.AT_LEAST_ONE_DELETION_DONE = True

575 576 577 578
        NewConfirmationDialog(
            _('Delete'),
            ngettext('Delete Conversation', 'Delete Conversations', paths_len),
            ngettext('Do you want to permanently delete this '
579 580
                     'conversation with <b>%s</b>?',
                     'Do you want to permanently delete these conversations?',
581 582
                     paths_len, liststore[list_of_paths[0]][0]),
            [DialogButton.make('Cancel'),
583 584
             DialogButton.make('Delete',
                               callback=on_ok)],
Philipp Hörist's avatar
Philipp Hörist committed
585
            transient_for=self._ui.history_manager_window).show()
586 587 588

    def _delete_logs(self, liststore, list_of_paths):
        paths_len = len(list_of_paths)
Dicson's avatar
Dicson committed
589
        if paths_len == 0:  # nothing is selected
590 591
            return

592
        def on_ok():
593 594
            # delete rows from db that match log_line_id
            list_of_rowrefs = []
Dicson's avatar
Dicson committed
595
            for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
596
                list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
597 598 599 600 601 602

            for rowref in list_of_rowrefs:
                path = rowref.get_path()
                if path is None:
                    continue
                log_line_id = liststore[path][0]
Dicson's avatar
Dicson committed
603
                del liststore[path]  # remove from UI
604 605 606 607 608 609 610 611 612 613
                # remove from db
                self.cur.execute('''
                        DELETE FROM logs
                        WHERE log_line_id = ?
                        ''', (log_line_id,))

            self.con.commit()

            self.AT_LEAST_ONE_DELETION_DONE = True

614 615 616
        NewConfirmationDialog(
            _('Delete'),
            ngettext('Delete Message', 'Delete Messages', paths_len),
617 618
            ngettext('Do you want to permanently delete this message?',
                     'Do you want to permanently delete these messages?',
619 620
                     paths_len),
            [DialogButton.make('Cancel'),
621 622
             DialogButton.make('Delete',
                               callback=on_ok)],
Philipp Hörist's avatar
Philipp Hörist committed
623
            transient_for=self._ui.history_manager_window).show()
624 625

    def on_search_db_button_clicked(self, widget):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
626
        text = self._ui.search_entry.get_text()
627 628 629
        if not text:
            return

Daniel Brötzmann's avatar
Daniel Brötzmann committed
630 631 632
        self._ui.welcome_box.hide()
        self._ui.logs_scrolledwindow.hide()
        self._ui.search_results_scrolledwindow.show()
633 634 635 636 637

        self._fill_search_results_listview(text)

    def on_search_results_listview_row_activated(self, widget, path, column):
        # get log_line_id, jid_id from row we double clicked
Daniel Brötzmann's avatar
Daniel Brötzmann committed
638
        log_line_id = str(self.search_results_liststore[path][0])
639
        jid = self.search_results_liststore[path][1]
640 641 642 643
        # make it string as in gtk liststores I have them all as strings
        # as this is what db returns so I don't have to fight with types
        jid_id = self._get_jid_id(jid)

644
        iter_ = self.jids_liststore.get_iter_first()
645 646 647 648 649 650 651 652 653 654
        while iter_:
            # self.jids_liststore[iter_][1] holds jid_ids
            if self.jids_liststore[iter_][1] == jid_id:
                break
            iter_ = self.jids_liststore.iter_next(iter_)

        if iter_ is None:
            return

        path = self.jids_liststore.get_path(iter_)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
655
        self._ui.jids_listview.set_cursor(path)
656

657
        iter_ = self.logs_liststore.get_iter_first()
658
        while iter_:
Daniel Brötzmann's avatar
Daniel Brötzmann committed
659
            # self.logs_liststore[iter_][0] holds log_line_ids
660 661 662 663 664
            if self.logs_liststore[iter_][0] == log_line_id:
                break
            iter_ = self.logs_liststore.iter_next(iter_)

        path = self.logs_liststore.get_path(iter_)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
665 666
        self._ui.logs_listview.scroll_to_cell(path)
        self._ui.logs_listview.get_selection().select_path(path)
667

668 669 670 671 672 673

def main():
    if sys.platform != 'win32':
        if os.geteuid() == 0:
            sys.exit("You must not launch gajim as root, it is insecure.")

674
    HistoryManager()
675
    Gtk.main()
676 677 678 679


if __name__ == '__main__':
    main()