2 ## src/history_window.py
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/>.
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()
52 # contact_name, date, message, time
63 Class for browsing logs of conversations with contacts
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
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
)
123 self
.jid_entry
.set_text(jid
)
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
)
173 # Map jid to info tuple
174 # Warning : This for is time critical with big DB
177 contact
= completion_dict
[completed
]
179 info_name
= contact
.get_shown_name()
180 info_completion
= info_name
181 info_jid
= contact
.jid
183 # Corrensponding account is offline, we know nothing
184 info_name
= completed
.split('@')[0]
185 info_completion
= 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
):
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
202 liststore
.append((pix
, completed
))
203 self
.completion_dict
[key
] = (info_jid
, info_acc
, info_name
,
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
)
213 def _get_account_for_jid(self
, jid
):
215 Return the corresponding account of the jid. May be None if an account
218 accounts
= gajim
.contacts
.get_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
:
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
:
235 self
.window
.destroy()
237 def on_close_button_clicked(self
, widget
):
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
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
]
265 self
.account
= account
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)
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)
280 # Are log disabled for jid ?
282 if self
.jid
in gajim
.config
.get_per('accounts', self
.account
,
283 'no_log_for').split(' '):
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)
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
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
):
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)
339 year
, month
, day
= widget
.get_date() # integers
341 widget
.select_month(0, 1900)
345 # in gtk January is 1, in python January is 0,
347 # first day of month is 1 not 0
349 month
= gtkgui_helpers
.make_gtk_month_python_month(month
)
350 days_in_this_month
= calendar
.monthrange(year
, month
)[1]
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
))
360 def _get_string_show_from_constant_int(self
, show
):
361 if show
== constants
.SHOW_ONLINE
:
363 elif show
== constants
.SHOW_CHAT
:
365 elif show
== constants
.SHOW_AWAY
:
367 elif show
== constants
.SHOW_XA
:
369 elif show
== constants
.SHOW_DND
:
371 elif show
== constants
.SHOW_OFFLINE
:
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
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],
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
):
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',
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
):
432 contact_name
= gajim
.nicks
[self
.account
]
434 # we don't have roster, we don't know our own nick, use first
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
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
}
447 message
= _('%(nick)s is now %(status)s') % {'nick': contact_name
,
448 'status': helpers
.get_uf_show(show
) }
451 # message here (if not None) is status message
452 if show
is None: # it means error
454 message
= _('Error: %s') % message
458 message
= _('Status is now: %(status)s: %(status_msg)s') % \
459 {'status': helpers
.get_uf_show(show
), 'status_msg': message
}
461 message
= _('Status is now: %(status)s') % { 'status':
462 helpers
.get_uf_show(show
) }
465 if message
.startswith('/me ') or message
.startswith('/me\n'):
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
)
480 message
= _('Subject: %s\n') % subject
+ message
483 self
.history_textview
.print_real_text(message
, [tag_msg
],
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()
493 self
.results_window
.set_property('visible', False)
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
]
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
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(
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)
516 contact_name
= row
[0]
519 if kind
== constants
.KIND_CHAT_MSG_SENT
: # it's us! :)
520 contact_name
= gajim
.nicks
[account
]
522 contact_name
= self
.completion_dict
[jid
][C_INFO_NAME
]
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
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:
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')
569 month
= gtkgui_helpers
.make_python_month_gtk_month(gtk_month
)
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
,
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
,
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?
611 no_log_for
= gajim
.config
.get_per('accounts', self
.account
,
612 'no_log_for').split()
613 if self
.jid
in no_log_for
:
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
)
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
)
633 # Only in that case because it's called by self._fill_completion_dict()
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()