PageRenderTime 100ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/indra/newview/lllogchat.cpp

https://bitbucket.org/lindenlab/viewer-beta/
C++ | 625 lines | 448 code | 82 blank | 95 comment | 83 complexity | 10c508361cba4488b659e9a2df7683a9 MD5 | raw file
Possible License(s): LGPL-2.1
  1. /**
  2. * @file lllogchat.cpp
  3. * @brief LLLogChat class implementation
  4. *
  5. * $LicenseInfo:firstyear=2002&license=viewerlgpl$
  6. * Second Life Viewer Source Code
  7. * Copyright (C) 2010, Linden Research, Inc.
  8. *
  9. * This library is free software; you can redistribute it and/or
  10. * modify it under the terms of the GNU Lesser General Public
  11. * License as published by the Free Software Foundation;
  12. * version 2.1 of the License only.
  13. *
  14. * This library is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  17. * Lesser General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Lesser General Public
  20. * License along with this library; if not, write to the Free Software
  21. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  22. *
  23. * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
  24. * $/LicenseInfo$
  25. */
  26. #include "llviewerprecompiledheaders.h"
  27. #include "llagent.h"
  28. #include "llagentui.h"
  29. #include "lllogchat.h"
  30. #include "lltrans.h"
  31. #include "llviewercontrol.h"
  32. #include "lldiriterator.h"
  33. #include "llinstantmessage.h"
  34. #include "llsingleton.h" // for LLSingleton
  35. #include <boost/algorithm/string/trim.hpp>
  36. #include <boost/algorithm/string/replace.hpp>
  37. #include <boost/regex.hpp>
  38. #include <boost/regex/v4/match_results.hpp>
  39. #if LL_MSVC
  40. #pragma warning(push)
  41. // disable warning about boost::lexical_cast unreachable code
  42. // when it fails to parse the string
  43. #pragma warning (disable:4702)
  44. #endif
  45. #include <boost/date_time/gregorian/gregorian.hpp>
  46. #if LL_MSVC
  47. #pragma warning(pop) // Restore all warnings to the previous state
  48. #endif
  49. #include <boost/date_time/posix_time/posix_time.hpp>
  50. #include <boost/date_time/local_time_adjustor.hpp>
  51. const S32 LOG_RECALL_SIZE = 2048;
  52. const std::string IM_TIME("time");
  53. const std::string IM_TEXT("message");
  54. const std::string IM_FROM("from");
  55. const std::string IM_FROM_ID("from_id");
  56. const static std::string IM_SEPARATOR(": ");
  57. const static std::string NEW_LINE("\n");
  58. const static std::string NEW_LINE_SPACE_PREFIX("\n ");
  59. const static std::string TWO_SPACES(" ");
  60. const static std::string MULTI_LINE_PREFIX(" ");
  61. /**
  62. * Chat log lines - timestamp and name are optional but message text is mandatory.
  63. *
  64. * Typical plain text chat log lines:
  65. *
  66. * SuperCar: You aren't the owner
  67. * [2:59] SuperCar: You aren't the owner
  68. * [2009/11/20 3:00] SuperCar: You aren't the owner
  69. * Katar Ivercourt is Offline
  70. * [3:00] Katar Ivercourt is Offline
  71. * [2009/11/20 3:01] Corba ProductEngine is Offline
  72. *
  73. * Note: "You" was used as an avatar names in viewers of previous versions
  74. */
  75. const static boost::regex TIMESTAMP_AND_STUFF("^(\\[\\d{4}/\\d{1,2}/\\d{1,2}\\s+\\d{1,2}:\\d{2}\\]\\s+|\\[\\d{1,2}:\\d{2}\\]\\s+)?(.*)$");
  76. /**
  77. * Regular expression suitable to match names like
  78. * "You", "Second Life", "Igor ProductEngine", "Object", "Mega House"
  79. */
  80. const static boost::regex NAME_AND_TEXT("([^:]+[:]{1})?(\\s*)(.*)");
  81. /**
  82. * These are recognizers for matching the names of ad-hoc conferences when generating the log file name
  83. * On invited side, an ad-hoc is named like "<first name> <last name> Conference 2010/11/19 03:43 f0f4"
  84. * On initiating side, an ad-hoc is named like Ad-hoc Conference hash<hash>"
  85. * If the naming system for ad-hoc conferences are change in LLIMModel::LLIMSession::buildHistoryFileName()
  86. * then these definition need to be adjusted as well.
  87. */
  88. const static boost::regex INBOUND_CONFERENCE("^[a-zA-Z]{1,31} [a-zA-Z]{1,31} Conference [0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2} [0-9a-f]{4}");
  89. const static boost::regex OUTBOUND_CONFERENCE("^Ad-hoc Conference hash[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}");
  90. //is used to parse complex object names like "Xstreet SL Terminal v2.2.5 st"
  91. const static std::string NAME_TEXT_DIVIDER(": ");
  92. // is used for timestamps adjusting
  93. const static char* DATE_FORMAT("%Y/%m/%d %H:%M");
  94. const static char* TIME_FORMAT("%H:%M");
  95. const static int IDX_TIMESTAMP = 1;
  96. const static int IDX_STUFF = 2;
  97. const static int IDX_NAME = 1;
  98. const static int IDX_TEXT = 3;
  99. using namespace boost::posix_time;
  100. using namespace boost::gregorian;
  101. class LLLogChatTimeScanner: public LLSingleton<LLLogChatTimeScanner>
  102. {
  103. public:
  104. LLLogChatTimeScanner()
  105. {
  106. // Note, date/time facets will be destroyed by string streams
  107. mDateStream.imbue(std::locale(mDateStream.getloc(), new date_input_facet(DATE_FORMAT)));
  108. mTimeStream.imbue(std::locale(mTimeStream.getloc(), new time_facet(TIME_FORMAT)));
  109. mTimeStream.imbue(std::locale(mTimeStream.getloc(), new time_input_facet(DATE_FORMAT)));
  110. }
  111. date getTodayPacificDate()
  112. {
  113. typedef boost::date_time::local_adjustor<ptime, -8, no_dst> pst;
  114. typedef boost::date_time::local_adjustor<ptime, -7, no_dst> pdt;
  115. time_t t_time = time(NULL);
  116. ptime p_time = LLStringOps::getPacificDaylightTime()
  117. ? pdt::utc_to_local(from_time_t(t_time))
  118. : pst::utc_to_local(from_time_t(t_time));
  119. struct tm s_tm = to_tm(p_time);
  120. return date_from_tm(s_tm);
  121. }
  122. void checkAndCutOffDate(std::string& time_str)
  123. {
  124. // Cuts off the "%Y/%m/%d" from string for todays timestamps.
  125. // Assume that passed string has at least "%H:%M" time format.
  126. date log_date(not_a_date_time);
  127. date today(getTodayPacificDate());
  128. // Parse the passed date
  129. mDateStream.str(LLStringUtil::null);
  130. mDateStream << time_str;
  131. mDateStream >> log_date;
  132. mDateStream.clear();
  133. days zero_days(0);
  134. days days_alive = today - log_date;
  135. if ( days_alive == zero_days )
  136. {
  137. // Yep, today's so strip "%Y/%m/%d" info
  138. ptime stripped_time(not_a_date_time);
  139. mTimeStream.str(LLStringUtil::null);
  140. mTimeStream << time_str;
  141. mTimeStream >> stripped_time;
  142. mTimeStream.clear();
  143. time_str.clear();
  144. mTimeStream.str(LLStringUtil::null);
  145. mTimeStream << stripped_time;
  146. mTimeStream >> time_str;
  147. mTimeStream.clear();
  148. }
  149. LL_DEBUGS("LLChatLogParser")
  150. << " log_date: "
  151. << log_date
  152. << " today: "
  153. << today
  154. << " days alive: "
  155. << days_alive
  156. << " new time: "
  157. << time_str
  158. << LL_ENDL;
  159. }
  160. private:
  161. std::stringstream mDateStream;
  162. std::stringstream mTimeStream;
  163. };
  164. //static
  165. std::string LLLogChat::makeLogFileName(std::string filename)
  166. {
  167. /**
  168. * Testing for in bound and out bound ad-hoc file names
  169. * if it is then skip date stamping.
  170. **/
  171. //LL_INFOS("") << "Befor:" << filename << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  172. boost::match_results<std::string::const_iterator> matches;
  173. bool inboundConf = boost::regex_match(filename, matches, INBOUND_CONFERENCE);
  174. bool outboundConf = boost::regex_match(filename, matches, OUTBOUND_CONFERENCE);
  175. if (!(inboundConf || outboundConf))
  176. {
  177. if( gSavedPerAccountSettings.getBOOL("LogFileNamewithDate") )
  178. {
  179. time_t now;
  180. time(&now);
  181. char dbuffer[20]; /* Flawfinder: ignore */
  182. if (filename == "chat")
  183. {
  184. strftime(dbuffer, 20, "-%Y-%m-%d", localtime(&now));
  185. }
  186. else
  187. {
  188. strftime(dbuffer, 20, "-%Y-%m", localtime(&now));
  189. }
  190. filename += dbuffer;
  191. }
  192. }
  193. //LL_INFOS("") << "After:" << filename << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  194. filename = cleanFileName(filename);
  195. filename = gDirUtilp->getExpandedFilename(LL_PATH_PER_ACCOUNT_CHAT_LOGS,filename);
  196. filename += ".txt";
  197. //LL_INFOS("") << "Full:" << filename << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  198. return filename;
  199. }
  200. std::string LLLogChat::cleanFileName(std::string filename)
  201. {
  202. std::string invalidChars = "\"\'\\/?*:.<>|[]{}~"; // Cannot match glob or illegal filename chars
  203. std::string::size_type position = filename.find_first_of(invalidChars);
  204. while (position != filename.npos)
  205. {
  206. filename[position] = '_';
  207. position = filename.find_first_of(invalidChars, position);
  208. }
  209. return filename;
  210. }
  211. std::string LLLogChat::timestamp(bool withdate)
  212. {
  213. time_t utc_time;
  214. utc_time = time_corrected();
  215. std::string timeStr;
  216. LLSD substitution;
  217. substitution["datetime"] = (S32) utc_time;
  218. if (withdate)
  219. {
  220. timeStr = "["+LLTrans::getString ("TimeYear")+"]/["
  221. +LLTrans::getString ("TimeMonth")+"]/["
  222. +LLTrans::getString ("TimeDay")+"] ["
  223. +LLTrans::getString ("TimeHour")+"]:["
  224. +LLTrans::getString ("TimeMin")+"]";
  225. }
  226. else
  227. {
  228. timeStr = "[" + LLTrans::getString("TimeHour") + "]:["
  229. + LLTrans::getString ("TimeMin")+"]";
  230. }
  231. LLStringUtil::format (timeStr, substitution);
  232. return timeStr;
  233. }
  234. //static
  235. void LLLogChat::saveHistory(const std::string& filename,
  236. const std::string& from,
  237. const LLUUID& from_id,
  238. const std::string& line)
  239. {
  240. std::string tmp_filename = filename;
  241. LLStringUtil::trim(tmp_filename);
  242. if (tmp_filename.empty())
  243. {
  244. std::string warn = "Chat history filename [" + filename + "] is empty!";
  245. llwarning(warn, 666);
  246. llassert(tmp_filename.size());
  247. return;
  248. }
  249. llofstream file (LLLogChat::makeLogFileName(filename), std::ios_base::app);
  250. if (!file.is_open())
  251. {
  252. llwarns << "Couldn't open chat history log! - " + filename << llendl;
  253. return;
  254. }
  255. LLSD item;
  256. if (gSavedPerAccountSettings.getBOOL("LogTimestamp"))
  257. item["time"] = LLLogChat::timestamp(gSavedPerAccountSettings.getBOOL("LogTimestampDate"));
  258. item["from_id"] = from_id;
  259. item["message"] = line;
  260. //adding "Second Life:" for all system messages to make chat log history parsing more reliable
  261. if (from.empty() && from_id.isNull())
  262. {
  263. item["from"] = SYSTEM_FROM;
  264. }
  265. else
  266. {
  267. item["from"] = from;
  268. }
  269. file << LLChatLogFormatter(item) << std::endl;
  270. file.close();
  271. }
  272. void LLLogChat::loadHistory(const std::string& filename, void (*callback)(ELogLineType, const LLSD&, void*), void* userdata)
  273. {
  274. if(!filename.size())
  275. {
  276. llwarns << "Filename is Empty!" << llendl;
  277. return ;
  278. }
  279. LLFILE* fptr = LLFile::fopen(makeLogFileName(filename), "r"); /*Flawfinder: ignore*/
  280. if (!fptr)
  281. {
  282. callback(LOG_EMPTY, LLSD(), userdata);
  283. return; //No previous conversation with this name.
  284. }
  285. else
  286. {
  287. char buffer[LOG_RECALL_SIZE]; /*Flawfinder: ignore*/
  288. char *bptr;
  289. S32 len;
  290. bool firstline=TRUE;
  291. if ( fseek(fptr, (LOG_RECALL_SIZE - 1) * -1 , SEEK_END) )
  292. { //File is smaller than recall size. Get it all.
  293. firstline = FALSE;
  294. if ( fseek(fptr, 0, SEEK_SET) )
  295. {
  296. fclose(fptr);
  297. return;
  298. }
  299. }
  300. while ( fgets(buffer, LOG_RECALL_SIZE, fptr) && !feof(fptr) )
  301. {
  302. len = strlen(buffer) - 1; /*Flawfinder: ignore*/
  303. for ( bptr = (buffer + len); (*bptr == '\n' || *bptr == '\r') && bptr>buffer; bptr--) *bptr='\0';
  304. if (!firstline)
  305. {
  306. LLSD item;
  307. std::string line(buffer);
  308. std::istringstream iss(line);
  309. if (!LLChatLogParser::parse(line, item))
  310. {
  311. item["message"] = line;
  312. callback(LOG_LINE, item, userdata);
  313. }
  314. else
  315. {
  316. callback(LOG_LLSD, item, userdata);
  317. }
  318. }
  319. else
  320. {
  321. firstline = FALSE;
  322. }
  323. }
  324. callback(LOG_END, LLSD(), userdata);
  325. fclose(fptr);
  326. }
  327. }
  328. void append_to_last_message(std::list<LLSD>& messages, const std::string& line)
  329. {
  330. if (!messages.size()) return;
  331. std::string im_text = messages.back()[IM_TEXT].asString();
  332. im_text.append(line);
  333. messages.back()[IM_TEXT] = im_text;
  334. }
  335. // static
  336. void LLLogChat::loadAllHistory(const std::string& file_name, std::list<LLSD>& messages)
  337. {
  338. if (file_name.empty())
  339. {
  340. llwarns << "Session name is Empty!" << llendl;
  341. return ;
  342. }
  343. //LL_INFOS("") << "Loading:" << file_name << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  344. //LL_INFOS("") << "Current:" << makeLogFileName(file_name) << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  345. LLFILE* fptr = LLFile::fopen(makeLogFileName(file_name), "r");/*Flawfinder: ignore*/
  346. if (!fptr)
  347. {
  348. fptr = LLFile::fopen(oldLogFileName(file_name), "r");/*Flawfinder: ignore*/
  349. if (!fptr)
  350. {
  351. if (!fptr) return; //No previous conversation with this name.
  352. }
  353. }
  354. //LL_INFOS("") << "Reading:" << file_name << LL_ENDL;
  355. char buffer[LOG_RECALL_SIZE]; /*Flawfinder: ignore*/
  356. char *bptr;
  357. S32 len;
  358. bool firstline = TRUE;
  359. if (fseek(fptr, (LOG_RECALL_SIZE - 1) * -1 , SEEK_END))
  360. { //File is smaller than recall size. Get it all.
  361. firstline = FALSE;
  362. if (fseek(fptr, 0, SEEK_SET))
  363. {
  364. fclose(fptr);
  365. return;
  366. }
  367. }
  368. while (fgets(buffer, LOG_RECALL_SIZE, fptr) && !feof(fptr))
  369. {
  370. len = strlen(buffer) - 1; /*Flawfinder: ignore*/
  371. for (bptr = (buffer + len); (*bptr == '\n' || *bptr == '\r') && bptr>buffer; bptr--) *bptr='\0';
  372. if (firstline)
  373. {
  374. firstline = FALSE;
  375. continue;
  376. }
  377. std::string line(buffer);
  378. //updated 1.23 plaint text log format requires a space added before subsequent lines in a multilined message
  379. if (' ' == line[0])
  380. {
  381. line.erase(0, MULTI_LINE_PREFIX.length());
  382. append_to_last_message(messages, '\n' + line);
  383. }
  384. else if (0 == len && ('\n' == line[0] || '\r' == line[0]))
  385. {
  386. //to support old format's multilined messages with new lines used to divide paragraphs
  387. append_to_last_message(messages, line);
  388. }
  389. else
  390. {
  391. LLSD item;
  392. if (!LLChatLogParser::parse(line, item))
  393. {
  394. item[IM_TEXT] = line;
  395. }
  396. messages.push_back(item);
  397. }
  398. }
  399. fclose(fptr);
  400. }
  401. //*TODO mark object's names in a special way so that they will be distinguishable form avatar name
  402. //which are more strict by its nature (only firstname and secondname)
  403. //Example, an object's name can be writen like "Object <actual_object's_name>"
  404. void LLChatLogFormatter::format(const LLSD& im, std::ostream& ostr) const
  405. {
  406. if (!im.isMap())
  407. {
  408. llwarning("invalid LLSD type of an instant message", 0);
  409. return;
  410. }
  411. if (im[IM_TIME].isDefined())
  412. {
  413. std::string timestamp = im[IM_TIME].asString();
  414. boost::trim(timestamp);
  415. ostr << '[' << timestamp << ']' << TWO_SPACES;
  416. }
  417. //*TODO mark object's names in a special way so that they will be distinguishable form avatar name
  418. //which are more strict by its nature (only firstname and secondname)
  419. //Example, an object's name can be writen like "Object <actual_object's_name>"
  420. if (im[IM_FROM].isDefined())
  421. {
  422. std::string from = im[IM_FROM].asString();
  423. boost::trim(from);
  424. if (from.size())
  425. {
  426. ostr << from << IM_SEPARATOR;
  427. }
  428. }
  429. if (im[IM_TEXT].isDefined())
  430. {
  431. std::string im_text = im[IM_TEXT].asString();
  432. //multilined text will be saved with prepended spaces
  433. boost::replace_all(im_text, NEW_LINE, NEW_LINE_SPACE_PREFIX);
  434. ostr << im_text;
  435. }
  436. }
  437. bool LLChatLogParser::parse(std::string& raw, LLSD& im)
  438. {
  439. if (!raw.length()) return false;
  440. im = LLSD::emptyMap();
  441. //matching a timestamp
  442. boost::match_results<std::string::const_iterator> matches;
  443. if (!boost::regex_match(raw, matches, TIMESTAMP_AND_STUFF)) return false;
  444. bool has_timestamp = matches[IDX_TIMESTAMP].matched;
  445. if (has_timestamp)
  446. {
  447. //timestamp was successfully parsed
  448. std::string timestamp = matches[IDX_TIMESTAMP];
  449. boost::trim(timestamp);
  450. timestamp.erase(0, 1);
  451. timestamp.erase(timestamp.length()-1, 1);
  452. LLLogChatTimeScanner::instance().checkAndCutOffDate(timestamp);
  453. im[IM_TIME] = timestamp;
  454. }
  455. else
  456. {
  457. //timestamp is optional
  458. im[IM_TIME] = "";
  459. }
  460. bool has_stuff = matches[IDX_STUFF].matched;
  461. if (!has_stuff)
  462. {
  463. return false; //*TODO should return false or not?
  464. }
  465. //matching a name and a text
  466. std::string stuff = matches[IDX_STUFF];
  467. boost::match_results<std::string::const_iterator> name_and_text;
  468. if (!boost::regex_match(stuff, name_and_text, NAME_AND_TEXT)) return false;
  469. bool has_name = name_and_text[IDX_NAME].matched;
  470. std::string name = name_and_text[IDX_NAME];
  471. //we don't need a name/text separator
  472. if (has_name && name.length() && name[name.length()-1] == ':')
  473. {
  474. name.erase(name.length()-1, 1);
  475. }
  476. if (!has_name || name == SYSTEM_FROM)
  477. {
  478. //name is optional too
  479. im[IM_FROM] = SYSTEM_FROM;
  480. im[IM_FROM_ID] = LLUUID::null;
  481. }
  482. //possibly a case of complex object names consisting of 3+ words
  483. if (!has_name)
  484. {
  485. U32 divider_pos = stuff.find(NAME_TEXT_DIVIDER);
  486. if (divider_pos != std::string::npos && divider_pos < (stuff.length() - NAME_TEXT_DIVIDER.length()))
  487. {
  488. im[IM_FROM] = stuff.substr(0, divider_pos);
  489. im[IM_TEXT] = stuff.substr(divider_pos + NAME_TEXT_DIVIDER.length());
  490. return true;
  491. }
  492. }
  493. if (!has_name)
  494. {
  495. //text is mandatory
  496. im[IM_TEXT] = stuff;
  497. return true; //parse as a message from Second Life
  498. }
  499. bool has_text = name_and_text[IDX_TEXT].matched;
  500. if (!has_text) return false;
  501. //for parsing logs created in very old versions of a viewer
  502. if (name == "You")
  503. {
  504. std::string agent_name;
  505. LLAgentUI::buildFullname(agent_name);
  506. im[IM_FROM] = agent_name;
  507. im[IM_FROM_ID] = gAgentID;
  508. }
  509. else
  510. {
  511. im[IM_FROM] = name;
  512. }
  513. im[IM_TEXT] = name_and_text[IDX_TEXT];
  514. return true; //parsed name and message text, maybe have a timestamp too
  515. }
  516. std::string LLLogChat::oldLogFileName(std::string filename)
  517. {
  518. std::string scanResult;
  519. std::string directory = gDirUtilp->getPerAccountChatLogsDir();/* get Users log directory */
  520. directory += gDirUtilp->getDirDelimiter();/* add final OS dependent delimiter */
  521. filename=cleanFileName(filename);/* lest make shure the file name has no invalad charecters befor making the pattern */
  522. std::string pattern = (filename+(( filename == "chat" ) ? "-???\?-?\?-??.txt" : "-???\?-??.txt"));/* create search pattern*/
  523. //LL_INFOS("") << "Checking:" << directory << " for " << pattern << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  524. std::vector<std::string> allfiles;
  525. LLDirIterator iter(directory, pattern);
  526. while (iter.next(scanResult))
  527. {
  528. //LL_INFOS("") << "Found :" << scanResult << LL_ENDL;
  529. allfiles.push_back(scanResult);
  530. }
  531. if (allfiles.size() == 0) // if no result from date search, return generic filename
  532. {
  533. scanResult = directory + filename + ".txt";
  534. }
  535. else
  536. {
  537. std::sort(allfiles.begin(), allfiles.end());
  538. scanResult = directory + allfiles.back();
  539. // thisfile is now the most recent version of the file.
  540. }
  541. //LL_INFOS("") << "Reading:" << scanResult << LL_ENDL;/* uncomment if you want to verify step, delete on commit */
  542. return scanResult;
  543. }