1 #include "otsdaq-utilities/Chat/ChatSupervisor.h"
2 #include "otsdaq/CgiDataUtilities/CgiDataUtilities.h"
3 #include "otsdaq/Macros/CoutMacros.h"
4 #include "otsdaq/Macros/StringMacros.h"
5 #include "otsdaq/MessageFacility/MessageFacility.h"
6 #include "otsdaq/XmlUtilities/HttpXmlDocument.h"
8 #include <xdaq/NamespaceURI.h>
23 #define __MF_SUBJECT__ "Chat"
32 ChatLastUpdateIndex = 1;
36 enableSlackChat = (std::getenv(
"OTS_EN_SLACK") !=
nullptr &&
37 std::string(std::getenv(
"OTS_EN_SLACK")) ==
"1");
40 const char* env = std::getenv(
"OTSDAQ_UTILITIES_DIR");
41 chatSupervisorToolsPath_ = env ? env :
"";
43 if(chatSupervisorToolsPath_.empty())
44 enableSlackChat =
false;
45 if(chatSupervisorToolsPath_.back() !=
'/')
46 chatSupervisorToolsPath_ +=
'/';
47 chatSupervisorToolsPath_ +=
"tools/";
49 const char* userData = std::getenv(
"USER_DATA");
51 slackInboxPath_ = std::string(userData) +
"/ChatSlackInbox.txt";
54 "/tmp/ots_slack_inbox_uid" + std::to_string(getuid()) +
".txt";
57 if(std::getenv(
"SLACK_BOT_TOKEN") ==
nullptr ||
58 std::getenv(
"SLACK_CHANNEL") ==
nullptr ||
59 std::getenv(
"SLACK_CHANNEL_ID") ==
nullptr)
60 enableSlackChat =
false;
62 __COUT__ <<
"ChatSupervisor: Slack chat "
63 << (enableSlackChat ?
"enabled" :
"disabled") << __E__;
64 __COUT__ <<
"ChatSupervisor path: " << chatSupervisorToolsPath_ << __E__;
72 ChatSupervisor::~ChatSupervisor(
void) { destroy(); }
75 void ChatSupervisor::destroy(
void) { stopSlackDaemon(); }
78 void ChatSupervisor::defaultPage(xgi::Input* , xgi::Output* out)
80 out->getHTTPResponseHeader().addHeader(
"Access-Control-Allow-Origin",
"*");
81 out->getHTTPResponseHeader().addHeader(
"Pragma",
"no-cache");
83 *out <<
"<!DOCTYPE HTML><html lang='en'><frameset col='100%' row='100%'><frame "
84 "src='/WebPath/html/Chat.html?urn="
85 << this->getApplicationDescriptor()->getLocalId() <<
"'></frameset></html>";
93 CorePropertySupervisorBase::setSupervisorProperty(
94 CorePropertySupervisorBase::SUPERVISOR_PROPERTIES.AutomatedRequestTypes,
105 const WebUsers::RequestUserInfo& )
107 __COUTVS__(40, requestType);
114 cleanupExpiredChats();
116 if(requestType ==
"RefreshChat")
120 std::string lastUpdateIndexString =
123 uint64_t lastUpdateIndex;
124 sscanf(lastUpdateIndexString.c_str(),
"%lu", &lastUpdateIndex);
126 insertChatRefresh(&xmlOut, lastUpdateIndex, user);
128 else if(requestType ==
"RefreshUsers")
130 insertActiveUsers(&xmlOut);
132 else if(requestType ==
"SendChat")
141 else if(requestType ==
"PageUser")
144 unsigned int topageId = CgiDataUtilities::postDataAsInt(cgiIn,
"topageId");
147 __COUT__ <<
"Paging = " << topage.substr(0, 10)
148 <<
"... from user = " << user.substr(0, 10) << std::endl;
152 theRemoteWebUsers_.sendSystemMessage(topage,
153 user +
" is paging you to come chat.");
157 __SUP_SS__ <<
"requestType Request, " << requestType
158 <<
", not recognized by the Chat Editor Supervisor (was it intended "
159 "for another Supervisor?)."
170 void ChatSupervisor::escapeChat(std::string& )
183 xmlOut->addTextElementToData(
"active_users", theRemoteWebUsers_.getActiveUserList());
194 uint64_t lastUpdateIndex,
195 const std::string& user)
199 if(!isLastUpdateIndexStale(lastUpdateIndex))
205 sprintf(tempStr,
"%lu", ChatLastUpdateIndex);
206 xmlOut->addTextElementToData(
"last_update_index", tempStr);
209 xmlOut->addTextElementToData(
"chat_users",
"");
210 for(uint64_t i = 0; i < ChatUsers_.size(); ++i)
216 xmlOut->addTextElementToData(
"chat_history",
"");
217 for(uint64_t i = 0; i < ChatHistoryEntry_.size(); ++i)
219 __COUTT__ <<
"Chat[" << i <<
"]: " << ChatHistoryIndex_[i] <<
" vs "
220 << lastUpdateIndex << __E__;
221 if(isChatOld(ChatHistoryIndex_[i], lastUpdateIndex))
225 "chat_entry", ChatHistoryEntry_[i],
"chat_history");
227 "chat_author", ChatHistoryAuthor_[i],
"chat_history");
228 sprintf(tempStr,
"%lu", ChatHistoryTime_[i]);
236 void ChatSupervisor::newUser(
const std::string& user)
238 for(uint64_t i = 0; i < ChatUsers_.size(); ++i)
239 if(ChatUsers_[i] == user)
241 ChatUsersTime_[i] = time(0);
245 __COUT__ <<
"New user: " << user << std::endl;
247 ChatUsers_.push_back(user);
248 ChatUsersTime_.push_back(time(0));
249 newChat(user +
" joined the chat.",
256 void ChatSupervisor::newChat(
const std::string& chat,
257 const std::string& user,
260 ChatHistoryEntry_.push_back(chat);
261 ChatHistoryAuthor_.push_back(user);
262 ChatHistoryTime_.push_back(time(0));
263 ChatHistoryIndex_.push_back(incrementAndGetLastUpdate());
264 if(enableSlackChat && !fromSlack)
265 sendToSlack(user, chat);
271 bool ChatSupervisor::isChatOld(uint64_t chatIndex, uint64_t last)
273 return (last - chatIndex < (uint64_t(1) << 62));
278 bool ChatSupervisor::isLastUpdateIndexStale(uint64_t last)
280 return ChatLastUpdateIndex != last;
285 uint64_t ChatSupervisor::incrementAndGetLastUpdate()
287 if(!++ChatLastUpdateIndex)
288 ++ChatLastUpdateIndex;
289 return ChatLastUpdateIndex;
295 void ChatSupervisor::cleanupExpiredChats()
297 for(uint64_t i = 0; i < ChatHistoryEntry_.size(); ++i)
298 if(i >= CHAT_HISTORY_MAX_ENTRIES ||
299 ChatHistoryTime_[i] + CHAT_HISTORY_EXPIRATION_TIME < time(0))
301 removeChatHistoryEntry(i);
308 for(uint64_t i = 0; i < ChatUsers_.size(); ++i)
309 if(ChatUsersTime_[i] + CHAT_HISTORY_EXPIRATION_TIME < time(0))
311 removeChatUserEntry(i);
321 void ChatSupervisor::removeChatHistoryEntry(uint64_t i)
323 ChatHistoryEntry_.erase(ChatHistoryEntry_.begin() + i);
324 ChatHistoryTime_.erase(ChatHistoryTime_.begin() + i);
325 ChatHistoryAuthor_.erase(ChatHistoryAuthor_.begin() + i);
326 ChatHistoryIndex_.erase(ChatHistoryIndex_.begin() + i);
331 void ChatSupervisor::removeChatUserEntry(uint64_t i)
333 newChat(ChatUsers_[i] +
" left the chat.",
335 ChatUsers_.erase(ChatUsers_.begin() + i);
336 ChatUsersTime_.erase(ChatUsersTime_.begin() + i);
341 void ChatSupervisor::sendToSlack(
const std::string& user,
const std::string& message)
346 std::string command =
"python3 " + chatSupervisorToolsPath_ +
"SendSlackChat.py " +
347 "--message " + StringMacros::encodeURIComponent(message) +
348 " --user " + StringMacros::encodeURIComponent(user);
349 __COUT__ <<
"Executing command: " << command << __E__;
355 if(!result.empty() && result.find(
"Error:") != std::string::npos)
356 __COUT__ <<
"Error from SendSlackChat.py: " << result << __E__;
357 else if(!result.empty())
358 __COUT__ <<
"Response from SendSlackChat.py: " << result << __E__;
360 catch(
const std::exception& e)
362 __COUT__ <<
"Exception while executing command: " << e.what() << __E__;
369 void ChatSupervisor::startSlackDaemon()
371 if(slackDaemonPid_ > 0)
374 std::string script = chatSupervisorToolsPath_ +
"ReceiveSlackChat.py";
375 const char* intervalEnv = std::getenv(
"OTS_SLACK_POLL_INTERVAL");
376 std::string interval = intervalEnv ? intervalEnv :
"30";
377 __COUT__ <<
"Starting Slack receive daemon: " << script <<
" -> " << slackInboxPath_
378 <<
" (interval=" << interval <<
"s)" << __E__;
383 __COUT__ <<
"Failed to fork Slack receive daemon" << __E__;
392 slackInboxPath_.c_str(),
399 slackDaemonPid_ = pid;
400 __COUT__ <<
"Slack receive daemon started with PID " << slackDaemonPid_ << __E__;
406 void ChatSupervisor::stopSlackDaemon()
408 if(slackDaemonPid_ <= 0)
412 pid_t probe_pid = waitpid(slackDaemonPid_, &status, WNOHANG);
415 __COUT__ <<
"Failed to probe Slack receive daemon PID " << slackDaemonPid_
416 <<
"; leaving daemon state and inbox files intact" << __E__;
419 bool can_manage = probe_pid != slackDaemonPid_;
423 __COUT__ <<
"Stopping Slack receive daemon PID " << slackDaemonPid_ << __E__;
424 kill(slackDaemonPid_, SIGTERM);
426 bool exited = !can_manage;
427 for(
int i = 0; i < 50 && !exited; ++i)
429 pid_t r = waitpid(slackDaemonPid_, &status, WNOHANG);
430 if(r == slackDaemonPid_)
444 __COUT__ <<
"Slack receive daemon did not exit after SIGTERM; sending SIGKILL"
446 kill(slackDaemonPid_, SIGKILL);
447 waitpid(slackDaemonPid_, &status, 0);
452 slackDaemonPid_, &status, WNOHANG);
455 slackDaemonPid_ = -1;
458 unlink(slackInboxPath_.c_str());
459 unlink((slackInboxPath_ +
".lock").c_str());
466 void ChatSupervisor::receiveFromSlack()
468 if(!enableSlackChat || slackInboxPath_.empty())
471 std::string lockPath = slackInboxPath_ +
".lock";
472 int lockFd = open(lockPath.c_str(), O_WRONLY | O_CREAT, 0644);
475 __COUT__ <<
"receiveFromSlack: cannot open lock file " << lockPath << __E__;
479 if(flock(lockFd, LOCK_EX | LOCK_NB) != 0)
485 std::vector<std::string> lines;
487 std::ifstream infile(slackInboxPath_);
491 while(std::getline(infile, line))
493 lines.push_back(line);
500 __COUT__ <<
"receiveFromSlack: read " << lines.size() <<
" line(s) from "
501 << slackInboxPath_ << __E__;
502 std::ofstream clearFile(slackInboxPath_, std::ios::trunc);
505 flock(lockFd, LOCK_UN);
508 for(
const auto& line : lines)
510 size_t firstTab = line.find(
'\t');
511 if(firstTab == std::string::npos)
513 __COUT__ <<
"receiveFromSlack: skipping malformed line (no tab): " << line
518 std::string tag = line.substr(0, firstTab);
521 __COUT__ <<
"receiveFromSlack: skipping unknown tag '" << tag <<
"'" << __E__;
525 size_t secondTab = line.find(
'\t', firstTab + 1);
526 if(secondTab == std::string::npos)
528 __COUT__ <<
"receiveFromSlack: skipping malformed MSG line: " << line
533 std::string user = line.substr(firstTab + 1, secondTab - firstTab - 1);
534 std::string message = line.substr(secondTab + 1);
537 [](std::string& s,
const std::string& from,
const std::string& to) {
539 while((pos = s.find(from, pos)) != std::string::npos)
541 s.replace(pos, from.size(), to);
547 replaceAll(user,
"&",
"%26");
548 replaceAll(user,
"<",
"%3C");
549 replaceAll(user,
">",
"%3E");
550 replaceAll(user,
"\"",
"%22");
551 replaceAll(user,
"'",
"%27");
555 decoded.reserve(message.size());
556 for(
size_t i = 0; i < message.size(); ++i)
558 if(message[i] ==
'\\' && i + 1 < message.size())
560 if(message[i + 1] ==
'\\')
562 decoded.push_back(
'\\');
566 if(message[i + 1] ==
'n')
573 decoded.push_back(message[i]);
575 message.swap(decoded);
577 replaceAll(message,
"&",
"%26");
578 replaceAll(message,
"<",
"%3C");
579 replaceAll(message,
">",
"%3E");
580 replaceAll(message,
"\"",
"%22");
581 replaceAll(message,
"'",
"%27");
582 replaceAll(message,
" ",
"%20%20");
584 __COUT__ <<
"receiveFromSlack: injecting message from user '" << user <<
"' ("
585 << message.size() <<
" bytes)" << __E__;
586 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)