1 // Copyright (C) 2007 Mark Pustjens <pustjens@dds.nl>
2 // Copyright (C) 2010-2015 Petr Pavlu <setup@dagobah.cz>
4 // This file is part of CenterIM.
6 // CenterIM is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
11 // CenterIM is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with CenterIM. If not, see <http://www.gnu.org/licenses/>.
19 #include "Conversation.h"
21 #include "BuddyList.h"
22 #include "Conversations.h"
26 #include <cppconsui/ColorScheme.h>
31 Conversation::Conversation(PurpleConversation
*conv
)
32 : Window(0, 0, 80, 24), conv_(conv
), filename_(nullptr), logfile_(nullptr),
33 input_text_length_(0), room_list_(nullptr), room_list_line_(nullptr)
35 g_assert(conv_
!= nullptr);
37 setColorScheme(CenterIM::SCHEME_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);
66 GError
*err
= nullptr;
67 logfile_
= g_io_channel_new_file(filename_
, "a", &err
);
68 if (logfile_
== nullptr) {
69 LOG
->error(_("Error opening conversation logfile '%s' (%s)."), filename_
,
80 Conversation::~Conversation()
83 if (logfile_
!= nullptr)
84 g_io_channel_unref(logfile_
);
87 bool Conversation::processInput(const TermKeyKey
&key
)
89 if (view_
->processInput(key
))
92 return Window::processInput(key
);
95 void Conversation::moveResize(int newx
, int newy
, int neww
, int newh
)
97 Window::moveResize(newx
, newy
, neww
, newh
);
99 int view_percentage
= purple_prefs_get_int(CONF_PREFIX
"/chat/partitioning");
100 view_percentage
= CLAMP(view_percentage
, 0, 100);
102 // Calculate inner area.
103 neww
= neww
< 2 ? 0 : neww
- 2;
104 newh
= newh
< 2 ? 0 : newh
- 2;
106 // ,- room_list_line_
108 // ,-----------------------------,
109 // | view_ | room_list_ |
112 // |-----------------------------|<- line_
114 // '-----------------------------'
116 int view_height
= (newh
* view_percentage
) / 100;
118 int roomlist_percentage
=
119 purple_prefs_get_int(CONF_PREFIX
"/chat/roomlist_partitioning");
120 roomlist_percentage
= CLAMP(roomlist_percentage
, 0, 100);
122 int view_width
= neww
;
123 if (room_list_
!= nullptr)
124 view_width
= (view_width
* roomlist_percentage
) / 100;
126 view_
->moveResize(1, 1, view_width
, view_height
);
127 line_
->moveResize(1, 1 + view_height
, neww
, 1);
128 input_
->moveResize(1, 1 + view_height
+ 1, neww
, newh
- view_height
- 2);
130 // Place the room list if it exists.
131 if (room_list_
!= nullptr) {
132 room_list_line_
->moveResize(1 + view_width
, 1, 1, view_height
);
133 room_list_
->moveResize(
134 1 + view_width
+ 1, 1, neww
- view_width
- 2, view_height
);
138 bool Conversation::restoreFocus()
140 FOOTER
->setText(_("%s buddy list, %s main menu, "
141 "%s/%s/%s next/prev/act conv, %s send, %s expand"),
142 "centerim|buddylist", "centerim|generalmenu", "centerim|conversation-next",
143 "centerim|conversation-prev", "centerim|conversation-active",
144 "conversation|send", "centerim|conversation-expand");
146 return Window::restoreFocus();
149 void Conversation::ungrabFocus()
151 FOOTER
->setText(nullptr);
152 Window::ungrabFocus();
155 void Conversation::show()
157 // Update the scrollbar setting. It is delayed until the conversation window
158 // is actually displayed, so screen lines recalculations in TextView (caused
159 // by changing the scrollbar setting) are not triggered if it is not really
161 view_
->setScrollBar(!CENTERIM
->isEnabledExpandedConversationMode());
166 void Conversation::close()
170 // Next line deletes this object. Do not touch any member variable after this
172 purple_conversation_destroy(conv_
);
175 void Conversation::onScreenResized()
177 CppConsUI::Rect r
= CENTERIM
->getScreenArea(CenterIM::CHAT_AREA
);
178 // Make room for conversation list.
184 void Conversation::write(const char *name
, const char * /*alias*/,
185 const char *message
, PurpleMessageFlags flags
, time_t mtime
)
188 if (!(flags
& PURPLE_MESSAGE_SEND
) &&
189 purple_prefs_get_bool(CONF_PREFIX
"/chat/beep_on_msg")) {
190 CppConsUI::Error error
;
191 if (CppConsUI::Curses::beep(error
) != 0)
192 LOG
->error("%s", error
.getString());
195 // Update the last_activity property.
196 PurpleConversationType type
= purple_conversation_get_type(conv_
);
197 time_t cur_time
= time(nullptr);
199 if (type
== PURPLE_CONV_TYPE_IM
) {
200 PurpleBlistNode
*bnode
= PURPLE_BLIST_NODE(
201 purple_find_buddy(purple_conversation_get_account(conv_
),
202 purple_conversation_get_name(conv_
)));
204 purple_blist_node_set_int(bnode
, "last_activity", cur_time
);
206 // Inform the buddy list node that it should update its state.
207 BUDDYLIST
->updateNode(bnode
);
211 // Write the message.
215 if (flags
& PURPLE_MESSAGE_SEND
) {
217 mtype
= "MSG2"; // cim5 message.
220 else if (flags
& PURPLE_MESSAGE_RECV
) {
222 mtype
= "MSG2"; // cim5 message.
231 // Write text into logfile.
232 if (!(flags
& PURPLE_MESSAGE_NO_LOG
)) {
234 if (type
== PURPLE_CONV_TYPE_CHAT
)
235 log_msg
= g_strdup_printf("\f\n%s\n%s\n%lu\n%lu\n%s: %s\n", dir
, mtype
,
236 mtime
, cur_time
, name
, message
);
238 log_msg
= g_strdup_printf(
239 "\f\n%s\n%s\n%lu\n%lu\n%s\n", dir
, mtype
, mtime
, cur_time
, message
);
240 if (logfile_
!= nullptr) {
241 GError
*err
= nullptr;
242 if (g_io_channel_write_chars(logfile_
, log_msg
, -1, nullptr, &err
) !=
243 G_IO_STATUS_NORMAL
) {
245 _("Error writing to conversation logfile (%s)."), err
->message
);
248 if (g_io_channel_flush(logfile_
, &err
) != G_IO_STATUS_NORMAL
) {
250 _("Error flushing conversation logfile (%s)."), err
->message
);
257 // We currently do not support displaying HTML in any way.
258 char *nohtml
= stripHTML(message
);
260 // Write text to the window.
261 char *time
= extractTime(mtime
, cur_time
);
263 if (type
== PURPLE_CONV_TYPE_CHAT
)
264 msg
= g_strdup_printf("%s %s: %s", time
, name
, nohtml
);
266 msg
= g_strdup_printf("%s %s", time
, nohtml
);
267 view_
->append(msg
, color
);
273 Conversation::ConversationLine::ConversationLine(const char *text
)
274 : AbstractLine(AUTOSIZE
, 1)
276 g_assert(text
!= nullptr);
278 text_
= g_strdup(text
);
279 text_width_
= CppConsUI::Curses::onScreenWidth(text_
);
282 Conversation::ConversationLine::~ConversationLine()
287 int Conversation::ConversationLine::draw(
288 CppConsUI::Curses::ViewPort area
, CppConsUI::Error
&error
)
290 if (real_width_
== 0 || real_height_
!= 1)
294 if (text_width_
+ 5 >= static_cast<unsigned>(real_width_
))
297 l
= real_width_
- text_width_
- 5;
299 // Use HorizontalLine colors.
302 CppConsUI::ColorScheme::PROPERTY_HORIZONTALLINE_LINE
, &attrs
, error
));
303 DRAW(area
.attrOn(attrs
, error
));
306 for (i
= 0; i
< l
; ++i
)
307 DRAW(area
.addLineChar(i
, 0, CppConsUI::Curses::LINE_HLINE
, error
));
309 DRAW(area
.addString(i
, 0, text_
, error
, &printed
));
311 for (; i
< real_width_
; ++i
)
312 DRAW(area
.addLineChar(i
, 0, CppConsUI::Curses::LINE_HLINE
, error
));
314 DRAW(area
.attrOff(attrs
, error
));
319 char *Conversation::stripHTML(const char *str
) const
321 // Almost copy&paste from libpurple/util.c:purple_markup_strip_html(), but
322 // this version does not convert tab character to a space.
329 bool closing_td_p
= false;
331 const gchar
*cdata_close_tag
= nullptr, *ent
;
332 gchar
*href
= nullptr;
335 str2
= g_strdup(str
);
337 for (i
= 0, j
= 0; str2
[i
] != '\0'; ++i
) {
338 if (str2
[i
] == '<') {
339 if (cdata_close_tag
) {
340 // Note: Do not even assume any other tag is a tag in CDATA.
341 if (g_ascii_strncasecmp(
342 str2
+ i
, cdata_close_tag
, !strlen(cdata_close_tag
))) {
343 i
+= strlen(cdata_close_tag
) - 1;
344 cdata_close_tag
= nullptr;
348 else if (!g_ascii_strncasecmp(str2
+ i
, "<td", 3) && closing_td_p
) {
352 else if (!g_ascii_strncasecmp(str2
+ i
, "</td>", 5)) {
357 closing_td_p
= false;
363 if (g_ascii_isspace(str2
[k
]))
366 // Scan until we end the tag either implicitly (closed start tag) or
367 // explicitly, using a sloppy method (i.e., < or > inside quoted
368 // attributes will screw us up).
369 while (str2
[k
] != '\0' && str2
[k
] != '<' && str2
[k
] != '>')
372 // If we have got an <a> tag with an href, save the address to print
374 if (g_ascii_strncasecmp(str2
+ i
, "<a", 2) == 0 &&
375 g_ascii_isspace(str2
[i
+ 2])) {
376 int st
; // Start of href, inclusive [.
377 int end
; // End of href, exclusive ).
379 // Find start of href.
380 for (st
= i
+ 3; st
< k
; ++st
) {
381 if (g_ascii_strncasecmp(str2
+ st
, "href=", 5) == 0) {
383 if (str2
[st
] == '"' || str2
[st
] == '\'') {
390 // Find end of address.
391 for (end
= st
; end
< k
&& str2
[end
] != delim
; ++end
) {
392 // All the work is done in the loop construct above.
395 // If there is an address, save it. If there was already one saved,
400 tmp
= g_strndup(str2
+ st
, end
- st
);
401 href
= purple_unescape_html(tmp
);
407 // Replace </a> with an ascii representation of the address the link was
409 else if (href
!= nullptr &&
410 g_ascii_strncasecmp(str2
+ i
, "</a>", 4) == 0) {
411 std::size_t hrlen
= std::strlen(href
);
413 // Only insert the href if it is different from the CDATA.
414 // 7 == strlen("http://").
415 if ((hrlen
!= (unsigned)(j
- href_st
) ||
416 std::strncmp(str2
+ href_st
, href
, hrlen
)) != 0 &&
417 (hrlen
!= (unsigned)(j
- href_st
+ 7) ||
418 std::strncmp(str2
+ href_st
, href
+ 7, hrlen
- 7) != 0)) {
421 g_memmove(str2
+ j
, href
, hrlen
);
429 // Check for tags which should be mapped to newline (but ignore some of
430 // the tags at the beginning of the text).
431 else if ((j
!= 0 && (g_ascii_strncasecmp(str2
+ i
, "<p>", 3) == 0 ||
432 g_ascii_strncasecmp(str2
+ i
, "<tr", 3) == 0 ||
433 g_ascii_strncasecmp(str2
+ i
, "<hr", 3) == 0 ||
434 g_ascii_strncasecmp(str2
+ i
, "<li", 3) == 0 ||
435 g_ascii_strncasecmp(str2
+ i
, "<div", 4) == 0)) ||
436 g_ascii_strncasecmp(str2
+ i
, "<br", 3) == 0 ||
437 g_ascii_strncasecmp(str2
+ i
, "</table>", 8) == 0)
439 else if (g_ascii_strncasecmp(str2
+ i
, "<script", 7) == 0)
440 cdata_close_tag
= "</script>";
441 else if (g_ascii_strncasecmp(str2
+ i
, "<style", 6) == 0)
442 cdata_close_tag
= "</style>";
443 // Update the index and continue checking after the tag.
444 i
= (str2
[k
] == '<' || str2
[k
] == '\0') ? k
- 1 : k
;
448 else if (cdata_close_tag
)
450 else if (!g_ascii_isspace(str2
[i
]))
453 if (str2
[i
] == '&' &&
454 (ent
= purple_markup_unescape_entity(str2
+ i
, &entlen
))) {
462 str2
[j
++] = g_ascii_isspace(str2
[i
]) && str
[i
] != '\t' ? ' ' : str2
[i
];
472 void Conversation::buildLogFilename()
474 PurpleAccount
*account
= purple_conversation_get_account(conv_
);
476 purple_find_prpl(purple_account_get_protocol_id(account
));
477 g_assert(prpl
!= nullptr);
479 const char *proto_name
= purple_account_get_protocol_name(account
);
481 char *acct_name
= g_strdup(purple_escape_filename(
482 purple_normalize(account
, purple_account_get_username(account
))));
484 const char *name
= purple_conversation_get_name(conv_
);
487 g_build_filename(purple_user_dir(), "clogs", proto_name
, acct_name
,
488 purple_escape_filename(purple_normalize(account
, name
)), nullptr);
490 char *dir
= g_path_get_dirname(filename_
);
491 if (g_mkdir_with_parents(dir
, S_IRUSR
| S_IWUSR
| S_IXUSR
) == -1)
492 LOG
->error(_("Error creating directory '%s'."), dir
);
498 char *Conversation::extractTime(time_t sent_time
, time_t show_time
) const
500 // Based on the extracttime() function from cim4.
502 // Convert to local time, note that localtime_r() should not really fail.
503 struct tm show_time_local
;
504 struct tm sent_time_local
;
505 if (localtime_r(&show_time
, &show_time_local
) == nullptr)
506 std::memset(&show_time_local
, 0, sizeof(show_time_local
));
507 if (localtime_r(&sent_time
, &sent_time_local
) == nullptr)
508 std::memset(&sent_time_local
, 0, sizeof(sent_time_local
));
511 char *t1
= g_strdup(purple_date_format_long(&show_time_local
));
512 char *t2
= g_strdup(purple_date_format_long(&sent_time_local
));
514 int tdiff
= std::abs(sent_time
- show_time
);
516 if (tdiff
> 5 && std::strcmp(t1
, t2
) != 0) {
517 char *res
= g_strdup_printf("%s [%s]", t1
, t2
);
527 void Conversation::loadHistory()
530 GError
*err
= nullptr;
531 GIOChannel
*chan
= g_io_channel_new_file(filename_
, "r", &err
);
532 if (chan
== nullptr) {
533 LOG
->error(_("Error opening conversation logfile '%s' (%s)."), filename_
,
538 // This should never fail.
539 g_io_channel_set_encoding(chan
, nullptr, nullptr);
543 bool new_msg
= false;
544 // Read conversation logfile line by line.
546 (st
= g_io_channel_read_line(chan
, &line
, nullptr, nullptr, &err
)) ==
547 G_IO_STATUS_NORMAL
) {
551 if (std::strcmp(line
, "\f\n") != 0) {
557 // Parse direction (in/out).
558 if ((st
= g_io_channel_read_line(chan
, &line
, nullptr, nullptr, &err
)) !=
562 if (std::strcmp(line
, "OUT\n") == 0)
564 else if (std::strcmp(line
, "IN\n") == 0)
569 if ((st
= g_io_channel_read_line(chan
, &line
, nullptr, nullptr, &err
)) !=
573 if (std::strcmp(line
, "MSG2\n") == 0)
575 else if (std::strcmp(line
, "OTHER\n") == 0) {
582 if ((st
= g_io_channel_read_line(chan
, &line
, nullptr, nullptr, &err
)) !=
585 time_t sent_time
= atol(line
);
589 if ((st
= g_io_channel_read_line(chan
, &line
, nullptr, nullptr, &err
)) !=
592 time_t show_time
= atol(line
);
596 // cim5, read only one line and strip it off HTML.
597 if ((st
= g_io_channel_read_line(chan
, &line
, nullptr, nullptr, &err
)) !=
602 if (!g_utf8_validate(line
, -1, nullptr)) {
604 LOG
->error(_("Invalid message detected in conversation logfile"
605 " '%s'. The message was skipped."),
610 // Write text to the window.
611 char *nohtml
= stripHTML(line
);
612 char *time
= extractTime(sent_time
, show_time
);
613 char *msg
= g_strdup_printf("%s %s", time
, nohtml
);
614 view_
->append(msg
, color
);
621 // cim4, read multiple raw lines.
624 while ((st
= g_io_channel_read_line(
625 chan
, &line
, &length
, nullptr, &err
)) == G_IO_STATUS_NORMAL
&&
627 if (std::strcmp(line
, "\f\n") == 0) {
632 // Strip '\r' if necessary.
633 if (length
> 1 && line
[length
- 2] == '\r') {
634 line
[length
- 2] = '\n';
635 line
[length
- 1] = '\0';
647 if (!g_utf8_validate(msg
.c_str(), -1, nullptr)) {
648 LOG
->error(_("Invalid message detected in conversation logfile"
649 " '%s'. The message was skipped."),
654 // Add the message to the window.
655 char *time
= extractTime(sent_time
, show_time
);
656 char *final_msg
= g_strdup_printf("%s %s", time
, msg
.c_str());
657 view_
->append(final_msg
, color
);
663 if (st
!= G_IO_STATUS_EOF
) {
664 LOG
->error(_("Error reading from conversation logfile '%s' (%s)."),
665 filename_
, err
->message
);
668 g_io_channel_unref(chan
);
671 bool Conversation::processCommand(const char *raw
, const char *html
)
673 // Check that it is a command.
674 if (std::strncmp(raw
, "/", 1) != 0)
677 purple_conversation_write(
678 conv_
, "", html
, PURPLE_MESSAGE_NO_LOG
, time(nullptr));
680 char *error
= nullptr;
681 // Strip the prefix and execute the command.
682 PurpleCmdStatus status
=
683 purple_cmd_do_command(conv_
, raw
+ 1, html
+ 1, &error
);
687 case PURPLE_CMD_STATUS_OK
:
689 case PURPLE_CMD_STATUS_NOT_FOUND
:
690 // It is not a valid command, process it as a message.
693 case PURPLE_CMD_STATUS_WRONG_ARGS
:
694 purple_conversation_write(conv_
, "",
695 _("Wrong number of arguments passed to the command."),
696 PURPLE_MESSAGE_NO_LOG
, time(nullptr));
698 case PURPLE_CMD_STATUS_FAILED
:
699 purple_conversation_write(conv_
, "",
700 error
? error
: _("The command failed for an unknown reason."),
701 PURPLE_MESSAGE_NO_LOG
, time(nullptr));
703 case PURPLE_CMD_STATUS_WRONG_TYPE
:
704 if (purple_conversation_get_type(conv_
) == PURPLE_CONV_TYPE_IM
)
705 purple_conversation_write(conv_
, "",
706 _("The command only works in chats, not IMs."), PURPLE_MESSAGE_NO_LOG
,
709 purple_conversation_write(conv_
, "",
710 _("The command only works in IMs, not chats."), PURPLE_MESSAGE_NO_LOG
,
713 case PURPLE_CMD_STATUS_WRONG_PRPL
:
714 purple_conversation_write(conv_
, "",
715 _("The command does not work on this protocol."), PURPLE_MESSAGE_NO_LOG
,
725 void Conversation::onInputTextChange(CppConsUI::TextEdit
&activator
)
727 PurpleConvIm
*im
= PURPLE_CONV_IM(conv_
);
731 if (!CONVERSATIONS
->getSendTypingPref()) {
732 input_text_length_
= 0;
736 std::size_t old_text_length
= input_text_length_
;
737 std::size_t new_text_length
= activator
.getTextLength();
738 input_text_length_
= new_text_length
;
740 if (new_text_length
== 0) {
741 // All text is deleted, turn off typing.
742 purple_conv_im_stop_send_typed_timeout(im
);
744 serv_send_typing(purple_conversation_get_gc(conv_
),
745 purple_conversation_get_name(conv_
), PURPLE_NOT_TYPING
);
749 purple_conv_im_stop_send_typed_timeout(im
);
750 purple_conv_im_start_send_typed_timeout(im
);
752 time_t again
= purple_conv_im_get_type_again(im
);
753 if ((old_text_length
== 0 && new_text_length
!= 0) ||
754 (again
!= 0 && time(nullptr) > again
)) {
755 // The first letter is inserted or update is required for typing status.
756 unsigned int timeout
= serv_send_typing(purple_conversation_get_gc(conv_
),
757 purple_conversation_get_name(conv_
), PURPLE_TYPING
);
758 purple_conv_im_set_type_again(im
, timeout
);
762 void Conversation::actionSend()
764 const char *str
= input_
->getText();
765 if (str
== nullptr || str
[0] == '\0')
770 char *escaped
= purple_markup_escape_text(str
, strlen(str
));
771 char *html
= purple_strdup_withhtml(escaped
);
772 if (processCommand(str
, html
)) {
773 // The command was processed.
776 PurpleConversationType type
= purple_conversation_get_type(conv_
);
777 if (type
== PURPLE_CONV_TYPE_CHAT
)
778 purple_conv_chat_send(PURPLE_CONV_CHAT(conv_
), html
);
779 else if (type
== PURPLE_CONV_TYPE_IM
)
780 purple_conv_im_send(PURPLE_CONV_IM(conv_
), html
);
787 void Conversation::declareBindables()
789 declareBindable("conversation", "send",
790 sigc::mem_fun(this, &Conversation::actionSend
),
791 InputProcessor::BINDABLE_OVERRIDE
);
794 // vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab: