# -*- coding:utf-8 -*- ## src/history_window.py ## ## 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 . ## from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib import time import calendar from enum import IntEnum, unique import gtkgui_helpers import conversation_textview import dialogs from common import gajim from common import helpers from common import exceptions from common.logger 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 = gtkgui_helpers.get_gtk_builder('history_window.ui') self.window = xml.get_object('history_window') self.calendar = xml.get_object('calendar') 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.checkbutton.connect('toggled', self.on_log_history_checkbutton_toggled) 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.jid_entry.connect('activate', self.on_jid_entry_activate) 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') # 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) col.set_sort_column_id(Column.CONTACT_NAME) # user can click this header and sort 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) col.set_sort_column_id(Column.UNIXTIME) # user can click this header and sort 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 = None 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.set_text(jid) else: self._load_history(None) gtkgui_helpers.resize_window(self.window, gajim.config.get('history_window_width'), gajim.config.get('history_window_height')) gtkgui_helpers.move_window(self.window, gajim.config.get('history_window_x-position'), gajim.config.get('history_window_y-position')) xml.connect_signals(self) self.window.show_all() 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 = gtkgui_helpers.get_completion_liststore(self.jid_entry) # Add all jids in logs.db: db_jids = gajim.logger.get_jids_in_db() completion_dict = dict.fromkeys(db_jids) self.accounts_seen_online = list(gajim.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_img = gtkgui_helpers.load_icon('muc_active') contact_img = gajim.interface.jabber_state_images['16']['online'] muc_active_pix = muc_active_img.get_pixbuf() contact_pix = contact_img.get_pixbuf() keys = list(completion_dict.keys()) # Move the actual jid at first so we load history faster actual_jid = self.jid_entry.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: # Corrensponding 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 gajim.logger.jid_is_room_jid(completed) or\ gajim.logger.jid_is_from_pm(completed): pix = muc_active_pix if gajim.logger.jid_is_from_pm(completed): # It's PM. Make it easier to find room, nick = gajim.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: pix = contact_pix if len(completed) > 70: completed = completed[:70] + '[\u2026]' liststore.append((pix, 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((pix, completed2)) self.completion_dict[completed2] = (info_jid, info_acc, info_name, info_completion2) if key == actual_jid: self._load_history(info_jid, 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 = gajim.contacts.get_accounts() account = None for acc in accounts: jid_list = gajim.contacts.get_jid_list(acc) gc_list = gajim.contacts.get_gc_list(acc) if jid in jid_list or jid in gc_list: account = acc break return account def on_history_window_destroy(self, widget): self.history_textview.del_handlers() del gajim.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_close_button_clicked(self, widget): self.save_state() self.window.destroy() def on_jid_entry_activate(self, widget): jid = self.jid_entry.get_text() account = None # we don't know the account, could be any. Search for it! self._load_history(jid, account) self.results_window.set_property('visible', False) def on_jid_entry_focus(self, widget, event): widget.select_region(0, -1) # select text 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 gajim.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 gajim.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] # select logs for last date we have logs with contact self.calendar.set_sensitive(True) last_log = \ gajim.logger.get_last_date_that_has_logs(self.jid, self.account) date = time.localtime(last_log) y, m, d = date[0], date[1], date[2] gtk_month = gtkgui_helpers.make_python_month_gtk_month(m) self.calendar.select_month(gtk_month, y) self.calendar.select_day(d) self.search_entry.set_sensitive(True) self.search_entry.grab_focus() title = _('Conversation History with %s') % info_name self.window.set_title(title) self.jid_entry.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.calendar.set_sensitive(False) self.calendar.clear_marks() self.results_window.set_property('visible', False) title = _('Conversation History') self.window.set_title(title) def on_calendar_day_selected(self, widget): if not self.jid: return year, month, day = self.calendar.get_date() # integers month = gtkgui_helpers.make_gtk_month_python_month(month) 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 # in gtk January is 1, in python January is 0, # I want the second # first day of month is 1 not 0 widget.clear_marks() month = gtkgui_helpers.make_gtk_month_python_month(month) days_in_this_month = calendar.monthrange(year, month)[1] try: log_days = gajim.logger.get_days_with_logs(self.jid, year, month, days_in_this_month, self.account) except exceptions.PysqliteOperationalError as e: dialogs.ErrorDialog(_('Disk Error'), str(e)) return for day in log_days: widget.mark_day(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() conversation = gajim.logger.get_conversation_for_date( self.jid, year, month, day, self.account) 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 gajim.config.get('print_time') == 'always': timestamp_str = gajim.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 gajim.config.get('print_time') == 'sometimes': every_foo_seconds = 60 * gajim.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') 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 = gajim.nicks[self.account] else: # we don't have roster, we don't know our own nick, use first # account one (urk!) account = list(gajim.contacts.get_accounts())[0] contact_name = gajim.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 = gajim.config.get('before_nickname') before_str = helpers.from_one_line(before_str) after_str = gajim.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('