2 * Copyright (C) 2007 Mark Pustjens <pustjens@dds.nl>
3 * Copyright (C) 2010-2015 Petr Pavlu <setup@dagobah.cz>
5 * This file is part of CenterIM.
7 * CenterIM is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * CenterIM is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "Conversation.h"
24 #include "BuddyList.h"
25 #include "Conversations.h"
31 Conversation::Conversation(PurpleConversation
*conv_
)
32 : Window(0, 0, 80, 24), conv(conv_
), filename(NULL
), logfile(NULL
),
33 input_text_length(0), room_list(NULL
), room_list_line(NULL
)
37 setColorScheme("conversation");
39 view
= new CppConsUI::TextView(width
- 2, height
, true, true);
40 input
= new CppConsUI::TextEdit(width
- 2, height
);
41 input
->signal_text_change
.connect(
42 sigc::mem_fun(this, &Conversation::onInputTextChange
));
43 char *name
= g_strdup_printf("[%s] %s",
44 purple_account_get_protocol_name(purple_conversation_get_account(conv
)),
45 purple_conversation_get_name(conv
));
46 line
= new ConversationLine(name
);
48 addWidget(*view
, 1, 0);
49 addWidget(*input
, 1, 1);
50 addWidget(*line
, 0, height
);
52 PurpleConversationType type
= purple_conversation_get_type(conv_
);
53 if (type
== PURPLE_CONV_TYPE_CHAT
) {
54 room_list
= new ConversationRoomList(1, 1, conv_
);
55 room_list_line
= new CppConsUI::VerticalLine(1);
57 addWidget(*room_list
, 1, 0);
58 addWidget(*room_list_line
, 1, 0);
67 if (!(logfile
= g_io_channel_new_file(filename
, "a", &err
))) {
68 LOG
->error(_("Error opening conversation logfile '%s' (%s)."), filename
,
79 Conversation::~Conversation()
83 g_io_channel_unref(logfile
);
86 bool Conversation::processInput(const TermKeyKey
&key
)
88 if (view
->processInput(key
))
91 return Window::processInput(key
);
94 void Conversation::moveResize(int newx
, int newy
, int neww
, int newh
)
96 Window::moveResize(newx
, newy
, neww
, newh
);
98 int view_percentage
= purple_prefs_get_int(CONF_PREFIX
"/chat/partitioning");
99 view_percentage
= CLAMP(view_percentage
, 0, 100);
101 int view_height
= (newh
* view_percentage
) / 100;
105 int input_height
= newh
- view_height
- 1;
106 if (input_height
< 1)
109 int roomlist_percentage
=
110 purple_prefs_get_int(CONF_PREFIX
"/chat/roomlist_partitioning");
111 roomlist_percentage
= CLAMP(roomlist_percentage
, 0, 100);
113 int view_width
= neww
- 2;
115 view_width
= (view_width
* roomlist_percentage
) / 100;
117 view
->moveResize(1, 0, view_width
, view_height
);
119 input
->moveResize(1, view_height
+ 1, neww
- 2, input_height
);
120 line
->moveResize(0, view_height
, neww
, 1);
122 // place the room list if a conversation window
124 // +2 accounts for borders
125 room_list_line
->moveResize(view_width
+ 1, 0, 1, view_height
);
126 // give it some padding to make it line up
127 room_list
->moveResize(
128 view_width
+ 3, 0, neww
- view_width
- 3, view_height
);
132 bool Conversation::restoreFocus()
134 FOOTER
->setText(_("%s buddy list, %s main menu, "
135 "%s/%s/%s next/prev/act conv, %s send, %s expand"),
136 "centerim|buddylist", "centerim|generalmenu", "centerim|conversation-next",
137 "centerim|conversation-prev", "centerim|conversation-active",
138 "conversation|send", "centerim|conversation-expand");
140 return Window::restoreFocus();
143 void Conversation::ungrabFocus()
145 FOOTER
->setText(NULL
);
146 Window::ungrabFocus();
149 void Conversation::show()
151 /* Update the scrollbar setting. It is delayed until the conversation window
152 * is actually displayed, so screen lines recalculations in TextView (caused
153 * by changing the scrollbar setting) aren't triggered if it isn't really
155 view
->setScrollBar(!CENTERIM
->isEnabledExpandedConversationMode());
160 void Conversation::close()
164 /* Next line deletes this object. Don't touch any member variable after this
166 purple_conversation_destroy(conv
);
169 void Conversation::onScreenResized()
171 CppConsUI::Rect r
= CENTERIM
->getScreenArea(CenterIM::CHAT_AREA
);
172 // make room for conversations list
178 void Conversation::write(const char *name
, const char * /*alias*/,
179 const char *message
, PurpleMessageFlags flags
, time_t mtime
)
182 if (!(flags
& PURPLE_MESSAGE_SEND
) &&
183 purple_prefs_get_bool(CONF_PREFIX
"/chat/beep_on_msg"))
184 CppConsUI::Curses::beep();
186 // update the last_activity property
187 PurpleConversationType type
= purple_conversation_get_type(conv
);
188 time_t cur_time
= time(NULL
);
190 if (type
== PURPLE_CONV_TYPE_IM
) {
191 PurpleBlistNode
*bnode
=
192 PURPLE_BLIST_NODE(purple_find_buddy(purple_conversation_get_account(conv
),
193 purple_conversation_get_name(conv
)));
195 purple_blist_node_set_int(bnode
, "last_activity", cur_time
);
197 // inform the buddy list node that it should update its state
198 BUDDYLIST
->updateNode(bnode
);
206 if (flags
& PURPLE_MESSAGE_SEND
) {
208 mtype
= "MSG2"; // cim5 message
211 else if (flags
& PURPLE_MESSAGE_RECV
) {
213 mtype
= "MSG2"; // cim5 message
222 // write text into logfile
223 if (!(flags
& PURPLE_MESSAGE_NO_LOG
)) {
225 if (type
== PURPLE_CONV_TYPE_CHAT
)
226 log_msg
= g_strdup_printf("\f\n%s\n%s\n%lu\n%lu\n%s: %s\n", dir
, mtype
,
227 mtime
, cur_time
, name
, message
);
229 log_msg
= g_strdup_printf(
230 "\f\n%s\n%s\n%lu\n%lu\n%s\n", dir
, mtype
, mtime
, cur_time
, message
);
233 if (g_io_channel_write_chars(logfile
, log_msg
, -1, NULL
, &err
) !=
234 G_IO_STATUS_NORMAL
) {
236 _("Error writing to conversation logfile (%s)."), err
->message
);
239 if (g_io_channel_flush(logfile
, &err
) != G_IO_STATUS_NORMAL
) {
241 _("Error flushing conversation logfile (%s)."), err
->message
);
248 // we currently don't support displaying HTML in any way
249 char *nohtml
= stripHTML(message
);
251 // write text to the window
252 char *time
= extractTime(mtime
, cur_time
);
254 if (type
== PURPLE_CONV_TYPE_CHAT
)
255 msg
= g_strdup_printf("%s %s: %s", time
, name
, nohtml
);
257 msg
= g_strdup_printf("%s %s", time
, nohtml
);
258 view
->append(msg
, color
);
264 Conversation::ConversationLine::ConversationLine(const char *text_
)
265 : AbstractLine(AUTOSIZE
, 1)
269 text
= g_strdup(text_
);
270 text_width
= CppConsUI::Curses::onScreenWidth(text
);
273 Conversation::ConversationLine::~ConversationLine()
278 void Conversation::ConversationLine::draw(CppConsUI::Curses::ViewPort area
)
280 if (real_width
== 0 || real_height
!= 1)
284 if (text_width
+ 5 >= static_cast<unsigned>(real_width
))
287 l
= real_width
- text_width
- 5;
289 // use HorizontalLine colors
290 int attrs
= getColorPair("horizontalline", "line");
294 for (i
= 0; i
< l
; i
++)
295 area
.addLineChar(i
, 0, CppConsUI::Curses::LINE_HLINE
);
296 i
+= area
.addString(i
, 0, text
);
297 for (; i
< real_width
; i
++)
298 area
.addLineChar(i
, 0, CppConsUI::Curses::LINE_HLINE
);
303 char *Conversation::stripHTML(const char *str
) const
305 /* Almost copy&paste from libpurple/util.c:purple_markup_strip_html(), but
306 * this version doesn't convert tab character to a space. */
310 bool closing_td_p
= false;
312 const gchar
*cdata_close_tag
= NULL
, *ent
;
319 str2
= g_strdup(str
);
321 for (i
= 0, j
= 0; str2
[i
]; i
++) {
322 if (str2
[i
] == '<') {
323 if (cdata_close_tag
) {
324 // note: don't even assume any other tag is a tag in CDATA
325 if (g_ascii_strncasecmp(
326 str2
+ i
, cdata_close_tag
, !strlen(cdata_close_tag
))) {
327 i
+= strlen(cdata_close_tag
) - 1;
328 cdata_close_tag
= NULL
;
332 else if (!g_ascii_strncasecmp(str2
+ i
, "<td", 3) && closing_td_p
) {
336 else if (!g_ascii_strncasecmp(str2
+ i
, "</td>", 5)) {
341 closing_td_p
= false;
347 if (g_ascii_isspace(str2
[k
]))
350 /* Scan until we end the tag either implicitly (closed start tag) or
351 * explicitly, using a sloppy method (i.e., < or > inside quoted
352 * attributes will screw us up). */
353 while (str2
[k
] && str2
[k
] != '<' && str2
[k
] != '>')
356 /* If we've got an <a> tag with an href, save the address to print
358 if (!g_ascii_strncasecmp(str2
+ i
, "<a", 2) &&
359 g_ascii_isspace(str2
[i
+ 2])) {
360 int st
; // start of href, inclusive [
361 int end
; // end of href, exclusive )
363 // find start of href
364 for (st
= i
+ 3; st
< k
; st
++) {
365 if (!g_ascii_strncasecmp(str2
+ st
, "href=", 5)) {
367 if (str2
[st
] == '"' || str2
[st
] == '\'') {
374 // find end of address
375 for (end
= st
; end
< k
&& str2
[end
] != delim
; end
++) {
376 // all the work is done in the loop construct above
379 /* If there's an address, save it. If there was already one saved,
384 tmp
= g_strndup(str2
+ st
, end
- st
);
385 href
= purple_unescape_html(tmp
);
391 /* Replace </a> with an ascii representation of the address the link
392 * was pointing to. */
393 else if (href
&& !g_ascii_strncasecmp(str2
+ i
, "</a>", 4)) {
394 size_t hrlen
= strlen(href
);
396 /* Only insert the href if it's different from the CDATA.
397 * 7 == strlen("http://") */
398 if ((hrlen
!= (unsigned)(j
- href_st
) ||
399 strncmp(str2
+ href_st
, href
, hrlen
)) &&
400 (hrlen
!= (unsigned)(j
- href_st
+ 7) ||
401 strncmp(str2
+ href_st
, href
+ 7, hrlen
- 7))) {
404 g_memmove(str2
+ j
, href
, hrlen
);
412 /* Check for tags which should be mapped to newline (but ignore some
413 * of the tags at the beginning of the text) */
414 else if ((j
&& (!g_ascii_strncasecmp(str2
+ i
, "<p>", 3) ||
415 !g_ascii_strncasecmp(str2
+ i
, "<tr", 3) ||
416 !g_ascii_strncasecmp(str2
+ i
, "<hr", 3) ||
417 !g_ascii_strncasecmp(str2
+ i
, "<li", 3) ||
418 !g_ascii_strncasecmp(str2
+ i
, "<div", 4))) ||
419 !g_ascii_strncasecmp(str2
+ i
, "<br", 3) ||
420 !g_ascii_strncasecmp(str2
+ i
, "</table>", 8))
422 // check for tags which begin CDATA and need to be closed
423 #if 0 // FIXME.. option is end tag optional, we can't handle this right now
424 else if (!g_ascii_strncasecmp(str2
+ i
, "<option", 7))
426 // FIXME we should not do this if the OPTION is SELECT'd
427 cdata_close_tag
= "</option>";
430 else if (!g_ascii_strncasecmp(str2
+ i
, "<script", 7))
431 cdata_close_tag
= "</script>";
432 else if (!g_ascii_strncasecmp(str2
+ i
, "<style", 6))
433 cdata_close_tag
= "</style>";
434 // update the index and continue checking after the tag
435 i
= (str2
[k
] == '<' || str2
[k
] == '\0') ? k
- 1 : k
;
439 else if (cdata_close_tag
)
441 else if (!g_ascii_isspace(str2
[i
]))
444 if (str2
[i
] == '&' &&
445 (ent
= purple_markup_unescape_entity(str2
+ i
, &entlen
))) {
453 str2
[j
++] = g_ascii_isspace(str2
[i
]) && str
[i
] != '\t' ? ' ' : str2
[i
];
463 void Conversation::buildLogFilename()
465 PurpleAccount
*account
;
467 const char *proto_name
;
472 account
= purple_conversation_get_account(conv
);
473 prpl
= purple_find_prpl(purple_account_get_protocol_id(account
));
476 proto_name
= purple_account_get_protocol_name(account
);
478 acct_name
= g_strdup(purple_escape_filename(
479 purple_normalize(account
, purple_account_get_username(account
))));
481 name
= purple_conversation_get_name(conv
);
483 filename
= g_build_filename(purple_user_dir(), "clogs", proto_name
, acct_name
,
484 purple_escape_filename(purple_normalize(account
, name
)), NULL
);
486 dir
= g_path_get_dirname(filename
);
487 if (g_mkdir_with_parents(dir
, S_IRUSR
| S_IWUSR
| S_IXUSR
) == -1)
488 LOG
->error(_("Error creating directory '%s'."), dir
);
494 char *Conversation::extractTime(time_t sent_time
, time_t show_time
) const
496 // based on the extracttime() function from cim4
498 // convert to local time, note that localtime_r() shouldn't really fail
499 struct tm show_time_local
;
500 struct tm sent_time_local
;
501 if (!localtime_r(&show_time
, &show_time_local
))
502 memset(&show_time_local
, 0, sizeof(show_time_local
));
503 if (!localtime_r(&sent_time
, &sent_time_local
))
504 memset(&sent_time_local
, 0, sizeof(sent_time_local
));
507 char *t1
= g_strdup(purple_date_format_long(&show_time_local
));
508 char *t2
= g_strdup(purple_date_format_long(&sent_time_local
));
510 int tdiff
= abs(sent_time
- show_time
);
512 if (tdiff
> 5 && strcmp(t1
, t2
)) {
513 char *res
= g_strdup_printf("%s [%s]", t1
, t2
);
523 void Conversation::loadHistory()
529 if ((chan
= g_io_channel_new_file(filename
, "r", &err
)) == NULL
) {
530 LOG
->error(_("Error opening conversation logfile '%s' (%s)."), filename
,
535 // this should never fail
536 g_io_channel_set_encoding(chan
, NULL
, NULL
);
540 bool new_msg
= false;
541 // read conversation logfile line by line
543 (st
= g_io_channel_read_line(chan
, &line
, NULL
, NULL
, &err
)) ==
544 G_IO_STATUS_NORMAL
) {
548 if (strcmp(line
, "\f\n")) {
554 // parse direction (in/out)
555 if ((st
= g_io_channel_read_line(chan
, &line
, NULL
, NULL
, &err
)) !=
559 if (!strcmp(line
, "OUT\n"))
561 else if (!strcmp(line
, "IN\n"))
566 if ((st
= g_io_channel_read_line(chan
, &line
, NULL
, NULL
, &err
)) !=
570 if (!strcmp(line
, "MSG2\n"))
572 else if (!strcmp(line
, "OTHER\n")) {
579 if ((st
= g_io_channel_read_line(chan
, &line
, NULL
, NULL
, &err
)) !=
582 time_t sent_time
= atol(line
);
586 if ((st
= g_io_channel_read_line(chan
, &line
, NULL
, NULL
, &err
)) !=
589 time_t show_time
= atol(line
);
593 // cim5, read only one line and strip it off HTML
594 if ((st
= g_io_channel_read_line(chan
, &line
, NULL
, NULL
, &err
)) !=
599 if (!g_utf8_validate(line
, -1, NULL
)) {
601 LOG
->error(_("Invalid message detected in conversation logfile"
602 " '%s'. The message was skipped."),
607 // write text to the window
608 char *nohtml
= stripHTML(line
);
609 char *time
= extractTime(sent_time
, show_time
);
610 char *msg
= g_strdup_printf("%s %s", time
, nohtml
);
611 view
->append(msg
, color
);
618 // cim4, read multiple raw lines
621 while ((st
= g_io_channel_read_line(chan
, &line
, &length
, NULL
, &err
)) ==
622 G_IO_STATUS_NORMAL
&&
624 if (!strcmp(line
, "\f\n")) {
629 // strip '\r' if necessary
630 if (length
> 1 && line
[length
- 2] == '\r') {
631 line
[length
- 2] = '\n';
632 line
[length
- 1] = '\0';
644 if (!g_utf8_validate(msg
.c_str(), -1, NULL
)) {
645 LOG
->error(_("Invalid message detected in conversation logfile"
646 " '%s'. The message was skipped."),
651 // add the message to the window
652 char *time
= extractTime(sent_time
, show_time
);
653 char *final_msg
= g_strdup_printf("%s %s", time
, msg
.c_str());
654 view
->append(final_msg
, color
);
660 if (st
!= G_IO_STATUS_EOF
) {
661 LOG
->error(_("Error reading from conversation logfile '%s' (%s)."),
662 filename
, err
->message
);
665 g_io_channel_unref(chan
);
668 bool Conversation::processCommand(const char *raw
, const char *html
)
670 // check that it is a command
671 if (strncmp(raw
, "/", 1))
674 purple_conversation_write(conv
, "", html
, PURPLE_MESSAGE_NO_LOG
, time(NULL
));
677 // strip the prefix and execute the command
678 PurpleCmdStatus status
=
679 purple_cmd_do_command(conv
, raw
+ 1, html
+ 1, &error
);
683 case PURPLE_CMD_STATUS_OK
:
685 case PURPLE_CMD_STATUS_NOT_FOUND
:
686 // it isn't a valid command, send it as a message
689 case PURPLE_CMD_STATUS_WRONG_ARGS
:
690 purple_conversation_write(conv
, "",
691 _("Wrong number of arguments passed to the command."),
692 PURPLE_MESSAGE_NO_LOG
, time(NULL
));
694 case PURPLE_CMD_STATUS_FAILED
:
695 purple_conversation_write(conv
, "",
696 error
? error
: _("The command failed for an unknown reason."),
697 PURPLE_MESSAGE_NO_LOG
, time(NULL
));
699 case PURPLE_CMD_STATUS_WRONG_TYPE
:
700 if (purple_conversation_get_type(conv
) == PURPLE_CONV_TYPE_IM
)
701 purple_conversation_write(conv
, "",
702 _("The command only works in chats, not IMs."), PURPLE_MESSAGE_NO_LOG
,
705 purple_conversation_write(conv
, "",
706 _("The command only works in IMs, not chats."), PURPLE_MESSAGE_NO_LOG
,
709 case PURPLE_CMD_STATUS_WRONG_PRPL
:
710 purple_conversation_write(conv
, "",
711 _("The command does not work on this protocol."), PURPLE_MESSAGE_NO_LOG
,
721 void Conversation::onInputTextChange(CppConsUI::TextEdit
&activator
)
723 PurpleConvIm
*im
= PURPLE_CONV_IM(conv
);
727 if (!CONVERSATIONS
->getSendTypingPref()) {
728 input_text_length
= 0;
732 size_t old_text_length
= input_text_length
;
733 size_t new_text_length
= activator
.getTextLength();
734 input_text_length
= new_text_length
;
736 if (!new_text_length
) {
737 // all text is deleted, turn off typing
738 purple_conv_im_stop_send_typed_timeout(im
);
740 serv_send_typing(purple_conversation_get_gc(conv
),
741 purple_conversation_get_name(conv
), PURPLE_NOT_TYPING
);
745 purple_conv_im_stop_send_typed_timeout(im
);
746 purple_conv_im_start_send_typed_timeout(im
);
748 time_t again
= purple_conv_im_get_type_again(im
);
749 if ((!old_text_length
&& new_text_length
) || (again
&& time(NULL
) > again
)) {
750 // the first letter is inserted or update is required for typing status
751 unsigned int timeout
= serv_send_typing(purple_conversation_get_gc(conv
),
752 purple_conversation_get_name(conv
), PURPLE_TYPING
);
753 purple_conv_im_set_type_again(im
, timeout
);
757 void Conversation::actionSend()
759 const char *str
= input
->getText();
765 char *escaped
= purple_markup_escape_text(str
, strlen(str
));
766 char *html
= purple_strdup_withhtml(escaped
);
767 if (processCommand(str
, html
)) {
768 // the command was processed
771 PurpleConversationType type
= purple_conversation_get_type(conv
);
772 if (type
== PURPLE_CONV_TYPE_CHAT
)
773 purple_conv_chat_send(PURPLE_CONV_CHAT(conv
), html
);
774 else if (type
== PURPLE_CONV_TYPE_IM
)
775 purple_conv_im_send(PURPLE_CONV_IM(conv
), html
);
782 void Conversation::declareBindables()
784 declareBindable("conversation", "send",
785 sigc::mem_fun(this, &Conversation::actionSend
),
786 InputProcessor::BINDABLE_OVERRIDE
);
789 /* vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab : */