2 ## src/history_manager.py
4 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
5 ## Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
6 ## Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
8 ## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
9 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
11 ## This file is part of Gajim.
13 ## Gajim is free software; you can redistribute it and/or modify
14 ## it under the terms of the GNU General Public License as published
15 ## by the Free Software Foundation; version 3 only.
17 ## Gajim is distributed in the hope that it will be useful,
18 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ## GNU General Public License for more details.
22 ## You should have received a copy of the GNU General Public License
23 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26 ## NOTE: some method names may match those of logger.py but that's it
27 ## someday (TM) should have common class
28 ## that abstracts db connections and helpers on it
29 ## the same can be said for history_window.py
35 warnings
.filterwarnings(action
='ignore')
37 if os
.path
.isdir('gtk'):
38 # Used to create windows installer with GTK included
39 paths
= os
.environ
['PATH']
40 list_
= paths
.split(';')
43 if p
.find('gtk') < 0 and p
.find('GTK') < 0:
45 new_list
.insert(0, 'gtk/lib')
46 new_list
.insert(0, 'gtk/bin')
47 os
.environ
['PATH'] = ';'.join(new_list
)
48 os
.environ
['GTK_BASEPATH'] = 'gtk'
58 from common
import i18n
65 longargs
= 'help config_path='
66 opts
= getopt
.getopt(sys
.argv
[1:], shortargs
, longargs
.split())[0]
67 except getopt
.error
, msg
:
69 print 'for help use --help'
72 if o
in ('-h', '--help'):
73 print 'history_manager [--help] [--config-path]'
75 elif o
in ('-c', '--config-path'):
79 config_path
= parseOpts()
82 import common
.configpaths
83 common
.configpaths
.gajimpaths
.init(config_path
)
85 common
.configpaths
.gajimpaths
.init_profile()
86 from common
import exceptions
87 from common
import gajim
89 from common
.logger
import LOG_DB_PATH
, constants
91 #FIXME: constants should implement 2 way mappings
92 status
= dict((constants
.__dict
__[i
], i
[5:].lower()) for i
in \
93 constants
.__dict
__.keys() if i
.startswith('SHOW_'))
94 from common
import helpers
97 # time, message, subject
106 import sqlite3
as sqlite
109 class HistoryManager
:
111 pix
= gtkgui_helpers
.get_icon_pixmap('gajim')
112 # set the icon to all newly opened windows
113 gtk
.window_set_default_icon(pix
)
115 if not os
.path
.exists(LOG_DB_PATH
):
116 dialogs
.ErrorDialog(_('Cannot find history logs database'),
117 '%s does not exist.' % LOG_DB_PATH
)
120 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui')
121 self
.window
= xml
.get_object('history_manager_window')
122 self
.jids_listview
= xml
.get_object('jids_listview')
123 self
.logs_listview
= xml
.get_object('logs_listview')
124 self
.search_results_listview
= xml
.get_object('search_results_listview')
125 self
.search_entry
= xml
.get_object('search_entry')
126 self
.logs_scrolledwindow
= xml
.get_object('logs_scrolledwindow')
127 self
.search_results_scrolledwindow
= xml
.get_object(
128 'search_results_scrolledwindow')
129 self
.welcome_vbox
= xml
.get_object('welcome_vbox')
131 self
.jids_already_in
= [] # holds jids that we already have in DB
132 self
.AT_LEAST_ONE_DELETION_DONE
= False
134 self
.con
= sqlite
.connect(LOG_DB_PATH
, timeout
=20.0,
135 isolation_level
='IMMEDIATE')
136 self
.cur
= self
.con
.cursor()
138 self
._init
_jids
_listview
()
139 self
._init
_logs
_listview
()
140 self
._init
_search
_results
_listview
()
142 self
._fill
_jids
_listview
()
144 self
.search_entry
.grab_focus()
146 self
.window
.show_all()
148 xml
.connect_signals(self
)
150 def _init_jids_listview(self
):
151 self
.jids_liststore
= gtk
.ListStore(str, str) # jid, jid_id
152 self
.jids_listview
.set_model(self
.jids_liststore
)
153 self
.jids_listview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
155 renderer_text
= gtk
.CellRendererText() # holds jid
156 col
= gtk
.TreeViewColumn(_('Contacts'), renderer_text
, text
=0)
157 self
.jids_listview
.append_column(col
)
159 self
.jids_listview
.get_selection().connect('changed',
160 self
.on_jids_listview_selection_changed
)
162 def _init_logs_listview(self
):
163 # log_line_id(HIDDEN), jid_id(HIDDEN), time, message, subject, nickname
164 self
.logs_liststore
= gtk
.ListStore(str, str, str, str, str, str)
165 self
.logs_listview
.set_model(self
.logs_liststore
)
166 self
.logs_listview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
168 renderer_text
= gtk
.CellRendererText() # holds time
169 col
= gtk
.TreeViewColumn(_('Date'), renderer_text
, text
=C_UNIXTIME
)
170 # user can click this header and sort
171 col
.set_sort_column_id(C_UNIXTIME
)
172 col
.set_resizable(True)
173 self
.logs_listview
.append_column(col
)
175 renderer_text
= gtk
.CellRendererText() # holds nickname
176 col
= gtk
.TreeViewColumn(_('Nickname'), renderer_text
, text
=C_NICKNAME
)
177 # user can click this header and sort
178 col
.set_sort_column_id(C_NICKNAME
)
179 col
.set_resizable(True)
180 col
.set_visible(False)
181 self
.nickname_col_for_logs
= col
182 self
.logs_listview
.append_column(col
)
184 renderer_text
= gtk
.CellRendererText() # holds message
185 col
= gtk
.TreeViewColumn(_('Message'), renderer_text
, markup
=C_MESSAGE
)
186 # user can click this header and sort
187 col
.set_sort_column_id(C_MESSAGE
)
188 col
.set_resizable(True)
189 self
.message_col_for_logs
= col
190 self
.logs_listview
.append_column(col
)
192 renderer_text
= gtk
.CellRendererText() # holds subject
193 col
= gtk
.TreeViewColumn(_('Subject'), renderer_text
, text
=C_SUBJECT
)
194 col
.set_sort_column_id(C_SUBJECT
) # user can click this header and sort
195 col
.set_resizable(True)
196 col
.set_visible(False)
197 self
.subject_col_for_logs
= col
198 self
.logs_listview
.append_column(col
)
200 def _init_search_results_listview(self
):
201 # log_line_id (HIDDEN), jid, time, message, subject, nickname
202 self
.search_results_liststore
= gtk
.ListStore(str, str, str, str, str,
204 self
.search_results_listview
.set_model(self
.search_results_liststore
)
206 renderer_text
= gtk
.CellRendererText() # holds JID (who said this)
207 col
= gtk
.TreeViewColumn(_('JID'), renderer_text
, text
=1)
208 col
.set_sort_column_id(1) # user can click this header and sort
209 col
.set_resizable(True)
210 self
.search_results_listview
.append_column(col
)
212 renderer_text
= gtk
.CellRendererText() # holds time
213 col
= gtk
.TreeViewColumn(_('Date'), renderer_text
, text
=C_UNIXTIME
)
214 # user can click this header and sort
215 col
.set_sort_column_id(C_UNIXTIME
)
216 col
.set_resizable(True)
217 self
.search_results_listview
.append_column(col
)
219 renderer_text
= gtk
.CellRendererText() # holds message
220 col
= gtk
.TreeViewColumn(_('Message'), renderer_text
, text
=C_MESSAGE
)
221 col
.set_sort_column_id(C_MESSAGE
) # user can click this header and sort
222 col
.set_resizable(True)
223 self
.search_results_listview
.append_column(col
)
225 renderer_text
= gtk
.CellRendererText() # holds subject
226 col
= gtk
.TreeViewColumn(_('Subject'), renderer_text
, text
=C_SUBJECT
)
227 col
.set_sort_column_id(C_SUBJECT
) # user can click this header and sort
228 col
.set_resizable(True)
229 self
.search_results_listview
.append_column(col
)
231 renderer_text
= gtk
.CellRendererText() # holds nickname
232 col
= gtk
.TreeViewColumn(_('Nickname'), renderer_text
, text
=C_NICKNAME
)
233 # user can click this header and sort
234 col
.set_sort_column_id(C_NICKNAME
)
235 col
.set_resizable(True)
236 self
.search_results_listview
.append_column(col
)
238 def on_history_manager_window_delete_event(self
, widget
, event
):
239 if not self
.AT_LEAST_ONE_DELETION_DONE
:
244 self
.cur
.execute('VACUUM')
251 dialog
= dialogs
.YesNoDialog(
252 _('Do you want to clean up the database? '
253 '(STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING)'),
254 _('Normally allocated database size will not be freed, '
255 'it will just become reusable. If you really want to reduce '
256 'database filesize, click YES, else click NO.'
257 '\n\nIn case you click YES, please wait...'),
258 on_response_yes
=on_yes
, on_response_no
=on_no
)
259 button_box
= dialog
.get_children()[0].get_children()[1]
260 button_box
.get_children()[0].grab_focus()
262 def _fill_jids_listview(self
):
263 # get those jids that have at least one entry in logs
264 self
.cur
.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN ('
265 'SELECT distinct logs.jid_id FROM logs) ORDER BY jid')
266 # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
267 rows
= self
.cur
.fetchall()
269 self
.jids_already_in
.append(row
[0]) # jid
270 self
.jids_liststore
.append(row
) # jid, jid_id
272 def on_jids_listview_selection_changed(self
, widget
, data
=None):
273 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
275 paths_len
= len(list_of_paths
)
276 if paths_len
== 0: # nothing is selected
279 self
.logs_liststore
.clear() # clear the store
281 self
.welcome_vbox
.hide()
282 self
.search_results_scrolledwindow
.hide()
283 self
.logs_scrolledwindow
.show()
286 for path
in list_of_paths
: # make them treerowrefs (it's needed)
287 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
289 for rowref
in list_of_rowrefs
: # FILL THE STORE, for all rows selected
290 path
= rowref
.get_path()
293 jid
= liststore
[path
][0].decode('utf-8') # jid
294 self
._fill
_logs
_listview
(jid
)
296 def _get_jid_id(self
, jid
):
298 jids table has jid and jid_id
299 logs table has log_id, jid_id, contact_name, time, kind, show, message
301 So to ask logs we need jid_id that matches our jid in jids table this
302 method wants jid and returns the jid_id for later sql-ing on logs
304 if jid
.find('/') != -1: # if it has a /
305 jid_is_from_pm
= self
._jid
_is
_from
_pm
(jid
)
306 if not jid_is_from_pm
: # it's normal jid with resource
307 jid
= jid
.split('/', 1)[0] # remove the resource
308 self
.cur
.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid
,))
309 jid_id
= self
.cur
.fetchone()[0]
312 def _get_jid_from_jid_id(self
, jid_id
):
314 jids table has jid and jid_id
316 This method accepts jid_id and returns the jid for later sql-ing on logs
318 self
.cur
.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id
,))
319 jid
= self
.cur
.fetchone()[0]
322 def _jid_is_from_pm(self
, jid
):
324 If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf
325 is not a normal guy and nkour is not his resource? We ask if gajim@conf
326 is already in jids (with type room jid). This fails if user disables
327 logging for room and only enables for pm (so higly unlikely) and if we
328 fail we do not go chaos (user will see the first pm as if it was message
329 in room's public chat) and after that everything is ok
331 possible_room_jid
= jid
.split('/', 1)[0]
333 self
.cur
.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
334 (possible_room_jid
, constants
.JID_ROOM_TYPE
))
335 row
= self
.cur
.fetchone()
341 def _jid_is_room_type(self
, jid
):
343 Return True/False if given id is room type or not eg. if it is room
345 self
.cur
.execute('SELECT type FROM jids WHERE jid = ?', (jid
,))
346 row
= self
.cur
.fetchone()
349 elif row
[0] == constants
.JID_ROOM_TYPE
:
354 def _fill_logs_listview(self
, jid
):
356 Fill the listview with all messages that user sent to or received from
359 # no need to lower jid in this context as jid is already lowered
360 # as we use those jids from db
361 jid_id
= self
._get
_jid
_id
(jid
)
363 SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
369 results
= self
.cur
.fetchall()
371 if self
._jid
_is
_room
_type
(jid
): # is it room?
372 self
.nickname_col_for_logs
.set_visible(True)
373 self
.subject_col_for_logs
.set_visible(False)
375 self
.nickname_col_for_logs
.set_visible(False)
376 self
.subject_col_for_logs
.set_visible(True)
379 # exposed in UI (TreeViewColumns) are only
380 # time, message, subject, nickname
381 # but store in liststore
382 # log_line_id, jid_id, time, message, subject, nickname
383 log_line_id
, jid_id
, time_
, kind
, message
, subject
, nickname
, \
386 time_
= time
.strftime('%x', time
.localtime(float(time_
))
387 ).decode(locale
.getpreferredencoding())
392 if kind
in (constants
.KIND_SINGLE_MSG_RECV
,
393 constants
.KIND_CHAT_MSG_RECV
, constants
.KIND_GC_MSG
):
394 # it is the other side
395 color
= gajim
.config
.get('inmsgcolor') # so incoming color
396 elif kind
in (constants
.KIND_SINGLE_MSG_SENT
,
397 constants
.KIND_CHAT_MSG_SENT
): # it is us
398 color
= gajim
.config
.get('outmsgcolor') # so outgoing color
399 elif kind
in (constants
.KIND_STATUS
,
400 constants
.KIND_GCSTATUS
): # is is statuses
402 color
= gajim
.config
.get('statusmsgcolor')
403 # include status into (status) message
407 message
= ' : ' + message
408 message
= helpers
.get_uf_show(gajim
.SHOW_LIST
[show
]) + \
413 message_
+= ' foreground="%s"' % color
414 message_
+= '>%s</span>' % \
415 gobject
.markup_escape_text(message
)
416 self
.logs_liststore
.append((log_line_id
, jid_id
, time_
,
417 message_
, subject
, nickname
))
419 def _fill_search_results_listview(self
, text
):
421 Ask db and fill listview with results that match text
423 self
.search_results_liststore
.clear()
424 like_sql
= '%' + text
+ '%'
426 SELECT log_line_id, jid_id, time, message, subject, contact_name
428 WHERE message LIKE ? OR subject LIKE ?
430 ''', (like_sql
, like_sql
))
432 results
= self
.cur
.fetchall()
434 # exposed in UI (TreeViewColumns) are only
435 # JID, time, message, subject, nickname
436 # but store in liststore
437 # log_line_id, jid (from jid_id), time, message, subject, nickname
438 log_line_id
, jid_id
, time_
, message
, subject
, nickname
= row
440 time_
= time
.strftime('%x', time
.localtime(float(time_
))
441 ).decode(locale
.getpreferredencoding())
445 jid
= self
._get
_jid
_from
_jid
_id
(jid_id
)
447 self
.search_results_liststore
.append((log_line_id
, jid
, time_
,
448 message
, subject
, nickname
))
450 def on_logs_listview_key_press_event(self
, widget
, event
):
451 liststore
, list_of_paths
= self
.logs_listview
.get_selection()\
453 if event
.keyval
== gtk
.keysyms
.Delete
:
454 self
._delete
_logs
(liststore
, list_of_paths
)
456 def on_listview_button_press_event(self
, widget
, event
):
457 if event
.button
== 3: # right click
458 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui',
460 if widget
.name
!= 'jids_listview':
461 xml
.get_object('export_menuitem').hide()
462 xml
.get_object('delete_menuitem').connect('activate',
463 self
.on_delete_menuitem_activate
, widget
)
465 xml
.connect_signals(self
)
466 xml
.get_object('context_menu').popup(None, None, None,
467 event
.button
, event
.time
)
470 def on_export_menuitem_activate(self
, widget
):
471 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui',
473 xml
.connect_signals(self
)
475 dlg
= xml
.get_object('filechooserdialog')
476 dlg
.set_title(_('Exporting History Logs...'))
477 dlg
.set_current_folder(gajim
.HOME_DIR
)
478 dlg
.props
.do_overwrite_confirmation
= True
481 if response
== gtk
.RESPONSE_OK
: # user want us to export ;)
482 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
484 path_to_file
= dlg
.get_filename()
485 self
._export
_jids
_logs
_to
_file
(liststore
, list_of_paths
,
490 def on_delete_menuitem_activate(self
, widget
, listview
):
491 widget_name
= gtk
.Buildable
.get_name(listview
)
492 liststore
, list_of_paths
= listview
.get_selection().get_selected_rows()
493 if widget_name
== 'jids_listview':
494 self
._delete
_jid
_logs
(liststore
, list_of_paths
)
495 elif widget_name
in ('logs_listview', 'search_results_listview'):
496 self
._delete
_logs
(liststore
, list_of_paths
)
497 else: # Huh ? We don't know this widget
500 def on_jids_listview_key_press_event(self
, widget
, event
):
501 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
503 if event
.keyval
== gtk
.keysyms
.Delete
:
504 self
._delete
_jid
_logs
(liststore
, list_of_paths
)
506 def _export_jids_logs_to_file(self
, liststore
, list_of_paths
, path_to_file
):
507 paths_len
= len(list_of_paths
)
508 if paths_len
== 0: # nothing is selected
512 for path
in list_of_paths
: # make them treerowrefs (it's needed)
513 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
515 for rowref
in list_of_rowrefs
:
516 path
= rowref
.get_path()
519 jid_id
= liststore
[path
][1]
521 SELECT time, kind, message, contact_name FROM logs
526 # FIXME: we may have two contacts selected to export. fix that
527 # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
528 results
= self
.cur
.fetchall()
530 file_
= open(path_to_file
, 'w')
532 # in store: time, kind, message, contact_name FROM logs
533 # in text: JID or You or nickname (if it's gc_msg), time, message
534 time_
, kind
, message
, nickname
= row
535 if kind
in (constants
.KIND_SINGLE_MSG_RECV
,
536 constants
.KIND_CHAT_MSG_RECV
):
537 who
= self
._get
_jid
_from
_jid
_id
(jid_id
)
538 elif kind
in (constants
.KIND_SINGLE_MSG_SENT
,
539 constants
.KIND_CHAT_MSG_SENT
):
541 elif kind
== constants
.KIND_GC_MSG
:
543 else: # status or gc_status. do not save
548 time_
= time
.strftime('%c', time
.localtime(float(time_
))
549 ).decode(locale
.getpreferredencoding())
553 file_
.write(_('%(who)s on %(time)s said: %(message)s\n') % {
554 'who': who
, 'time': time_
, 'message': message
})
556 def _delete_jid_logs(self
, liststore
, list_of_paths
):
557 paths_len
= len(list_of_paths
)
558 if paths_len
== 0: # nothing is selected
561 def on_ok(liststore
, list_of_paths
):
562 # delete all rows from db that match jid_id
564 for path
in list_of_paths
: # make them treerowrefs (it's needed)
565 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
567 for rowref
in list_of_rowrefs
:
568 path
= rowref
.get_path()
571 jid_id
= liststore
[path
][1]
572 del liststore
[path
] # remove from UI
579 # now delete "jid, jid_id" row from jids table
587 self
.AT_LEAST_ONE_DELETION_DONE
= True
589 pri_text
= i18n
.ngettext(
590 'Do you really want to delete logs of the selected contact?',
591 'Do you really want to delete logs of the selected contacts?',
593 dialog
= dialogs
.ConfirmationDialog(pri_text
,
594 _('This is an irreversible operation.'), on_response_ok
=(on_ok
,
595 liststore
, list_of_paths
))
596 ok_button
= dialog
.get_children()[0].get_children()[1].get_children()[0]
597 ok_button
.grab_focus()
598 dialog
.set_transient_for(self
.window
)
600 def _delete_logs(self
, liststore
, list_of_paths
):
601 paths_len
= len(list_of_paths
)
602 if paths_len
== 0: # nothing is selected
605 def on_ok(liststore
, list_of_paths
):
606 # delete rows from db that match log_line_id
608 for path
in list_of_paths
: # make them treerowrefs (it's needed)
609 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
611 for rowref
in list_of_rowrefs
:
612 path
= rowref
.get_path()
615 log_line_id
= liststore
[path
][0]
616 del liststore
[path
] # remove from UI
620 WHERE log_line_id = ?
625 self
.AT_LEAST_ONE_DELETION_DONE
= True
627 pri_text
= i18n
.ngettext(
628 'Do you really want to delete the selected message?',
629 'Do you really want to delete the selected messages?', paths_len
)
630 dialog
= dialogs
.ConfirmationDialog(pri_text
,
631 _('This is an irreversible operation.'), on_response_ok
=(on_ok
,
632 liststore
, list_of_paths
))
633 ok_button
= dialog
.get_children()[0].get_children()[1].get_children()[0]
634 ok_button
.grab_focus()
635 dialog
.set_transient_for(self
.window
)
637 def on_search_db_button_clicked(self
, widget
):
638 text
= self
.search_entry
.get_text().decode('utf-8')
642 self
.welcome_vbox
.hide()
643 self
.logs_scrolledwindow
.hide()
644 self
.search_results_scrolledwindow
.show()
646 self
._fill
_search
_results
_listview
(text
)
648 def on_search_results_listview_row_activated(self
, widget
, path
, column
):
649 # get log_line_id, jid_id from row we double clicked
650 log_line_id
= self
.search_results_liststore
[path
][0]
651 jid
= self
.search_results_liststore
[path
][1].decode('utf-8')
652 # make it string as in gtk liststores I have them all as strings
653 # as this is what db returns so I don't have to fight with types
654 jid_id
= self
._get
_jid
_id
(jid
)
656 iter_
= self
.jids_liststore
.get_iter_root()
658 # self.jids_liststore[iter_][1] holds jid_ids
659 if self
.jids_liststore
[iter_
][1] == jid_id
:
661 iter_
= self
.jids_liststore
.iter_next(iter_
)
666 path
= self
.jids_liststore
.get_path(iter_
)
667 self
.jids_listview
.set_cursor(path
)
669 iter_
= self
.logs_liststore
.get_iter_root()
671 # self.logs_liststore[iter_][0] holds lon_line_ids
672 if self
.logs_liststore
[iter_
][0] == log_line_id
:
674 iter_
= self
.logs_liststore
.iter_next(iter_
)
676 path
= self
.logs_liststore
.get_path(iter_
)
677 self
.logs_listview
.scroll_to_cell(path
)
679 if __name__
== '__main__':
680 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
) # ^C exits the application