# Copyright (C) 2003-2014 Yann Leboulanger # Copyright (C) 2005 Vincent Hanquez # Copyright (C) 2005-2006 Nikos Kouremenos # Copyright (C) 2006 Dimitur Kirov # Travis Shirk # Copyright (C) 2006-2008 Jean-Marie Traissard # Copyright (C) 2007-2008 Stephan Erb # Copyright (C) 2008 Brendan Taylor # # 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 . import time import datetime from enum import IntEnum, unique from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib from gajim import conversation_textview from gajim.gtk.dialogs import ErrorDialog from gajim.gtk import util from gajim.gtk.util import python_month, gtk_month from gajim.common import app from gajim.common import helpers from gajim.common import exceptions from gajim.common.const import ShowConstant, KindConstant @unique class InfoColumn(IntEnum): '''Completion dict''' JID = 0 ACCOUNT = 1 NAME = 2 COMPLETION = 3 @unique class Column(IntEnum): LOG_JID = 0 CONTACT_NAME = 1 UNIXTIME = 2 MESSAGE = 3 TIME = 4 LOG_LINE_ID = 5 class HistoryWindow: """ Class for browsing logs of conversations with contacts """ def __init__(self, jid=None, account=None): xml = util.get_builder('history_window.ui') self.window = xml.get_object('history_window') self.window.set_application(app.app) self.calendar = xml.get_object('calendar') self.button_first_day = xml.get_object('button_first_day') self.button_previous_day = xml.get_object('button_previous_day') self.button_next_day = xml.get_object('button_next_day') self.button_last_day = xml.get_object('button_last_day') scrolledwindow = xml.get_object('scrolledwindow') self.history_textview = conversation_textview.ConversationTextview( account, used_in_history_window=True) scrolledwindow.add(self.history_textview.tv) self.history_buffer = self.history_textview.tv.get_buffer() self.history_buffer.create_tag('highlight', background='yellow') self.history_buffer.create_tag('invisible', invisible=True) self.checkbutton = xml.get_object('log_history_checkbutton') self.show_status_checkbutton = xml.get_object('show_status_checkbutton') self.search_entry = xml.get_object('search_entry') self.query_liststore = xml.get_object('query_liststore') self.jid_entry = xml.get_object('query_entry') self.results_treeview = xml.get_object('results_treeview') self.results_window = xml.get_object('results_scrolledwindow') self.search_in_date = xml.get_object('search_in_date') self.date_label = xml.get_object('date_label') self.search_menu_button = xml.get_object('search_menu_button') self.clearing_search = False # jid, contact_name, date, message, time, log_line_id model = Gtk.ListStore(str, str, str, str, str, int) self.results_treeview.set_model(model) col = Gtk.TreeViewColumn(_('Name')) self.results_treeview.append_column(col) renderer = Gtk.CellRendererText() col.pack_start(renderer, True) col.add_attribute(renderer, 'text', Column.CONTACT_NAME) # user can click this header and sort col.set_sort_column_id(Column.CONTACT_NAME) col.set_resizable(True) col = Gtk.TreeViewColumn(_('Date')) self.results_treeview.append_column(col) renderer = Gtk.CellRendererText() col.pack_start(renderer, True) col.add_attribute(renderer, 'text', Column.UNIXTIME) # user can click this header and sort col.set_sort_column_id(Column.UNIXTIME) col.set_resizable(True) col = Gtk.TreeViewColumn(_('Message')) self.results_treeview.append_column(col) renderer = Gtk.CellRendererText() col.pack_start(renderer, True) col.add_attribute(renderer, 'text', Column.MESSAGE) col.set_resizable(True) self.jid = None # The history we are currently viewing self.account = account self.completion_dict = {} self.accounts_seen_online = [] # Update dict when new accounts connect self.jids_to_search = [] # This will load history too task = self._fill_completion_dict() GLib.idle_add(next, task) if jid: self.jid_entry.get_child().set_text(jid) else: self._load_history(None) util.resize_window(self.window, app.config.get('history_window_width'), app.config.get('history_window_height')) util.move_window(self.window, app.config.get('history_window_x-position'), app.config.get('history_window_y-position')) xml.connect_signals(self) self.window.show_all() # PluginSystem: adding GUI extension point for # HistoryWindow instance object app.plugin_manager.gui_extension_point( 'history_window', self) def _fill_completion_dict(self): """ Fill completion_dict for key auto completion. Then load history for current jid (by calling another function) Key will be either jid or full_completion_name (contact name or long description like "pm-contact from groupchat...."). {key : (jid, account, nick_name, full_completion_name} This is a generator and does pseudo-threading via idle_add(). """ liststore = util.get_completion_liststore( self.jid_entry.get_child()) liststore.set_sort_column_id(1, Gtk.SortType.ASCENDING) self.jid_entry.get_child().get_completion().connect( 'match-selected', self.on_jid_entry_match_selected) self.jid_entry.set_model(liststore) # Add all jids in logs.db: db_jids = app.logger.get_jids_in_db() completion_dict = dict.fromkeys(db_jids) self.accounts_seen_online = list(app.contacts.get_accounts()) # Enhance contacts of online accounts with contact. # Needed for mapping below for account in self.accounts_seen_online: completion_dict.update( helpers.get_contact_dict_for_account(account)) muc_active_icon = util.get_iconset_name_for('muc-active') online_icon = util.get_iconset_name_for('online') keys = list(completion_dict.keys()) # Move the actual jid at first so we load history faster actual_jid = self.jid_entry.get_child().get_text() if actual_jid in keys: keys.remove(actual_jid) keys.insert(0, actual_jid) if '' in keys: keys.remove('') if None in keys: keys.remove(None) # Map jid to info tuple # Warning : This for is time critical with big DB for key in keys: completed = key completed2 = None contact = completion_dict[completed] if contact: info_name = contact.get_shown_name() info_completion = info_name info_jid = contact.jid else: # Corresponding account is offline, we know nothing info_name = completed.split('@')[0] info_completion = completed info_jid = completed info_acc = self._get_account_for_jid(info_jid) if (app.logger.jid_is_room_jid(completed) or app.logger.jid_is_from_pm(completed)): icon = muc_active_icon if app.logger.jid_is_from_pm(completed): # It's PM. Make it easier to find room, nick = app.get_room_and_nick_from_fjid(completed) info_completion = '%s from %s' % (nick, room) completed = info_completion info_completion2 = '%s/%s' % (room, nick) completed2 = info_completion2 info_name = nick else: icon = online_icon if len(completed) > 70: completed = completed[:70] + '[\u2026]' liststore.append((icon, completed)) self.completion_dict[key] = ( info_jid, info_acc, info_name, info_completion) self.completion_dict[completed] = ( info_jid, info_acc, info_name, info_completion) if completed2: if len(completed2) > 70: completed2 = completed2[:70] + '[\u2026]' liststore.append((icon, completed2)) self.completion_dict[completed2] = ( info_jid, info_acc, info_name, info_completion2) if key == actual_jid: self._load_history(info_jid, self.account or info_acc) yield True keys.sort() yield False def _get_account_for_jid(self, jid): """ Return the corresponding account of the jid. May be None if an account could not be found """ accounts = app.contacts.get_accounts() account = None for acc in accounts: jid_list = app.contacts.get_jid_list(acc) gc_list = app.contacts.get_gc_list(acc) if jid in jid_list or jid in gc_list: account = acc break return account def on_history_window_delete_event(self, widget, *args): self.save_state() def on_history_window_destroy(self, widget): # PluginSystem: removing GUI extension points connected with # HistoryWindow instance object app.plugin_manager.remove_gui_extension_point( 'history_window', self) self.history_textview.del_handlers() del app.interface.instances['logs'] def on_history_window_key_press_event(self, widget, event): if event.keyval == Gdk.KEY_Escape: self.save_state() self.window.destroy() def on_jid_entry_match_selected(self, widget, model, iter_, *args): self._jid_entry_search(model[iter_][1]) return True def on_jid_entry_changed(self, widget): # only if selected from combobox jid = self.jid_entry.get_child().get_text() if jid == self.jid_entry.get_active_id(): self._jid_entry_search(jid) def on_jid_entry_activate(self, widget): self._jid_entry_search(self.jid_entry.get_child().get_text()) def _jid_entry_search(self, jid): self._load_history(jid, self.account) self.results_window.set_property('visible', False) def _load_history(self, jid_or_name, account=None): """ Load history for the given jid/name and show it """ if jid_or_name and jid_or_name in self.completion_dict: # a full qualified jid or a contact name was entered info_jid, info_account, _info_name, info_completion = self.completion_dict[jid_or_name] self.jids_to_search = [info_jid] self.jid = info_jid if account: self.account = account else: self.account = info_account if self.account is None: # We don't know account. Probably a gc not opened or an # account not connected. # Disable possibility to say if we want to log or not self.checkbutton.set_sensitive(False) else: # Are log disabled for account ? if self.account in app.config.get_per( 'accounts', self.account, 'no_log_for').split(' '): self.checkbutton.set_active(False) self.checkbutton.set_sensitive(False) else: # Are log disabled for jid ? log = True if self.jid in app.config.get_per( 'accounts', self.account, 'no_log_for').split(' '): log = False self.checkbutton.set_active(log) self.checkbutton.set_sensitive(True) self.jids_to_search = [info_jid] # Get first/last date we have logs with contact self.first_log = app.logger.get_first_date_that_has_logs( self.account, self.jid) self.first_day = self._get_date_from_timestamp(self.first_log) self.last_log = app.logger.get_last_date_that_has_logs( self.account, self.jid) self.last_day = self._get_date_from_timestamp(self.last_log) # Select logs for last date we have logs with contact self.search_menu_button.set_sensitive(True) month = gtk_month(self.last_day.month) self.calendar.select_month(month, self.last_day.year) self.calendar.select_day(self.last_day.day) self.button_previous_day.set_sensitive(True) self.button_next_day.set_sensitive(True) self.button_first_day.set_sensitive(True) self.button_last_day.set_sensitive(True) self.search_entry.set_sensitive(True) self.search_entry.grab_focus() self.jid_entry.get_child().set_text(info_completion) else: # neither a valid jid, nor an existing contact name was entered # we have got nothing to show or to search in self.jid = None self.account = None self.history_buffer.set_text('') # clear the buffer self.search_entry.set_sensitive(False) self.checkbutton.set_sensitive(False) self.search_menu_button.set_sensitive(False) self.calendar.clear_marks() self.button_previous_day.set_sensitive(False) self.button_next_day.set_sensitive(False) self.button_first_day.set_sensitive(False) self.button_last_day.set_sensitive(False) self.results_window.set_property('visible', False) def on_calendar_day_selected(self, widget): if not self.jid: return year, month, day = self.calendar.get_date() # integers month = python_month(month) date_str = datetime.date(year, month, day).strftime('%x') self.date_label.set_text(date_str) self._load_conversation(year, month, day) def on_calendar_month_changed(self, widget): """ Ask for days in this month, if they have logs it bolds them (marks them) """ if not self.jid: return year, month, _day = widget.get_date() # integers if year < 1900: widget.select_month(0, 1900) widget.select_day(1) return widget.clear_marks() month = python_month(month) try: log_days = app.logger.get_days_with_logs( self.account, self.jid, year, month) except exceptions.PysqliteOperationalError as error: ErrorDialog(_('Disk Error'), str(error)) return for date in log_days: widget.mark_day(date.day) def _get_date_from_timestamp(self, timestamp): # Conversion from timestamp to date log = time.localtime(timestamp) y, m, d = log[0], log[1], log[2] date = datetime.datetime(y, m, d) return date def _change_date(self, widget): # Get day selected in calendar y, m, d = self.calendar.get_date() py_m = python_month(m) _date = datetime.datetime(y, py_m, d) if widget is self.button_first_day: gtk_m = gtk_month(self.first_day.month) self.calendar.select_month(gtk_m, self.first_day.year) self.calendar.select_day(self.first_day.day) return if widget is self.button_last_day: gtk_m = gtk_month( self.last_day.month) self.calendar.select_month(gtk_m, self.last_day.year) self.calendar.select_day(self.last_day.day) return if widget is self.button_previous_day: end_date = self.first_day timedelta = datetime.timedelta(days=-1) if end_date >= _date: return elif widget is self.button_next_day: end_date = self.last_day timedelta = datetime.timedelta(days=1) if end_date <= _date: return # Iterate through days until log entry found or # supplied end_date (first_log / last_log) reached logs = None while logs is None: _date = _date + timedelta if _date == end_date: break try: logs = app.logger.get_date_has_logs( self.account, self.jid, _date) except exceptions.PysqliteOperationalError as e: ErrorDialog(_('Disk Error'), str(e)) return gtk_m = gtk_month(_date.month) self.calendar.select_month(gtk_m, _date.year) self.calendar.select_day(_date.day) def _get_string_show_from_constant_int(self, show): if show == ShowConstant.ONLINE: show = 'online' elif show == ShowConstant.CHAT: show = 'chat' elif show == ShowConstant.AWAY: show = 'away' elif show == ShowConstant.XA: show = 'xa' elif show == ShowConstant.DND: show = 'dnd' elif show == ShowConstant.OFFLINE: show = 'offline' return show def _load_conversation(self, year, month, day): """ Load the conversation between `self.jid` and `self.account` held on the given date into the history textbuffer. Values for `month` and `day` are 1-based. """ self.history_buffer.set_text('') self.last_time_printout = 0 show_status = self.show_status_checkbutton.get_active() date = datetime.datetime(year, month, day) conversation = app.logger.get_conversation_for_date( self.account, self.jid, date) for message in conversation: if not show_status and message.kind in (KindConstant.GCSTATUS, KindConstant.STATUS): continue self._add_message(message) def _add_message(self, msg): if not msg.message and msg.kind not in (KindConstant.STATUS, KindConstant.GCSTATUS): return tim = msg.time kind = msg.kind show = msg.show message = msg.message subject = msg.subject log_line_id = msg.log_line_id contact_name = msg.contact_name additional_data = msg.additional_data buf = self.history_buffer end_iter = buf.get_end_iter() # Make the beginning of every message searchable by its log_line_id buf.create_mark(str(log_line_id), end_iter, left_gravity=True) if app.config.get('print_time') == 'always': timestamp_str = app.config.get('time_stamp') timestamp_str = helpers.from_one_line(timestamp_str) tim = time.strftime(timestamp_str, time.localtime(float(tim))) buf.insert(end_iter, tim) elif app.config.get('print_time') == 'sometimes': every_foo_seconds = 60 * app.config.get( 'print_ichat_every_foo_minutes') seconds_passed = tim - self.last_time_printout if seconds_passed > every_foo_seconds: self.last_time_printout = tim tim = time.strftime('%X ', time.localtime(float(tim))) buf.insert_with_tags_by_name( end_iter, tim + '\n', 'time_sometimes') # print the encryption icon if kind in (KindConstant.CHAT_MSG_SENT, KindConstant.CHAT_MSG_RECV): self.history_textview.print_encryption_status( end_iter, additional_data) tag_name = '' tag_msg = '' show = self._get_string_show_from_constant_int(show) if kind == KindConstant.GC_MSG: tag_name = 'incoming' elif kind in (KindConstant.SINGLE_MSG_RECV, KindConstant.CHAT_MSG_RECV): contact_name = self.completion_dict[self.jid][InfoColumn.NAME] tag_name = 'incoming' tag_msg = 'incomingtxt' elif kind in (KindConstant.SINGLE_MSG_SENT, KindConstant.CHAT_MSG_SENT): if self.account: contact_name = app.nicks[self.account] else: # we don't have roster, we don't know our own nick, use first # account one (urk!) account = list(app.contacts.get_accounts())[0] contact_name = app.nicks[account] tag_name = 'outgoing' tag_msg = 'outgoingtxt' elif kind == KindConstant.GCSTATUS: # message here (if not None) is status message if message: message = _('%(nick)s is now %(status)s: %(status_msg)s') % { 'nick': contact_name, 'status': helpers.get_uf_show(show), 'status_msg': message} else: message = _('%(nick)s is now %(status)s') % { 'nick': contact_name, 'status': helpers.get_uf_show(show)} tag_msg = 'status' else: # 'status' # message here (if not None) is status message if show is None: # it means error if message: message = _('Error: %s') % message else: message = _('Error') elif message: message = _('Status is now: %(status)s: %(status_msg)s') % { 'status': helpers.get_uf_show(show), 'status_msg': message} else: message = _('Status is now: %(status)s') % { 'status': helpers.get_uf_show(show)} tag_msg = 'status' if message.startswith('/me ') or message.startswith('/me\n'): tag_msg = tag_name else: # do not do this if gcstats, avoid dupping contact_name # eg. nkour: nkour is now Offline if contact_name and kind != KindConstant.GCSTATUS: # add stuff before and after contact name before_str = app.config.get('before_nickname') before_str = helpers.from_one_line(before_str) after_str = app.config.get('after_nickname') after_str = helpers.from_one_line(after_str) format_ = before_str + contact_name + after_str + ' ' if tag_name: buf.insert_with_tags_by_name(end_iter, format_, tag_name) else: buf.insert(end_iter, format_) if subject: message = _('Subject: %s\n') % subject + message xhtml = None if message.startswith('