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 that abstracts db connections and helpers on it
28 ## the same can be said for history_window.py
34 warnings
.filterwarnings(action
='ignore')
36 if os
.path
.isdir('gtk'):
37 # Used to create windows installer with GTK included
38 paths
= os
.environ
['PATH']
39 list_
= paths
.split(';')
42 if p
.find('gtk') < 0 and p
.find('GTK') < 0:
44 new_list
.insert(0, 'gtk/lib')
45 new_list
.insert(0, 'gtk/bin')
46 os
.environ
['PATH'] = ';'.join(new_list
)
47 os
.environ
['GTK_BASEPATH'] = 'gtk'
57 from common
import i18n
64 longargs
= 'help config_path='
65 opts
= getopt
.getopt(sys
.argv
[1:], shortargs
, longargs
.split())[0]
66 except getopt
.error
, msg
:
68 print 'for help use --help'
71 if o
in ('-h', '--help'):
72 print 'history_manager [--help] [--config-path]'
74 elif o
in ('-c', '--config-path'):
78 config_path
= parseOpts()
81 import common
.configpaths
82 common
.configpaths
.gajimpaths
.init(config_path
)
84 common
.configpaths
.gajimpaths
.init_profile()
85 from common
import exceptions
86 from common
import gajim
88 from common
.logger
import LOG_DB_PATH
, constants
90 #FIXME: constants should implement 2 way mappings
91 status
= dict((constants
.__dict
__[i
], i
[5:].lower()) for i
in \
92 constants
.__dict
__.keys() if i
.startswith('SHOW_'))
93 from common
import helpers
96 # time, message, subject
105 import sqlite3
as sqlite
108 class HistoryManager
:
110 pix
= gtkgui_helpers
.get_icon_pixmap('gajim')
111 gtk
.window_set_default_icon(pix
) # set the icon to all newly opened windows
113 if not os
.path
.exists(LOG_DB_PATH
):
114 dialogs
.ErrorDialog(_('Cannot find history logs database'),
115 '%s does not exist.' % LOG_DB_PATH
)
118 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui')
119 self
.window
= xml
.get_object('history_manager_window')
120 self
.jids_listview
= xml
.get_object('jids_listview')
121 self
.logs_listview
= xml
.get_object('logs_listview')
122 self
.search_results_listview
= xml
.get_object('search_results_listview')
123 self
.search_entry
= xml
.get_object('search_entry')
124 self
.logs_scrolledwindow
= xml
.get_object('logs_scrolledwindow')
125 self
.search_results_scrolledwindow
= xml
.get_object(
126 'search_results_scrolledwindow')
127 self
.welcome_vbox
= xml
.get_object('welcome_vbox')
129 self
.jids_already_in
= [] # holds jids that we already have in DB
130 self
.AT_LEAST_ONE_DELETION_DONE
= False
132 self
.con
= sqlite
.connect(LOG_DB_PATH
, timeout
= 20.0,
133 isolation_level
= 'IMMEDIATE')
134 self
.cur
= self
.con
.cursor()
136 self
._init
_jids
_listview
()
137 self
._init
_logs
_listview
()
138 self
._init
_search
_results
_listview
()
140 self
._fill
_jids
_listview
()
142 self
.search_entry
.grab_focus()
144 self
.window
.show_all()
146 xml
.connect_signals(self
)
148 def _init_jids_listview(self
):
149 self
.jids_liststore
= gtk
.ListStore(str, str) # jid, jid_id
150 self
.jids_listview
.set_model(self
.jids_liststore
)
151 self
.jids_listview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
153 renderer_text
= gtk
.CellRendererText() # holds jid
154 col
= gtk
.TreeViewColumn(_('Contacts'), renderer_text
, text
= 0)
155 self
.jids_listview
.append_column(col
)
157 self
.jids_listview
.get_selection().connect('changed',
158 self
.on_jids_listview_selection_changed
)
160 def _init_logs_listview(self
):
161 # log_line_id (HIDDEN), jid_id (HIDDEN), time, message, subject, nickname
162 self
.logs_liststore
= gtk
.ListStore(str, str, str, str, str, str)
163 self
.logs_listview
.set_model(self
.logs_liststore
)
164 self
.logs_listview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
166 renderer_text
= gtk
.CellRendererText() # holds time
167 col
= gtk
.TreeViewColumn(_('Date'), renderer_text
, text
= C_UNIXTIME
)
168 col
.set_sort_column_id(C_UNIXTIME
) # user can click this header and sort
169 col
.set_resizable(True)
170 self
.logs_listview
.append_column(col
)
172 renderer_text
= gtk
.CellRendererText() # holds nickname
173 col
= gtk
.TreeViewColumn(_('Nickname'), renderer_text
, text
= C_NICKNAME
)
174 col
.set_sort_column_id(C_NICKNAME
) # user can click this header and sort
175 col
.set_resizable(True)
176 col
.set_visible(False)
177 self
.nickname_col_for_logs
= col
178 self
.logs_listview
.append_column(col
)
180 renderer_text
= gtk
.CellRendererText() # holds message
181 col
= gtk
.TreeViewColumn(_('Message'), renderer_text
, markup
= C_MESSAGE
)
182 col
.set_sort_column_id(C_MESSAGE
) # user can click this header and sort
183 col
.set_resizable(True)
184 self
.message_col_for_logs
= col
185 self
.logs_listview
.append_column(col
)
187 renderer_text
= gtk
.CellRendererText() # holds subject
188 col
= gtk
.TreeViewColumn(_('Subject'), renderer_text
, text
= C_SUBJECT
)
189 col
.set_sort_column_id(C_SUBJECT
) # user can click this header and sort
190 col
.set_resizable(True)
191 col
.set_visible(False)
192 self
.subject_col_for_logs
= col
193 self
.logs_listview
.append_column(col
)
195 def _init_search_results_listview(self
):
196 # log_line_id (HIDDEN), jid, time, message, subject, nickname
197 self
.search_results_liststore
= gtk
.ListStore(str, str, str, str, str, str)
198 self
.search_results_listview
.set_model(self
.search_results_liststore
)
200 renderer_text
= gtk
.CellRendererText() # holds JID (who said this)
201 col
= gtk
.TreeViewColumn(_('JID'), renderer_text
, text
= 1)
202 col
.set_sort_column_id(1) # user can click this header and sort
203 col
.set_resizable(True)
204 self
.search_results_listview
.append_column(col
)
206 renderer_text
= gtk
.CellRendererText() # holds time
207 col
= gtk
.TreeViewColumn(_('Date'), renderer_text
, text
= C_UNIXTIME
)
208 col
.set_sort_column_id(C_UNIXTIME
) # 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 message
213 col
= gtk
.TreeViewColumn(_('Message'), renderer_text
, text
= C_MESSAGE
)
214 col
.set_sort_column_id(C_MESSAGE
) # user can click this header and sort
215 col
.set_resizable(True)
216 self
.search_results_listview
.append_column(col
)
218 renderer_text
= gtk
.CellRendererText() # holds subject
219 col
= gtk
.TreeViewColumn(_('Subject'), renderer_text
, text
= C_SUBJECT
)
220 col
.set_sort_column_id(C_SUBJECT
) # user can click this header and sort
221 col
.set_resizable(True)
222 self
.search_results_listview
.append_column(col
)
224 renderer_text
= gtk
.CellRendererText() # holds nickname
225 col
= gtk
.TreeViewColumn(_('Nickname'), renderer_text
, text
= C_NICKNAME
)
226 col
.set_sort_column_id(C_NICKNAME
) # user can click this header and sort
227 col
.set_resizable(True)
228 self
.search_results_listview
.append_column(col
)
230 def on_history_manager_window_delete_event(self
, widget
, event
):
231 if self
.AT_LEAST_ONE_DELETION_DONE
:
233 self
.cur
.execute('VACUUM')
241 _('Do you want to clean up the database? '
242 '(STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING)'),
243 _('Normally allocated database size will not be freed, '
244 'it will just become reusable. If you really want to reduce '
245 'database filesize, click YES, else click NO.'
246 '\n\nIn case you click YES, please wait...'),
247 on_response_yes
=on_yes
, on_response_no
=on_no
)
252 def _fill_jids_listview(self
):
253 # get those jids that have at least one entry in logs
254 self
.cur
.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN (SELECT '
255 'distinct logs.jid_id FROM logs) ORDER BY jid')
256 rows
= self
.cur
.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
258 self
.jids_already_in
.append(row
[0]) # jid
259 self
.jids_liststore
.append(row
) # jid, jid_id
261 def on_jids_listview_selection_changed(self
, widget
, data
= None):
262 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
264 paths_len
= len(list_of_paths
)
265 if paths_len
== 0: # nothing is selected
268 self
.logs_liststore
.clear() # clear the store
270 self
.welcome_vbox
.hide()
271 self
.search_results_scrolledwindow
.hide()
272 self
.logs_scrolledwindow
.show()
275 for path
in list_of_paths
: # make them treerowrefs (it's needed)
276 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
278 for rowref
in list_of_rowrefs
: # FILL THE STORE, for all rows selected
279 path
= rowref
.get_path()
282 jid
= liststore
[path
][0].decode('utf-8') # jid
283 self
._fill
_logs
_listview
(jid
)
285 def _get_jid_id(self
, jid
):
287 jids table has jid and jid_id
288 logs table has log_id, jid_id, contact_name, time, kind, show, message
290 So to ask logs we need jid_id that matches our jid in jids table this
291 method wants jid and returns the jid_id for later sql-ing on logs
293 if jid
.find('/') != -1: # if it has a /
294 jid_is_from_pm
= self
._jid
_is
_from
_pm
(jid
)
295 if not jid_is_from_pm
: # it's normal jid with resource
296 jid
= jid
.split('/', 1)[0] # remove the resource
297 self
.cur
.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid
,))
298 jid_id
= self
.cur
.fetchone()[0]
301 def _get_jid_from_jid_id(self
, jid_id
):
303 jids table has jid and jid_id
305 This method accepts jid_id and returns the jid for later sql-ing on logs
307 self
.cur
.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id
,))
308 jid
= self
.cur
.fetchone()[0]
311 def _jid_is_from_pm(self
, jid
):
313 If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf
314 is not a normal guy and nkour is not his resource? We ask if gajim@conf
315 is already in jids (with type room jid). This fails if user disables
316 logging for room and only enables for pm (so higly unlikely) and if we
317 fail we do not go chaos (user will see the first pm as if it was message
318 in room's public chat) and after that everything is ok
320 possible_room_jid
= jid
.split('/', 1)[0]
322 self
.cur
.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
323 (possible_room_jid
, constants
.JID_ROOM_TYPE
))
324 row
= self
.cur
.fetchone()
330 def _jid_is_room_type(self
, jid
):
332 Return True/False if given id is room type or not eg. if it is room
334 self
.cur
.execute('SELECT type FROM jids WHERE jid = ?', (jid
,))
335 row
= self
.cur
.fetchone()
338 elif row
[0] == constants
.JID_ROOM_TYPE
:
343 def _fill_logs_listview(self
, jid
):
345 Fill the listview with all messages that user sent to or received from
348 # no need to lower jid in this context as jid is already lowered
349 # as we use those jids from db
350 jid_id
= self
._get
_jid
_id
(jid
)
352 SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
358 results
= self
.cur
.fetchall()
360 if self
._jid
_is
_room
_type
(jid
): # is it room?
361 self
.nickname_col_for_logs
.set_visible(True)
362 self
.subject_col_for_logs
.set_visible(False)
364 self
.nickname_col_for_logs
.set_visible(False)
365 self
.subject_col_for_logs
.set_visible(True)
368 # exposed in UI (TreeViewColumns) are only
369 # time, message, subject, nickname
370 # but store in liststore
371 # log_line_id, jid_id, time, message, subject, nickname
372 log_line_id
, jid_id
, time_
, kind
, message
, subject
, nickname
, show
= row
374 time_
= time
.strftime('%x', time
.localtime(float(time_
))).decode(
375 locale
.getpreferredencoding())
380 if kind
in (constants
.KIND_SINGLE_MSG_RECV
,
381 constants
.KIND_CHAT_MSG_RECV
, constants
.KIND_GC_MSG
):
382 # it is the other side
383 color
= gajim
.config
.get('inmsgcolor') # so incoming color
384 elif kind
in (constants
.KIND_SINGLE_MSG_SENT
,
385 constants
.KIND_CHAT_MSG_SENT
): # it is us
386 color
= gajim
.config
.get('outmsgcolor') # so outgoing color
387 elif kind
in (constants
.KIND_STATUS
,
388 constants
.KIND_GCSTATUS
): # is is statuses
389 color
= gajim
.config
.get('statusmsgcolor') # so status color
390 # include status into (status) message
394 message
= ' : ' + message
395 message
= helpers
.get_uf_show(gajim
.SHOW_LIST
[show
]) + message
399 message_
+= ' foreground="%s"' % color
400 message_
+= '>%s</span>' % \
401 gobject
.markup_escape_text(message
)
402 self
.logs_liststore
.append((log_line_id
, jid_id
, time_
, message_
,
405 def _fill_search_results_listview(self
, text
):
407 Ask db and fill listview with results that match text
409 self
.search_results_liststore
.clear()
410 like_sql
= '%' + text
+ '%'
412 SELECT log_line_id, jid_id, time, message, subject, contact_name
414 WHERE message LIKE ? OR subject LIKE ?
416 ''', (like_sql
, like_sql
))
418 results
= self
.cur
.fetchall()
420 # exposed in UI (TreeViewColumns) are only
421 # JID, time, message, subject, nickname
422 # but store in liststore
423 # log_line_id, jid (from jid_id), time, message, subject, nickname
424 log_line_id
, jid_id
, time_
, message
, subject
, nickname
= row
426 time_
= time
.strftime('%x', time
.localtime(float(time_
))).decode(
427 locale
.getpreferredencoding())
431 jid
= self
._get
_jid
_from
_jid
_id
(jid_id
)
433 self
.search_results_liststore
.append((log_line_id
, jid
, time_
,
434 message
, subject
, nickname
))
436 def on_logs_listview_key_press_event(self
, widget
, event
):
437 liststore
, list_of_paths
= self
.logs_listview
.get_selection()\
439 if event
.keyval
== gtk
.keysyms
.Delete
:
440 self
._delete
_logs
(liststore
, list_of_paths
)
442 def on_listview_button_press_event(self
, widget
, event
):
443 if event
.button
== 3: # right click
444 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui', 'context_menu')
445 if widget
.name
!= 'jids_listview':
446 xml
.get_object('export_menuitem').hide()
447 xml
.get_object('delete_menuitem').connect('activate',
448 self
.on_delete_menuitem_activate
, widget
)
450 xml
.connect_signals(self
)
451 xml
.get_object('context_menu').popup(None, None, None,
452 event
.button
, event
.time
)
455 def on_export_menuitem_activate(self
, widget
):
456 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui', 'filechooserdialog')
457 xml
.connect_signals(self
)
459 dlg
= xml
.get_object('filechooserdialog')
460 dlg
.set_title(_('Exporting History Logs...'))
461 dlg
.set_current_folder(gajim
.HOME_DIR
)
462 dlg
.props
.do_overwrite_confirmation
= True
465 if response
== gtk
.RESPONSE_OK
: # user want us to export ;)
466 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
468 path_to_file
= dlg
.get_filename()
469 self
._export
_jids
_logs
_to
_file
(liststore
, list_of_paths
, path_to_file
)
473 def on_delete_menuitem_activate(self
, widget
, listview
):
474 widget_name
= gtk
.Buildable
.get_name(listview
)
475 liststore
, list_of_paths
= listview
.get_selection().get_selected_rows()
476 if widget_name
== 'jids_listview':
477 self
._delete
_jid
_logs
(liststore
, list_of_paths
)
478 elif widget_name
in ('logs_listview', 'search_results_listview'):
479 self
._delete
_logs
(liststore
, list_of_paths
)
480 else: # Huh ? We don't know this widget
483 def on_jids_listview_key_press_event(self
, widget
, event
):
484 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
486 if event
.keyval
== gtk
.keysyms
.Delete
:
487 self
._delete
_jid
_logs
(liststore
, list_of_paths
)
489 def _export_jids_logs_to_file(self
, liststore
, list_of_paths
, path_to_file
):
490 paths_len
= len(list_of_paths
)
491 if paths_len
== 0: # nothing is selected
495 for path
in list_of_paths
: # make them treerowrefs (it's needed)
496 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
498 for rowref
in list_of_rowrefs
:
499 path
= rowref
.get_path()
502 jid_id
= liststore
[path
][1]
504 SELECT time, kind, message, contact_name FROM logs
509 # FIXME: we may have two contacts selected to export. fix that
510 # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
511 results
= self
.cur
.fetchall()
513 file_
= open(path_to_file
, 'w')
515 # in store: time, kind, message, contact_name FROM logs
516 # in text: JID or You or nickname (if it's gc_msg), time, message
517 time_
, kind
, message
, nickname
= row
518 if kind
in (constants
.KIND_SINGLE_MSG_RECV
,
519 constants
.KIND_CHAT_MSG_RECV
):
520 who
= self
._get
_jid
_from
_jid
_id
(jid_id
)
521 elif kind
in (constants
.KIND_SINGLE_MSG_SENT
,
522 constants
.KIND_CHAT_MSG_SENT
):
524 elif kind
== constants
.KIND_GC_MSG
:
526 else: # status or gc_status. do not save
531 time_
= time
.strftime('%c', time
.localtime(float(time_
))).decode(
532 locale
.getpreferredencoding())
536 file_
.write(_('%(who)s on %(time)s said: %(message)s\n') % {'who': who
,
537 'time': time_
, 'message': message
})
539 def _delete_jid_logs(self
, liststore
, list_of_paths
):
540 paths_len
= len(list_of_paths
)
541 if paths_len
== 0: # nothing is selected
544 def on_ok(liststore
, list_of_paths
):
545 # delete all rows from db that match jid_id
547 for path
in list_of_paths
: # make them treerowrefs (it's needed)
548 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
550 for rowref
in list_of_rowrefs
:
551 path
= rowref
.get_path()
554 jid_id
= liststore
[path
][1]
555 del liststore
[path
] # remove from UI
562 # now delete "jid, jid_id" row from jids table
570 self
.AT_LEAST_ONE_DELETION_DONE
= True
572 pri_text
= i18n
.ngettext(
573 'Do you really want to delete logs of the selected contact?',
574 'Do you really want to delete logs of the selected contacts?',
576 dialogs
.ConfirmationDialog(pri_text
,
577 _('This is an irreversible operation.'), on_response_ok
= (on_ok
,
578 liststore
, list_of_paths
))
580 def _delete_logs(self
, liststore
, list_of_paths
):
581 paths_len
= len(list_of_paths
)
582 if paths_len
== 0: # nothing is selected
585 def on_ok(liststore
, list_of_paths
):
586 # delete rows from db that match log_line_id
588 for path
in list_of_paths
: # make them treerowrefs (it's needed)
589 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
591 for rowref
in list_of_rowrefs
:
592 path
= rowref
.get_path()
595 log_line_id
= liststore
[path
][0]
596 del liststore
[path
] # remove from UI
600 WHERE log_line_id = ?
605 self
.AT_LEAST_ONE_DELETION_DONE
= True
608 pri_text
= i18n
.ngettext(
609 'Do you really want to delete the selected message?',
610 'Do you really want to delete the selected messages?', paths_len
)
611 dialogs
.ConfirmationDialog(pri_text
,
612 _('This is an irreversible operation.'), on_response_ok
= (on_ok
,
613 liststore
, list_of_paths
))
615 def on_search_db_button_clicked(self
, widget
):
616 text
= self
.search_entry
.get_text().decode('utf-8')
620 self
.welcome_vbox
.hide()
621 self
.logs_scrolledwindow
.hide()
622 self
.search_results_scrolledwindow
.show()
624 self
._fill
_search
_results
_listview
(text
)
626 def on_search_results_listview_row_activated(self
, widget
, path
, column
):
627 # get log_line_id, jid_id from row we double clicked
628 log_line_id
= self
.search_results_liststore
[path
][0]
629 jid
= self
.search_results_liststore
[path
][1].decode('utf-8')
630 # make it string as in gtk liststores I have them all as strings
631 # as this is what db returns so I don't have to fight with types
632 jid_id
= self
._get
_jid
_id
(jid
)
635 iter_
= self
.jids_liststore
.get_iter_root()
637 # self.jids_liststore[iter_][1] holds jid_ids
638 if self
.jids_liststore
[iter_
][1] == jid_id
:
640 iter_
= self
.jids_liststore
.iter_next(iter_
)
645 path
= self
.jids_liststore
.get_path(iter_
)
646 self
.jids_listview
.set_cursor(path
)
648 iter_
= self
.logs_liststore
.get_iter_root()
650 # self.logs_liststore[iter_][0] holds lon_line_ids
651 if self
.logs_liststore
[iter_
][0] == log_line_id
:
653 iter_
= self
.logs_liststore
.iter_next(iter_
)
655 path
= self
.logs_liststore
.get_path(iter_
)
656 self
.logs_listview
.scroll_to_cell(path
)
658 if __name__
== '__main__':
659 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
) # ^C exits the application