1 #include "otsdaq-utilities/Chat/ChatSupervisor.h"
2 #include "otsdaq/CgiDataUtilities/CgiDataUtilities.h"
3 #include "otsdaq/GatewaySupervisor/GatewaySupervisor.h"
4 #include "otsdaq/Macros/CoutMacros.h"
5 #include "otsdaq/Macros/StringMacros.h"
6 #include "otsdaq/MessageFacility/MessageFacility.h"
7 #include "otsdaq/XmlUtilities/HttpXmlDocument.h"
9 #include <xdaq/NamespaceURI.h>
24 #define __MF_SUBJECT__ "Chat"
33 ChatLastUpdateIndex = 1;
37 enableSlackChat = (std::getenv(
"OTS_EN_SLACK") !=
nullptr &&
38 std::string(std::getenv(
"OTS_EN_SLACK")) ==
"1");
41 const char* env = std::getenv(
"OTSDAQ_UTILITIES_DIR");
42 chatSupervisorToolsPath_ = env ? env :
"";
44 if(chatSupervisorToolsPath_.empty())
45 enableSlackChat =
false;
46 if(chatSupervisorToolsPath_.back() !=
'/')
47 chatSupervisorToolsPath_ +=
'/';
48 chatSupervisorToolsPath_ +=
"tools/";
50 const char* userData = std::getenv(
"USER_DATA");
52 slackInboxPath_ = std::string(userData) +
"/ChatSlackInbox.txt";
55 "/tmp/ots_slack_inbox_uid" + std::to_string(getuid()) +
".txt";
58 if(std::getenv(
"SLACK_BOT_TOKEN") ==
nullptr ||
59 std::getenv(
"SLACK_CHANNEL") ==
nullptr ||
60 std::getenv(
"SLACK_CHANNEL_ID") ==
nullptr)
61 enableSlackChat =
false;
63 __COUT__ <<
"ChatSupervisor: Slack chat "
64 << (enableSlackChat ?
"enabled" :
"disabled") << __E__;
65 __COUT__ <<
"ChatSupervisor path: " << chatSupervisorToolsPath_ << __E__;
73 ChatSupervisor::~ChatSupervisor(
void) { destroy(); }
76 void ChatSupervisor::destroy(
void) { stopSlackDaemon(); }
79 void ChatSupervisor::defaultPage(xgi::Input* , xgi::Output* out)
81 out->getHTTPResponseHeader().addHeader(
"Access-Control-Allow-Origin",
"*");
82 out->getHTTPResponseHeader().addHeader(
"Pragma",
"no-cache");
84 *out <<
"<!DOCTYPE HTML><html lang='en'><frameset col='100%' row='100%'><frame "
85 "src='/WebPath/html/Chat.html?urn="
86 << this->getApplicationDescriptor()->getLocalId() <<
"'></frameset></html>";
94 CorePropertySupervisorBase::setSupervisorProperty(
95 CorePropertySupervisorBase::SUPERVISOR_PROPERTIES.AutomatedRequestTypes,
106 const WebUsers::RequestUserInfo& )
108 __COUTVS__(40, requestType);
115 cleanupExpiredChats();
117 if(requestType ==
"RefreshChat")
121 std::string lastUpdateIndexString =
124 uint64_t lastUpdateIndex;
125 sscanf(lastUpdateIndexString.c_str(),
"%lu", &lastUpdateIndex);
127 insertChatRefresh(&xmlOut, lastUpdateIndex, user);
129 else if(requestType ==
"RefreshUsers")
131 insertActiveUsers(&xmlOut);
133 else if(requestType ==
"SendChat")
142 newChat(chat, user, type);
144 if(notify ==
"1" || notify ==
"true")
145 GatewaySupervisor::addSystemMessage(
"*",
"[USER] " + chat);
147 else if(requestType ==
"PageUser")
150 unsigned int topageId = CgiDataUtilities::postDataAsInt(cgiIn,
"topageId");
153 __COUT__ <<
"Paging = " << topage.substr(0, 10)
154 <<
"... from user = " << user.substr(0, 10) << std::endl;
158 theRemoteWebUsers_.sendSystemMessage(topage,
159 user +
" is paging you to come chat.");
163 __SUP_SS__ <<
"requestType Request, " << requestType
164 <<
", not recognized by the Chat Editor Supervisor (was it intended "
165 "for another Supervisor?)."
176 void ChatSupervisor::escapeChat(std::string& )
189 xmlOut->addTextElementToData(
"active_users", theRemoteWebUsers_.getActiveUserList());
200 uint64_t lastUpdateIndex,
201 const std::string& user)
205 if(!isLastUpdateIndexStale(lastUpdateIndex))
211 sprintf(tempStr,
"%lu", ChatLastUpdateIndex);
212 xmlOut->addTextElementToData(
"last_update_index", tempStr);
215 xmlOut->addTextElementToData(
"chat_users",
"");
216 for(uint64_t i = 0; i < ChatUsers_.size(); ++i)
222 xmlOut->addTextElementToData(
"chat_history",
"");
223 for(uint64_t i = 0; i < ChatHistoryEntry_.size(); ++i)
225 __COUTT__ <<
"Chat[" << i <<
"]: " << ChatHistoryIndex_[i] <<
" vs "
226 << lastUpdateIndex << __E__;
227 if(isChatOld(ChatHistoryIndex_[i], lastUpdateIndex))
231 "chat_entry", ChatHistoryEntry_[i],
"chat_history");
233 "chat_author", ChatHistoryAuthor_[i],
"chat_history");
235 sprintf(tempStr,
"%lu", ChatHistoryTime_[i]);
243 void ChatSupervisor::newUser(
const std::string& user)
245 for(uint64_t i = 0; i < ChatUsers_.size(); ++i)
246 if(ChatUsers_[i] == user)
248 ChatUsersTime_[i] = time(0);
252 __COUT__ <<
"New user: " << user << std::endl;
254 ChatUsers_.push_back(user);
255 ChatUsersTime_.push_back(time(0));
256 newChat(user +
" joined the chat.",
263 void ChatSupervisor::newChat(
const std::string& chat,
264 const std::string& user,
265 const std::string& type,
268 ChatHistoryEntry_.push_back(chat);
269 ChatHistoryAuthor_.push_back(user);
270 ChatHistoryType_.push_back(type);
271 ChatHistoryTime_.push_back(time(0));
272 ChatHistoryIndex_.push_back(incrementAndGetLastUpdate());
273 if(enableSlackChat && !fromSlack)
274 sendToSlack(user, chat);
280 bool ChatSupervisor::isChatOld(uint64_t chatIndex, uint64_t last)
282 return (last - chatIndex < (uint64_t(1) << 62));
287 bool ChatSupervisor::isLastUpdateIndexStale(uint64_t last)
289 return ChatLastUpdateIndex != last;
294 uint64_t ChatSupervisor::incrementAndGetLastUpdate()
296 if(!++ChatLastUpdateIndex)
297 ++ChatLastUpdateIndex;
298 return ChatLastUpdateIndex;
304 void ChatSupervisor::cleanupExpiredChats()
306 for(uint64_t i = 0; i < ChatHistoryEntry_.size(); ++i)
307 if(i >= CHAT_HISTORY_MAX_ENTRIES ||
308 ChatHistoryTime_[i] + CHAT_HISTORY_EXPIRATION_TIME < time(0))
310 removeChatHistoryEntry(i);
317 for(uint64_t i = 0; i < ChatUsers_.size(); ++i)
318 if(ChatUsersTime_[i] + CHAT_HISTORY_EXPIRATION_TIME < time(0))
320 removeChatUserEntry(i);
330 void ChatSupervisor::removeChatHistoryEntry(uint64_t i)
332 ChatHistoryEntry_.erase(ChatHistoryEntry_.begin() + i);
333 ChatHistoryAuthor_.erase(ChatHistoryAuthor_.begin() + i);
334 ChatHistoryType_.erase(ChatHistoryType_.begin() + i);
335 ChatHistoryTime_.erase(ChatHistoryTime_.begin() + i);
336 ChatHistoryIndex_.erase(ChatHistoryIndex_.begin() + i);
341 void ChatSupervisor::removeChatUserEntry(uint64_t i)
343 newChat(ChatUsers_[i] +
" left the chat.",
345 ChatUsers_.erase(ChatUsers_.begin() + i);
346 ChatUsersTime_.erase(ChatUsersTime_.begin() + i);
351 void ChatSupervisor::sendToSlack(
const std::string& user,
const std::string& message)
356 std::string command =
"python3 " + chatSupervisorToolsPath_ +
"SendSlackChat.py " +
357 "--message " + StringMacros::encodeURIComponent(message) +
358 " --user " + StringMacros::encodeURIComponent(user);
359 __COUT__ <<
"Executing command: " << command << __E__;
365 if(!result.empty() && result.find(
"Error:") != std::string::npos)
366 __COUT__ <<
"Error from SendSlackChat.py: " << result << __E__;
367 else if(!result.empty())
368 __COUT__ <<
"Response from SendSlackChat.py: " << result << __E__;
370 catch(
const std::exception& e)
372 __COUT__ <<
"Exception while executing command: " << e.what() << __E__;
379 void ChatSupervisor::startSlackDaemon()
381 if(slackDaemonPid_ > 0)
384 std::string script = chatSupervisorToolsPath_ +
"ReceiveSlackChat.py";
385 const char* intervalEnv = std::getenv(
"OTS_SLACK_POLL_INTERVAL");
386 std::string interval = intervalEnv ? intervalEnv :
"30";
387 __COUT__ <<
"Starting Slack receive daemon: " << script <<
" -> " << slackInboxPath_
388 <<
" (interval=" << interval <<
"s)" << __E__;
393 __COUT__ <<
"Failed to fork Slack receive daemon" << __E__;
402 slackInboxPath_.c_str(),
409 slackDaemonPid_ = pid;
410 __COUT__ <<
"Slack receive daemon started with PID " << slackDaemonPid_ << __E__;
416 void ChatSupervisor::stopSlackDaemon()
418 if(slackDaemonPid_ <= 0)
422 pid_t probe_pid = waitpid(slackDaemonPid_, &status, WNOHANG);
425 __COUT__ <<
"Failed to probe Slack receive daemon PID " << slackDaemonPid_
426 <<
"; leaving daemon state and inbox files intact" << __E__;
429 bool can_manage = probe_pid != slackDaemonPid_;
433 __COUT__ <<
"Stopping Slack receive daemon PID " << slackDaemonPid_ << __E__;
434 kill(slackDaemonPid_, SIGTERM);
436 bool exited = !can_manage;
437 for(
int i = 0; i < 50 && !exited; ++i)
439 pid_t r = waitpid(slackDaemonPid_, &status, WNOHANG);
440 if(r == slackDaemonPid_)
454 __COUT__ <<
"Slack receive daemon did not exit after SIGTERM; sending SIGKILL"
456 kill(slackDaemonPid_, SIGKILL);
457 waitpid(slackDaemonPid_, &status, 0);
462 slackDaemonPid_, &status, WNOHANG);
465 slackDaemonPid_ = -1;
468 unlink(slackInboxPath_.c_str());
469 unlink((slackInboxPath_ +
".lock").c_str());
476 void ChatSupervisor::receiveFromSlack()
478 if(!enableSlackChat || slackInboxPath_.empty())
481 std::string lockPath = slackInboxPath_ +
".lock";
482 int lockFd = open(lockPath.c_str(), O_WRONLY | O_CREAT, 0644);
485 __COUT__ <<
"receiveFromSlack: cannot open lock file " << lockPath << __E__;
489 if(flock(lockFd, LOCK_EX | LOCK_NB) != 0)
495 std::vector<std::string> lines;
497 std::ifstream infile(slackInboxPath_);
501 while(std::getline(infile, line))
503 lines.push_back(line);
510 __COUT__ <<
"receiveFromSlack: read " << lines.size() <<
" line(s) from "
511 << slackInboxPath_ << __E__;
512 std::ofstream clearFile(slackInboxPath_, std::ios::trunc);
515 flock(lockFd, LOCK_UN);
518 for(
const auto& line : lines)
520 size_t firstTab = line.find(
'\t');
521 if(firstTab == std::string::npos)
523 __COUT__ <<
"receiveFromSlack: skipping malformed line (no tab): " << line
528 std::string tag = line.substr(0, firstTab);
531 __COUT__ <<
"receiveFromSlack: skipping unknown tag '" << tag <<
"'" << __E__;
535 size_t secondTab = line.find(
'\t', firstTab + 1);
536 if(secondTab == std::string::npos)
538 __COUT__ <<
"receiveFromSlack: skipping malformed MSG line: " << line
543 std::string user = line.substr(firstTab + 1, secondTab - firstTab - 1);
544 std::string message = line.substr(secondTab + 1);
547 [](std::string& s,
const std::string& from,
const std::string& to) {
549 while((pos = s.find(from, pos)) != std::string::npos)
551 s.replace(pos, from.size(), to);
557 replaceAll(user,
"&",
"%26");
558 replaceAll(user,
"<",
"%3C");
559 replaceAll(user,
">",
"%3E");
560 replaceAll(user,
"\"",
"%22");
561 replaceAll(user,
"'",
"%27");
565 decoded.reserve(message.size());
566 for(
size_t i = 0; i < message.size(); ++i)
568 if(message[i] ==
'\\' && i + 1 < message.size())
570 if(message[i + 1] ==
'\\')
572 decoded.push_back(
'\\');
576 if(message[i + 1] ==
'n')
583 decoded.push_back(message[i]);
585 message.swap(decoded);
587 replaceAll(message,
"&",
"%26");
588 replaceAll(message,
"<",
"%3C");
589 replaceAll(message,
">",
"%3E");
590 replaceAll(message,
"\"",
"%22");
591 replaceAll(message,
"'",
"%27");
592 replaceAll(message,
" ",
"%20%20");
594 __COUT__ <<
"receiveFromSlack: injecting message from user '" << user <<
"' ("
595 << message.size() <<
" bytes)" << __E__;
596 newChat(message,
"[slack] " + user,
"" ,
true);
static std::string postData(cgicc::Cgicc &cgi, const std::string &needle)
virtual void request(const std::string &requestType, cgicc::Cgicc &cgiIn, HttpXmlDocument &xmlOut, const WebUsers::RequestUserInfo &userInfo) override
end forceSupervisorPropertyValues()
virtual void forceSupervisorPropertyValues(void) override
override to force supervisor property values (and ignore user settings)
xercesc::DOMElement * addTextElementToParent(const std::string &childName, const std::string &childText, xercesc::DOMElement *parent)
void INIT_MF(const char *name)
static std::string exec(const char *cmd)