Warning: session_start(): open(/var/lib/php/sessions/sess_8tn7tdl8n26bd1gbi5tnb6rrpn, O_RDWR) failed: No space left on device (28) in /var/www/tb-usercake/models/config.php on line 51
Warning: session_start(): Failed to read session data: files (path: /var/lib/php/sessions) in /var/www/tb-usercake/models/config.php on line 51
!include once #1029875 // concept Worker
abstract sclass DynNewBot2 > DynPrintLogAndEnabled {
set flag NoNanoHTTPD.
!include #1029545 // API for Eleu
int maxRefsToShow = 5;
volatile long requestsServed;
transient S templateID = #1029809;
sS cssID = #1029808;
transient S botName = "DynNewBot2";
transient S heading = "DynNewBot2";
transient S adminName = "DynNewBot2 Admin";
transient S botImageID = #1102935;
transient S userImageID = #1102803;
transient S chatHeaderImageID = #1102802;
transient S timeZone = ukTimeZone_string();
transient S baseLink = "";
transient bool newDesign = true; // use new chat bot design
transient bool ariaLiveTrick = false;
transient bool ariaLiveTrick2 = true;
!include once #1029876 // WorkerChat
transient new WorkerChat workerChat;
transient ReliableSingleThread rstBotActions = dm_rst(this, r botActions);
transient S defaultHeaderColorLeft = "#2a27da";
transient S defaultHeaderColorRight = "#00ccff";
transient bool enableUsers;
transient bool useWebSockets;
transient bool showRegisterLink;
transient bool showTalkToBotLink;
transient bool alwaysRedirectToHttps;
transient bool redirectOnLogout; // remove that ugly logout=1 from url
transient bool showFullErrors;
transient bool lockWhileDoingBotActions;
transient bool standardButtonsAsSymbols;
transient bool enableVars; // $userName etc. in bot messages
transient bool enableAvatars;
transient bool recordExecutionsAndInputs = true;
transient bool phoneNumberSpecialInputField = true;
transient bool enableRadioButtons; // allow radio buttons for single-choice selection in bot dialogs
transient bool newDialogAfterWindowClosed; // always restart conversation when user comes back
transient bool showJoiningConversation; // show "... joining conversation"
transient bool quickRadioButtons; // don't show an OK button, make radio buttons submit instantly
transient int typingTimeshift = 2000; // make typing bubble appear more reliably
transient new ThreadLocal currentReq;
transient volatile Scorer consistencyCheckResults;
transient Lock statsLock = lock();
transient SS specialPurposes = litcimap(
"bad email address", "Please enter a valid e-mail address",
"bad phone number", "Please enter a valid phone number");
// end of variables
void start {
super.start();
dm_setModuleName(botName);
dm_assertFirstSibling();
concepts_setUnlistedByDefault(true);
standardTimeZone();
standardTimeZone_name = timeZone;
print("DB program ID: " + dbProgramID());
realPW(); // make password
pWebChatBot();
doEvery(60.0, r cleanConversations);
rstBotActions.trigger();
}
afterVisualize {
addToControlArea(jPopDownButton_noText(popDownButtonEntries()));
}
O[] popDownButtonEntries() {
ret litobjectarray(
"Show/edit master password...", rThreadEnter editMasterPassword,
);
}
sclass Req {
IWebRequest webRequest;
S uri;
SS params;
AuthedDialogID auth;
Domain authDomainObj;
S authDomain;
HTMLFramer1 framer;
bool masterAuthed;
Conversation conv;
S uri() { ret uri; }
bool requestAuthed() { ret auth != null; }
S get(S param) { ret webRequest.get(param); }
void markNoSpam { webRequest.noSpam(); }
}
bool calcMasterAuthed(Req req) {
ret req.auth != null && req.auth.master;
}
O html(IWebRequest request) enter {
try {
ret html2(request);
} catch print e {
ret serve500(showFullErrors ? getStackTrace(e) : "Error.");
}
}
void requestServed {}
O html2(IWebRequest request) {
htmlencode_forParams_useV2();
{
lock statsLock;
requestsServed++;
change();
requestServed();
}
S uri = request.uri();
SS params = request.params();
new Req req;
req.webRequest = request;
req.uri = uri;
req.params = params;
if (alwaysRedirectToHttps)
try object redirectToHttps(req);
if (eq(params.get("_newConvCookie"), "1"))
ret hrefresh(appendQueryToURL(req.uri, mapPlus(mapMinus(req.params, "_newConvCookie"),
cookie := "test_" + aGlobalID())));
// cookie comes either from a URI parameter or from the request header
// Note: this can be either a JS-generated (in dynamic chat bot part)
// or server-generated cookie (when loading initial chat bot script or admin)
// To distinguish: JS-generated cookies are shorter and can contain numbers
S cookie = request.cookie();
//print("Request cookie: " + cookie);
bool workerMode = nempty(params.get("workerMode")) || startsWith(uri, "/worker");
// find out which domain we're on ("delivered domain")
S domain = request.domain(), _domain = domain;
saveDeliveredDomain(domain);
Domain domainObj = findDomainObj(domain);
// get conversation
S convCookie = params.get("cookie");
//if (eq(params.get("_newConvCookie"), "1")) convCookie = "conv-" + aGlobalID();
Conversation conv = isRequestFromBot(req) ? null
: nempty(convCookie) ? getConv(convCookie)
: nempty(cookie) ? getConv(cookie) : null;
req.conv = conv;
AutoCloseable tempThing = conv == null ? null : temp_printPrefix("Conv " + conv.cookie + ": ");
temp tempThing;
temp tempSetTL(currentReq, req);
S botConfig = params.get("_botConfig");
SS botConfigParams = decodeURIParams(botConfig);
S simulatedDomain = botConfigParams.get("domain");
Domain domainObj2 = nempty(simulatedDomain) ? findDomainObj(simulatedDomain) : domainObj;
if (nempty(botConfigParams)) cset(conv, botConfig := botConfigParams);
// save ip & domain in conversation
if (conv != null && !workerMode)
if (cset_trueIfChanged(conv, ip := request.clientIP(), +domain, domainObj := domainObj2)) {
calcCountry(conv);
initAvatar(conv);
}
//print("URI: " + uri + ", cookie: " + cookie + ", msgs: " + l(conv.msgs));
try object handleAuth(req, cookie);
// TODO: instead of maxCache, check file date & request header (If-Unmodified-Since?)
new Matches m;
if (startsWith(uri, "/worker-image/", m)) {
long id = parseLong(m.rest());
ret subBot_serveFile_maxCache(workerImageFile(id), "image/jpeg");
}
if (startsWith(uri, "/uploaded-image/", m)) {
long id = parseLong(m.rest());
UploadedImage img = getConcept UploadedImage(id);
ret img == null ? serve404() : subBot_serveFile_maxCache(img.imageFile(), "image/jpeg");
}
if (startsWith(uri, "/uploaded-sound/", m)) {
long id = parseLong(m.rest());
UploadedSound sound = getConcept UploadedSound(id);
ret sound == null ? serve404() : subBot_serveFile_maxCache(sound.soundFile(), mp3mimeType());
}
if (startsWith(uri, "/uploaded-file/", m)) {
long id = parseLong(dropAfterSlash(m.rest()));
UploadedFile f = getConcept UploadedFile(id);
ret f == null ? serve404() : subBot_serveFile_maxCache(f.theFile(),
or2(params.get("ct"), or2(trim(f.mimeType), binaryMimeType())));
}
AuthedDialogID auth = authObject(cookie);
if (eq(params.get('logout), "1")) {
cdelete(auth);
if (redirectOnLogout) ret hrefresh(req.uri);
auth = null;
}
req.auth = auth;
bool requestAuthed = auth != null; // any authentication
bool masterAuthed = req.masterAuthed = calcMasterAuthed(req);
Domain authDomainObj = !requestAuthed ? null : auth.restrictedToDomain!;
req.authDomainObj = authDomainObj;
S authDomain = !requestAuthed ? null : auth.domain();
req.authDomain = authDomain;
if (requestAuthed) req.markNoSpam();
try object serve2(req);
if (!requestAuthed && settings().talkToBotOnlyWithAuth)
ret serveAuthForm(params.get('uri));
if (eq(uri, "/emoji-picker/index.js"))
//ret serveWithContentType(loadTextFile(loadLibrary(#1400436)), "text/javascript"); // TODO: optimize
ret withHeader(subBot_maxCacheHeaders(serveInputStream(bufferedFileInputStream(loadLibrary(#1400436)), "text/javascript")));
if (eq(uri, "/emoji-picker-test"))
ret loadSnippet(#1029870);
if (eq(uri, "/logs")) {
if (!masterAuthed) ret serveAuthForm(rawLink(uri));
ret webChatBotLogsHTML2(rawLink(uri), params);
}
if (eq(uri, "/refchecker")) {
if (!masterAuthed) ret serveAuthForm(rawLink(uri));
ConceptsRefChecker refChecker = new (db_mainConcepts());
L errors = refChecker.run();
if (eq(params.get("fix"), "1"))
ret serveText(refChecker.fixAll());
else
ret serveText(jsonEncode_breakAtLevels(2, litorderedmap(errors := allToString(errors))));
}
if (eq(uri, "/backupDBNow")) {
if (!masterAuthed) ret serveAuthForm(rawLink(uri));
ret serveText("Backed up DB as " + fileInfo(backupConceptsNow()));
}
if (eq(uri, "/auth-only")) {
if (!requestAuthed) ret serveAuthForm(params.get('uri));
ret "";
}
if (eq(uri, "/leads-api"))
ret serveLeadsAPI(request);
if (workerChat != null) // TODO: don't permit when worker chat is disabled
try object workerChat.html(req);
S message = trim(params.get("btn"));
if (empty(message)) message = trim(params.get("message"));
if (match("new dialog", message)) {
lock dbLock();
conv.newDialog();
message = null;
}
if (eqic(message, "!toggle notifications")) {
cset(conv, notificationsOn := !conv.notificationsOn);
message = null;
}
this.conv.set(conv);
if (nempty(message) && !lastUserMessageWas(conv, message)) {
lock dbLock();
print("Adding message: " + message);
possiblyTriggerNewDialog(conv);
if (workerMode) {
Msg msg = new(false, message);
msg.fromWorker = auth.loggedIn;
conv.add(msg);
} else {
// add user message
conv.add(new Msg(true, message));
//conv.botTyping = now();
addScheduledAction(new OnUserMessage(conv));
}
}
S testMode = params.get("testMode");
if (nempty(testMode)) {
print("Setting testMode", testMode);
cset(conv, testMode := eq("1", testMode));
}
if (eq(uri, "/msg")) ret withHeader("OK");
if (eq(uri, "/typing")) {
if (workerMode) {
conv.botTyping = now();
print(conv.botTyping + " Bot typing in: " + conv.cookie);
} else {
conv.userTyping = now();
print(conv.userTyping + " User typing in: " + conv.cookie);
}
ret withHeader("OK");
}
// render all of a conversation's messages for inspection
if (eq(uri, "/renderMessages")) {
long msgTime = parseLong(params.get("msgTime"));
L msgs = conv.allMsgs();
if (msgTime != 0)
msgs = filter(msgs, msg -> msg.time == msgTime);
new StringBuilder buf;
renderMessages(conv, buf, msgs,
msg -> false); // don't show buttons
ret hhtml_title_body(nMessages(msgs),
hstylesheetsrc(cssURL())
+ p(nMessages(msgs) + " found")
+ buf);
}
if (eq(uri, "/incremental")) {
if (newDialogAfterWindowClosed && !conv.isActive()) {
print("Clearing conversation, timed out");
lock dbLock();
conv.newDialog();
}
long start = sysNow(), start2 = now();
print(+start2);
cset(conv, lastPing := now());
possiblyTriggerNewDialog(conv);
int a = parseInt(params.get("a"));
int reloadCounter = conv.reloadCounter;
L msgs;
bool first = true;
int timeout = toInt(req.params.get("timeout"));
long endTime = start+(timeout <= 0 || timeout > longPollMaxWait/1000 ? longPollMaxWait : timeout*1000L);
while (licensed() && sysNow() < endTime) {
int as = conv.archiveSize();
msgs = cloneSubList(conv.msgs, a-as); // just the new messages
bool reloadTriggered = conv.reloadCounter > reloadCounter;
bool actuallyNewDialog = a < as;
bool newDialog = actuallyNewDialog || reloadTriggered;
if (newDialog)
msgs = cloneList(conv.msgs);
long typing = workerMode ? conv.userTyping : conv.botTyping;
bool otherPartyTyping = typing >= start2-typingTimeshift;
bool anyEvent = nempty(msgs) || newDialog || otherPartyTyping;
if (!anyEvent) {
if (first) {
//print("Long poll starting on " + cookie + ", " + a + "/" + a);
first = false;
}
sleep(longPollTick);
} else {
if (!first) print("Long poll ended.");
if (eq(req.params.get("json"), "1"))
ret serveJSON_breakAtLevels(2, litorderedmap(
n := conv.allCount(),
newDialog := trueOrNull(newDialog),
otherPartyTyping := trueOrNull(otherPartyTyping),
msgs := map(msgs, msg ->
litorderedmap(
time := msg.time,
fromUser := msg.fromUser,
fromWorker := msg.fromWorker == null ?: msg.fromWorker.displayName,
text := msg.text,
// TODO: out
labels := msg.labels))));
new StringBuilder buf;
if (newDialog) {
// send header colors & notification status
S l = or2_trim(domainObj2.headerColorLeft, defaultDomain().headerColorLeft, defaultHeaderColorLeft),
r = or2_trim(domainObj2.headerColorRight, defaultDomain().headerColorRight, defaultHeaderColorRight);
buf.append(hcss(".chat_header { background: linear-gradient(135deg, "
+ hexColorToCSSRGB(l) + " 0%, " + hexColorToCSSRGB(r) + " 100%); }"));
buf.append(hscript("$('#chatBot_notiToggleText').text(" + jsQuote("Turn " + (conv.notificationsOn ? "off" : "on")
+ " notifications") + ");"));
if (showJoiningConversation) {
buf.append(hscript("$('#otherSideTyping .joining').html("
+ jsQuote(nameOfBotSide(conv) + " now joining conversation...") + ");"
+ "setTimeout(function() { $('#otherSideTyping .joining').html(''); }, 5000);"));
}
}
if (otherPartyTyping) {
print("Noticed " + (workerMode ? "user" : "bot") + " typing in " + conv.cookie);
buf.append(hscript("showTyping(" + jsQuote(conv.botImg()) + ");"));
}
renderMessages(conv, buf, msgs);
if (ariaLiveTrick2 && !workerMode) {
Msg msg = lastBotMsg(msgs);
if (msg != null) {
S author = msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : botName;
buf.append(hscript([[$("#screenreadertrick").html(]] + jsQuote(author + " says: " + msg.text) + ");"));
}
}
if (a != 0 && anyInterestingMessages(msgs, workerMode))
buf.append(hscript(
stringIf(conv.notificationsOn, "window.playChatNotification();\n") +
"window.setTitleStatus(" + jsQuote((workerMode ? "User" : botName) + " says…") + ");"
));
S html = str(buf);
// TODO: hack for notransition
//if (newDialog && !actuallyNewDialog) html = html.replace([[class="chat_msg_item]], [[class="notransition chat_msg_item]]);
ret withHeader("\n" + html);
}
}
ret withHeader("");
}
if (eqOneOf(uri, "/script", "/demo")) {
lock dbLock();
S myTemplateID = templateID;
S templateIDParam = req.params.get("templateID");
if (nempty(templateIDParam) && allowedTemplateID(templateIDParam))
myTemplateID = templateIDParam;
S html = loadSnippet_cached(myTemplateID);
S botLayout = req.params.get("botLayout");
S layout = or2(botLayout, defaultBotLayout());
html = modifyTemplateBeforeDelivery(html, req);
S heading = or2(headingForReq(req), or2(trim(domainObj2.botName), this.heading));
S botImg = botImageForDomain(domainObj);
UploadedSound sound = settings().notificationSound!;
S notificationSound = sound != null ? sound.soundURL() : defaultNotificationSound();
S miscParam = workerMode ? "workerMode=1&" : "";
if (nempty(botLayout))
miscParam += "botLayout=" + urlencode(botLayout) + "&";
S incrementalURL = baseLink + "/incremental?" + miscParam + "a=";
S typingURL = baseLink + "/typing?" + miscParam;
S msgURL = baseLink + "/msg?" + miscParam + "message=";
if (eqic(layout, "sahil")) {
html = replaceDollarVars(html,
+incrementalURL,
+typingURL,
+msgURL,
n := 0,
+notificationSound,
+workerMode,
+heading,
+botImg);
} else {
S langlinks = "";
if (html.contains(langlinks))
html = html.replace(langlinks,
ahref(rawLink("eng"), "English") + " | " + ahref(rawLink("deu"), "German"));
//html = html.replace("#COUNTRYCODE_OPTIONS#", countryCodeOptions(conv));
html = html.replace("#COUNTRY#", lower(conv.country));
html = html.replace("#BOTIMG#", botImg);
html = html.replace("#N#", "0");
html = html.replace("#INCREMENTALURL#", incrementalURL);
html = html.replace("#MSGURL#", msgURL);
html = html.replace("#TYPINGURL#", typingURL);
html = html.replace("#CSS_ID#", psI_str(cssID));
if (ariaLiveTrick || ariaLiveTrick2)
html = html.replace([[aria-live="polite">]], ">");
html = html.replace("#OTHERSIDE#", workerMode ? "User" : "Representative");
if (nempty(params.get("debug")))
html = html.replace("var showActions = false;", "var showActions = true;");
html = html.replace("#AUTOOPEN#", jsBool(workerMode || eq(params.get("_autoOpenBot"), "1") || botAutoOpen(domainObj2)));
html = html.replace("#BOT_ON#", jsBool(botOn() || eq(uri, "/demo")));
html = html.replace("$HEADING", heading);
html = html.replace("#WORKERMODE", jsBool(workerMode));
html = html.replace("#NOTIFICATIONSOUND#", notificationSound);
html = html.replace("", "");
html = hreplaceTitle(html, heading);
}
if (eq(uri, "/demo"))
ret hhtml(hhead(
htitle(heading)
+ loadJQuery2()
) + hbody(hjavascript(html)));
else
ret withHeader(subBot_serveJavaScript(html));
}
try object serveOtherPage(req);
if (eq(uri, "/")) try object serveHomePage();
// serve uploaded files
if (!startsWith(uri, "/crud/")) {
// TODO: more caching
UploadedFile fileToServe = conceptWhere UploadedFile(liveURI := urldecode(dropSlashPrefix(uri)));
if (fileToServe != null)
ret subBot_serveFile(fileToServe.theFile(), nempty(fileToServe.mimeType)
? fileToServe.mimeType
: guessMimeTypeFromFileName(afterLastSlash(fileToServe.liveURI)));
}
// add more uris here
// serve admin
if (!requestAuthed) ret serveAuthForm(params.get('uri));
// authed from here (not necessarily master-authed though)
if (masterAuthed && eq(uri, "/dialogTree")) {
BotStep step = getConcept BotStep(toLong(params.get("stepID")));
if (step == null) ret serve404("Step not found");
ret hhtml_head_title_body("Dialog Tree for " + step,
hmobilefix() + hsansserif() + h2("Dialog Tree")
+ hcss_linkColorInherit()
+ hcss([[
.dialogTree li { margin-top: 0.8em; }
]])
+ renderDialogTree(step));
}
if (eq(uri, "/thoughts"))
ret serveThoughts(req);
if (masterAuthed && eq(uri, "/search"))
ret serveSearch(req);
if (eq(uri, "/leads-csv")) {
S text = leadsToCSV(conceptsWhere(Lead, mapToParams(filtersForClass(Lead, req))));
S name = "bot-leads"
+ (authDomainObj == null ? "" : "-" + replace(str(authDomainObj), "/", "-"))
+ "-" + ymd_minus_hm() + ".csv";
ret serveCSVWithFileName(name, text);
}
if (eq(uri, "/cleanConversations") && masterAuthed) {
cleanConversations();
ret hrefresh(baseLink + "/crud/Conversation");
}
if (eq(uri, "/deleteDeliveredDomains") && masterAuthed) {
deleteDeliveredDomains();
ret hrefresh(baseLink + "/crud/DeliveredDomain");
}
makeFramer(req);
HTMLFramer1 framer = req.framer;
L classes = crudClasses(req);
L cmdClasses = req.masterAuthed ? botCmdClasses() : null;
for (Class c : (Set) asSet(flattenList2(classes, DeliveredDomain.class, cmdClasses)))
if (eq(uri, dropUriPrefix(baseLink, crudLink(c)))) {
S help = mapGet(crudHelp(), c);
if (nempty(help)) framer.add(p(help));
HCRUD crud = makeCRUD(c, req);
if (c == UserKeyword) {
Scorer scorer = consistencyCheckResults();
framer.add(p("Consistency check results: " + nTests(scorer.successes) + " OK, " + nErrors(scorer.errors)));
if (nempty(scorer.errors))
framer.add(ul(map(scorer.errors, e -> "Error: " + e)));
}
// render CRUD
S json = crud.handleComboSearch(params);
if (nempty(json)) ret serveText(json);
crud.processSortParameter(params);
framer.add(crud.renderPage(params));
// javascript magic to highlight table row according to anchor in URL
framer.add(hjs_markRowMagic());
}
if (eq(uri, "/emojis")) {
framer.title = "Emoji List";
framer.contents.add(htmlTable2(map(emojiShortNameMap(),
(code, emoji) -> litorderedmap("Shortcode" := code, "Emoji" := emoji))));
}
if (eq(uri, "/embedCode")) {
S goDaddyEmbedCode = "";
framer.title = "Embed Code";
framer.contents.add(h2("Chat bot embed code")
+ (empty(goDaddyEmbedCode) ? "" :
h3("GoDaddy Site Builder")
+ p("Add an HTML box with this code:")
+ pre(htmlEncode2(goDaddyEmbedCode))
+ h3("Other"))
+ p("Add this code in front of your " + tt(htmlEncode2("