1 /*
2  *  The ManaPlus Client
3  *  Copyright (C) 2008-2009  The Mana World Development Team
4  *  Copyright (C) 2009-2010  The Mana Developers
5  *  Copyright (C) 2011-2019  The ManaPlus Developers
6  *  Copyright (C) 2019-2021  Andrei Karas
7  *
8  *  This file is part of The ManaPlus Client.
9  *
10  *  This program is free software; you can redistribute it and/or modify
11  *  it under the terms of the GNU General Public License as published by
12  *  the Free Software Foundation; either version 2 of the License, or
13  *  any later version.
14  *
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU General Public License for more details.
19  *
20  *  You should have received a copy of the GNU General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include "gui/widgets/tabs/chat/chattab.h"
25 
26 #include "chatlogger.h"
27 #include "configuration.h"
28 #include "settings.h"
29 #include "soundmanager.h"
30 
31 #include "being/localplayer.h"
32 
33 #include "const/sound.h"
34 
35 #include "gui/chatlog.h"
36 #include "gui/windowmanager.h"
37 
38 #include "gui/windows/chatwindow.h"
39 #include "gui/windows/helpwindow.h"
40 
41 #include "gui/widgets/scrollarea.h"
42 #include "gui/widgets/itemlinkhandler.h"
43 #include "gui/widgets/tabbedarea.h"
44 
45 #include "input/inputmanager.h"
46 
47 #include "net/chathandler.h"
48 #include "net/net.h"
49 
50 #include "utils/chatutils.h"
51 #include "utils/delete2.h"
52 #include "utils/gettext.h"
53 
54 #ifdef WIN32
55 #include <sys/time.h>
56 #endif  // WIN32
57 
58 #include <sstream>
59 
60 #include "debug.h"
61 
62 ChatTab *localChatTab = nullptr;
63 ChatTab *debugChatTab = nullptr;
64 
65 static const unsigned int MAX_WORD_SIZE = 50;
66 
ChatTab(const Widget2 * const widget,const std::string & name,const std::string & channel,const std::string & logName,const ChatTabTypeT & type)67 ChatTab::ChatTab(const Widget2 *const widget,
68                  const std::string &name,
69                  const std::string &channel,
70                  const std::string &logName,
71                  const ChatTabTypeT &type) :
72     Tab(widget),
73     mTextOutput(new BrowserBox(this, Opaque_true,
74        "browserbox.xml")),
75     mScrollArea(new ScrollArea(this,
76         mTextOutput, Opaque_false, std::string())),
77     mChannelName(channel),
78     mLogName(logName),
79     mType(type),
80     mAllowHightlight(true),
81     mRemoveNames(false),
82     mNoAway(false),
83     mShowOnline(false)
84 {
85     setCaption(name);
86 
87     mTextOutput->setOpaque(Opaque_false);
88     mTextOutput->setMaxRow(config.getIntValue("ChatLogLength"));
89     if (chatWindow != nullptr)
90         mTextOutput->setLinkHandler(chatWindow->mItemLinkHandler);
91     mTextOutput->setAlwaysUpdate(false);
92 
93     mScrollArea->setScrollPolicy(ScrollArea::SHOW_NEVER,
94         ScrollArea::SHOW_ALWAYS);
95     mScrollArea->setScrollAmount(0, 1);
96 
97     if (chatWindow != nullptr)
98         chatWindow->addTab(this);
99     mTextOutput->updateSize(true);
100 }
101 
~ChatTab()102 ChatTab::~ChatTab()
103 {
104     if (chatWindow != nullptr)
105         chatWindow->removeTab(this);
106 
107     delete2(mTextOutput)
108     delete2(mScrollArea)
109 }
110 
chatLog(std::string line,ChatMsgTypeT own,const IgnoreRecord ignoreRecord,const TryRemoveColors tryRemoveColors)111 void ChatTab::chatLog(std::string line,
112                       ChatMsgTypeT own,
113                       const IgnoreRecord ignoreRecord,
114                       const TryRemoveColors tryRemoveColors)
115 {
116     // Trim whitespace
117     trim(line);
118 
119     if (line.empty())
120         return;
121 
122     if (tryRemoveColors == TryRemoveColors_true &&
123         own == ChatMsgType::BY_OTHER &&
124         config.getBoolValue("removeColors"))
125     {
126         line = removeColors(line);
127         if (line.empty())
128             return;
129     }
130 
131     const unsigned lineLim = config.getIntValue("chatMaxCharLimit");
132     if (lineLim > 0 && line.length() > lineLim)
133         line = line.substr(0, lineLim);
134 
135     if (line.empty())
136         return;
137 
138     CHATLOG tmp;
139     tmp.own = own;
140     tmp.nick.clear();
141     tmp.text = line;
142 
143     const size_t pos = line.find(" : ");
144     if (pos != std::string::npos)
145     {
146         if (line.length() <= pos + 3)
147             return;
148 
149         tmp.nick = line.substr(0, pos);
150         tmp.text = line.substr(pos + 3);
151     }
152     else
153     {
154         // Fix the owner of welcome message.
155         if (line.length() > 7 && line.substr(0, 7) == "Welcome")
156             own = ChatMsgType::BY_SERVER;
157     }
158 
159     // *implements actions in a backwards compatible way*
160     if ((own == ChatMsgType::BY_PLAYER || own == ChatMsgType::BY_OTHER) &&
161         tmp.text.at(0) == '*' &&
162         tmp.text.at(tmp.text.length()-1) == '*')
163     {
164         tmp.text[0] = ' ';
165         tmp.text.erase(tmp.text.length() - 1);
166         own = ChatMsgType::ACT_IS;
167     }
168 
169     std::string lineColor("##C");
170     switch (own)
171     {
172         case ChatMsgType::BY_GM:
173             if (tmp.nick.empty())
174             {
175                 // TRANSLATORS: chat message
176                 tmp.nick = std::string(_("Global announcement:")).append(" ");
177                 lineColor = "##G";
178             }
179             else
180             {
181                 // TRANSLATORS: chat message
182                 tmp.nick = strprintf(_("Global announcement from %s:"),
183                                      tmp.nick.c_str()).append(" ");
184                 lineColor = "##g";  // Equiv. to BrowserBox::RED
185             }
186             break;
187         case ChatMsgType::BY_PLAYER:
188             tmp.nick.append(": ");
189             lineColor = "##Y";
190             break;
191         case ChatMsgType::BY_OTHER:
192         case ChatMsgType::BY_UNKNOWN:
193             tmp.nick.append(": ");
194             lineColor = "##C";
195             break;
196         case ChatMsgType::BY_SERVER:
197             // TRANSLATORS: chat message
198             tmp.nick.clear();
199             tmp.text = line;
200             lineColor = "##S";
201             break;
202         case ChatMsgType::BY_CHANNEL:
203             tmp.nick.clear();
204             lineColor = "##2";  // Equiv. to BrowserBox::GREEN
205             break;
206         case ChatMsgType::ACT_WHISPER:
207             // TRANSLATORS: chat message
208             tmp.nick = strprintf(_("%s whispers: %s"), tmp.nick.c_str(), "");
209             lineColor = "##W";
210             break;
211         case ChatMsgType::ACT_IS:
212             lineColor = "##I";
213             break;
214         case ChatMsgType::BY_LOGGER:
215             tmp.nick.clear();
216             tmp.text = line;
217             lineColor = "##L";
218             break;
219         default:
220             break;
221     }
222 
223     if (tmp.nick == ": ")
224     {
225         tmp.nick.clear();
226         lineColor = "##S";
227     }
228 
229     // if configured, move magic messages log to debug chat tab
230     if (Net::getNetworkType() == ServerType::TMWATHENA
231         && (localChatTab != nullptr) && this == localChatTab
232         && ((config.getBoolValue("showMagicInDebug")
233         && own == ChatMsgType::BY_PLAYER
234         && tmp.text.length() > 1
235         && tmp.text.at(0) == '#'
236         && tmp.text.at(1) != '#')
237         || (config.getBoolValue("serverMsgInDebug")
238         && (own == ChatMsgType::BY_SERVER
239         || tmp.nick.empty()))))
240     {
241         if (debugChatTab != nullptr)
242             debugChatTab->chatLog(line, own, ignoreRecord, tryRemoveColors);
243         return;
244     }
245 
246     // Get the current system time
247     time_t t;
248     time(&t);
249 
250     if (config.getBoolValue("useLocalTime"))
251     {
252         const tm *const timeInfo = localtime(&t);
253         if (timeInfo != nullptr)
254         {
255             line = strprintf("%s[%02d:%02d] %s%s", lineColor.c_str(),
256                 timeInfo->tm_hour, timeInfo->tm_min, tmp.nick.c_str(),
257                 tmp.text.c_str());
258         }
259         else
260         {
261             line = strprintf("%s %s%s", lineColor.c_str(),
262                 tmp.nick.c_str(), tmp.text.c_str());
263         }
264     }
265     else
266     {
267         // Format the time string properly
268         std::stringstream timeStr;
269         timeStr << "[" << ((((t / 60) / 60) % 24 < 10) ? "0" : "")
270             << CAST_S32(((t / 60) / 60) % 24)
271             << ":" << (((t / 60) % 60 < 10) ? "0" : "")
272             << CAST_S32((t / 60) % 60)
273             << "] ";
274         line = std::string(lineColor).append(timeStr.str()).append(
275             tmp.nick).append(tmp.text);
276     }
277 
278     if (config.getBoolValue("enableChatLog"))
279         saveToLogFile(line);
280 
281     mTextOutput->setMaxRow(config.getIntValue("chatMaxLinesLimit"));
282 
283     // We look if the Vertical Scroll Bar is set at the max before
284     // adding a row, otherwise the max will always be a row higher
285     // at comparison.
286     if (mScrollArea->getVerticalScrollAmount() + 2 >=
287         mScrollArea->getVerticalMaxScroll())
288     {
289         addRow(line);
290         mScrollArea->setVerticalScrollAmount(
291             mScrollArea->getVerticalMaxScroll());
292     }
293     else
294     {
295         addRow(line);
296     }
297 
298     if ((chatWindow != nullptr) && this == localChatTab)
299         chatWindow->addToAwayLog(line);
300 
301     mScrollArea->logic();
302     if (own != ChatMsgType::BY_PLAYER)
303     {
304         if (own == ChatMsgType::BY_SERVER &&
305             (getType() == ChatTabType::PARTY ||
306             getType() == ChatTabType::CHANNEL ||
307             getType() == ChatTabType::GUILD))
308         {
309             return;
310         }
311 
312         const TabbedArea *const tabArea = getTabbedArea();
313         if (tabArea == nullptr)
314             return;
315 
316         const bool notFocused = WindowManager::getIsMinimized() ||
317             (!settings.mouseFocused &&
318             settings.inputFocused == KeyboardFocus::Unfocused);
319 
320         if (this != tabArea->getSelectedTab() || notFocused)
321         {
322             if (getFlash() == 0)
323             {
324                 if (chatWindow != nullptr &&
325                     chatWindow->findHighlight(tmp.text))
326                 {
327                     setFlash(2);
328                     soundManager.playGuiSound(SOUND_HIGHLIGHT);
329                 }
330                 else
331                 {
332                     setFlash(1);
333                 }
334             }
335             else if (getFlash() == 2)
336             {
337                 if (chatWindow != nullptr &&
338                     chatWindow->findHighlight(tmp.text))
339                 {
340                     soundManager.playGuiSound(SOUND_HIGHLIGHT);
341                 }
342             }
343         }
344 
345         if ((getAllowHighlight() ||
346             own == ChatMsgType::BY_GM) &&
347             (this != tabArea->getSelectedTab() ||
348             notFocused))
349         {
350             if (own == ChatMsgType::BY_GM)
351             {
352                 if (chatWindow != nullptr)
353                     chatWindow->unHideWindow();
354                 soundManager.playGuiSound(SOUND_GLOBAL);
355             }
356             else if (own != ChatMsgType::BY_SERVER)
357             {
358                 if (chatWindow != nullptr)
359                     chatWindow->unHideWindow();
360                 playNewMessageSound();
361             }
362             WindowManager::newChatMessage();
363         }
364     }
365 }
366 
chatLog(const std::string & nick,std::string msg)367 void ChatTab::chatLog(const std::string &nick, std::string msg)
368 {
369     if (localPlayer == nullptr)
370         return;
371 
372     const ChatMsgTypeT byWho = (nick == localPlayer->getName()
373         ? ChatMsgType::BY_PLAYER : ChatMsgType::BY_OTHER);
374     if (byWho == ChatMsgType::BY_OTHER && config.getBoolValue("removeColors"))
375         msg = removeColors(msg);
376     chatLog(std::string(nick).append(" : ").append(msg),
377         byWho,
378         IgnoreRecord_false,
379         TryRemoveColors_false);
380 }
381 
chatInput(const std::string & message)382 void ChatTab::chatInput(const std::string &message)
383 {
384     std::string msg = message;
385     trim(msg);
386 
387     if (msg.empty())
388         return;
389 
390     replaceItemLinks(msg);
391     replaceVars(msg);
392 
393     switch (msg[0])
394     {
395         case '/':
396             handleCommandStr(std::string(msg, 1));
397             break;
398         case '?':
399             if (msg.size() > 1 &&
400                 msg[1] != '!' &&
401                 msg[1] != '?' &&
402                 msg[1] != '.' &&
403                 msg[1] != ' ' &&
404                 msg[1] != ',')
405             {
406                 handleHelp(std::string(msg, 1));
407             }
408             else
409             {
410                 handleInput(msg);
411             }
412             break;
413         default:
414             handleInput(msg);
415             break;
416     }
417 }
418 
scroll(const int amount)419 void ChatTab::scroll(const int amount)
420 {
421     const int range = mScrollArea->getHeight() / 8 * amount;
422     Rect scr;
423     scr.y = mScrollArea->getVerticalScrollAmount() + range;
424     scr.height = abs(range);
425     mTextOutput->showPart(scr);
426 }
427 
clearText()428 void ChatTab::clearText()
429 {
430     mTextOutput->clearRows();
431 }
432 
handleInput(const std::string & msg)433 void ChatTab::handleInput(const std::string &msg)
434 {
435     if (chatHandler)
436     {
437         chatHandler->talk(ChatWindow::doReplace(msg));
438     }
439 }
440 
handleCommandStr(const std::string & msg)441 void ChatTab::handleCommandStr(const std::string &msg)
442 {
443     const size_t pos = msg.find(' ');
444     const std::string type(msg, 0, pos);
445     std::string args(msg, pos == std::string::npos ? msg.size() : pos + 1);
446 
447     args = trim(args);
448     if (!handleCommand(type, args))
449         inputManager.executeChatCommand(type, args, this);
450 }
451 
handleHelp(const std::string & msg)452 void ChatTab::handleHelp(const std::string &msg)
453 {
454     if (helpWindow != nullptr)
455     {
456         helpWindow->search(msg);
457         helpWindow->requestMoveToTop();
458     }
459 }
460 
handleCommands(const std::string & type,const std::string & args)461 bool ChatTab::handleCommands(const std::string &type, const std::string &args)
462 {
463     // need split to commands and call each
464 
465     return handleCommand(type, args);
466 }
467 
saveToLogFile(const std::string & msg) const468 void ChatTab::saveToLogFile(const std::string &msg) const
469 {
470     if (chatLogger != nullptr)
471     {
472         if (getType() == ChatTabType::INPUT)
473         {
474             chatLogger->log(msg);
475         }
476         else if (getType() == ChatTabType::DEBUG)
477         {
478             if (config.getBoolValue("enableDebugLog"))
479                 chatLogger->log("#Debug", msg);
480         }
481         else if (!mLogName.empty())
482         {
483             chatLogger->log(mLogName, msg);
484         }
485     }
486 }
487 
addRow(std::string & line)488 void ChatTab::addRow(std::string &line)
489 {
490     if (line.find("[@@http") == std::string::npos)
491     {
492         size_t idx = 0;
493         for (size_t f = 0; f < line.length(); f++)
494         {
495             if (line.at(f) == ' ')
496             {
497                 idx = f;
498             }
499             else if (f - idx > MAX_WORD_SIZE)
500             {
501                 line.insert(f, " ");
502                 idx = f;
503             }
504         }
505     }
506     mTextOutput->addRow(line,
507         false);
508 }
509 
loadFromLogFile(const std::string & name)510 void ChatTab::loadFromLogFile(const std::string &name)
511 {
512     if (chatLogger != nullptr)
513     {
514         std::list<std::string> list;
515         chatLogger->loadLast(name, list, 5);
516         std::list<std::string>::const_iterator i = list.begin();
517         while (i != list.end())
518         {
519             std::string line("##o" + *i);
520             addRow(line);
521             ++i;
522         }
523     }
524 }
525 
addNewRow(std::string & line)526 void ChatTab::addNewRow(std::string &line)
527 {
528     if (mScrollArea->getVerticalScrollAmount() >=
529         mScrollArea->getVerticalMaxScroll())
530     {
531         addRow(line);
532         mScrollArea->setVerticalScrollAmount(
533             mScrollArea->getVerticalMaxScroll());
534     }
535     else
536     {
537         addRow(line);
538     }
539     mScrollArea->logic();
540 }
541 
playNewMessageSound() const542 void ChatTab::playNewMessageSound() const
543 {
544     soundManager.playGuiSound(SOUND_WHISPER);
545 }
546 
showOnline(const std::string & nick,const Online online)547 void ChatTab::showOnline(const std::string &nick,
548                          const Online online)
549 {
550     if (!mShowOnline)
551         return;
552 
553     if (online == Online_true)
554     {
555         // TRANSLATORS: chat message
556         chatLog(strprintf(_("%s is now Online."), nick.c_str()),
557             ChatMsgType::BY_SERVER,
558             IgnoreRecord_false,
559             TryRemoveColors_true);
560     }
561     else
562     {
563         // TRANSLATORS: chat message
564         chatLog(strprintf(_("%s is now Offline."), nick.c_str()),
565             ChatMsgType::BY_SERVER,
566             IgnoreRecord_false,
567             TryRemoveColors_true);
568     }
569 }
570