history_manager.py 26 KB
Newer Older
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

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
from gi.repository import Gtk
Dicson's avatar
Dicson committed
36
from gi.repository import Gdk
Yann Leboulanger's avatar
Yann Leboulanger committed
37
from gi.repository import GLib
38
from gi.repository import Gio
39
from gi.repository import Pango
40

41
from gajim.common import app
42
from gajim.common import configpaths
Philipp Hörist's avatar
Philipp Hörist committed
43
from gajim.common.i18n import _
44
from gajim.common.i18n import ngettext
Philipp Hörist's avatar
Philipp Hörist committed
45
from gajim.common.const import StyleAttr
46 47
from gajim.common.const import JIDConstant
from gajim.common.const import KindConstant
48
from gajim.common.const import ShowConstant
49 50 51 52 53 54 55 56 57 58 59

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():
60
    try:
61 62
        shortargs = 'hvsc:l:p:'
        longargs = 'help verbose separate config-path= loglevel= profile='
63
        opts = getopt.getopt(sys.argv[1:], shortargs, longargs.split())[0]
64
    except getopt.error as msg:
65 66
        print(str(msg))
        print('for help use --help')
67 68 69
        sys.exit(2)
    for o, a in opts:
        if o in ('-h', '--help'):
70 71 72 73 74
            print(_('Usage:') + \
                '\n  gajim-history-manager [options] filename\n\n' + \
                _('Options:') + \
                '\n  -h, --help         ' + \
                    _('Show this help message and exit') + \
75
                '\n  -c, --config-path  ' + _('Choose folder for logfile') + '\n')
76 77
            sys.exit()
        elif o in ('-c', '--config-path'):
78
            configpaths.set_config_root(a)
79

80
    configpaths.init()
81
    app.load_css_config()
82

83
# pylint: disable=C0413
André's avatar
André committed
84
from gajim.common import helpers
85
from gajim.gtk.dialogs import ErrorDialog
86 87
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.dialogs import DialogButton
88
from gajim.gtk.filechoosers import FileSaveDialog
89
from gajim.gtk.util import convert_rgb_to_hex
90 91
from gajim.gtk.util import get_builder
from gajim.gtk.util import get_app_icon_list
92
# pylint: enable=C0413
93

94
@unique
95 96 97 98 99
class Column(IntEnum):
    UNIXTIME = 2
    MESSAGE = 3
    SUBJECT = 4
    NICKNAME = 5
100

101

102
class HistoryManager:
103
    def __init__(self):
104 105
        log_db_path = configpaths.get('LOG_DB')
        if not os.path.exists(log_db_path):
106
            ErrorDialog(_('Cannot find history logs database'),
107
                        _('%s does not exist.') % log_db_path)
108 109
            sys.exit()

110 111 112
        self._ui = get_builder('history_manager.ui')
        Gtk.Window.set_default_icon_list(get_app_icon_list(
            self._ui.history_manager_window))
113

Dicson's avatar
Dicson committed
114
        self.jids_already_in = []  # holds jids that we already have in DB
115 116
        self.AT_LEAST_ONE_DELETION_DONE = False

117 118
        self.con = sqlite3.connect(
            log_db_path, timeout=20.0, isolation_level='IMMEDIATE')
119
        self.con.execute("PRAGMA secure_delete=1")
120 121 122 123 124 125 126 127
        self.cur = self.con.cursor()

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

        self._fill_jids_listview()

128
        self._ui.search_entry.grab_focus()
129

130
        self._ui.history_manager_window.show_all()
131

132
        self._ui.connect_signals(self)
133 134

    def _init_jids_listview(self):
135
        self.jids_liststore = Gtk.ListStore(str, str)  # jid, jid_id
136 137
        self._ui.jids_listview.set_model(self.jids_liststore)
        self._ui.jids_listview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
138

139
        renderer_text = Gtk.CellRendererText()  # holds jid
140
        col = Gtk.TreeViewColumn(_('XMPP Address'), renderer_text, text=0)
141
        self._ui.jids_listview.append_column(col)
142

143
        self._ui.jids_listview.get_selection().connect('changed',
144 145 146
                self.on_jids_listview_selection_changed)

    def _init_logs_listview(self):
Dicson's avatar
Dicson committed
147
        # log_line_id(HIDDEN), jid_id(HIDDEN), time, message, subject, nickname
148
        self.logs_liststore = Gtk.ListStore(str, str, str, str, str, str)
149 150
        self._ui.logs_listview.set_model(self.logs_liststore)
        self._ui.logs_listview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
151

152
        renderer_text = Gtk.CellRendererText()  # holds time
153
        col = Gtk.TreeViewColumn(_('Date'), renderer_text, text=Column.UNIXTIME)
Dicson's avatar
Dicson committed
154
        # user can click this header and sort
155
        col.set_sort_column_id(Column.UNIXTIME)
156
        col.set_resizable(True)
157
        self._ui.logs_listview.append_column(col)
158

159
        renderer_text = Gtk.CellRendererText()  # holds nickname
160
        col = Gtk.TreeViewColumn(_('Nickname'), renderer_text, text=Column.NICKNAME)
Dicson's avatar
Dicson committed
161
        # user can click this header and sort
162
        col.set_sort_column_id(Column.NICKNAME)
163 164 165
        col.set_resizable(True)
        col.set_visible(False)
        self.nickname_col_for_logs = col
166
        self._ui.logs_listview.append_column(col)
167

168
        renderer_text = Gtk.CellRendererText()  # holds message
169 170
        renderer_text.set_property('width_chars', 60)
        renderer_text.set_property('ellipsize', Pango.EllipsizeMode.END)
171
        col = Gtk.TreeViewColumn(_('Message'), renderer_text, markup=Column.MESSAGE)
Dicson's avatar
Dicson committed
172
        # user can click this header and sort
173
        col.set_sort_column_id(Column.MESSAGE)
174 175
        col.set_resizable(True)
        self.message_col_for_logs = col
176
        self._ui.logs_listview.append_column(col)
177

178
        renderer_text = Gtk.CellRendererText()  # holds subject
179
        col = Gtk.TreeViewColumn(_('Subject'), renderer_text, text=Column.SUBJECT)
180 181
        # user can click this header and sort
        col.set_sort_column_id(Column.SUBJECT)
182 183 184
        col.set_resizable(True)
        col.set_visible(False)
        self.subject_col_for_logs = col
185
        self._ui.logs_listview.append_column(col)
186 187 188

    def _init_search_results_listview(self):
        # log_line_id (HIDDEN), jid, time, message, subject, nickname
Dicson's avatar
Dicson committed
189
        self.search_results_liststore = Gtk.ListStore(int, str, str, str, str,
Dicson's avatar
Dicson committed
190
            str)
191
        self._ui.search_results_listview.set_model(self.search_results_liststore)
192

193
        renderer_text = Gtk.CellRendererText()  # holds JID (who said this)
194
        col = Gtk.TreeViewColumn(_('XMPP Address'), renderer_text, text=1)
195 196
        # user can click this header and sort
        col.set_sort_column_id(1)
197
        col.set_resizable(True)
198
        self._ui.search_results_listview.append_column(col)
199

200
        renderer_text = Gtk.CellRendererText()  # holds time
201
        col = Gtk.TreeViewColumn(_('Date'), renderer_text, text=Column.UNIXTIME)
Dicson's avatar
Dicson committed
202
        # user can click this header and sort
203
        col.set_sort_column_id(Column.UNIXTIME)
204
        col.set_resizable(True)
205
        self._ui.search_results_listview.append_column(col)
206

207
        renderer_text = Gtk.CellRendererText()  # holds message
208 209
        renderer_text.set_property('width_chars', 60)
        renderer_text.set_property('ellipsize', Pango.EllipsizeMode.END)
210
        col = Gtk.TreeViewColumn(_('Message'), renderer_text, text=Column.MESSAGE)
211 212
        # user can click this header and sort
        col.set_sort_column_id(Column.MESSAGE)
213
        col.set_resizable(True)
214
        self._ui.search_results_listview.append_column(col)
215

216
        renderer_text = Gtk.CellRendererText()  # holds subject
217
        col = Gtk.TreeViewColumn(_('Subject'), renderer_text, text=Column.SUBJECT)
218 219
        # user can click this header and sort
        col.set_sort_column_id(Column.SUBJECT)
220
        col.set_resizable(True)
221
        self._ui.search_results_listview.append_column(col)
222

223
        renderer_text = Gtk.CellRendererText()  # holds nickname
224
        col = Gtk.TreeViewColumn(_('Nickname'), renderer_text, text=Column.NICKNAME)
Dicson's avatar
Dicson committed
225
        # user can click this header and sort
226
        col.set_sort_column_id(Column.NICKNAME)
227
        col.set_resizable(True)
228
        self._ui.search_results_listview.append_column(col)
229 230

    def on_history_manager_window_delete_event(self, widget, event):
Dicson's avatar
Dicson committed
231
        if not self.AT_LEAST_ONE_DELETION_DONE:
232
            if is_standalone():
233
                Gtk.main_quit()
234 235
            return

236
        def _on_yes():
Dicson's avatar
Dicson committed
237 238
            self.cur.execute('VACUUM')
            self.con.commit()
239
            if is_standalone():
240
                Gtk.main_quit()
Dicson's avatar
Dicson committed
241

242
        def _on_no():
243
            if is_standalone():
244
                Gtk.main_quit()
Dicson's avatar
Dicson committed
245

246 247
        NewConfirmationDialog(
            _('Database Cleanup'),
248 249 250
            _('Clean up the database?'),
            _('This is STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING.\n'
              'Normally, the allocated database size will not be freed, it '
251 252 253 254 255 256
              '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()
257 258 259

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

Dicson's avatar
Dicson committed
268
    def on_jids_listview_selection_changed(self, widget, data=None):
269
        liststore, list_of_paths = self._ui.jids_listview.get_selection()\
270 271
                .get_selected_rows()

272 273 274
        self.logs_liststore.clear()
        if not list_of_paths:
            return
275

276 277 278
        self._ui.welcome_box.hide()
        self._ui.search_results_scrolledwindow.hide()
        self._ui.logs_scrolledwindow.show()
279 280

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

Dicson's avatar
Dicson committed
284
        for rowref in list_of_rowrefs:  # FILL THE STORE, for all rows selected
285 286 287
            path = rowref.get_path()
            if path is None:
                continue
288
            jid = liststore[path][0]  # jid
289 290 291 292 293 294 295 296 297 298
            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
299
        if jid.find('/') != -1:  # if it has a /
300
            jid_is_from_pm = self._jid_is_from_pm(jid)
Dicson's avatar
Dicson committed
301 302
            if not jid_is_from_pm:  # it's normal jid with resource
                jid = jid.split('/', 1)[0]  # remove the resource
303 304 305 306 307 308 309 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
        logging for room and only enables for pm (so higly unlikely) and if we
        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 = ?',
329
                (possible_room_jid, JIDConstant.ROOM_TYPE))
330 331 332
        row = self.cur.fetchone()
        if row is None:
            return False
333
        return True
334 335 336 337 338 339 340

    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
341
        return row[0] == JIDConstant.ROOM_TYPE
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359

    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
360
        if self._jid_is_room_type(jid):  # is it room?
361 362 363 364 365 366 367 368 369 370 371
            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)

        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
372 373
            log_line_id, jid_id, time_, kind, message, subject, nickname, \
                show = row
374
            try:
Dicson's avatar
Dicson committed
375
                time_ = time.strftime('%x', time.localtime(float(time_)))
376 377 378 379
            except ValueError:
                pass
            else:
                color = None
380
                if kind in (KindConstant.SINGLE_MSG_RECV,
381 382 383 384
                            KindConstant.CHAT_MSG_RECV,
                            KindConstant.GC_MSG):
                    color = app.css_config.get_value(
                        '.gajim-incoming-nickname', StyleAttr.COLOR)
385
                elif kind in (KindConstant.SINGLE_MSG_SENT,
386 387 388
                              KindConstant.CHAT_MSG_SENT):
                    color = app.css_config.get_value(
                        '.gajim-outgoing-nickname', StyleAttr.COLOR)
389
                elif kind in (KindConstant.STATUS,
390 391 392
                              KindConstant.GCSTATUS):
                    color = app.css_config.get_value(
                        '.gajim-status-message', StyleAttr.COLOR)
393 394 395 396 397
                    # include status into (status) message
                    if message is None:
                        message = ''
                    else:
                        message = ' : ' + message
398 399

                    message = helpers.get_uf_show(ShowConstant(show)) + message
400 401 402

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

    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()
        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:
Dicson's avatar
Dicson committed
430
                time_ = time.strftime('%x', time.localtime(float(time_)))
431 432 433 434 435 436 437 438 439
            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):
440
        liststore, list_of_paths = self._ui.logs_listview.get_selection()\
441
                .get_selected_rows()
442
        if event.keyval == Gdk.KEY_Delete:
443 444 445
            self._delete_logs(liststore, list_of_paths)

    def on_listview_button_press_event(self, widget, event):
Dicson's avatar
Dicson committed
446
        if event.button == 3:  # right click
447
            _ui = get_builder('history_manager.ui', ['context_menu'])
Dicson's avatar
Dicson committed
448
            if Gtk.Buildable.get_name(widget) != 'jids_listview':
449 450
                _ui.export_menuitem.hide()
            _ui.delete_menuitem.connect('activate',
451 452
                    self.on_delete_menuitem_activate, widget)

453 454
            _ui.connect_signals(self)
            _ui.context_menu.popup(None, None, None, None,
455 456 457 458
                    event.button, event.time)
            return True

    def on_export_menuitem_activate(self, widget):
459
        FileSaveDialog(self._on_export,
460
                       transient_for=self._ui.history_manager_window,
461
                       modal=True)
462

463
    def _on_export(self, filename):
464
        liststore, list_of_paths = self._ui.jids_listview.get_selection()\
465 466
                .get_selected_rows()
        self._export_jids_logs_to_file(liststore, list_of_paths, filename)
467 468

    def on_delete_menuitem_activate(self, widget, listview):
469
        widget_name = Gtk.Buildable.get_name(listview)
470
        liststore, list_of_paths = listview.get_selection().get_selected_rows()
471
        if widget_name == 'jids_listview':
472
            self._delete_jid_logs(liststore, list_of_paths)
473
        elif widget_name in ('logs_listview', 'search_results_listview'):
474
            self._delete_logs(liststore, list_of_paths)
Dicson's avatar
Dicson committed
475
        else:  # Huh ? We don't know this widget
476 477 478
            return

    def on_jids_listview_key_press_event(self, widget, event):
479
        liststore, list_of_paths = self._ui.jids_listview.get_selection()\
480
                .get_selected_rows()
481
        if event.keyval == Gdk.KEY_Delete:
482 483 484 485
            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
486
        if paths_len == 0:  # nothing is selected
487 488 489
            return

        list_of_rowrefs = []
Dicson's avatar
Dicson committed
490
        for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
491
            list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507

        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]
508
        file_ = open(path_to_file, 'w', encoding='utf-8')
509 510 511 512
        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
513 514
            if kind in (KindConstant.SINGLE_MSG_RECV,
                    KindConstant.CHAT_MSG_RECV):
515
                who = self._get_jid_from_jid_id(jid_id)
