[kepi] ability to use subkeys. Fixes #6051
[gajim.git] / src / history_window.py
blob961b45f3be97ef6929dcd1053a55be11cc6082bd
1 # -*- coding:utf-8 -*-
2 ## src/history_window.py
3 ##
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2005 Vincent Hanquez <tab AT snarc.org>
6 ## Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
8 ## Travis Shirk <travis AT pobox.com>
9 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10 ## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
11 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
13 ## This file is part of Gajim.
15 ## Gajim is free software; you can redistribute it and/or modify
16 ## it under the terms of the GNU General Public License as published
17 ## by the Free Software Foundation; version 3 only.
19 ## Gajim is distributed in the hope that it will be useful,
20 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ## GNU General Public License for more details.
24 ## You should have received a copy of the GNU General Public License
25 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
28 import gtk
29 import gobject
30 import time
31 import calendar
33 import gtkgui_helpers
34 import conversation_textview
36 from common import gajim
37 from common import helpers
38 from common import exceptions
40 from common.logger import Constants
42 constants = Constants()
44 # Completion dict
46 C_INFO_JID,
47 C_INFO_ACCOUNT,
48 C_INFO_NAME,
49 C_INFO_COMPLETION
50 ) = range(4)
52 # contact_name, date, message, time
54 C_LOG_JID,
55 C_CONTACT_NAME,
56 C_UNIXTIME,
57 C_MESSAGE,
58 C_TIME
59 ) = range(5)
61 class HistoryWindow:
62 """
63 Class for browsing logs of conversations with contacts
64 """
66 def __init__(self, jid = None, account = None):
67 xml = gtkgui_helpers.get_gtk_builder('history_window.ui')
68 self.window = xml.get_object('history_window')
69 self.calendar = xml.get_object('calendar')
70 scrolledwindow = xml.get_object('scrolledwindow')
71 self.history_textview = conversation_textview.ConversationTextview(
72 account, used_in_history_window = True)
73 scrolledwindow.add(self.history_textview.tv)
74 self.history_buffer = self.history_textview.tv.get_buffer()
75 self.history_buffer.create_tag('highlight', background = 'yellow')
76 self.checkbutton = xml.get_object('log_history_checkbutton')
77 self.checkbutton.connect('toggled',
78 self.on_log_history_checkbutton_toggled)
79 self.query_entry = xml.get_object('query_entry')
80 self.query_combobox = xml.get_object('query_combobox')
81 self.jid_entry = self.query_combobox.child
82 self.jid_entry.connect('activate', self.on_jid_entry_activate)
83 self.query_combobox.set_active(0)
84 self.results_treeview = xml.get_object('results_treeview')
85 self.results_window = xml.get_object('results_scrolledwindow')
87 # contact_name, date, message, time
88 model = gtk.ListStore(str, str, str, str, str)
89 self.results_treeview.set_model(model)
90 col = gtk.TreeViewColumn(_('Name'))
91 self.results_treeview.append_column(col)
92 renderer = gtk.CellRendererText()
93 col.pack_start(renderer)
94 col.set_attributes(renderer, text = C_CONTACT_NAME)
95 col.set_sort_column_id(C_CONTACT_NAME) # user can click this header and sort
96 col.set_resizable(True)
98 col = gtk.TreeViewColumn(_('Date'))
99 self.results_treeview.append_column(col)
100 renderer = gtk.CellRendererText()
101 col.pack_start(renderer)
102 col.set_attributes(renderer, text = C_UNIXTIME)
103 col.set_sort_column_id(C_UNIXTIME) # user can click this header and sort
104 col.set_resizable(True)
106 col = gtk.TreeViewColumn(_('Message'))
107 self.results_treeview.append_column(col)
108 renderer = gtk.CellRendererText()
109 col.pack_start(renderer)
110 col.set_attributes(renderer, text = C_MESSAGE)
111 col.set_resizable(True)
113 self.jid = None # The history we are currently viewing
114 self.account = None
115 self.completion_dict = {}
116 self.accounts_seen_online = [] # Update dict when new accounts connect
117 self.jids_to_search = []
119 # This will load history too
120 gobject.idle_add(self._fill_completion_dict().next)
122 if jid:
123 self.jid_entry.set_text(jid)
124 else:
125 self._load_history(None)
127 gtkgui_helpers.resize_window(self.window,
128 gajim.config.get('history_window_width'),
129 gajim.config.get('history_window_height'))
130 gtkgui_helpers.move_window(self.window,
131 gajim.config.get('history_window_x-position'),
132 gajim.config.get('history_window_y-position'))
134 xml.connect_signals(self)
135 self.window.show_all()
137 def _fill_completion_dict(self):
139 Fill completion_dict for key auto completion. Then load history for
140 current jid (by calling another function)
142 Key will be either jid or full_completion_name (contact name or long
143 description like "pm-contact from groupchat....").
145 {key : (jid, account, nick_name, full_completion_name}
146 This is a generator and does pseudo-threading via idle_add().
148 liststore = gtkgui_helpers.get_completion_liststore(self.jid_entry)
150 # Add all jids in logs.db:
151 db_jids = gajim.logger.get_jids_in_db()
152 completion_dict = dict.fromkeys(db_jids)
154 self.accounts_seen_online = gajim.contacts.get_accounts()[:]
156 # Enhance contacts of online accounts with contact. Needed for mapping below
157 for account in self.accounts_seen_online:
158 completion_dict.update(helpers.get_contact_dict_for_account(account))
160 muc_active_img = gtkgui_helpers.load_icon('muc_active')
161 contact_img = gajim.interface.jabber_state_images['16']['online']
162 muc_active_pix = muc_active_img.get_pixbuf()
163 contact_pix = contact_img.get_pixbuf()
165 keys = completion_dict.keys()
166 # Move the actual jid at first so we load history faster
167 actual_jid = self.jid_entry.get_text().decode('utf-8')
168 if actual_jid in keys:
169 keys.remove(actual_jid)
170 keys.insert(0, actual_jid)
171 if None in keys:
172 keys.remove(None)
173 # Map jid to info tuple
174 # Warning : This for is time critical with big DB
175 for key in keys:
176 completed = key
177 contact = completion_dict[completed]
178 if contact:
179 info_name = contact.get_shown_name()
180 info_completion = info_name
181 info_jid = contact.jid
182 else:
183 # Corrensponding account is offline, we know nothing
184 info_name = completed.split('@')[0]
185 info_completion = completed
186 info_jid = completed
188 info_acc = self._get_account_for_jid(info_jid)
190 if gajim.logger.jid_is_room_jid(completed) or\
191 gajim.logger.jid_is_from_pm(completed):
192 pix = muc_active_pix
193 if gajim.logger.jid_is_from_pm(completed):
194 # It's PM. Make it easier to find
195 room, nick = gajim.get_room_and_nick_from_fjid(completed)
196 info_completion = '%s from %s' % (nick, room)
197 completed = info_completion
198 info_name = nick
199 else:
200 pix = contact_pix
202 liststore.append((pix, completed))
203 self.completion_dict[key] = (info_jid, info_acc, info_name,
204 info_completion)
205 self.completion_dict[completed] = (info_jid, info_acc,
206 info_name, info_completion)
207 if key == actual_jid:
208 self._load_history(info_jid, info_acc)
209 yield True
210 keys.sort()
211 yield False
213 def _get_account_for_jid(self, jid):
215 Return the corresponding account of the jid. May be None if an account
216 could not be found
218 accounts = gajim.contacts.get_accounts()
219 account = None
220 for acc in accounts:
221 jid_list = gajim.contacts.get_jid_list(acc)
222 gc_list = gajim.contacts.get_gc_list(acc)
223 if jid in jid_list or jid in gc_list:
224 account = acc
225 break
226 return account
228 def on_history_window_destroy(self, widget):
229 self.history_textview.del_handlers()
230 del gajim.interface.instances['logs']
232 def on_history_window_key_press_event(self, widget, event):
233 if event.keyval == gtk.keysyms.Escape:
234 self.save_state()
235 self.window.destroy()
237 def on_close_button_clicked(self, widget):
238 self.save_state()
239 self.window.destroy()
241 def on_jid_entry_activate(self, widget):
242 if not self.query_combobox.get_active() < 0:
243 # Don't disable querybox when we have changed the combobox
244 # to GC or All and hit enter
245 return
246 jid = self.jid_entry.get_text().decode('utf-8')
247 account = None # we don't know the account, could be any. Search for it!
248 self._load_history(jid, account)
249 self.results_window.set_property('visible', False)
251 def on_jid_entry_focus(self, widget, event):
252 widget.select_region(0, -1) # select text
254 def _load_history(self, jid_or_name, account=None):
256 Load history for the given jid/name and show it
258 if jid_or_name and jid_or_name in self.completion_dict:
259 # a full qualified jid or a contact name was entered
260 info_jid, info_account, info_name, info_completion = self.completion_dict[jid_or_name]
261 self.jids_to_search = [info_jid]
262 self.jid = info_jid
264 if account:
265 self.account = account
266 else:
267 self.account = info_account
268 if self.account is None:
269 # We don't know account. Probably a gc not opened or an
270 # account not connected.
271 # Disable possibility to say if we want to log or not
272 self.checkbutton.set_sensitive(False)
273 else:
274 # Are log disabled for account ?
275 if self.account in gajim.config.get_per('accounts', self.account,
276 'no_log_for').split(' '):
277 self.checkbutton.set_active(False)
278 self.checkbutton.set_sensitive(False)
279 else:
280 # Are log disabled for jid ?
281 log = True
282 if self.jid in gajim.config.get_per('accounts', self.account,
283 'no_log_for').split(' '):
284 log = False
285 self.checkbutton.set_active(log)
286 self.checkbutton.set_sensitive(True)
288 self.jids_to_search = [info_jid]
290 # select logs for last date we have logs with contact
291 self.calendar.set_sensitive(True)
292 last_log = \
293 gajim.logger.get_last_date_that_has_logs(self.jid, self.account)
295 date = time.localtime(last_log)
297 y, m, d = date[0], date[1], date[2]
298 gtk_month = gtkgui_helpers.make_python_month_gtk_month(m)
299 self.calendar.select_month(gtk_month, y)
300 self.calendar.select_day(d)
302 self.query_entry.set_sensitive(True)
303 self.query_entry.grab_focus()
305 title = _('Conversation History with %s') % info_name
306 self.window.set_title(title)
307 self.jid_entry.set_text(info_completion)
309 else: # neither a valid jid, nor an existing contact name was entered
310 # we have got nothing to show or to search in
311 self.jid = None
312 self.account = None
314 self.history_buffer.set_text('') # clear the buffer
315 self.query_entry.set_sensitive(False)
317 self.checkbutton.set_sensitive(False)
318 self.calendar.set_sensitive(False)
319 self.calendar.clear_marks()
321 self.results_window.set_property('visible', False)
323 title = _('Conversation History')
324 self.window.set_title(title)
326 def on_calendar_day_selected(self, widget):
327 if not self.jid:
328 return
329 year, month, day = widget.get_date() # integers
330 month = gtkgui_helpers.make_gtk_month_python_month(month)
331 self._add_lines_for_date(year, month, day)
333 def on_calendar_month_changed(self, widget):
335 Ask for days in this month, if they have logs it bolds them (marks them)
337 if not self.jid:
338 return
339 year, month, day = widget.get_date() # integers
340 if year < 1900:
341 widget.select_month(0, 1900)
342 widget.select_day(1)
343 return
345 # in gtk January is 1, in python January is 0,
346 # I want the second
347 # first day of month is 1 not 0
348 widget.clear_marks()
349 month = gtkgui_helpers.make_gtk_month_python_month(month)
350 days_in_this_month = calendar.monthrange(year, month)[1]
351 try:
352 log_days = gajim.logger.get_days_with_logs(self.jid, year, month,
353 days_in_this_month, self.account)
354 except exceptions.PysqliteOperationalError, e:
355 dialogs.ErrorDialog(_('Disk Error'), str(e))
356 return
357 for day in log_days:
358 widget.mark_day(day)
360 def _get_string_show_from_constant_int(self, show):
361 if show == constants.SHOW_ONLINE:
362 show = 'online'
363 elif show == constants.SHOW_CHAT:
364 show = 'chat'
365 elif show == constants.SHOW_AWAY:
366 show = 'away'
367 elif show == constants.SHOW_XA:
368 show = 'xa'
369 elif show == constants.SHOW_DND:
370 show = 'dnd'
371 elif show == constants.SHOW_OFFLINE:
372 show = 'offline'
374 return show
376 def _add_lines_for_date(self, year, month, day):
378 Add all the lines for given date in textbuffer
380 self.history_buffer.set_text('') # clear the buffer first
381 self.last_time_printout = 0
383 lines = gajim.logger.get_conversation_for_date(self.jid, year, month, day, self.account)
384 # lines holds list with tupples that have:
385 # contact_name, time, kind, show, message
386 for line in lines:
387 # line[0] is contact_name, line[1] is time of message
388 # line[2] is kind, line[3] is show, line[4] is message
389 self._add_new_line(line[0], line[1], line[2], line[3], line[4],
390 line[5])
392 def _add_new_line(self, contact_name, tim, kind, show, message, subject):
394 Add a new line in textbuffer
396 if not message and kind not in (constants.KIND_STATUS,
397 constants.KIND_GCSTATUS):
398 return
399 buf = self.history_buffer
400 end_iter = buf.get_end_iter()
402 if gajim.config.get('print_time') == 'always':
403 timestamp_str = gajim.config.get('time_stamp')
404 timestamp_str = helpers.from_one_line(timestamp_str)
405 tim = time.strftime(timestamp_str, time.localtime(float(tim)))
406 buf.insert(end_iter, tim) # add time
407 elif gajim.config.get('print_time') == 'sometimes':
408 every_foo_seconds = 60 * gajim.config.get(
409 'print_ichat_every_foo_minutes')
410 seconds_passed = tim - self.last_time_printout
411 if seconds_passed > every_foo_seconds:
412 self.last_time_printout = tim
413 tim = time.strftime('%X ', time.localtime(float(tim)))
414 buf.insert_with_tags_by_name(end_iter, tim + '\n',
415 'time_sometimes')
417 tag_name = ''
418 tag_msg = ''
420 show = self._get_string_show_from_constant_int(show)
422 if kind == constants.KIND_GC_MSG:
423 tag_name = 'incoming'
424 elif kind in (constants.KIND_SINGLE_MSG_RECV,
425 constants.KIND_CHAT_MSG_RECV):
426 contact_name = self.completion_dict[self.jid][C_INFO_NAME]
427 tag_name = 'incoming'
428 tag_msg = 'incomingtxt'
429 elif kind in (constants.KIND_SINGLE_MSG_SENT,
430 constants.KIND_CHAT_MSG_SENT):
431 if self.account:
432 contact_name = gajim.nicks[self.account]
433 else:
434 # we don't have roster, we don't know our own nick, use first
435 # account one (urk!)
436 account = gajim.contacts.get_accounts()[0]
437 contact_name = gajim.nicks[account]
438 tag_name = 'outgoing'
439 tag_msg = 'outgoingtxt'
440 elif kind == constants.KIND_GCSTATUS:
441 # message here (if not None) is status message
442 if message:
443 message = _('%(nick)s is now %(status)s: %(status_msg)s') %\
444 {'nick': contact_name, 'status': helpers.get_uf_show(show),
445 'status_msg': message }
446 else:
447 message = _('%(nick)s is now %(status)s') % {'nick': contact_name,
448 'status': helpers.get_uf_show(show) }
449 tag_msg = 'status'
450 else: # 'status'
451 # message here (if not None) is status message
452 if show is None: # it means error
453 if message:
454 message = _('Error: %s') % message
455 else:
456 message = _('Error')
457 elif message:
458 message = _('Status is now: %(status)s: %(status_msg)s') % \
459 {'status': helpers.get_uf_show(show), 'status_msg': message}
460 else:
461 message = _('Status is now: %(status)s') % { 'status':
462 helpers.get_uf_show(show) }
463 tag_msg = 'status'
465 if message.startswith('/me ') or message.startswith('/me\n'):
466 tag_msg = tag_name
467 else:
468 # do not do this if gcstats, avoid dupping contact_name
469 # eg. nkour: nkour is now Offline
470 if contact_name and kind != constants.KIND_GCSTATUS:
471 # add stuff before and after contact name
472 before_str = gajim.config.get('before_nickname')
473 before_str = helpers.from_one_line(before_str)
474 after_str = gajim.config.get('after_nickname')
475 after_str = helpers.from_one_line(after_str)
476 format = before_str + contact_name + after_str + ' '
477 buf.insert_with_tags_by_name(end_iter, format, tag_name)
479 if subject:
480 message = _('Subject: %s\n') % subject + message
481 message += '\n'
482 if tag_msg:
483 self.history_textview.print_real_text(message, [tag_msg],
484 name=contact_name)
485 else:
486 self.history_textview.print_real_text(message, name=contact_name)
488 def on_query_entry_activate(self, widget):
489 text = self.query_entry.get_text()
490 model = self.results_treeview.get_model()
491 model.clear()
492 if text == '':
493 self.results_window.set_property('visible', False)
494 return
495 else:
496 self.results_window.set_property('visible', True)
498 # perform search in preselected jids
499 # jids are preselected with the query_combobox (all, single jid...)
500 for jid in self.jids_to_search:
501 account = self.completion_dict[jid][C_INFO_ACCOUNT]
502 if account is None:
503 # We do not know an account. This can only happen if the contact is offine,
504 # or if we browse a groupchat history. The account is not needed, a dummy can
505 # be set.
506 # This may leed to wrong self nick in the displayed history (Uggh!)
507 account = gajim.contacts.get_accounts()[0]
509 # contact_name, time, kind, show, message, subject
510 results = gajim.logger.get_search_results_for_query(
511 jid, text, account)
512 #FIXME:
513 # add "subject: | message: " in message column if kind is single
514 # also do we need show at all? (we do not search on subject)
515 for row in results:
516 contact_name = row[0]
517 if not contact_name:
518 kind = row[2]
519 if kind == constants.KIND_CHAT_MSG_SENT: # it's us! :)
520 contact_name = gajim.nicks[account]
521 else:
522 contact_name = self.completion_dict[jid][C_INFO_NAME]
523 tim = row[1]
524 message = row[4]
525 local_time = time.localtime(tim)
526 date = time.strftime('%Y-%m-%d', local_time)
528 # jid (to which log is assigned to), name, date, message,
529 # time (full unix time)
530 model.append((jid, contact_name, date, message, tim))
532 def on_query_combobox_changed(self, widget):
533 if self.query_combobox.get_active() < 0:
534 return # custom entry
535 self.account = None
536 self.jid = None
537 self.jids_to_search = []
538 self._load_history(None) # clear textview
540 if self.query_combobox.get_active() == 0:
541 # JID or Contact name
542 self.query_entry.set_sensitive(False)
543 self.jid_entry.grab_focus()
544 if self.query_combobox.get_active() == 1:
545 # Groupchat Histories
546 self.query_entry.set_sensitive(True)
547 self.query_entry.grab_focus()
548 self.jids_to_search = (jid for jid in gajim.logger.get_jids_in_db()
549 if gajim.logger.jid_is_room_jid(jid))
550 if self.query_combobox.get_active() == 2:
551 # All Chat Histories
552 self.query_entry.set_sensitive(True)
553 self.query_entry.grab_focus()
554 self.jids_to_search = gajim.logger.get_jids_in_db()
556 def on_results_treeview_row_activated(self, widget, path, column):
558 A row was double clicked, get date from row, and select it in calendar
559 which results to showing conversation logs for that date
561 # get currently selected date
562 cur_year, cur_month = self.calendar.get_date()[0:2]
563 cur_month = gtkgui_helpers.make_gtk_month_python_month(cur_month)
564 model = widget.get_model()
565 # make it a tupple (Y, M, D, 0, 0, 0...)
566 tim = time.strptime(model[path][C_UNIXTIME], '%Y-%m-%d')
567 year = tim[0]
568 gtk_month = tim[1]
569 month = gtkgui_helpers.make_python_month_gtk_month(gtk_month)
570 day = tim[2]
572 # switch to belonging logfile if necessary
573 log_jid = model[path][C_LOG_JID]
574 if log_jid != self.jid:
575 self._load_history(log_jid, None)
577 # avoid reruning mark days algo if same month and year!
578 if year != cur_year or gtk_month != cur_month:
579 self.calendar.select_month(month, year)
581 self.calendar.select_day(day)
582 unix_time = model[path][C_TIME]
583 self._scroll_to_result(unix_time)
584 #FIXME: one day do not search just for unix_time but the whole and user
585 # specific format of the textbuffer line [time] nick: message
586 # and highlight all that
588 def _scroll_to_result(self, unix_time):
590 Scroll to the result using unix_time and highlight line
592 start_iter = self.history_buffer.get_start_iter()
593 local_time = time.localtime(float(unix_time))
594 tim = time.strftime('%X', local_time)
595 result = start_iter.forward_search(tim, gtk.TEXT_SEARCH_VISIBLE_ONLY,
596 None)
597 if result is not None:
598 match_start_iter, match_end_iter = result
599 match_start_iter.backward_char() # include '[' or other character before time
600 match_end_iter.forward_line() # highlight all message not just time
601 self.history_buffer.apply_tag_by_name('highlight', match_start_iter,
602 match_end_iter)
604 match_start_mark = self.history_buffer.create_mark('match_start',
605 match_start_iter, True)
606 self.history_textview.tv.scroll_to_mark(match_start_mark, 0, True)
608 def on_log_history_checkbutton_toggled(self, widget):
609 # log conversation history?
610 oldlog = True
611 no_log_for = gajim.config.get_per('accounts', self.account,
612 'no_log_for').split()
613 if self.jid in no_log_for:
614 oldlog = False
615 log = widget.get_active()
616 if not log and not self.jid in no_log_for:
617 no_log_for.append(self.jid)
618 if log and self.jid in no_log_for:
619 no_log_for.remove(self.jid)
620 if oldlog != log:
621 gajim.config.set_per('accounts', self.account, 'no_log_for',
622 ' '.join(no_log_for))
624 def open_history(self, jid, account):
626 Load chat history of the specified jid
628 self.jid_entry.set_text(jid)
629 if account and account not in self.accounts_seen_online:
630 # Update dict to not only show bare jid
631 gobject.idle_add(self._fill_completion_dict().next)
632 else:
633 # Only in that case because it's called by self._fill_completion_dict()
634 # otherwise
635 self._load_history(jid, account)
636 self.results_window.set_property('visible', False)
638 def save_state(self):
639 x, y = self.window.window.get_root_origin()
640 width, height = self.window.get_size()
642 gajim.config.set('history_window_x-position', x)
643 gajim.config.set('history_window_y-position', y)
644 gajim.config.set('history_window_width', width);
645 gajim.config.set('history_window_height', height);
647 gajim.interface.save_config()