516 517
            elif kind in (KindConstant.SINGLE_MSG_SENT,
                    KindConstant.CHAT_MSG_SENT):
518
                who = _('You')
519
            elif kind == KindConstant.GC_MSG:
520
                who = nickname
Dicson's avatar
Dicson committed
521
            else:  # status or gc_status. do not save
522 523 524 525
                #print kind
                continue

            try:
Dicson's avatar
Dicson committed
526
                time_ = time.strftime('%c', time.localtime(float(time_)))
527 528 529
            except ValueError:
                pass

Dicson's avatar
Dicson committed
530 531
            file_.write(_('%(who)s on %(time)s said: %(message)s\n') % {
                'who': who, 'time': time_, 'message': message})
532 533 534

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

538
        def on_ok():
539 540
            # delete all rows from db that match jid_id
            list_of_rowrefs = []
Dicson's avatar
Dicson committed
541
            for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
542
                list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
543 544 545 546 547 548

            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
549
                del liststore[path]  # remove from UI
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
                # 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

566 567 568 569
        NewConfirmationDialog(
            _('Delete'),
            ngettext('Delete Conversation', 'Delete Conversations', paths_len),
            ngettext('Do you want to permanently delete this '
570 571
                     'conversation with <b>%s</b>?',
                     'Do you want to permanently delete these conversations?',
572 573
                     paths_len, liststore[list_of_paths[0]][0]),
            [DialogButton.make('Cancel'),
574 575
             DialogButton.make('Delete',
                               callback=on_ok)],
576
            transient_for=self._ui.history_manager_window).show()
577 578 579

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

583
        def on_ok():
584 585
            # delete rows from db that match log_line_id
            list_of_rowrefs = []
Dicson's avatar
Dicson committed
586
            for path in list_of_paths:  # make them treerowrefs (it's needed)
Dicson's avatar
Dicson committed
587
                list_of_rowrefs.append(Gtk.TreeRowReference.new(liststore, path))
588 589 590 591 592 593

            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
594
                del liststore[path]  # remove from UI
595 596 597 598 599 600 601 602 603 604
                # 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

605 606 607 608 609 610 611
        NewConfirmationDialog(
            _('Delete'),
            ngettext('Delete Message', 'Delete Messages', paths_len),
            ngettext('Do you want to permanently delete this message',
                     'Do you want to permanently delete these messages',
                     paths_len),
            [DialogButton.make('Cancel'),
612 613
             DialogButton.make('Delete',
                               callback=on_ok)],
614
            transient_for=self._ui.history_manager_window).show()
615 616

    def on_search_db_button_clicked(self, widget):
617
        text = self._ui.search_entry.get_text()
618 619 620
        if not text:
            return

621 622 623
        self._ui.welcome_box.hide()
        self._ui.logs_scrolledwindow.hide()
        self._ui.search_results_scrolledwindow.show()
624 625 626 627 628

        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
629
        log_line_id = str(self.search_results_liststore[path][0])
630
        jid = self.search_results_liststore[path][1]
631 632 633 634
        # 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)

635
        iter_ = self.jids_liststore.get_iter_first()
636 637 638 639 640 641 642 643 644 645
        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_)
646
        self._ui.jids_listview.set_cursor(path)
647

648
        iter_ = self.logs_liststore.get_iter_first()
649
        while iter_:
650
            # self.logs_liststore[iter_][0] holds log_line_ids
651 652 653 654 655
            if self.logs_liststore[iter_][0] == log_line_id:
                break
            iter_ = self.logs_liststore.iter_next(iter_)

        path = self.logs_liststore.get_path(iter_)
656 657
        self._ui.logs_listview.scroll_to_cell(path)
        self._ui.logs_listview.get_selection().select_path(path)
658

659 660 661 662 663 664

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

665
    HistoryManager()
666
    Gtk.main()
667 668 669 670


if __name__ == '__main__':
    main()