Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

3012
LINES

< > BotCompany Repo | #1030652 // DynNewBot2 [backup before undo states]

JavaX fragment (include)

1  
!include once #1029875 // concept Worker
2  
3  
abstract sclass DynNewBot2 > DynPrintLogAndEnabled {
4  
  set flag NoNanoHTTPD.
5  
  !include #1029545 // API for Eleu
6  
7  
  int maxRefsToShow = 5;
8  
  volatile long requestsServed;
9  
  
10  
  transient S templateID = #1029809;
11  
  sS cssID = #1029808;
12  
13  
  transient S botName = "DynNewBot2";
14  
  transient S heading = "DynNewBot2";
15  
  transient S adminName = "DynNewBot2 Admin";
16  
  transient S botImageID = #1102935;
17  
  transient S userImageID = #1102803;
18  
  transient S chatHeaderImageID = #1102802;
19  
  transient S timeZone = ukTimeZone_string(); 
20  
  
21  
  transient S baseLink = "";
22  
  
23  
  transient bool newDesign = true; // use new chat bot design
24  
  transient bool ariaLiveTrick = false;
25  
  transient bool ariaLiveTrick2 = true;
26  
  
27  
  !include once #1029876 // WorkerChat 
28  
  transient new WorkerChat workerChat;
29  
  
30  
  transient ReliableSingleThread rstBotActions = dm_rst(this, r botActions);
31  
  
32  
  transient S defaultHeaderColorLeft = "#2a27da";
33  
  transient S defaultHeaderColorRight = "#00ccff";
34  
  transient bool enableUsers;
35  
  transient bool useWebSockets;
36  
  transient bool showRegisterLink;
37  
  transient bool showTalkToBotLink;
38  
  transient bool alwaysRedirectToHttps;
39  
  transient bool redirectOnLogout; // remove that ugly logout=1 from url
40  
  transient bool showFullErrors;
41  
  transient bool lockWhileDoingBotActions;
42  
  transient bool standardButtonsAsSymbols;
43  
  transient bool enableVars; // $userName etc. in bot messages
44  
  transient bool enableAvatars;
45  
  transient bool recordExecutionsAndInputs = true;
46  
  transient bool phoneNumberSpecialInputField = true;
47  
  transient bool enableRadioButtons; // allow radio buttons for single-choice selection in bot dialogs
48  
  transient bool newDialogAfterWindowClosed; // always restart conversation when user comes back
49  
  transient bool showJoiningConversation; // show "... joining conversation"
50  
  transient bool quickRadioButtons; // don't show an OK button, make radio buttons submit instantly
51  
  transient bool useDynamicComboBoxes;
52  
  transient bool prefixPrintsWithConvID = true;
53  
  transient bool authedDialogIDForEveryCookie;
54  
55  
  S mailSenderInfo; // empty for SMTP localhost, otherwise: "senderURL#pw"
56  
  
57  
  transient int typingTimeshift = 2000; // make typing bubble appear more reliably
58  
59  
  transient new ThreadLocal<Req> currentReq;
60  
61  
  transient volatile Scorer consistencyCheckResults;
62  
63  
  transient Lock statsLock = lock();
64  
65  
  transient SS specialPurposes = litcimap(
66  
    "bad email address", "Please enter a valid e-mail address",
67  
    "bad phone number", "Please enter a valid phone number");
68  
69  
  // end of variables
70  
71  
  void start {
72  
    super.start();
73  
    dm_setModuleName(botName);
74  
    dm_assertFirstSibling();
75  
    concepts_setUnlistedByDefault(true);
76  
    standardTimeZone();
77  
    standardTimeZone_name = timeZone;
78  
    print("DB program ID: " + dbProgramID());
79  
    realPW(); // make password
80  
    pWebChatBot();
81  
    doEvery(60.0, r cleanConversations);
82  
    rstBotActions.trigger();
83  
  }
84  
85  
  void indexAllLinkableClasses {
86  
    ensureConceptClassesAreIndexed(allLinkableClasses());
87  
  }
88  
89  
  afterVisualize {
90  
    addToControlArea(jPopDownButton_noText(popDownButtonEntries()));
91  
  }
92  
93  
  O[] popDownButtonEntries() {
94  
    ret litobjectarray(
95  
      "Show/edit master password...", rThreadEnter editMasterPassword,
96  
      "Show/edit mail sender info...", rThreadEnter editMailSenderInfo,
97  
      );
98  
  }
99  
100  
  sclass Req {
101  
    IWebRequest webRequest;
102  
    S uri;
103  
    SS params;
104  
    AuthedDialogID auth;
105  
    Domain authDomainObj;
106  
    S authDomain;
107  
    HTMLFramer1 framer;
108  
    bool masterAuthed;
109  
    Conversation conv;
110  
111  
    S uri() { ret uri; }
112  
    bool requestAuthed() { ret auth != null; }
113  
    S get(S param) { ret webRequest.get(param); }
114  
115  
    void markNoSpam { webRequest.noSpam(); }
116  
  }
117  
118  
  bool calcMasterAuthed(Req req) {
119  
    ret req.auth != null && req.auth.master;
120  
  }
121  
122  
  O html(IWebRequest request) enter {
123  
    try {
124  
      ret html2(request);
125  
    } catch e {
126  
      printStackTrace(e);
127  
      ret serve500(showFullErrors ? getStackTrace(e) : "Error.");
128  
    }
129  
  }
130  
131  
  void requestServed {}
132  
133  
  O html2(IWebRequest request) {
134  
    htmlencode_forParams_useV2();
135  
136  
    {
137  
      lock statsLock;
138  
      requestsServed++;
139  
      change();
140  
      requestServed();
141  
    }
142  
    
143  
    S uri = request.uri();
144  
    SS params = request.params();
145  
146  
    new Req req;
147  
    req.webRequest = request;
148  
    req.uri = uri;
149  
    req.params = params;
150  
151  
    if (alwaysRedirectToHttps)
152  
      try object redirectToHttps(req);
153  
154  
    if (eq(params.get("_newConvCookie"), "1"))
155  
      ret hrefresh(appendQueryToURL(req.uri, mapPlus(mapMinus(req.params, "_newConvCookie"),
156  
        cookie := "test_" + aGlobalID())));
157  
158  
    // cookie comes either from a URI parameter or from the request header
159  
    // Note: this can be either a JS-generated (in dynamic chat bot part)
160  
    // or server-generated cookie (when loading initial chat bot script or admin)
161  
    // To distinguish: JS-generated cookies are shorter and can contain numbers
162  
    S cookie = request.cookie();
163  
    //print("Request cookie: " + cookie);
164  
   
165  
    bool workerMode = nempty(params.get("workerMode")) || startsWith(uri, "/worker");
166  
    
167  
    // find out which domain we're on ("delivered domain")
168  
169  
    S domain = request.domain(), _domain = domain;
170  
    saveDeliveredDomain(domain);
171  
    Domain domainObj = findDomainObj(domain);
172  
173  
    // get conversation
174  
    S convCookie = params.get("cookie");
175  
    //if (eq(params.get("_newConvCookie"), "1")) convCookie = "conv-" + aGlobalID();
176  
    Conversation conv = isRequestFromBot(req) ? null
177  
      : nempty(convCookie) ? getConv(convCookie)
178  
      : nempty(cookie) ? getConv(cookie) : null;
179  
    req.conv = conv;
180  
181  
    AutoCloseable tempThing = conv == null || !prefixPrintsWithConvID ? null : temp_printPrefix("Conv " + conv.cookie + ": ");
182  
    temp tempThing;
183  
    temp tempSetTL(currentReq, req);
184  
185  
    S botConfig = params.get("_botConfig");
186  
    SS botConfigParams = decodeURIParams(botConfig);
187  
    S simulatedDomain = botConfigParams.get("domain");
188  
    Domain domainObj2 = nempty(simulatedDomain) ? findDomainObj(simulatedDomain) : domainObj;
189  
    if (nempty(botConfigParams)) cset(conv, botConfig := botConfigParams);
190  
191  
    // save ip & domain in conversation
192  
    if (conv != null && !workerMode)
193  
      if (cset_trueIfChanged(conv, ip := request.clientIP(), +domain, domainObj := domainObj2)) {
194  
        calcCountry(conv);
195  
        initAvatar(conv);
196  
      }
197  
      
198  
    //print("URI: " + uri + ", cookie: " + cookie + ", msgs: " + l(conv.msgs));
199  
200  
    try object handleAuth(req, cookie);
201  
202  
    // TODO: instead of maxCache, check file date & request header (If-Unmodified-Since?)
203  
    
204  
    new Matches m;
205  
    if (startsWith(uri, "/worker-image/", m)) {
206  
      long id = parseLong(m.rest());
207  
      ret subBot_serveFile_maxCache(workerImageFile(id), "image/jpeg");
208  
    }
209  
    
210  
    if (startsWith(uri, "/uploaded-image/", m)) {
211  
      long id = parseLong(m.rest());
212  
      UploadedImage img = getConcept UploadedImage(id);
213  
      ret img == null ? serve404() : subBot_serveFile_maxCache(img.imageFile(), "image/jpeg");
214  
    }
215  
    
216  
    if (startsWith(uri, "/uploaded-sound/", m)) {
217  
      long id = parseLong(m.rest());
218  
      UploadedSound sound = getConcept UploadedSound(id);
219  
      ret sound == null ? serve404() : subBot_serveFile_maxCache(sound.soundFile(), mp3mimeType());
220  
    }
221  
    
222  
    if (startsWith(uri, "/uploaded-file/", m)) {
223  
      long id = parseLong(dropAfterSlash(m.rest()));
224  
      UploadedFile f = getConcept UploadedFile(id);
225  
      ret f == null ? serve404() : subBot_serveFile_maxCache(f.theFile(),
226  
        or2(params.get("ct"), or2(trim(f.mimeType), binaryMimeType())));
227  
    }
228  
    
229  
    AuthedDialogID auth = authObject(cookie);
230  
    if (eq(params.get('logout), "1")) {
231  
      cdelete(auth);
232  
      if (redirectOnLogout) ret hrefresh(req.uri);
233  
      auth = null;
234  
    }
235  
    
236  
    req.auth = auth;
237  
    bool requestAuthed = auth != null; // any authentication
238  
    bool masterAuthed = req.masterAuthed = calcMasterAuthed(req);
239  
    Domain authDomainObj = !requestAuthed ? null : auth.restrictedToDomain!;
240  
    req.authDomainObj = authDomainObj;
241  
    S authDomain = !requestAuthed ? null : auth.domain();
242  
    req.authDomain = authDomain;
243  
244  
    if (requestAuthed) req.markNoSpam();
245  
246  
    try object serve2(req);
247  
248  
    if (!requestAuthed && settings().talkToBotOnlyWithAuth)
249  
      ret serveAuthForm(params.get('uri));
250  
251  
    if (eq(uri, "/emoji-picker/index.js"))
252  
      //ret serveWithContentType(loadTextFile(loadLibrary(#1400436)), "text/javascript"); // TODO: optimize
253  
      ret withHeader(subBot_maxCacheHeaders(serveInputStream(bufferedFileInputStream(loadLibrary(#1400436)), "text/javascript")));
254  
255  
    if (eq(uri, "/emoji-picker-test"))
256  
      ret loadSnippet(#1029870);
257  
258  
    if (eq(uri, "/logs")) {
259  
      if (!masterAuthed) ret serveAuthForm(rawLink(uri));
260  
      ret webChatBotLogsHTML2(rawLink(uri), params);
261  
    }
262  
    
263  
    if (eq(uri, "/refchecker")) {
264  
      if (!masterAuthed) ret serveAuthForm(rawLink(uri));
265  
      ConceptsRefChecker refChecker = new (db_mainConcepts());
266  
      L errors = refChecker.run();
267  
      if (eq(params.get("fix"), "1"))
268  
        ret serveText(refChecker.fixAll());
269  
      else
270  
        ret serveText(jsonEncode_breakAtLevels(2, litorderedmap(errors := allToString(errors))));
271  
    }
272  
    
273  
    if (eq(uri, "/backupDBNow")) {
274  
      if (!masterAuthed) ret serveAuthForm(rawLink(uri));
275  
      ret serveText("Backed up DB as " + fileInfo(backupConceptsNow()));
276  
    }
277  
    
278  
    if (eq(uri, "/auth-only")) {
279  
      if (!requestAuthed) ret serveAuthForm(params.get('uri));
280  
      ret "";
281  
    }
282  
283  
    if (eq(uri, "/leads-api"))
284  
      ret serveLeadsAPI(request);
285  
    
286  
    if (workerChat != null) // TODO: don't permit when worker chat is disabled
287  
      try object workerChat.html(req);
288  
  
289  
    S message = trim(params.get("btn"));
290  
    if (empty(message)) message = trim(params.get("message"));
291  
    
292  
    if (match("new dialog", message)) {
293  
      lock dbLock();
294  
      conv.newDialog();
295  
      message = null;
296  
    }
297  
298  
    if (eqic(message, "!toggle notifications")) {
299  
      cset(conv, notificationsOn := !conv.notificationsOn);
300  
      message = null;
301  
    }
302  
    
303  
    this.conv.set(conv);
304  
    
305  
    if (nempty(message) && !lastUserMessageWas(conv, message)) {
306  
      lock dbLock();
307  
      print("Adding message: " + message);
308  
309  
      possiblyTriggerNewDialog(conv);
310  
      
311  
      if (workerMode) {
312  
        Msg msg = new(false, message);
313  
        msg.fromWorker = auth.loggedIn;
314  
        conv.add(msg);
315  
      } else {
316  
        // add user message
317  
        conv.add(new Msg(true, message));
318  
        //conv.botTyping = now();
319  
        addScheduledAction(new OnUserMessage(conv));
320  
      }
321  
    }
322  
    
323  
    S testMode = params.get("testMode");
324  
    if (nempty(testMode)) {
325  
      print("Setting testMode", testMode);
326  
      cset(conv, testMode := eq("1", testMode));
327  
    }
328  
    
329  
    if (eq(uri, "/msg")) ret withHeader("OK");
330  
    
331  
    if (eq(uri, "/typing")) {
332  
      if (workerMode) {
333  
        conv.botTyping = now();
334  
        print(conv.botTyping + " Bot typing in: " + conv.cookie);
335  
      } else {
336  
        conv.userTyping = now();
337  
        print(conv.userTyping + " User typing in: " + conv.cookie);
338  
      }
339  
      ret withHeader("OK");
340  
    }
341  
342  
    // render all of a conversation's messages for inspection
343  
    if (eq(uri, "/renderMessages")) {
344  
      long msgTime = parseLong(params.get("msgTime"));
345  
      L<Msg> msgs = conv.allMsgs();
346  
      if (msgTime != 0)
347  
        msgs = filter(msgs, msg -> msg.time == msgTime);
348  
      new StringBuilder buf;
349  
      renderMessages(conv, buf, msgs,
350  
        msg -> false); // don't show buttons
351  
      ret hhtml_title_body(nMessages(msgs),
352  
        hstylesheetsrc(cssURL())
353  
        + p(nMessages(msgs) + " found")
354  
        + buf);
355  
    }
356  
357  
    if (eq(uri, "/incremental")) {
358  
      if (newDialogAfterWindowClosed && !conv.isActive()) {
359  
        print("Clearing conversation, timed out");
360  
        lock dbLock();
361  
        conv.newDialog();
362  
      }
363  
      
364  
      long start = sysNow(), start2 = now();
365  
      print(+start2);
366  
      
367  
      cset(conv, lastPing := now());
368  
      possiblyTriggerNewDialog(conv);
369  
      int a = parseInt(params.get("a"));
370  
  
371  
      int reloadCounter = conv.reloadCounter;
372  
      L<Msg> msgs;
373  
      bool first = true;
374  
      int timeout = toInt(req.params.get("timeout"));
375  
      long endTime = start+(timeout <= 0 || timeout > longPollMaxWait/1000 ? longPollMaxWait : timeout*1000L);
376  
      while (licensed() && sysNow() < endTime) {
377  
        int as = conv.archiveSize();
378  
        msgs = cloneSubList(conv.msgs, a-as); // just the new messages
379  
        bool reloadTriggered = conv.reloadCounter > reloadCounter;
380  
        bool actuallyNewDialog = a < as;
381  
        bool newDialog = actuallyNewDialog || reloadTriggered;
382  
        if (newDialog)
383  
          msgs = cloneList(conv.msgs);
384  
        long typing = workerMode ? conv.userTyping : conv.botTyping;
385  
        bool otherPartyTyping = typing >= start2-typingTimeshift;
386  
387  
        bool anyEvent = nempty(msgs) || newDialog || otherPartyTyping;
388  
        
389  
        if (!anyEvent) {
390  
          if (first) {
391  
            //print("Long poll starting on " + cookie + ", " + a + "/" + a);
392  
            first = false;
393  
          }
394  
          sleep(longPollTick);
395  
        } else {
396  
          if (!first) print("Long poll ended.");
397  
398  
          if (eq(req.params.get("json"), "1"))
399  
            ret serveJSON_breakAtLevels(2, litorderedmap(
400  
              n := conv.allCount(),
401  
              newDialog := trueOrNull(newDialog),
402  
              otherPartyTyping := trueOrNull(otherPartyTyping),
403  
              msgs := map(msgs, msg ->
404  
                litorderedmap(
405  
                  time := msg.time,
406  
                  fromUser := msg.fromUser,
407  
                  fromWorker := msg.fromWorker == null ?: msg.fromWorker.displayName,
408  
                  text := msg.text,
409  
                  // TODO: out
410  
                  labels := msg.labels))));
411  
          
412  
          new StringBuilder buf;
413  
414  
          if (newDialog) {
415  
            // send header colors & notification status
416  
            S l = or2_trim(domainObj2.headerColorLeft, defaultDomain().headerColorLeft, defaultHeaderColorLeft),
417  
              r = or2_trim(domainObj2.headerColorRight, defaultDomain().headerColorRight, defaultHeaderColorRight);
418  
            buf.append(hcss(".chat_header { background: linear-gradient(135deg, "
419  
              + hexColorToCSSRGB(l) + " 0%, " + hexColorToCSSRGB(r) + " 100%); }"));
420  
            buf.append(hscript("$('#chatBot_notiToggleText').text(" + jsQuote("Turn " + (conv.notificationsOn ? "off" : "on")
421  
              + " notifications") + ");"));
422  
              
423  
            if (showJoiningConversation) {
424  
              buf.append(hscript("$('#otherSideTyping .joining').html("
425  
                + jsQuote(nameOfBotSide(conv) + " now joining conversation...") + ");"
426  
                + "setTimeout(function() { $('#otherSideTyping .joining').html(''); }, 5000);"));
427  
            }
428  
          }
429  
430  
          if (otherPartyTyping) {
431  
            print("Noticed " + (workerMode ? "user" : "bot") + " typing in " + conv.cookie);
432  
            buf.append(hscript("showTyping(" + jsQuote(conv.botImg()) + ");"));
433  
          }
434  
          renderMessages(conv, buf, msgs);
435  
          if (ariaLiveTrick2 && !workerMode) {
436  
            Msg msg = lastBotMsg(msgs);
437  
            if (msg != null) {
438  
              S author = msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : botName;
439  
              buf.append(hscript([[$("#screenreadertrick").html(]] + jsQuote(author + " says: " + msg.text) + ");"));
440  
              }
441  
          }
442  
          if (a != 0 && anyInterestingMessages(msgs, workerMode))
443  
            buf.append(hscript(
444  
              stringIf(conv.notificationsOn, "window.playChatNotification();\n") +
445  
              "window.setTitleStatus(" + jsQuote((workerMode ? "User" : botName) + " says…") + ");"
446  
            ));
447  
          S html = str(buf);
448  
449  
          // TODO: hack for notransition
450  
          //if (newDialog && !actuallyNewDialog) html = html.replace([[class="chat_msg_item]], [[class="notransition chat_msg_item]]);
451  
          
452  
          ret withHeader("<!-- " + conv.allCount() + " " + (newDialog ? actuallyNewDialog ? "NEW DIALOG " : "NO NOTIFY NEW DIALOG " : "") + "-->\n"  + html);
453  
        }
454  
      }
455  
      ret withHeader("");
456  
    }
457  
458  
    if (eqOneOf(uri, "/script", "/demo")) {
459  
      lock dbLock();
460  
      S myTemplateID = templateID;
461  
      S templateIDParam = req.params.get("templateID");
462  
      if (nempty(templateIDParam) && allowedTemplateID(templateIDParam))
463  
        myTemplateID = templateIDParam;
464  
        
465  
      S html = loadSnippet_cached(myTemplateID);
466  
      S botLayout = req.params.get("botLayout");
467  
      S layout = or2(botLayout, defaultBotLayout());
468  
469  
      html = modifyTemplateBeforeDelivery(html, req);
470  
      
471  
      S heading = or2(headingForReq(req), or2(trim(domainObj2.botName), this.heading));
472  
      S botImg = botImageForDomain(domainObj);
473  
474  
      UploadedSound sound = settings().notificationSound!;
475  
      S notificationSound = sound != null ? sound.soundURL() : defaultNotificationSound();
476  
477  
      S miscParam = workerMode ? "workerMode=1&" : "";
478  
      if (nempty(botLayout))
479  
        miscParam += "botLayout=" + urlencode(botLayout) + "&";
480  
      S incrementalURL = baseLink + "/incremental?" + miscParam + "a=";
481  
      S typingURL = baseLink + "/typing?" + miscParam;
482  
      S msgURL = baseLink + "/msg?" + miscParam + "message=";
483  
       
484  
      if (eqic(layout, "sahil")) {
485  
        html = replaceDollarVars(html,
486  
          +incrementalURL,
487  
          +typingURL,
488  
          +msgURL,
489  
          n := 0,
490  
          +notificationSound,
491  
          +workerMode,
492  
          +heading,
493  
          +botImg);
494  
      } else {
495  
        S langlinks = "<!-- langlinks here -->";
496  
        if (html.contains(langlinks))
497  
          html = html.replace(langlinks,
498  
            ahref(rawLink("eng"), "English") + " | " + ahref(rawLink("deu"), "German"));
499  
  
500  
        //html = html.replace("#COUNTRYCODE_OPTIONS#", countryCodeOptions(conv));
501  
        html = html.replace("#COUNTRY#", lower(conv.country));
502  
        html = html.replace("#BOTIMG#", botImg);
503  
        html = html.replace("#N#", "0");
504  
        html = html.replace("#INCREMENTALURL#", incrementalURL);
505  
        html = html.replace("#MSGURL#", msgURL);
506  
        html = html.replace("#TYPINGURL#", typingURL);
507  
        html = html.replace("#CSS_ID#", psI_str(cssID));
508  
        if (ariaLiveTrick || ariaLiveTrick2)
509  
          html = html.replace([[aria-live="polite">]], ">");
510  
        html = html.replace("#OTHERSIDE#", workerMode ? "User" : "Representative");
511  
        if (nempty(params.get("debug")))
512  
          html = html.replace("var showActions = false;", "var showActions = true;");
513  
      
514  
        html = html.replace("#AUTOOPEN#", jsBool(workerMode || eq(params.get("_autoOpenBot"), "1") || botAutoOpen(domainObj2)));
515  
        html = html.replace("#BOT_ON#", jsBool(botOn() || eq(uri, "/demo")));
516  
        html = html.replace("$HEADING", heading);
517  
        html = html.replace("#WORKERMODE", jsBool(workerMode));
518  
        html = html.replace("#NOTIFICATIONSOUND#", notificationSound);
519  
        html = html.replace("<!-- MSGS HERE -->", "");
520  
        html = hreplaceTitle(html, heading);
521  
      }
522  
523  
      if (eq(uri, "/demo"))
524  
        ret hhtml(hhead(
525  
          htitle(heading)
526  
          + loadJQuery2()
527  
        ) + hbody(hjavascript(html)));
528  
      else
529  
        ret withHeader(subBot_serveJavaScript(html));
530  
    }
531  
532  
    try object serveOtherPage(req);
533  
    
534  
    if (eq(uri, "/")) try object serveHomePage();
535  
536  
    // serve uploaded files
537  
538  
    if (!startsWith(uri, "/crud/")) {
539  
      // TODO: more caching
540  
      UploadedFile fileToServe = conceptWhere UploadedFile(liveURI := urldecode(dropSlashPrefix(uri)));
541  
      if (fileToServe != null)
542  
        ret subBot_serveFile(fileToServe.theFile(), nempty(fileToServe.mimeType)
543  
          ? fileToServe.mimeType
544  
          : guessMimeTypeFromFileName(afterLastSlash(fileToServe.liveURI)));
545  
    }
546  
547  
    // add more uris here
548  
549  
    // serve admin
550  
    
551  
    if (!requestAuthed) ret serveAuthForm(params.get('uri));
552  
    // authed from here (not necessarily master-authed though)
553  
554  
    if (masterAuthed && eq(uri, "/dialogTree")) {
555  
      BotStep step = getConcept BotStep(toLong(params.get("stepID")));
556  
      if (step == null) ret serve404("Step not found");
557  
      ret hhtml_head_title_body("Dialog Tree for " + step,
558  
        hmobilefix() + hsansserif() + h2("Dialog Tree")
559  
          + hcss_linkColorInherit()
560  
          + hcss([[
561  
            .dialogTree li { margin-top: 0.8em; }
562  
          ]])
563  
          + renderDialogTree(step));
564  
    }
565  
566  
    if (eq(uri, "/thoughts"))
567  
      ret serveThoughts(req);
568  
569  
    if (masterAuthed && eq(uri, "/search"))
570  
      ret serveSearch(req);
571  
572  
    if (eq(uri, "/leads-csv")) {
573  
      S text = leadsToCSV(conceptsWhere(Lead, mapToParams(filtersForClass(Lead, req))));
574  
      S name = "bot-leads"
575  
        + (authDomainObj == null ? "" : "-" + replace(str(authDomainObj), "/", "-"))
576  
        + "-" + ymd_minus_hm() + ".csv";
577  
      ret serveCSVWithFileName(name, text);
578  
    }
579  
580  
    if (eq(uri, "/cleanConversations") && masterAuthed) {
581  
      cleanConversations();
582  
      ret hrefresh(baseLink + "/crud/Conversation");
583  
    }
584  
    
585  
    if (eq(uri, "/deleteDeliveredDomains") && masterAuthed) {
586  
      deleteDeliveredDomains();
587  
      ret hrefresh(baseLink + "/crud/DeliveredDomain");
588  
    }
589  
590  
    makeFramer(req);
591  
    HTMLFramer1 framer = req.framer;
592  
593  
    L<Class> classes = crudClasses(req);
594  
    L<Class> cmdClasses = req.masterAuthed ? botCmdClasses() : null;
595  
    for (Class c : (Set<Class>) asSet(flattenList2(classes, DeliveredDomain.class, cmdClasses)))
596  
      if (eq(uri, dropUriPrefix(baseLink, crudLink(c)))) {
597  
        S help = mapGet(crudHelp(), c);
598  
        if (nempty(help)) framer.add(p(help));
599  
600  
        HCRUD crud = makeCRUD(c, req);
601  
602  
        S json = crud.handleComboSearch(params);
603  
        if (nempty(json)) ret serveText(json);
604  
605  
        // display things above CRUD
606  
607  
        new LS aboveTable;
608  
609  
        if (c == UserKeyword) {
610  
          Scorer scorer = consistencyCheckResults();
611  
          framer.add(p("Consistency check results: " + nTests(scorer.successes) + " OK, " + nErrors(scorer.errors)));
612  
          if (nempty(scorer.errors))
613  
            framer.add(ul(map(scorer.errors, e -> "Error: " + e)));
614  
        }
615  
        
616  
        addThingsAboveCRUDTable(req, c, aboveTable);
617  
618  
        // render CRUD
619  
620  
        S baseTitle = framer.title;
621  
        crud.makeFrame = (title, contents) -> {
622  
          framer.title = joinNemptiesWithVBar(hTitleClean(title), baseTitle);
623  
          ret h1(title) + lines(aboveTable) + contents;
624  
        };
625  
        
626  
        crud.processSortParameter(params);
627  
        framer.add(crud.renderPage(params));
628  
629  
        // javascript magic to highlight table row according to anchor in URL
630  
        framer.add(hjs_markRowMagic());
631  
      }
632  
      
633  
    if (eq(uri, "/emojis")) {
634  
      framer.title = "Emoji List";
635  
      framer.contents.add(htmlTable2(map(emojiShortNameMap(),
636  
        (code, emoji) -> litorderedmap("Shortcode" := code, "Emoji" := emoji))));
637  
    }
638  
639  
    if (eq(uri, "/embedCode")) {
640  
      S goDaddyEmbedCode = "";
641  
      framer.title = "Embed Code";
642  
      framer.contents.add(h2("Chat bot embed code")
643  
        + (empty(goDaddyEmbedCode) ? "" :
644  
          h3("GoDaddy Site Builder")
645  
          + p("Add an HTML box with this code:")
646  
          + pre(htmlEncode2(goDaddyEmbedCode))
647  
          + h3("Other"))
648  
          
649  
        + p("Add this code in front of your " + tt(htmlEncode2("</body>")) + " tag:")
650  
        + pre(htmlEncode2(hjavascript_src_withType("https://" + request.domain() + baseLink + "/script"))));
651  
    }
652  
  
653  
    if (eq(uri, "/stats")) {
654  
      long us = db_mainConcepts().uncompressedSize;
655  
      framer.add(htableRaw2(llNonNulls(
656  
        ll("DB size on disk", htmlEncode2(toK_str(fileSize(conceptsFile())))),
657  
        us == 0 ? null : ll("DB size uncompressed", htmlEncode2(toK_str(us))),
658  
        ll("DB load time", htmlEncode2(renderDBLoadTime())),
659  
        ll("DB save time", htmlEncode2(renderDBSaveTime())),
660  
        ll("Last saved", htmlEncode2(renderHowLongAgo(db_mainConcepts().lastSaveWas))),
661  
        ), ll(class := "responstable"), null, null));
662  
    }
663  
664  
    if (eq(uri, "/unpackToWeb") && req.masterAuthed) {
665  
      UploadedFile file = getConcept UploadedFile(parseLong(req.params.get("fileID")));
666  
      if (file == null) ret serve404("File not found");
667  
      S path = unnull(req.params.get("path"));
668  
      File zipFile = file.theFile();
669  
      int changes = 0;
670  
      for (S filePath : listZIP(zipFile)) {
671  
        S liveURI = appendSlash(path) + filePath;
672  
        UploadedFile entry = uniq UploadedFile(+liveURI);
673  
        byte[] data = loadBinaryFromZip(zipFile, filePath);
674  
        if (!fileContentsIs(entry.theFile(), data)) {
675  
          ++changes;
676  
          saveBinaryFile(entry.theFile(), data);
677  
          touchConcept(entry);
678  
        }
679  
      }
680  
681  
      ret "Unpacked, " + nChanges(changes);
682  
    }
683  
684  
    // put more AUTHED uris here
685  
686  
    //if (masterAuthed) framer.addNavItem(baseLink + "/search", "Search");
687  
688  
    if (settings().enableWorkerChat)
689  
      framer.addNavItem(baseLink + "/worker", "Worker chat");
690  
      
691  
    framer.addNavItem(baseLink + "?logout=1", "Log out");
692  
    ret framer.render();
693  
  } // end of html2
694  
695  
  // put early stuff here
696  
  O serve2(Req req) { null; }
697  
698  
  O serveOtherPage(Req req) { null; }
699  
700  
  <A> A getDomainValue(Domain domainObj, IF1<Domain, A> getter, A defaultValue) {
701  
    if (domainObj != null)
702  
      try object A val = getter.get(domainObj);
703  
    try object A val = getter.get(defaultDomain());
704  
    ret defaultValue;
705  
  }
706  
707  
  void makeFramer(Req req) {
708  
    new HTMLFramer1 framer;
709  
    req.framer = framer;
710  
    framer.title = adminName + " " + squareBracket(
711  
      loggedInUserDesc(req));
712  
    framer.addInHead(hsansserif() + hmobilefix() + hresponstable()
713  
      + hcss_responstableForForms());
714  
    framer.addInHead(loadJQuery2());
715  
    framer.addInHead(hjs_selectize());
716  
    framer.addInHead(hjs_copyToClipboard());
717  
    framer.addInHead(hNotificationPopups());
718  
    framer.addInHead(hcss_linkColorInherit());
719  
    if (useWebSockets) {
720  
      framer.addInHead(hreconnectingWebSockets());
721  
722  
      framer.addInHead(hjs([[
723  
        var webSocketQuery = "";
724  
        var ws;
725  
        $(document).ready(function() {
726  
          ws = new ReconnectingWebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + ]]
727  
            + jsQuote(baseLink + req.uri) + [[ + webSocketQuery);
728  
          var wsReady = false;
729  
          ws.onopen = function(event) {
730  
            wsReady = true;
731  
            console.log("WebSocket ready!");
732  
            ws.onmessage = ]] + js_evalOnWebSocketMessage() + [[;
733  
          };
734  
        });
735  
      ]]));
736  
    }
737  
    if (showTalkToBotLink)
738  
      framer.addNavItem(simulateDomainLink(req.authDomain), "Talk to bot", targetBlank := true);
739  
    L<Class> classes = crudClasses(req);
740  
    L<Class> cmdClasses = req.masterAuthed ? botCmdClasses() : null;
741  
    for (Class c : classes)
742  
      framer.addNavItem(makeClassNavItem(c, req));
743  
    if (nempty(cmdClasses))
744  
      framer.contents.add(p("Bot actions: " + joinWithVBar(map(cmdClasses, c -> makeClassNavItem(c, req)))));
745  
  }
746  
747  
  // make and adapt CRUD for class    
748  
  <A extends Concept> HCRUD makeCRUD(Class<A> c, Req req, HTMLFramer1 framer default req.framer) {
749  
    HCRUD_Concepts data = crudData(c, req);
750  
    data.referencesBlockDeletion = true;
751  
    HCRUD crud = new(crudLink(c), data);
752  
    crud.params = req.params;
753  
    crud.buttonsOnTop = neqOneOf(c, UploadedFile, UploadedSound, UploadedImage);
754  
    crud.haveJQuery = crud.haveSelectizeJS = true;
755  
    crud.sortable = true;
756  
    crud.paginate = true;
757  
    crud.paginator.step = 25;
758  
    crud.cmdsLeft = true;
759  
    crud.showCheckBoxes = true;
760  
    crud.tableClass = "responstable";
761  
    //framer.addInHead(hcss(".crudForm { width: 800px; }"));
762  
    crud.formTableClass = "responstableForForms";
763  
764  
    crud.renderValue_inner = value -> {
765  
      S html = crud.renderValue_inner_base(value);
766  
      if (value cast Concept)
767  
        ret ahref(conceptLink(value), html);
768  
      ret html;
769  
    };
770  
    
771  
    if (c == Conversation) {
772  
      // Note: fields for list are created from scratch in massageItemMapForList
773  
      crud.nav = () -> joinNemptiesWithVBar(crud.nav_base(),
774  
        ahref(baseLink + "/cleanConversations", "Clean list"));
775  
      crud.unshownFields = litset("oldDialogs", "worker", "botOn", "lastPing", "cookie", "form", "testMode", "userMessageProcessed", "newDialogTriggered");
776  
    }
777  
    if (c == DeliveredDomain || c == Conversation || c == Lead || c == ConversationFeedback) 
778  
      crud.allowCreate = crud.allowEdit = false;
779  
      
780  
    if (c == Settings) {
781  
      crud.singleton = countConcepts(Settings) == 1;
782  
      if (!settings().multiLanguageMode) crud.unshownFields = litset("defaultLanguage");
783  
      framer?.add(p(joinNemptiesWithVBar(
784  
        ahref(baseLink + "/emojis", "Emojis"),
785  
        ahref(baseLink + "/stats", "Stats"),
786  
        ahref(baseLink + "/embedCode", "Embed Code"))));
787  
    }
788  
789  
    if (c == Lead)
790  
      framer?.add(p(ahref(baseLink + "/leads-csv", "Export as CSV")));
791  
      
792  
    if (c == BotOutgoingQuestion) {
793  
      crud.unlistedFields = litset("multipleChoiceSeparator", "placeholder");
794  
      if (!enableRadioButtons) crud.unlistedFields.add("radioButtons");
795  
796  
      crud.massageFormMatrix = (map, matrix) -> {
797  
        int idx = indexOfPred(matrix, row -> cic(first(row), "Actions"));
798  
        BotOutgoingQuestion item = getConcept BotOutgoingQuestion(toLong(map.get(crud.idField())));
799  
        printVars_str("massageFormMatrix", +item, +idx);
800  
        if (item == null) ret;
801  
        if (idx < 0) ret;
802  
        LS row = matrix.get(idx);
803  
        row.set(1, hcrud_mergeTables(row.get(1), tag table(map(s -> tr(td() + td(htmlEncode2(s))), item.buttons)), "for:"));
804  
      };
805  
    }
806  
807  
    data.addFilters(filtersForClass(c, req));
808  
809  
    if (c == Domain) {
810  
      crud.renderCmds = map -> {
811  
        Domain dom = getConcept Domain(toLong(crud.itemID(map)));
812  
        ret joinNemptiesWithVBar(crud.renderCmds_base(map),
813  
          targetBlank(simulateDomainLink(dom.domainAndPath), "Talk to bot"));
814  
      };
815  
      framer?.add(p("See also the " + ahref(crudLink(DeliveredDomain), "list of domains the bot was delivered on")));
816  
    }
817  
    
818  
    if (c == DeliveredDomain) {
819  
      crud.nav = () -> joinNemptiesWithVBar(crud.nav_base(),
820  
        ahref("/deleteDeliveredDomains", "Delete all"));
821  
      crud.renderCmds = map -> {
822  
        DeliveredDomain dom = getConcept DeliveredDomain(toLong(crud.itemID(map)));
823  
        ret joinNemptiesWithVBar(crud.renderCmds_base(map),
824  
          targetBlank(simulateDomainLink(dom.domain), "Talk to bot"));
825  
      };
826  
    }
827  
828  
    if (c == UploadedImage) {
829  
      crud.massageFormMatrix = (map, matrix) -> {
830  
        UploadedImage item = getConcept UploadedImage(toLong(crud.itemID(map)));
831  
        addInFront(matrix, ll("Upload image",
832  
          hjs_imgUploadBase64Encoder()
833  
          + himageupload(id := "imgUploader")
834  
          + hhiddenWithIDAndName("f_img_base64")));
835  
      };
836  
      crud.formParameters = () -> paramsPlus(crud.formParameters_base(), onsubmit := "return submitWithImageConversion(this)");
837  
    }
838  
839  
    if (c == UploadedSound) {
840  
      crud.massageFormMatrix = (map, matrix) -> {
841  
        UploadedSound item = getConcept UploadedSound(toLong(crud.itemID(map)));
842  
        matrix.add(ll("Upload MP3",
843  
          hjs_fileUploadBase64Encoder()
844  
          + hmp3upload(id := "fileUploader")
845  
          + hhiddenWithIDAndName("f_file_base64")));
846  
      };
847  
      crud.formParameters = () -> litparams(onsubmit := "return submitWithFileConversion(this)");
848  
    }
849  
850  
    if (c == UploadedFile) {
851  
      crud.massageFormMatrix = (map, matrix) -> {
852  
        UploadedFile item = getConcept UploadedFile(toLong(crud.itemID(map)));
853  
        matrix.add(ll("Upload File",
854  
          hjs_fileUploadBase64Encoder()
855  
          + hfileupload(id := "fileUploader")
856  
          + hhiddenWithIDAndName("f_file_base64")
857  
          + hjs([[
858  
            $("input[name=thefile]").change(function(e) {
859  
              var name = e.target.files[0].name;
860  
              if (name)
861  
                $("input[name=f_name]").val(name);              
862  
            });
863  
           ]]
864  
          )));
865  
      };
866  
      crud.formParameters = () -> litparams(onsubmit := "return submitWithFileConversion(this)");
867  
    }
868  
869  
    if (isSubclassOf(c, BotStep)) {
870  
      crud.renderCmds = map -> {
871  
        BotStep step = getConcept BotStep(toLong(crud.itemID(map)));
872  
        ret joinNemptiesWithVBar(crud.renderCmds_base(map),
873  
          targetBlank(simulateScriptLink(step), "Test in bot"),
874  
          hPopDownButton(
875  
            targetBlank(baseLink + "/dialogTree?stepID=" + step.id, "Show Dialog Tree")));
876  
      };
877  
      framer?.add(p("See also the " + ahref(crudLink(DeliveredDomain), "list of domains the bot was delivered on")));
878  
    }
879  
880  
    if (c == Worker) {
881  
      crud.unshownFields = litset("lastOnline");
882  
      crud.uneditableFields = litset("available", "lastOnline");
883  
    }
884  
885  
    // show references
886  
    
887  
    crud.postProcessTableRow = (item, rendered) -> {
888  
      long id = parseLong(item.get("id"));
889  
      Concept concept = getConcept(id);
890  
      if (concept == null) ret rendered;
891  
      Cl<Concept> refs = allBackRefs(concept);
892  
      if (empty(refs)) ret rendered;
893  
      refs = sortedByConceptID(refs);
894  
      int more = l(refs)-maxRefsToShow;
895  
      ret mapPlus(rendered, span_title("Where is this object used", "References"),
896  
        joinMap(takeFirst(maxRefsToShow, refs), ref -> p(ahref(conceptLink(ref),
897  
          htmlEncode_nlToBr_withIndents(str(ref)))))
898  
        + (more > 0 ? "<br>+" + more + " more" : ""));
899  
    };
900  
901  
    ret crud;
902  
  }
903  
904  
  bool isLinkableConcept(Concept c, Req req) {
905  
    ret req != null && c != null && contains(allLinkableClasses(req), _getClass(c));
906  
  }
907  
908  
  S conceptLink(Concept c, Req req default null) {
909  
    if (req != null && !isLinkableConcept(c, req)) null;
910  
    ret c == null ? null : appendQueryToURL(crudLink(c.getClass()), selectObj := c.id) + "#obj" + c.id;
911  
  }
912  
913  
  S conceptEditLink(Concept c, O... _) {
914  
    ret c == null ? null : appendQueryToURL(crudLink(c.getClass()), paramsPlus(_, edit := c.id));
915  
  }
916  
917  
  S conceptDuplicateLink(Concept c) {
918  
    ret c == null ? null : appendQueryToURL(crudLink(c.getClass()), duplicate := c.id);
919  
  }
920  
921  
  <A extends Concept> S makeClassNavItem(Class<A> c, Req req) {
922  
    HCRUD_Concepts<A> data = crudData(c, req);
923  
    HCRUD crud = makeCRUD(c, req, null);
924  
    Map filters = filtersForClass(c, req);
925  
    int count = countConcepts(c, mapToParams(filters));
926  
    //print("Count for " + c + " with " + filters + ": " + count);
927  
    ret (crud.singleton ? ""
928  
      : n2(count) + " ")
929  
      + ahref(crudLink(c), count == 1 ? data.itemName() : data.itemNamePlural())
930  
      + (!crud.actuallyAllowCreate() ? "" : " " + ahref(crud.newLink(), "+", title := "Create New " + data.itemName()));
931  
  }
932  
933  
  Cl<Class> allLinkableClasses() {
934  
    new Req req;
935  
    req.masterAuthed = true;
936  
    ret allLinkableClasses(req);
937  
  }
938  
939  
  Cl<Class> allLinkableClasses(Req req) {
940  
    ret joinSets(crudClasses(req), botCmdClasses());
941  
  }
942  
943  
  L<Class> botCmdClasses() {
944  
    ret dynNewBot2_botCmdClasses();
945  
  }
946  
947  
  L<Class> dynNewBot2_botCmdClasses() {
948  
    L<Class> l = ll(BotMessage, UploadedImage, BotImage, BotOutgoingQuestion, BotPause, Sequence);
949  
    if (settings().multiLanguageMode) l.add(BotSwitchLanguage);
950  
    ret l;
951  
  }
952  
953  
  L<Class> crudClasses(Req req) {
954  
    ret dynNewBot2_crudClasses(req);
955  
  }
956  
  
957  
  L<Class> dynNewBot2_crudClasses(Req req) {
958  
    if (req?.masterAuthed) {
959  
      L<Class> l = ll(Conversation, Lead, ConversationFeedback, Domain, UserKeyword, UploadedSound, UploadedFile);
960  
      if (enableAvatars) l.add(Avatar);
961  
      l.add(Settings);
962  
      if (settings().multiLanguageMode) l.add(Language);
963  
      if (settings().enableWorkerChat) l.add(Worker);
964  
      if (recordExecutionsAndInputs) addAll(l, ExecutedStep, InputHandled);
965  
      ret l;
966  
    } else
967  
      ret ll(Conversation, Lead, ConversationFeedback);
968  
  }
969  
970  
  MapSO filtersForClass(Class c, Req req) {
971  
    if (c == Conversation.class && req.authDomainObj != null)
972  
      ret litmap(domainObj := req.authDomainObj);
973  
    if (eqOneOf(c, Lead.class, ConversationFeedback.class) && req.authDomainObj != null)
974  
      ret litmap(domain := req.authDomainObj);
975  
    null;
976  
  }
977  
978  
  S crudLink(Class c) {
979  
    ret baseLink + "/crud/" + shortName(c);
980  
  }
981  
982  
  <A extends Concept> HCRUD_Concepts<A> crudData(Class<A> c, Req req) {
983  
    HCRUD_Concepts<A> cc = new(c);
984  
    if (useDynamicComboBoxes) cc.useDynamicComboBoxes = true;
985  
    if (eq(req.get("dynamicComboBoxes"), "1")) cc.useDynamicComboBoxes = true;
986  
    cc.trimAllSingleLineValues = true;
987  
    cc.fieldHelp("comment", "Put any comment about this object here");
988  
    cc.itemName = () -> replaceIfEquals(
989  
      dropPrefix("Bot ", cc.itemName_base()), "Jump Button", "Button");
990  
      
991  
    cc.valueConverter = new DefaultValueConverterForField {
992  
      public OrError<O> convertValue(O object, Field field, O value) {
993  
        // e.g. for "buttons" field
994  
        print("convertValue " + field + " " + className(value));
995  
        if (value instanceof S && eq(field.getGenericType(), type_LS())) {
996  
          print("tlft");
997  
          ret OrError(tlft((S) value));
998  
        }
999  
        ret super.convertValue(object, field, value);
1000  
      }
1001  
    };
1002  
1003  
    if (c == InputHandled) {
1004  
      cc.itemNamePlural = () -> "Inputs Handled";
1005  
    }
1006  
    
1007  
    if (c == Domain) {
1008  
      cc.fieldHelp(
1009  
        "domainAndPath", "without http:// or https://",
1010  
        "botName", "Bot name for this domain (optional)",
1011  
        "headerColorLeft", "Hex color for left end of header gradient (optional)",
1012  
        "headerColorRight", "Hex color for right end of header gradient (optional)",
1013  
        autoOpenBot := "Open the bot when page is loaded");
1014  
      cc.massageItemMapForList = (item, map) -> {
1015  
        map.put("domainAndPath", HTML(b(htmlEncode2(item/Domain.domainAndPath))));
1016  
        map.put("password", SecretValue(map.get("password")));
1017  
      };
1018  
    }
1019  
1020  
    if (c == BotOutgoingQuestion) {
1021  
      cc.dropEmptyListValues = false; // for button actions
1022  
      cc.addRenderer("displayText", new HCRUD_Data.TextArea(80, 10));
1023  
      cc.addRenderer("buttons", new HCRUD_Data.TextArea(80, 10, o -> lines_rtrim((L) o)));
1024  
      cc.fieldHelp(
1025  
        displayText := displayTextHelp(),
1026  
        key := [[Internal key for question (any format, can be empty, put "-" to disable storing answer)]],
1027  
        defaultValue := "What the input field is prefilled with",
1028  
        placeholder := "A text the empty input field shows as a hint",
1029  
        buttons := "Buttons to offer as standard answers (one per line, use | to add a shortened version submitted as user input)",
1030  
        allowFreeText := "Can user enter free text in addition to clicking a button?",
1031  
        multipleChoice := "Can user select multiple buttons?",
1032  
        optional := "Can user skip the question by entering nothing?",
1033  
        multipleChoiceSeparator := "Internal field, just leave as it is",
1034  
        answerCheck := "Select this to validate user's answer against a pattern",
1035  
        buttonActions := "Optional actions (one for each button in the list above)",
1036  
        radioButtons := "Show buttons as radio buttons",
1037  
      );
1038  
      cc.addRenderer("answerCheck", new HCRUD_Data.ComboBox("", "email address", "phone number"));
1039  
      cc.massageItemMapForList = (item, map) -> {
1040  
        map.put("buttons", HTML(ol_htmlEncode(item/BotOutgoingQuestion.buttons)));
1041  
        map.put("steps", HTML(ul(
1042  
          map(item/BotOutgoingQuestion.buttonActions, action ->
1043  
            ahref(conceptLink(action), htmlEncode2_nlToBr(str(action)))))));
1044  
       };
1045  
    }
1046  
1047  
    if (c == UserKeyword) {
1048  
      cc.fieldHelp(
1049  
        language := "Language that this keyword matches in (optional)",
1050  
        pattern := targetBlank("http://code.botcompany.de:8081/getraw.php?id=1030319", "'PhraseCache'") + " matching pattern (most important field)",
1051  
        examples := "Example inputs that should be matched (one per line, optional)",
1052  
        counterexamples := "Example inputs that should be NOT matched (one per line, optional)",
1053  
        action := "What bot does when input is matched",
1054  
        enabled := "Uncheck to disable this keyword",
1055  
        priority := "If set, this keyword overrides other input handlers (e.g. from outgoing questions)",
1056  
        precedence := "Precedence over other keywords (higher value = match first)",
1057  
      );
1058  
        
1059  
      for (S field : ll("examples", "counterexamples"))
1060  
        cc.addRenderer(field, new HCRUD_Data.TextArea(80, 5, o -> lines_rtrim((L) o)));
1061  
      cc.massageItemMapForList = (item, map) -> {
1062  
        map.put("examples", lines_rtrim(item/UserKeyword.examples));
1063  
        map.put("counterexamples", lines_rtrim(item/UserKeyword.counterexamples));
1064  
      };
1065  
    }
1066  
1067  
    if (c == Settings) {
1068  
      cc.fieldHelp(
1069  
        mainDomainName := "Domain where bot is hosted (to correctly send image and sound links)",
1070  
        botTypingDelay := "Delay in seconds before bot sends message, base value",
1071  
        botTypingDelayPerWord := "Delay in seconds before bot sends message, per word",
1072  
        botTypingMaxDelay := "Delay in seconds before bot sends message, max value",
1073  
        preferredCountryCodes := [[Country codes to display first in list (e.g. "+1, +91")]],
1074  
        notificationSound := "Bot message notification sound (can leave empty, then we use the "
1075  
          + ahref(defaultNotificationSound(), "default sound"),
1076  
        talkToBotOnlyWithAuth := "Show bot only to authorized persons",
1077  
        mailLeadsTo := "Where to send a mail when a new lead occurs (addresses separated by comma)",
1078  
        leadMailSubject := "Subject for lead notification mails");
1079  
    }
1080  
1081  
    if (c == Conversation)
1082  
      cc.massageItemMapForList = (item, map) -> {
1083  
        replaceMap(map, litorderedmap(
1084  
          id := map.get("id"),
1085  
          "IP" := map.get("ip"),
1086  
          "started" := formatLocalDateWithMinutes(item/Conversation.created),
1087  
          country := map.get("country"),
1088  
          msgs := lines_rtrim(item/Conversation.msgs),
1089  
          avatar := map.get("avatar"),
1090  
          answers := htmlEncode2(renderColonProperties(item/Conversation.answers)),
1091  
          stack := HTML(ol(
1092  
            cloneMap(item/Conversation.stack, activeSeq -> htmlEncode2(str(activeSeq))))),
1093  
        ));
1094  
      };
1095  
1096  
    if (c == BotMessage) {
1097  
      cc.addRenderer("text", new HCRUD_Data.TextArea(80, 10));
1098  
      cc.addRenderer("specialPurpose", new HCRUD_Data.ComboBox(itemPlus("", keys(specialPurposes))));
1099  
      cc.fieldHelp(
1100  
        text := displayTextHelp(),
1101  
        specialPurpose := "Special occasion when to display this message",
1102  
        disableInput := "Check to disable user input while this message is showing");
1103  
    }
1104  
1105  
    if (c == Form)
1106  
      cc.massageItemMapForList = (item, map) -> {
1107  
        map.put("steps", pnlToStringWithEmptyLines_rtrim(item/Form.steps));
1108  
      };
1109  
1110  
    if (c == Sequence)
1111  
      cc.massageItemMapForList = (item, map) -> {
1112  
        map.put("steps", HTML(ol(
1113  
          map(item/Sequence.steps, step ->
1114  
            ahref(conceptLink(step), htmlEncode2_nlToBr(str(step)))))));
1115  
      };
1116  
      
1117  
    if (c == BotImage)
1118  
      cc.massageItemMapForList = (item, map) -> {
1119  
        S url = item/BotImage.imageURL;
1120  
        if (isURL(url))
1121  
          map.put("Image Preview", HTML(himg(url, style := hcrud_imagePreviewStyle())));
1122  
        map.put(cc.fieldNameToHTML("imageURL"), HTML(
1123  
            htmlEncode2(url) + " "
1124  
          + himgsnippet(#1101381, onclick := 
1125  
              "copyToClipboard(" + jsQuote(url) + "); "
1126  
            + "window.createNotification({ theme: 'success', showDuration: 3000 })({ message: "
1127  
            + jsQuote("Image URL copied to clipboard")
1128  
            + "});")));
1129  
      };
1130  
1131  
    if (c == UploadedImage) {
1132  
      cc.massageItemMapForList = (item, map) -> {
1133  
        S url = item/UploadedImage.imageURL();
1134  
        File f = item/UploadedImage.imageFile();
1135  
        map.put("Image Size", toK_str(fileSize(f)));
1136  
        if (fileSize(f) > 0)
1137  
          map.put("Image Preview", HTML(himg(url, style := hcrud_imagePreviewStyle())));
1138  
      };
1139  
      IVF2 massageItemMapForUpdate_old = cc.massageItemMapForUpdate;
1140  
      cc.massageItemMapForUpdate = (item, map) -> {
1141  
        cc.massageItemMapForUpdate_fallback(massageItemMapForUpdate_old, item, map);
1142  
        S base64 = (S) map.get("img_base64");
1143  
        print("Got base64 data: " + l(base64));
1144  
        if (nempty(base64)) {
1145  
          File f = item/UploadedImage.imageFile();
1146  
          saveFileVerbose(f, base64decode(base64));
1147  
        }
1148  
        map.remove("img_base64");
1149  
      };
1150  
    }
1151  
    
1152  
    if (c == UploadedSound) {
1153  
      cc.massageItemMapForList = (item, map) -> {
1154  
        S url = item/UploadedSound.soundURL();
1155  
        File f = item/UploadedSound.soundFile();
1156  
        map.put("Sound Size", toK_str(fileSize(f)));
1157  
        if (fileSize(f) > 0)
1158  
          map.put("Test Sound", HTML(ahref(url, "Test Sound")));
1159  
      };
1160  
      IVF2 massageItemMapForUpdate_old = cc.massageItemMapForUpdate;
1161  
      cc.massageItemMapForUpdate = (item, map) -> {
1162  
        cc.massageItemMapForUpdate_fallback(massageItemMapForUpdate_old, item, map);
1163  
        S base64 = (S) map.get("file_base64");
1164  
        print("Got base64 data: " + l(base64));
1165  
        if (nempty(base64)) {
1166  
          File f = item/UploadedSound.soundFile();
1167  
          saveFileVerbose(f, base64decode(base64));
1168  
        }
1169  
        map.remove("file_base64");
1170  
      };
1171  
    }
1172  
1173  
    if (c == UploadedFile) {
1174  
      cc.fieldHelp(
1175  
        name := "Also used as file name when downloading",
1176  
        mimeType := "Content type (MIME type) for this file (optional)");
1177  
      cc.massageItemMapForList = (item, map) -> {
1178  
        S url = item/UploadedFile.downloadURL();
1179  
        File f = item/UploadedFile.theFile();
1180  
        map.put("File Size", toK_str(fileSize(f)));
1181  
        if (fileSize(f) > 0)
1182  
          map.put("Download", HTML(ahref(url, "Download")));
1183  
      };
1184  
      IVF2 massageItemMapForUpdate_old = cc.massageItemMapForUpdate;
1185  
      cc.massageItemMapForUpdate = (item, map) -> {
1186  
        cc.massageItemMapForUpdate_fallback(massageItemMapForUpdate_old, item, map);
1187  
        S base64 = (S) map.get("file_base64");
1188  
        print("Got base64 data: " + l(base64));
1189  
        if (nempty(base64)) {
1190  
          File f = item/UploadedFile.theFile();
1191  
          saveFileVerbose(f, base64decode(base64));
1192  
        }
1193  
        map.remove("file_base64");
1194  
      };
1195  
    }
1196  
1197  
    if (c == Worker) {
1198  
      cc.massageItemMapForList = (item, map) -> {
1199  
        AbstractBotImage image = item/Worker.image!;
1200  
        if (image != null)
1201  
          map.put("Image Preview", HTML(himg(image.imageURL(), style := hcrud_imagePreviewStyle())));
1202  
      };
1203  
    }
1204  
1205  
    if (c == Avatar) {
1206  
      cc.fieldHelp(shift := [[hours when avatar is active, e.g. 8-18 (leave empty if always on shift, put 0-0 if never on shift)]]);
1207  
      cc.massageItemMapForList = (item, map) -> {
1208  
        map.put("On Shift Now" := item/Avatar.onShift());
1209  
      };
1210  
    }
1211  
1212  
    ret cc;
1213  
  }
1214  
1215  
  Map<Class, S> crudHelp() {
1216  
    ret litmap(DeliveredDomain, "This list is filled by the bot with the domains it was delivered on. "
1217  
      + "Some may be bogus, we write down whatever the browser sends.");
1218  
  }
1219  
1220  
  void saveDeliveredDomain(S domain) {
1221  
    if (empty(domain)) ret;
1222  
    uniqCI(DeliveredDomain, domain := beforeColonOrAll(domain));
1223  
  }
1224  
1225  
  void cleanConversations {
1226  
    withDBLock(r {
1227  
      cdelete(filter(list(Conversation), c -> l(c.msgs) <= 1
1228  
        && elapsedSeconds_timestamp(c.created) >= 60 & c.lastPing == 0));
1229  
    });
1230  
  }
1231  
  
1232  
  void deleteDeliveredDomains {
1233  
    cdelete DeliveredDomain();
1234  
  }
1235  
1236  
  O serveThoughts(Req req) {
1237  
    new HTMLFramer1 framer;
1238  
    framer.addInHead(hsansserif() + hmobilefix());
1239  
    framer.add(div_floatRight(hbutton("Reload", onclick := "location.reload()")));
1240  
    framer.add(h2("Bot Thoughts"));
1241  
    if (req.conv == null) framer.add("No conversation");
1242  
    else {
1243  
      framer.add(p("Conversation cookie: " + req.conv.cookie));
1244  
      Conversation conv = req.conv;
1245  
      framer.add(h3("Stack"));
1246  
      framer.add(empty(conv.stack) ? p("empty")
1247  
        : ol(lmap htmlEncode2_gen(reversed(conv.stack))));
1248  
    }
1249  
    ret framer.render();
1250  
  }
1251  
1252  
  O serveSearch(Req req) {
1253  
    makeFramer(req);
1254  
    HTMLFramer1 framer = req.framer;
1255  
    framer.add("Search here");
1256  
    ret framer.render();
1257  
  }
1258  
  
1259  
  transient bool debug;
1260  
1261  
  // always returns an object
1262  
  Domain findDomainObj(S domain) {
1263  
    Domain domainObj = conceptWhereCI Domain(domainAndPath := domain);
1264  
    if (domainObj == null) domainObj = defaultDomain();
1265  
    ret domainObj;
1266  
  }
1267  
  
1268  
  Domain defaultDomain() {
1269  
    ret uniqCI Domain(domainAndPath := "<default>");
1270  
  }
1271  
  
1272  
  // Web Chat Bot Include
1273  
  
1274  
  transient int longPollTick = 200;
1275  
  transient int longPollMaxWait = 1000*30; // lowered to 30 seconds
1276  
  transient int activeConversationSafetyMargin = 30000; // allow client 15 seconds to reload
1277  
  
1278  
  transient Set<S> specialButtons = litciset("Cancel", "Back", "Abbrechen", "Zurück");
1279  
  
1280  
  S dbStats() {
1281  
    Cl<Conversation> all = list(Conversation);
1282  
    int nRealConvos = countPred(all, c -> l(c.msgs) > 1);
1283  
    //int nTestConvos = countPred(all, c -> startsWith(c.cookie, "test_"));
1284  
    //ret n2(l(all)-nTestConvos, "real conversation") + ", " + n2(nTestConvos, "test conversation");
1285  
    ret nConversations(nRealConvos);
1286  
  }
1287  
  
1288  
  S realPW() {
1289  
    ret loadTextFileOrCreateWithRandomID(realPWFile());
1290  
  }
1291  
1292  
  File realPWFile() {
1293  
    ret secretProgramFile("password.txt");
1294  
  }
1295  
  
1296  
  S serveAuthForm(S redirect) {
1297  
    redirect = dropParamFromURL(redirect, "logout"); // don't log us out right again
1298  
    if (empty(redirect)) redirect = baseLink + "/";
1299  
    ret hhtml(hhead(htitle(botName + ": Log-In")
1300  
      + hsansserif() + hmobilefix()) + hbody(hfullcenter(
1301  
      authFormHeading()
1302  
      + hpostform(
1303  
          hhidden(+redirect)
1304  
          + tag table(
1305  
            (enableUsers ? tr(td("User:") + td(hinputfield("user")) + td()) : "")
1306  
          + tr(td("Password:") + td(hpassword("pw")) + td(hsubmit("Log in")))),
1307  
        action := baseLink)
1308  
      + (!showTalkToBotLink? "" : p(ahref(baseLink + "/demo", "Talk to bot")))
1309  
      + (!showRegisterLink ? "" : p(hbuttonLink(baseLink + "/register", "Register as new user")))
1310  
      + authFormMoreContent()
1311  
      )));
1312  
  }
1313  
1314  
  S authFormHeading() { ret h3_htmlEncode(adminName); }  
1315  
  S authFormMoreContent() { ret ""; }
1316  
  
1317  
  S leadsToCSV(Cl<Lead> leads) {
1318  
    LS fields = ciContentsIndexedList();
1319  
    new LL rows;
1320  
    fOr (Lead lead : leads) {
1321  
      new L row;
1322  
      fOr (S key, O val : mapPlus_inFront((MapSO) (Map) lead.answers,
1323  
        "Date" := formatLocalDateWithMinutes(lead.date.date),
1324  
        "Domain" := lead.domain!,
1325  
        "Conversation ID" := conceptID(lead.conversation!))) {
1326  
        int idx = fields.indexOf(key);
1327  
        if (idx < 0) idx = addAndReturnIndex(fields, key);
1328  
        listSet(row, idx, val, "");
1329  
      }
1330  
      rows.add(row);
1331  
    }
1332  
    ret formatCSVFileForExcel2(itemPlusList(fields, rows));
1333  
  }
1334  
  
1335  
  S countryCodeOptions(Conversation conv) {
1336  
    Cl<S> countryCodes = putSetElementsFirst(keys(countryDialCodesMultiMap()), splitAtComma_trim(settings().preferredCountryCodes));
1337  
    S selected = dialCodeStringForCountryCode(conv.country);
1338  
    ret mapToLines(c -> { // c == dial code
1339  
      L<CountryDialCode> cdc = countryDialCodesMultiMap().get(c);
1340  
      S text = nempty(cdc) ? c + " [" + joinWithComma(collectSorted countryCode(cdc)) + "]" : c;
1341  
      ret tag option(text, value := c, selected := eq(c, selected) ? html_valueLessParam() : null);
1342  
    }, itemPlus("", countryCodes));
1343  
  }
1344  
  
1345  
  L<BotStep> addTypingDelays(L<BotStep> steps) {
1346  
    //printStackTrace();
1347  
    ret concatMap(steps, step -> {
1348  
      double delay = step.preTypingDelay();
1349  
      if (delay <= 0) ret ll(step);
1350  
      ret ll(new BotSendTyping, new BotPause(delay), step);
1351  
    });
1352  
  }
1353  
  
1354  
  BotMessage messageForPurpose(S specialPurpose) {
1355  
    ret conceptWhere BotMessage(+specialPurpose);
1356  
  }
1357  
  
1358  
  O serveLeadsAPI(virtual WebRequest req) {
1359  
    SS headers = cast rcall headers(req);
1360  
    SS params = cast get params(req);
1361  
    S tenantID = params.get("tenantID");
1362  
    if (empty(tenantID)) ret serveJSONError("Empty tenantID");
1363  
    S pw = params.get("pw");
1364  
    if (empty(pw)) pw = dropPrefix_trim("Bearer ", headers.get("Authorization"));
1365  
    if (empty(pw)) ret serveJSONError("Empty password");
1366  
    Domain domain = conceptWhere Domain(+tenantID);
1367  
    if (domain == null) ret serveJSONError("Tenant ID not found");
1368  
    if (neq(domain.password, pw)) ret serveJSONError("Bad passsword");
1369  
  
1370  
    Cl<Lead> leads = conceptsWhere Lead(+domain);
1371  
    ret serveJSON(map(leads, lead -> {
1372  
      Map map = litorderedmap(+tenantID,
1373  
        leadID := lead.id,
1374  
        domain := str(lead.domain),
1375  
        date := formatLocalDateWithSeconds(lead.created),
1376  
        data := lead.answers
1377  
      );
1378  
      ret map;
1379  
    }));
1380  
  }
1381  
  
1382  
  O serveJSONError(S error) {
1383  
    ret serveJSON(litorderedmap(+error));
1384  
  }
1385  
  
1386  
  S botImageForDomain(Domain domainObj) {
1387  
    S botImg = imageSnippetURLOrEmptyGIF(chatHeaderImageID);
1388  
    if (domainObj != null && domainObj.botImage.has())
1389  
      botImg = domainObj.botImage->imageURL();
1390  
    else if (defaultDomain().botImage.has())
1391  
      botImg = defaultDomain().botImage->imageURL();
1392  
    ret botImg;
1393  
  }
1394  
  
1395  
  S simulateDomainLink(S domain) {
1396  
    ret appendQueryToURL(baseLink + "/demo", _botConfig := makePostData(+domain), _newConvCookie := 1, _autoOpenBot := 1);
1397  
  }
1398  
  
1399  
  S simulateScriptLink(BotStep script) {
1400  
    ret appendQueryToURL(baseLink + "/demo", _botConfig := makePostData(mainScript := script.id), _newConvCookie := 1, _autoOpenBot := 1);
1401  
  }
1402  
1403  
  Scorer consistencyCheckResults() {
1404  
    if (consistencyCheckResults == null)
1405  
      consistencyCheckResults = doConsistencyChecks();
1406  
    ret consistencyCheckResults;
1407  
  }
1408  
1409  
  Scorer doConsistencyChecks() {
1410  
    Scorer scorer = scorerWithSuccessesAndErrors();
1411  
    for (UserKeyword uk) try {
1412  
      localConsistencyCheck(uk, scorer);
1413  
    } catch e {
1414  
      scorer.addError(htmlEncode2(str(e)));
1415  
    }
1416  
    try {
1417  
      globalConsistencyCheck(scorer);
1418  
    } catch e {
1419  
      scorer.addError(htmlEncode2(str(e)));
1420  
    }
1421  
    ret scorer;
1422  
  }
1423  
1424  
  void localConsistencyCheck(UserKeyword uk, Scorer scorer) {
1425  
    S patHTML = span_title(str(uk.parsedPattern()), htmlEncode2(uk.pattern));
1426  
    S item = "[Entry " + ahref(conceptLink(uk), uk.id) + "] ";
1427  
    
1428  
    fOr (S input : uk.examples) {
1429  
      bool ok = uk.matchWithTypos(input);
1430  
      scorer.add(ok, ok ? item + "Example OK: " + patHTML + " => " + htmlEncode2(quote(input))
1431  
        : item + "Example doesn't match pattern: " + patHTML + " => " + htmlEncode2(quote(input)));
1432  
    }
1433  
    
1434  
    fOr (S input : uk.counterexamples) {
1435  
      bool ok = !uk.matchWithTypos(input);
1436  
      scorer.add(ok, ok ? item + "Counterexample OK: " + patHTML + " => " + htmlEncode2(quote(input))
1437  
        : item + "Counterexample matches pattern: " + patHTML + " => " + htmlEncode2(quote(input)));
1438  
    }
1439  
  }
1440  
1441  
  void globalConsistencyCheck(Scorer scorer) {
1442  
    L<UserKeyword> list = concatLists(enabledUserKeywordsForPriority(true), enabledUserKeywordsForPriority(false));
1443  
    for (UserKeyword uk : list)
1444  
      fOr (S example : uk.examples) {
1445  
        S item1 = ahref(conceptLink(uk), uk.id);
1446  
        UserKeyword found = firstThat(list, uk2 -> uk2.matchWithTypos(example));
1447  
        if (found == null) continue; // Error should be found in local check
1448  
        if (found == uk)
1449  
          scorer.addOK("[Entry " + item1 + "] Global check OK for input " + htmlEncode2(quote(example)));
1450  
        else {
1451  
          S item2 = ahref(conceptLink(found), found.id);
1452  
          S items = "[Entries " + item1 + " and " + item2 + "] ";
1453  
          scorer.addError(items + "Input " + htmlEncode2(quote(example)) + " shadowed by pattern: " + htmlEncode2(quote(found.pattern))
1454  
            + ". Try reducing precedence value of entry " + item2 + " or increasing precedence value of entry " + item1);
1455  
        }
1456  
      }
1457  
  }
1458  
1459  
  // sorted by precedence
1460  
  static Cl<UserKeyword> enabledUserKeywordsForPriority(bool priority) {
1461  
    ret sortByFieldDesc precedence(conceptsWhere UserKeyword(onlyNonNullParams(+priority)));
1462  
  }
1463  
  
1464  
  bool handleGeneralUserInput(Conversation conv, Msg msg, Bool priority) {
1465  
    for (UserKeyword qa : enabledUserKeywordsForPriority(priority)) {
1466  
      if (qa.enabled && qa.matchWithTypos(msg.text)) {
1467  
        print("Matched pattern: " + qa.pattern + " / " + msg.text);
1468  
        conv.noteEvent(EvtMatchedUserKeyword(qa, msg));
1469  
        conv.callSubroutine(qa.action!);
1470  
        conv.scheduleNextStep();
1471  
        true;
1472  
      }
1473  
    }
1474  
  
1475  
    false;
1476  
  }
1477  
  
1478  
  void didntUnderstandUserInput(Conversation conv, Msg msg) {
1479  
    // TODO
1480  
  }
1481  
  
1482  
  transient simplyCached Settings settings() {
1483  
    ret conceptWhere(Settings);
1484  
  }
1485  
  
1486  
  S displayTextHelp() {
1487  
    ret "Text to show to user (can include HTML and " + targetBlank(baseLink + "/emojis", "emojis") + ")";
1488  
  }  
1489  
  
1490  
  void botActions {
1491  
    lock lockWhileDoingBotActions ? dbLock() : null;
1492  
    while licensed {
1493  
      ScheduledAction action = lowestConceptByField ScheduledAction("time");
1494  
      if (action == null) break;
1495  
      if (action.time > now()) {
1496  
        //print("Postponing next bot action - " + (action.time-now()));
1497  
        doAfter(100, rstBotActions);
1498  
        break;
1499  
      }
1500  
      cdelete(action);
1501  
      print("Executing action " + action);
1502  
      pcall { action.run(); }
1503  
    }
1504  
  }
1505  
  
1506  
  // action must be unlisted
1507  
  void addScheduledAction(ScheduledAction action, long delay default 0) {
1508  
    action.time = now()+delay;
1509  
    registerConcept(action);
1510  
    rstBotActions.trigger();
1511  
  }
1512  
  
1513  
  transient new ThreadLocal<Out> out;
1514  
  transient new ThreadLocal<Conversation> conv;
1515  
1516  
  transient long lastConversationChange = now();
1517  
1518  
  void makeIndices {
1519  
    indexConceptFieldDesc(ScheduledAction, 'time);
1520  
    indexConceptFieldCI(Domain, 'domainAndPath);
1521  
    indexConceptFieldCI(DeliveredDomain, 'domain);
1522  
    indexConceptFieldCI(CannedAnswer, 'hashTag);
1523  
    indexConceptFieldCI(Language, 'languageName);
1524  
    indexConceptField(Lead, 'domain);
1525  
    indexConceptField(ConversationFeedback, 'domain);
1526  
    indexConceptField(UploadedFile, 'liveURI);
1527  
  }
1528  
  
1529  
  void pWebChatBot {
1530  
    dbIndexing(Conversation, 'cookie, Conversation, 'worker,
1531  
      Conversation, 'lastPing,
1532  
      Worker, 'loginName, AuthedDialogID, 'cookie);
1533  
      
1534  
    db_mainConcepts().miscMapPut(DynNewBot2, this);
1535  
1536  
    indexSingletonConcept(Settings);
1537  
    uniq(Settings);
1538  
  
1539  
    makeIndices();
1540  
    indexAllLinkableClasses();
1541  
  
1542  
    // legacy clean-up
1543  
1544  
    // Make other singleton concepts
1545  
    uniq(BotSaveLead);
1546  
    uniq(BotSaveFeedback);
1547  
    uniq(BotClearStack);
1548  
    uniq(BotEndConversation);
1549  
    uniq(BotDoNothing);
1550  
    uniq(BotIdleMode);
1551  
    if (enableAvatars) uniq(RandomAvatar);
1552  
  }
1553  
  
1554  
  void addReplyToConvo(Conversation conv, IF0<S> think) {
1555  
    out.set(new Out);
1556  
    S reply = "";
1557  
    pcall {
1558  
      reply = think!;
1559  
    }
1560  
    Msg msg = new Msg(false, reply);
1561  
    msg.out = out!;
1562  
    conv.add(msg);
1563  
  }
1564  
  
1565  
  Msg msgFromThinkFunction(IF0<S> think) {
1566  
    out.set(new Out);
1567  
    S reply = "";
1568  
    pcall {
1569  
      reply = think!;
1570  
    }
1571  
    Msg msg = new Msg(false, reply);
1572  
    msg.out = out!;
1573  
    ret msg;
1574  
  }
1575  
  
1576  
  
1577  
  O withHeader(S html) {
1578  
    ret withHeader(subBot_noCacheHeaders(subBot_serveHTML(html)));
1579  
  }
1580  
  
1581  
  O withHeader(O response) {
1582  
    call(response, 'addHeader, "Access-Control-Allow-Origin", "*");
1583  
    ret response;
1584  
  }
1585  
  
1586  
  S renderMessageText(S text, bool htmlEncode) {
1587  
    text = trim(text);
1588  
    if (eqic(text, "!rate conversation")) text = "Rate this conversation";
1589  
    if (htmlEncode) text = htmlEncode2(text);
1590  
    text = nlToBr(text);
1591  
    ret html_emojisToUnicode(text);
1592  
  
1593  
  }
1594  
  
1595  
  void renderMessages(Conversation conv, StringBuilder buf, L<Msg> msgs,
1596  
   IPred<Msg> showButtons default msg -> msg == last(msgs)) {
1597  
    if (empty(msgs)) ret;
1598  
    ChatRenderer renderer = makeChatRenderer(currentReq!);  
1599  
    for (Msg m : msgs) {
1600  
      bool showTheButtons = showButtons.get(m);
1601  
1602  
      // render message if from user, not empty or having buttons
1603  
      if (m.fromUser || neqOneOf(m.text, "-", "") || showTheButtons && m.out != null && nempty(m.out.buttons))
1604  
        renderer.appendMsg(conv, buf, m, showTheButtons);
1605  
  
1606  
      if (showTheButtons) {
1607  
        // yeah we're doing it too much
1608  
        buf.append(hscript(
1609  
          botMod().phoneNumberSpecialInputField
1610  
            ? [[$("#chat_telephone, .iti").hide(); $("#chat_message").show().prop('disabled', false).focus();]]
1611  
            : [[$("#chat_message").prop('disabled', false).focus();]]));
1612  
        
1613  
        if (m.out != null && nempty(m.out.javaScript))
1614  
          buf.append(hscript(m.out.javaScript));
1615  
      }
1616  
    }
1617  
  }
1618  
1619  
  abstract class ChatRenderer {
1620  
    abstract void appendMsg(Conversation conv, StringBuilder buf, Msg m, bool showButtons);
1621  
1622  
    Conversation conv;
1623  
    S id;
1624  
    Msg m;
1625  
    S html;
1626  
    S name;
1627  
    S time;
1628  
    S text;
1629  
    bool bot;
1630  
    Worker fromWorker;
1631  
    Out out;
1632  
    LS labels;
1633  
    bool useTrick;
1634  
    S author;
1635  
    S imgURL;
1636  
      
1637  
    void prepare(Conversation conv, Msg m) {
1638  
      this.conv = conv;
1639  
      this.m = m;
1640  
      html = renderMessageText(m.text, shouldHtmlEncodeMsg(m));
1641  
      name = m.fromUser ? defaultUserName() : botName;
1642  
      time = formatTime(m.time);
1643  
      text = html;
1644  
      bot = !m.fromUser;
1645  
      fromWorker = m.fromWorker;
1646  
      out = m.out;
1647  
      labels = m.labels;
1648  
      useTrick = ariaLiveTrick;
1649  
      id = randomID();
1650  
      author = fromWorker != null ? htmlEncode2(fromWorker.displayName) : botName;
1651  
1652  
      if (bot) {
1653  
        if (fromWorker != null && fileExists(workerImageFile(fromWorker.id)))
1654  
          imgURL = fullRawLink("worker-image/" + fromWorker.id);
1655  
        else
1656  
          imgURL = conv.botImg();
1657  
      }
1658  
    }
1659  
    
1660  
    abstract S renderMultipleChoice(LS buttons, Cl<S> selections, S multipleChoiceSeparator);
1661  
    
1662  
    // render single-choice buttons
1663  
    S renderSingleChoice(LS buttons) {
1664  
      new LS out;
1665  
      for i over buttons: {
1666  
        S code = buttons.get(i);
1667  
        S text = replaceButtonText(code);
1668  
        S userInput = htmldecode_dropAllTags(text);
1669  
        LS split = splitAtVerticalBar(text);
1670  
        if (l(split) == 2) {
1671  
          userInput = second(split);
1672  
          text = code = first(split);
1673  
        }
1674  
        out.add(hbuttonOnClick_returnFalse(text, "submitAMsg(" + jsQuote(userInput) + ")", class := "chatbot-choice-button automated-message", title := eq(code, text) ? null : code));
1675  
        if (!specialButtons.contains(code)
1676  
          && i+1 < l(buttons) && specialButtons.contains(buttons.get(i+1)))
1677  
          out.add("&nbsp;&nbsp;");
1678  
      }
1679  
      ret lines(out);
1680  
    }
1681  
    
1682  
  }
1683  
1684  
  class ChatRendererSahilStyle extends ChatRenderer {
1685  
    void appendMsg(Conversation conv, StringBuilder buf, Msg m, bool showButtons) {
1686  
      prepare(conv, m);
1687  
1688  
      new LS div;
1689  
      new LS inMessage;
1690  
      new LS afterMessage;
1691  
      
1692  
      if (!bot)
1693  
        imgURL = "/pays5/imgs/icons/avatar.svg";
1694  
        
1695  
      if (nempty(imgURL)) {
1696  
        //div.add(himg(imgURL, alt := "Avatar"));
1697  
        div.add(div("", class := "bg-img", role := "img", "aria-labelledby" := m.fromUser ? "Avatar" : "Bot",
1698  
          style := "background-image: url('" + imgURL + "')"));
1699  
      }
1700  
1701  
      if (showButtons)
1702  
        appendButtons(inMessage, afterMessage, m.out);
1703  
1704  
      div.add(span(text + lines(inMessage), class := "message"));
1705  
      div.add(span(time, class := "message-timestamp"));
1706  
1707  
      buf.append(div(lines(div) + lines(afterMessage), class := "message-wrapper" + stringUnless(bot, " user"))).append("\n");
1708  
    }
1709  
    
1710  
    void appendButtons(LS inMessage, LS afterMessage, Out out) {
1711  
      S placeholder = out == null ? "" : unnull(out.placeholder);
1712  
      S defaultInput = out == null ? "" : unnull(out.defaultInput);
1713  
      inMessage.add(hscript("chatBot_setInput(" + jsQuote(defaultInput) + ", " + jsQuote(placeholder) + ");"));
1714  
      if (out == null) ret;
1715  
      LS buttons = out.buttons;
1716  
      if (empty(buttons)) ret;
1717  
      S buttonsHtml;
1718  
      if (out.multipleChoice)
1719  
        inMessage.add(span(renderMultipleChoice(buttons,
1720  
          text_multipleChoiceSplit(out.defaultInput, out.multipleChoiceSeparator), out.multipleChoiceSeparator),
1721  
          class := "select-options"));
1722  
      else if (out.radioButtons)
1723  
        inMessage.add(span(renderRadioButtons(buttons, out.defaultInput),
1724  
          class := "select-options"));
1725  
      else
1726  
        afterMessage.add(div(renderSingleChoice(buttons), class := "automated-messages"));
1727  
    }
1728  
    
1729  
    S renderMultipleChoice(LS buttons, Cl<S> selections, S multipleChoiceSeparator) {
1730  
      Set<S> selectionSet = asCISet(selections);
1731  
      S rand = randomID();
1732  
      S className = "chat_multiplechoice_" + rand;
1733  
      S allCheckboxes = [[$(".]] + className + [[")]];
1734  
      if (eq(multipleChoiceSeparator, ","))
1735  
        multipleChoiceSeparator += " ";
1736  
      
1737  
      ret joinWithBR(map(buttons, name ->
1738  
        span(
1739  
          span("", class := "square-box") + " "
1740  
          + name
1741  
          , class := className + " option" + stringIf(contains(selectionSet, name), " selected"),
1742  
          "data-value" := name)
1743  
        ))
1744  
        + "<br>" + hbuttonOnClick_returnFalse("OK", "submitMsg()", class := "btn btn-ok")
1745  
        + hscript(replaceDollarVars([[
1746  
          $(".$className *, .$className").click(function(e) {
1747  
            e.stopImmediatePropagation(); // prevent double firing
1748  
1749  
            // toggle check box
1750  
            console.log("Target: " + e.target);
1751  
            $(e.target).closest(".option").toggleClass("selected");
1752  
1753  
            // update message
1754  
            var theList = $('.$className.selected').map(function() { return this.dataset.value; }).get();
1755  
            console.log('theList: ' + theList);
1756  
            $('#chat_message').val(theList.join($sep));
1757  
          });
1758  
          //$(".$className *").click(function(e) { e.stopPropagation(); });
1759  
        ]], +className, sep := jsQuote(multipleChoiceSeparator)));
1760  
    }
1761  
    
1762  
    S renderRadioButtons(LS buttons, S selection) {
1763  
      S rand = randomID();
1764  
      S className = "chat_radio_" + rand;
1765  
      S allRadioButtons = [[$(".]] + className + [[")]];
1766  
      
1767  
      ret joinWithBR(map(buttons, name ->
1768  
        span(
1769  
          span("", class := "square-box round") + " "
1770  
          + name
1771  
          , class := className + " option" + stringIf(eqic(selection, name), " selected"),
1772  
          "data-value" := name)
1773  
        ))
1774  
        + (quickRadioButtons ? "" : "<br>" + hbuttonOnClick_returnFalse("OK", "submitMsg()", class := "btn btn-ok"))
1775  
        + hscript(replaceDollarVars([[
1776  
          $(".$className *, .$className").click(function(e) {
1777  
            e.stopImmediatePropagation(); // prevent double firing
1778  
            var option = $(e.target).closest(".option");
1779  
            
1780  
            // clear other radio buttons
1781  
            $(".$className").removeClass("selected");
1782  
1783  
            // activate radio button
1784  
            option.addClass("selected");
1785  
1786  
            // update message
1787  
            $('#chat_message').val(option[0].dataset.value);
1788  
1789  
            if ($quickRadioButtons)
1790  
              submitMsg();
1791  
          });
1792  
          //$(".$className *").click(function(e) { e.stopPropagation(); });
1793  
        ]], +className, +quickRadioButtons));
1794  
    }
1795  
  } // end of ChatRendererSahilStyle
1796  
1797  
  class ChatRendererHusainStyle extends ChatRenderer {
1798  
    void appendMsg(Conversation conv, StringBuilder buf, Msg m, bool showButtons) {
1799  
      prepare(conv, m);
1800  
      S tag = useTrick ? "div" : "span";
1801  
      if (bot) { // msg from bot (show avatar)
1802  
        if (nempty(m.text)) {
1803  
          if (fromWorker != null) buf.append([[<div class="chat_botname"><p>]] + author + [[</p>]]);
1804  
          buf.append("<" + tag + [[ class="chat_msg_item chat_msg_item_admin"]] + (useTrick ? [[ id="]] + id + [[" aria-live="polite" tabindex="-1"]] : "") + ">");
1805  
  
1806  
          if (nempty(imgURL))
1807  
            buf.append([[
1808  
              <div class="chat_avatar">
1809  
                <img src="$IMG"/>
1810  
              </div>]]
1811  
              .replace("$IMG", imgURL));
1812  
            
1813  
          buf.append([[<span class="sr-only">]] + (fromWorker != null ? "" : botName + " ") + [[says</span>]]);
1814  
          buf.append(text);
1815  
          buf.append([[</]] + tag + [[>]]);
1816  
          if (fromWorker != null) buf.append("</div>");
1817  
          if (useTrick) buf.append(hscript("$('#" + id + "').focus();"));
1818  
        }
1819  
      } else // msg from user (no avatar)
1820  
        buf.append(([[
1821  
          <span class="sr-only">You say</span>
1822  
          <]] + tag + [[ class="chat_msg_item chat_msg_item_user"]] + (useTrick ? [[ aria-live="polite"]] : "") + [[>$TEXT</]] + tag + [[>
1823  
        ]]).replace("$TEXT", text));
1824  
  
1825  
      if (nempty(labels))
1826  
        buf.append(span(
1827  
          join(" &nbsp; ", map(labels, lbl -> span("&nbsp; " + lbl + " &nbsp;"))),
1828  
          class := "labels chat_msg_item chat_msg_item_user")).append("\n");
1829  
          
1830  
      if (showButtons)
1831  
        appendButtons(buf, out, null);
1832  
    }
1833  
    
1834  
    void appendButtons(StringBuilder buf, Out out, Set<S> buttonsToSkip) {
1835  
      S placeholder = out == null ? "" : unnull(out.placeholder);
1836  
      S defaultInput = out == null ? "" : unnull(out.defaultInput);
1837  
      if (out != null && out.disableInput)
1838  
        buf.append(hscript(jsDisableInputField()));
1839  
      else
1840  
        buf.append(hscript("chatBot_setInput(" + jsQuote(defaultInput) + ", " + jsQuote(placeholder) + ");"));
1841  
      if (out == null) ret;
1842  
      LS buttons = listMinusSet(out.buttons, buttonsToSkip);
1843  
      if (empty(buttons)) ret;
1844  
      printVars_str(+buttons, +buttonsToSkip);
1845  
      S buttonsHtml;
1846  
      if (out.multipleChoice)
1847  
        buttonsHtml = renderMultipleChoice(buttons,
1848  
          text_multipleChoiceSplit(out.defaultInput, out.multipleChoiceSeparator), out.multipleChoiceSeparator);
1849  
      else
1850  
        buttonsHtml = renderSingleChoice(buttons);
1851  
      
1852  
      buf.append(span(buttonsHtml,
1853  
        class := "chat_msg_item chat_msg_item_admin chat_buttons"
1854  
          + stringIf(!out.multipleChoice, " single-choice")));
1855  
    }
1856  
    
1857  
    S renderMultipleChoice(LS buttons, Cl<S> selections, S multipleChoiceSeparator) {
1858  
      Set<S> selectionSet = asCISet(selections);
1859  
      S rand = randomID();
1860  
      S className = "chat_multiplechoice_" + rand;
1861  
      S allCheckboxes = [[$(".]] + className + [[")]];
1862  
      ret joinWithBR(map(buttons, name ->
1863  
        hcheckbox("", contains(selectionSet, name),
1864  
          value := name,
1865  
          class := className) + " " + name))
1866  
        + "<br>" + hbuttonOnClick_returnFalse("OK", "submitMsg()", class := "btn btn-ok")
1867  
        + hscript(allCheckboxes
1868  
          + ".change(function() {"
1869  
          //+ "  console.log('multiple choice change');"
1870  
          + " var theList = $('." + className + ":checkbox:checked').map(function() { return this.value; }).get();"
1871  
          + "  console.log('theList: ' + theList);"
1872  
          + "  $('#chat_message').val(theList.join(" + jsQuote(multipleChoiceSeparator) + "));"
1873  
          + "});");
1874  
    }
1875  
  } // end of ChatRendererHusainStyle
1876  
  
1877  
  S replaceButtonText(S s) {
1878  
    if (standardButtonsAsSymbols) {
1879  
      if (eqicOneOf(s, "back", "zurück")) ret unicode_undoArrow();
1880  
      if (eqicOneOf(s, "cancel", "Abbrechen")) ret unicode_crossProduct();
1881  
    }
1882  
    ret s;
1883  
  }
1884  
  
1885  
  void appendDate(StringBuilder buf, S date) {
1886  
    buf.append([[
1887  
    <div class="chat-box-single-line">
1888  
      <abbr class="timestamp">DATE</abbr>
1889  
    </div>]].replace("DATE", date));
1890  
  }
1891  
  
1892  
  bool lastUserMessageWas(Conversation conv, S message) {
1893  
    Msg m = last(conv.msgs);
1894  
    ret m != null && m.fromUser && eq(m.text, message);
1895  
  }
1896  
  
1897  
  S formatTime(long time) {
1898  
    ret timeInTimeZoneWithOptionalDate_24(timeZone, time);
1899  
  }
1900  
  
1901  
  S formatDialog(S id, L<Msg> msgs) {
1902  
    new L<S> lc;
1903  
    for (Msg m : msgs)
1904  
      lc.add(htmlencode((m.fromUser ? "> " : "< ") + m.text));
1905  
    ret id + ul(lc);
1906  
  }
1907  
  
1908  
  Conversation getConv(fS cookie) {
1909  
    ret uniq(Conversation, +cookie);
1910  
  }
1911  
  
1912  
  S errorMsg(S msg) {
1913  
    ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "<br><br>" + ahref(jsBackLink(), "Back"))));
1914  
  }
1915  
  
1916  
  S defaultUserName() {
1917  
    ret "You";
1918  
  }
1919  
  
1920  
  bool botOn() {
1921  
    true;
1922  
  }
1923  
  
1924  
  bool botAutoOpen(Domain domain) {
1925  
    ret getDomainValue(domain, d -> d.autoOpenBot, false);
1926  
  }
1927  
  
1928  
  File workerImageFile(long id) {
1929  
    ret id == 0 ? null : javaxDataDir("adaptive-bot/images/" + id + ".jpg");
1930  
  }
1931  
  
1932  
  long activeConversationTimeout() {
1933  
    ret longPollMaxWait+activeConversationSafetyMargin;
1934  
  }
1935  
  
1936  
  AuthedDialogID authObject(S cookie) {
1937  
    AuthedDialogID auth = null;
1938  
    if (nempty(cookie))
1939  
      auth = authedDialogIDForEveryCookie
1940  
        ? uniq AuthedDialogID(+cookie)
1941  
        : conceptWhere AuthedDialogID(+cookie);
1942  
    //printVars_str(+cookie, +auth);
1943  
    ret auth;
1944  
  }
1945  
  
1946  
  bool anyInterestingMessages(L<Msg> msgs, bool workerMode) {
1947  
    ret any(msgs, m -> m.fromUser == workerMode);
1948  
  }
1949  
  
1950  
  bool shouldHtmlEncodeMsg(Msg msg) {
1951  
    ret msg.fromUser;
1952  
  }
1953  
  
1954  
  void calcCountry(Conversation c) {
1955  
    cset(c, country := "");
1956  
    getCountry(c);
1957  
  }
1958  
1959  
  S getCountry(Conversation c) {
1960  
    if (empty(c.country) && nempty(c.ip))
1961  
      cset(c, country := ipToCountry2020_safe(c.ip));
1962  
    ret or2(c.country, "?");
1963  
  }
1964  
  
1965  
  void initAvatar(Conversation c) {
1966  
    printVars_str("initAvatar", +c, +enableAvatars, domain := c.domainObj, avatar := c.avatar);
1967  
    if (enableAvatars && !c.avatar.has() && c.domainObj.has()) {
1968  
      AbstractAvatar a = c.domainObj->avatar!;
1969  
      Avatar avatar = a?.getActualAvatar();
1970  
      print("initAvatar " + c + " => " + avatar);
1971  
      cset(c, +avatar);
1972  
    }
1973  
  }
1974  
  
1975  
  void noteConversationChange {
1976  
    lastConversationChange = now();
1977  
  }
1978  
  
1979  
  S template(S hashtag, O... params) {
1980  
    ret replaceSquareBracketVars(getCannedAnswer(hashtag), params);
1981  
  }
1982  
  
1983  
  // TODO: redefine this
1984  
  S getCannedAnswer(S hashTag, Conversation conv default null) {
1985  
    ret hashTag;
1986  
    //if (!startsWith(hashTag, "#")) ret hashTag;
1987  
    //ret or2(trim(text(conceptWhereCI CannedAnswer(+hashTag))), hashTag);
1988  
  }
1989  
  
1990  
  LS text_multipleChoiceSplit(S input, S multipleChoiceSeparator) {
1991  
    ret trimAll(splitAt(input, dropSpaces(multipleChoiceSeparator)));
1992  
  }
1993  
  
1994  
  Msg lastBotMsg(L<Msg> l) {
1995  
    ret lastThat(l, msg -> !msg.fromUser);
1996  
  }
1997  
  
1998  
  // TODO
1999  
  File uploadedImagesDir() {
2000  
    ret programDir("uploadedImages");
2001  
  }
2002  
  
2003  
  File uploadedSoundsDir() {
2004  
    ret programDir("uploadedSounds");
2005  
  }
2006  
2007  
  File uploadedFilesDir() {
2008  
    ret programDir("uploadedFiles");
2009  
  }
2010  
2011  
  S defaultNotificationSound() {
2012  
    ret "https://botcompany.de/files/1400403/notification.mp3";
2013  
  }
2014  
2015  
  S convertToAbsoluteURL(S url) {
2016  
    S domain = settings().mainDomainName;
2017  
    ret empty(domain) ? url : "https://" + domain + addSlashPrefix(url);
2018  
  }
2019  
2020  
  // only an absolute URL if settings().mainDomainName is set
2021  
  S absoluteAdminURL() {
2022  
    ret convertToAbsoluteURL(baseLink);
2023  
  }
2024  
2025  
  void editMasterPassword {
2026  
    if (!confirmOKCancel("Are you sure? This will reveal the password.")) ret;
2027  
    JTextField tf = jtextfield(realPW());
2028  
    showFormTitled2(botName + " master password",
2029  
      "Password", tf,
2030  
      r {
2031  
        S pw = gtt(tf);
2032  
        if (empty(pw)) infoBox("Empty password, won't save");
2033  
        else {
2034  
          saveTextFile(realPWFile(), pw);
2035  
          infoBox("Saved new password for " + adminName);
2036  
        }
2037  
      });
2038  
  }
2039  
2040  
  void editMailSenderInfo {
2041  
    JTextField tf = jtextfield(realPW());
2042  
    inputText("Mail sender info", mailSenderInfo, voidfunc(S mailSenderInfo) { setField(+mailSenderInfo); });
2043  
  }
2044  
2045  
  // handle user log-in (create/update AuthedDialogID concept)
2046  
  O handleAuth(Req req, S cookie) null {
2047  
    S pw = trim(req.params.get('pw));
2048  
    if (nempty(pw) && nempty(cookie)) {
2049  
      Domain authDomain;
2050  
      if (eq(pw, realPW())) 
2051  
        cset(uniq AuthedDialogID(+cookie), restrictedToDomain := null, master := true);
2052  
      else if ((authDomain = conceptWhere Domain(password := pw)) != null)
2053  
        cset(uniq AuthedDialogID(+cookie), restrictedToDomain := authDomain, master := false);
2054  
      else
2055  
        ret errorMsg("Bad password, please try again");
2056  
2057  
      S redirect = req.params.get('redirect);
2058  
      if (nempty(redirect))
2059  
        ret hrefresh(redirect);
2060  
    }
2061  
  }
2062  
2063  
  S loggedInUserDesc(Req req) {
2064  
    ret req.masterAuthed ? "super user" : htmlEncode2(req.authDomain);
2065  
  }
2066  
2067  
  O redirectToHttps(Req req) null {
2068  
    if (!req.webRequest.isHttps())
2069  
      ret subBot_serveRedirect("https://" + req.webRequest.domain() + req.uri + htmlQuery(req.params));
2070  
  }
2071  
2072  
  void startMainScript(Conversation conv) {
2073  
    initAvatar(conv);
2074  
2075  
    Domain domain = conv.domainObj!;
2076  
    if (domain == null)
2077  
      ret with botMod().addReplyToConvo(conv, () -> "Error: No domain set");
2078  
    BotStep seq = getConcept BotStep(parseLong(mapGet(conv.botConfig, "mainScript")));
2079  
    if (seq == null)
2080  
      seq = domain.mainScript!;
2081  
    if (seq == null)
2082  
      ret with botMod().addReplyToConvo(conv, () -> "Error: No main script set for " + htmlEncode2(str(domain)));
2083  
    if (executeStep(seq, conv))
2084  
      nextStep(conv);
2085  
  }
2086  
2087  
  bool isRequestFromBot(Req req) { false; }
2088  
2089  
  S modifyTemplateBeforeDelivery(S html, Req req) { ret html; }
2090  
2091  
  S headingForReq(Req req) { null; }
2092  
2093  
  O serveHomePage() { null; }
2094  
2095  
  void possiblyTriggerNewDialog(Conversation conv) {
2096  
    if (empty(conv.msgs) && conv.newDialogTriggered < conv.archiveSize()) {
2097  
      cset(conv, newDialogTriggered := conv.archiveSize());
2098  
      addScheduledAction(OnNewDialog(conv));
2099  
    }
2100  
  }
2101  
2102  
  S rewriteBotMessage(Conversation conv, S text) {
2103  
    if (!enableVars || conv == null) ret text;
2104  
2105  
    // replace <if> tags
2106  
    text = html_evaluateIfTags(text, var -> eqic("true",
2107  
      print("if " + var + ": ", calcVar(conv, dropDollarPrefix(var)))));
2108  
    
2109  
    // replace dollar vars
2110  
    print(+text);
2111  
    text = replaceDollarVars_dyn(text, var -> print("calcVar " + var, calcVar(conv, var)));
2112  
2113  
    ret text;
2114  
  }
2115  
2116  
  S calcVar(Conversation conv, S var) {
2117  
    if (conv == null) null;
2118  
    try object S val = syncGet(conv.answers, var);
2119  
    if (eqic(var, "botName") && conv.avatar.has())
2120  
      ret conv.avatar->name;
2121  
    null;
2122  
  }
2123  
2124  
  // must match what template does with CSS_ID
2125  
  S cssURL() {
2126  
    ret serveSnippetURL(cssID);
2127  
  }
2128  
2129  
  class RenderDialogTree {
2130  
    new Set<Concept> seen;
2131  
2132  
    S render(BotStep step) {
2133  
      if (!seen.add(step))
2134  
        ret "[see above] " + htmlEncode2(str(step));
2135  
        
2136  
      new LS children;
2137  
        
2138  
      if (step cast Sequence)
2139  
        for (int i, BotStep step2 : unpair iterateWithIndex1(step.steps))
2140  
          children.add("Step " + i + ": " + renderDialogTree(step2));
2141  
2142  
      if (step cast BotOutgoingQuestion)
2143  
        for (S input, BotStep step2 : unpair step.buttonsWithActions())
2144  
          children.add("If user says " + b(htmlEncode2(quote(last(splitAtVerticalBar(input)))))
2145  
            + "<br><br> => " + (step2 == null ? "no action defined"
2146  
              : renderDialogTree(step2)));
2147  
  
2148  
      ret stepToHTMLFull(step) + ulIfNempty(children, class := "dialogTree");
2149  
    }
2150  
  }
2151  
2152  
  S stepToHTMLFull(BotStep step) {
2153  
    if (step == null) ret "-";
2154  
    S link = conceptLink(step, currentReq!);
2155  
    ret ahref_unstyled(link, prependSquareBracketed(step.id))
2156  
      + ahref(link, htmlEncode2_nlToBr(step.fullToString()));
2157  
  }
2158  
2159  
  S renderDialogTree(BotStep step) {
2160  
    ret new RenderDialogTree().render(step);
2161  
  }
2162  
2163  
  ChatRenderer makeChatRenderer(Req req) {
2164  
    S botLayout = req.params.get("botLayout");
2165  
    S layout = or2(botLayout, defaultBotLayout());
2166  
    ret eqic(layout, "sahil") ? new ChatRendererSahilStyle : new ChatRendererHusainStyle;
2167  
  }
2168  
2169  
  S defaultBotLayout() { ret "husain"; }
2170  
2171  
  bool allowedTemplateID(S id) {
2172  
    false;
2173  
  }
2174  
2175  
  double typingDelayForText(S text) {
2176  
    double perWord = settings().botTypingDelayPerWord;
2177  
    double x = settings().botTypingDelay;
2178  
    if (perWord != 0)
2179  
      x += countWords(dropHTMLTags(text)) * settings().botTypingDelayPerWord;
2180  
    double max = settings().botTypingMaxDelay;
2181  
    if (max != 0) x = min(x, max);
2182  
    ret x;
2183  
  }
2184  
2185  
  S nameOfBotSide(Conversation conv) {
2186  
    try answer calcVar(conv, "botName");
2187  
    ret "Representative";
2188  
  }
2189  
  
2190  
  void addThingsAboveCRUDTable(Req req, Class c, LS aboveTable) {
2191  
  }
2192  
2193  
  bool checkPhoneNumber(S s) {
2194  
    ret isValidInternationalPhoneNumber(s);
2195  
  }
2196  
2197  
  void onNewLead(Lead lead) {
2198  
    // TODO: notify someone in case of mail error
2199  
    for (S address : splitAtComma_trim(settings().mailLeadsTo))
2200  
      try {
2201  
        S text = formatColonProperties(lead.answers);
2202  
        sendMailThroughScript(mailSenderInfo, "auto@botcompany.de", address, settings().leadMailSubject, text);
2203  
      } catch e {
2204  
        printStackTrace(e);
2205  
        appendToFile(programFile("mail-errors"), getStackTrace(e));
2206  
      }
2207  
  }
2208  
} // end of module
2209  
2210  
// start of concepts
2211  
2212  
concept ExecutedStep {
2213  
  new Ref<BotStep> step;
2214  
  new Ref<Conversation> conversation;
2215  
  Msg msg; // msg generated by step (if any)
2216  
}
2217  
2218  
concept InputHandled {
2219  
  // what was the input and where/when did it occur?
2220  
  
2221  
  S input; // store again here in case msg is edited later or something
2222  
  new Ref<Conversation> conversation;
2223  
  Msg msg;
2224  
2225  
  // what was the input handler?
2226  
  
2227  
  new Ref inputHandler;
2228  
  bool handled; // did inputHandler return true?
2229  
}
2230  
2231  
abstract concept BotStep {
2232  
  S comment;
2233  
2234  
  // returns true iff execution should continue immediately
2235  
  bool run(Conversation conv) { false; }
2236  
2237  
  ScheduledAction nextStepAction(Conversation conv) {
2238  
    ret Action_NextStep(conv, conv.executedSteps);
2239  
  }
2240  
2241  
  S fullToString() { ret toString(); }
2242  
2243  
  double preTypingDelay() { ret 0; }
2244  
}
2245  
2246  
concept Sequence > BotStep {
2247  
  new RefL<BotStep> steps;
2248  
2249  
  toString { ret "Sequence " + quoteOr(comment, "(unnamed)")
2250  
    + spaceRoundBracketed(nSteps(steps))
2251  
    /*+ ": " + joinWithComma(steps)*/;
2252  
  }
2253  
2254  
  bool run(Conversation conv) {
2255  
    // add me to stack
2256  
    syncAdd(conv.stack, new ActiveSequence(this));
2257  
    conv.change();
2258  
    true;
2259  
  }
2260  
}
2261  
2262  
concept BotMessage > BotStep {
2263  
  S text;
2264  
  S specialPurpose;
2265  
  bool disableInput;
2266  
  //S hashtag; // optional
2267  
2268  
  sS _fieldOrder = "text specialPurpose";
2269  
2270  
  public S toString(int shorten default 40) { ret "Message" + spacePlusRoundBracketedIfNempty(comment) + ": " + newLinesToSpaces2(shorten(text, shorten)); }
2271  
2272  
  S fullToString() {
2273  
    ret toString(Int.MAX_VALUE);
2274  
  }
2275  
2276  
  bool run(Conversation conv) {
2277  
    Msg msg = new Msg(false, text);
2278  
    msg.out = new Out;
2279  
    msg.out.disableInput = disableInput;
2280  
    conv.add(msg);
2281  
    true;
2282  
  }
2283  
  
2284  
  double preTypingDelay() { ret botMod().typingDelayForText(text); }
2285  
}
2286  
2287  
abstract concept AbstractBotImage > BotStep {
2288  
  S altText;
2289  
2290  
  bool run(Conversation conv) {
2291  
    botMod().addReplyToConvo(conv, () -> himgsrc(imageURL(), title := altText, alt := altText, class := "chat_contentImage"));
2292  
    true;
2293  
  }
2294  
2295  
  abstract S imageURL();
2296  
  
2297  
  double preTypingDelay() { ret botMod().settings().botTypingDelay; }
2298  
}
2299  
2300  
// external image
2301  
concept BotImage > AbstractBotImage {
2302  
  S imageURL, altText;
2303  
2304  
  sS _fieldOrder = "imageURL";
2305  
2306  
  S imageURL() { ret imageURL; }
2307  
2308  
  toString { ret "Image" + spacePlusRoundBracketedIfNempty(comment) + ": " + imageURL; }
2309  
}
2310  
2311  
concept UploadedImage > AbstractBotImage {
2312  
  S imageURL() { ret botMod().convertToAbsoluteURL(botMod().baseLink + "/uploaded-image/" + id); }
2313  
2314  
  File imageFile() { ret newFile(botMod().uploadedImagesDir(), id + ".png"); }
2315  
  
2316  
  toString { ret "Image" + spacePlusRoundBracketedIfNempty(altText)
2317  
    + spacePlusRoundBracketedIfNempty(comment); }
2318  
2319  
  void delete :: before {
2320  
    if (concepts() == db_mainConcepts())
2321  
      deleteFile(imageFile());
2322  
  }
2323  
}
2324  
2325  
concept UploadedSound {
2326  
  S comment;
2327  
  
2328  
  S soundURL() { ret botMod().convertToAbsoluteURL(botMod().baseLink + "/uploaded-sound/" + id); }
2329  
2330  
  File soundFile() { ret newFile(botMod().uploadedSoundsDir(), id + ".mp3"); }
2331  
  
2332  
  toString { ret "Sound" + spacePlusRoundBracketedIfNempty(comment); }
2333  
2334  
  void delete :: before {
2335  
    if (concepts() == db_mainConcepts())
2336  
      deleteFile(soundFile());
2337  
  }
2338  
}
2339  
2340  
concept UploadedFile {
2341  
  S name, comment, mimeType, liveURI;
2342  
  
2343  
  S downloadURL(S contentType default null) {
2344  
    ret botMod().convertToAbsoluteURL(appendQueryToURL(botMod().baseLink
2345  
      + "/uploaded-file/" + id + (empty(name) ? "" : "/" + urlencode(name)),
2346  
      ct := contentType));
2347  
  }
2348  
2349  
  File theFile() { ret newFile(botMod().uploadedFilesDir(), id + ".png"); }
2350  
  
2351  
  toString { ret "File " + spacePlusRoundBracketedIfNempty(name)
2352  
    + spacePlusRoundBracketedIfNempty(comment); }
2353  
2354  
  void delete :: before {
2355  
    if (concepts() == db_mainConcepts())
2356  
      deleteFile(theFile());
2357  
  }
2358  
}
2359  
2360  
concept BotDoNothing > BotStep {
2361  
  bool run(Conversation conv) { false; }
2362  
2363  
  toString { ret "Do nothing"; }
2364  
}
2365  
2366  
concept BotSendTyping > BotStep {
2367  
  bool run(Conversation conv) {
2368  
    conv.botTyping = print("Bot typing", now());
2369  
    true;
2370  
  }
2371  
}
2372  
2373  
concept BotPause > BotStep {
2374  
  double seconds;
2375  
2376  
  *() {}
2377  
  *(double *seconds) {}
2378  
2379  
  bool run(Conversation conv) {
2380  
    botMod().addScheduledAction(nextStepAction(conv), toMS(seconds));
2381  
    false;
2382  
  }
2383  
2384  
  toString { ret "Pause for " + (seconds == 1 ? "1 second" : formatDouble(seconds, 1) + " seconds"); }
2385  
}
2386  
2387  
concept BotOutgoingQuestion > BotStep implements IInputHandler {
2388  
  S displayText;
2389  
  S key;
2390  
  S defaultValue;
2391  
  S placeholder; // null for same as displayText, "" for none
2392  
  LS buttons;
2393  
  bool allowFreeText = true; // only matters when there are buttons
2394  
  bool multipleChoice; // buttons are checkboxes
2395  
  bool radioButtons; // show radio buttons instead of normal buttons
2396  
  S multipleChoiceSeparator = ", "; // how to join choices into value
2397  
  bool optional; // can be empty
2398  
  S answerCheck; // e.g. "email address"
2399  
  new RefL<BotStep> buttonActions; // what to do on each button
2400  
2401  
  sS _fieldOrder = "displayText key defaultValue placeholder buttons buttonActions allowFreeText multipleChoice optional answerCheck";
2402  
2403  
  toString { ret "Question: " + orEmptyQuotes(displayText); }
2404  
2405  
  LPair<S, BotStep> buttonsWithActions() {
2406  
    ret zipTwoListsToPairs_longer(buttons, buttonActions);
2407  
  }
2408  
2409  
  bool run(Conversation conv) {
2410  
    S text = botMod().rewriteBotMessage(conv, displayText);
2411  
    Msg msg = new Msg(false, text);
2412  
    msg.out = makeMsgOut();
2413  
    conv.add(msg);
2414  
    cset(conv, inputHandler := this);
2415  
    false;
2416  
  }
2417  
2418  
  Out makeMsgOut() {
2419  
    new Out out;
2420  
    out.placeholder = or(placeholder, displayText);
2421  
    out.defaultInput = defaultValue;
2422  
    out.buttons = cloneList(buttons);
2423  
    out.multipleChoice = multipleChoice;
2424  
    out.multipleChoiceSeparator = multipleChoiceSeparator;
2425  
    out.radioButtons = radioButtons;
2426  
    if (eqic(answerCheck, "phone number") && botMod().phoneNumberSpecialInputField)
2427  
      //out.javaScript = [[$("#chat_countrycode").addClass("visible");]];
2428  
      out.javaScript = [[$("#chat_message").hide(); $("#chat_telephone, .iti").show(); $("#chat_telephone").val("").focus();]];
2429  
    else if (!allowFreeText) {
2430  
      out.javaScript = jsDisableInputField();
2431  
      if (empty(out.placeholder)) out.placeholder = " ";
2432  
    }
2433  
2434  
    ret out;
2435  
  }
2436  
2437  
  public bool handleInput(S s, Conversation conv) {
2438  
    print("BotOutgoingQuestion handleInput " + s);
2439  
2440  
    // Store answer
2441  
    s = trim(s);
2442  
    S theKey = or2(key, htmldecode_dropAllTags(trim(displayText)));
2443  
    if (nempty(theKey) && !eq(theKey, "-")) syncPut(conv.answers, theKey, s);
2444  
    conv.change();
2445  
2446  
    // Validate
2447  
    if (eqic(answerCheck, "email address") && !isValidEmailAddress_simple(s))
2448  
      ret true with handleValidationFail(conv, "bad email address");
2449  
    if (eqic(answerCheck, "phone number") && !botMod().checkPhoneNumber(s))
2450  
      ret true with handleValidationFail(conv, "bad phone number");
2451  
2452  
    conv.removeInputHandler(this);
2453  
    
2454  
    int idx = indexOfIC(trimAll(lmap dropStuffBeforeVerticalBar(buttons)), s);
2455  
    print("Button index of " + quote(s) + " in " + sfu(buttons) + " => " + idx);
2456  
    BotStep target = get(buttonActions, idx);
2457  
    print("Button action: " + target);
2458  
    if (target != null)
2459  
      //conv.jumpTo(target);
2460  
      conv.callSubroutine(target); // We now interpret the target as a subroutine call
2461  
2462  
    // Go to next step
2463  
    print("Scheduling next step in " + conv);
2464  
    conv.scheduleNextStep();
2465  
    true; // acknowledge that we handled the input
2466  
  }
2467  
  
2468  
  double preTypingDelay() { ret empty(displayText) ? 0 : botMod().typingDelayForText(displayText); }
2469  
2470  
  // show error msg and reschedule input handling
2471  
  void handleValidationFail(Conversation conv, S purpose) {
2472  
    BotMessage msg = botMod().messageForPurpose(purpose);
2473  
    S text = or2(or2(msg?.text, getOrKeep(botMod().specialPurposes, purpose)), "Invalid input, please try again");
2474  
    printVars_str("handleValidationFail", +purpose, +msg, +text);
2475  
    new Msg m;
2476  
    m.text = text;
2477  
    m.out = makeMsgOut();
2478  
    conv.add(m);
2479  
    cset(conv, inputHandler := this);
2480  
    ret;
2481  
  }
2482  
}
2483  
2484  
concept BotIdleMode > BotStep {
2485  
  toString { ret "Idle Mode (wait indefinitely)"; }
2486  
  
2487  
  bool run(Conversation conv) {
2488  
    false;
2489  
  }
2490  
}
2491  
2492  
concept BotEndConversation > BotStep {
2493  
  toString { ret "End Conversation (disable user input)"; }
2494  
  
2495  
  bool run(Conversation conv) {
2496  
    Msg msg = new Msg(false, "");
2497  
    msg.out = new Out;
2498  
    msg.out.javaScript = jsDisableInputField();
2499  
    msg.out.placeholder = " ";
2500  
    conv.add(msg);
2501  
    true;
2502  
  }
2503  
}
2504  
2505  
sS jsDisableInputField() {
2506  
  ret [[$("#chat_message").prop('disabled', true).attr('placeholder', '');]];
2507  
}
2508  
2509  
concept BotSaveLead > BotStep {
2510  
  toString { ret "Save Lead"; }
2511  
  
2512  
  bool run(Conversation conv) {
2513  
    Lead lead = cnew Lead(conversation := conv,
2514  
      domain := conv.domainObj,
2515  
      date := Timestamp(now()),
2516  
      answers := cloneMap(conv.answers));
2517  
    botMod().onNewLead(lead);
2518  
    true;
2519  
  }
2520  
}
2521  
2522  
concept BotSaveFeedback > BotStep {
2523  
  toString { ret "Save Conversation Feedback"; }
2524  
  
2525  
  bool run(Conversation conv) {
2526  
    cnew ConversationFeedback(conversation := conv,
2527  
      domain := conv.domainObj,
2528  
      date := Timestamp(now()),
2529  
      answers := filterKeys(conv.answers, swic$("Conversation Feedback:")));
2530  
    true;
2531  
  }
2532  
}
2533  
2534  
concept BotClearStack > BotStep {
2535  
  bool run(Conversation conv) {
2536  
    syncRemoveAllExceptLast(conv.stack);
2537  
    conv.change();
2538  
    true;
2539  
  }
2540  
2541  
  toString { ret "Clear Stack [forget all running procedures in conversation]"; }
2542  
}
2543  
2544  
concept BotSwitchLanguage > BotStep {
2545  
  new Ref<Language> language;
2546  
  
2547  
  bool run(Conversation conv) {
2548  
    cset(conv, +language);
2549  
    true;
2550  
  }
2551  
2552  
  toString { ret "Switch to " + language; }
2553  
}
2554  
2555  
concept UserKeyword {
2556  
  new Ref<Language> language;
2557  
  S pattern;
2558  
  LS examples, counterexamples;
2559  
  new Ref<BotStep> action;
2560  
  bool enabled = true;
2561  
  bool priority;
2562  
  int precedence;
2563  
  transient MMOPattern parsedPattern;
2564  
2565  
  void change { parsedPattern = null; botMod().consistencyCheckResults = null; super.change(); }
2566  
  
2567  
  MMOPattern parsedPattern() {
2568  
    if (parsedPattern == null) ret parsedPattern = mmo2_parsePattern(pattern);
2569  
    ret parsedPattern;
2570  
  }
2571  
2572  
  toString { ret "User Keyword: " + pattern; }
2573  
2574  
  bool matchWithTypos(S s) {
2575  
    ret mmo2_matchWithTypos(parsedPattern(), s);
2576  
  }
2577  
}
2578  
2579  
concept Language {
2580  
  S languageName;
2581  
  S comment;
2582  
2583  
  toString { ret languageName + spaceRoundBracketed(comment); }
2584  
}
2585  
2586  
// event concepts are stored as part of the Conversation object, so they don't have IDs
2587  
// (might change this)
2588  
2589  
concept Evt {}
2590  
2591  
concept EvtMatchedUserKeyword > Evt {
2592  
  new Ref<UserKeyword> userKeyword;
2593  
  Msg msg;
2594  
2595  
  *() {}
2596  
  *(UserKeyword userKeyword, Msg *msg) { this.userKeyword.set(userKeyword); }
2597  
 }
2598  
 
2599  
concept EvtJumpTo > Evt {
2600  
  new Ref<BotStep> target;
2601  
2602  
  *() {}
2603  
  *(BotStep target) { this.target.set(target); }
2604  
}
2605  
2606  
concept EvtCallSubroutine > Evt {
2607  
  new Ref<BotStep> target;
2608  
2609  
  *() {}
2610  
  *(BotStep target) { this.target.set(target); }
2611  
}
2612  
2613  
abstract concept AbstractAvatar {
2614  
  abstract Avatar getActualAvatar();
2615  
}
2616  
2617  
concept RandomAvatar > AbstractAvatar {
2618  
  Avatar getActualAvatar() {
2619  
    ret random(filter(list(Avatar), a -> a.onShift()));
2620  
  }
2621  
2622  
  toString { ret "Choose a random avatar that is on shift"; }
2623  
}
2624  
2625  
concept Avatar > AbstractAvatar {
2626  
  S name, comment;
2627  
  S shift; // hours (e.g. "8-18", optional)
2628  
  new Ref<AbstractBotImage> image;
2629  
2630  
  Avatar getActualAvatar() { this; }
2631  
2632  
  bool onShift() {
2633  
    L<IntRange> shifts = parseBusinessHours_pcall(shift);
2634  
    if (empty(shifts)) true;
2635  
    shifts = splitBusinessHoursAtMidnight(shifts);
2636  
    int minute = minuteInDay(timeZone(botMod().timeZone));
2637  
    ret anyIntRangeContains(shifts, minute);
2638  
  }
2639  
2640  
  toString { ret empty(name) ? super.toString() : "Avatar " + name + appendBracketed(comment); }
2641  
}
2642  
2643  
/*concept ActorInConversation {
2644  
  new Ref<Conversation> conv;
2645  
  new Ref<Avatar> avatar;
2646  
  new L<ActiveSequence> stack;
2647  
}*/
2648  
2649  
// end of concepts
2650  
2651  
// special options for a message
2652  
sclass Out extends DynamicObject {
2653  
  LS buttons;
2654  
  bool multipleChoice, radioButtons;
2655  
  S multipleChoiceSeparator;
2656  
  S placeholder;
2657  
  S defaultInput;
2658  
  S javaScript;
2659  
  bool disableInput;
2660  
}
2661  
2662  
sclass Msg extends DynamicObject {
2663  
  long time;
2664  
  bool fromUser;
2665  
  //Avatar avatar;
2666  
  Worker fromWorker;
2667  
  S text;
2668  
  Out out;
2669  
  LS labels;
2670  
  
2671  
  *() {}
2672  
  *(bool *fromUser, S *text) { time = now(); }
2673  
  *(S *text, bool *fromUser) { time = now(); }
2674  
2675  
  toString { ret (fromUser ? "User" : "Bot") + ": " + text; }
2676  
2677  
}
2678  
2679  
concept AuthedDialogID {
2680  
  S cookie;
2681  
  bool master;
2682  
  new Ref<Domain> restrictedToDomain;
2683  
  Worker loggedIn; // who is logged in with this cookie
2684  
  
2685  
  S domain() {
2686  
    ret !restrictedToDomain.has() ? null : restrictedToDomain->domainAndPath;
2687  
  }
2688  
}
2689  
2690  
//concept Session {} // LEGACY
2691  
2692  
sclass ActiveSequence {
2693  
  Sequence originalSequence;
2694  
  L<BotStep> steps;
2695  
  int stepIndex;
2696  
2697  
  *() {}
2698  
  *(BotStep step) {
2699  
    if (step cast Sequence) {
2700  
      steps = cloneList(step.steps);
2701  
      originalSequence = step;
2702  
    } else
2703  
      steps = ll(step);
2704  
    steps = botMod().addTypingDelays(steps);
2705  
  }
2706  
    
2707  
  bool done() { ret stepIndex >= l(steps); }
2708  
  BotStep currentStep() { ret get(steps, stepIndex); }
2709  
  void nextStep() { ++stepIndex; }
2710  
2711  
  toString {
2712  
    ret (stepIndex >= l(steps) ? "DONE " : "Step " + (stepIndex+1) + "/" + l(steps) + " of ")
2713  
      + (originalSequence != null ? str(originalSequence) : squareBracket(joinWithComma(steps)));
2714  
  }
2715  
}
2716  
2717  
// our base concept - a conversation between a user and a bot or sales representative
2718  
concept Conversation {
2719  
  S cookie, ip, country, domain;
2720  
  new Ref<Domain> domainObj;
2721  
  new LL<Msg> oldDialogs;
2722  
  new L<Msg> msgs;
2723  
  long lastPing;
2724  
  bool botOn = true;
2725  
  bool notificationsOn = true;
2726  
  Worker worker; // who are we talking to?
2727  
  transient volatile long userTyping, botTyping; // timestamps
2728  
  bool testMode;
2729  
  long nextActionTime = Long.MAX_VALUE;
2730  
  long userMessageProcessed; // timestamp
2731  
  int newDialogTriggered = -1;
2732  
  transient bool dryRun;
2733  
  new Ref<Avatar> avatar;
2734  
  
2735  
  new L<ActiveSequence> stack;
2736  
  int executedSteps;
2737  
  IInputHandler inputHandler;
2738  
  new Ref<Language> language;
2739  
  //Long lastProposedDate;
2740  
  SS answers = litcimap();
2741  
  SS botConfig;
2742  
  new RefL<Concept> events; // just logging stuff
2743  
  int reloadCounter; // increment this to have whole conversation reloaded in browser
2744  
2745  
  void add(Msg m) {
2746  
    m.text = trim(m.text);
2747  
    //if (!m.fromUser && empty(m.text) && (m.out == null || empty(m.out.buttons))) ret; // don't store empty msgs from bot
2748  
    syncAdd(msgs, m);
2749  
    botMod().noteConversationChange();
2750  
    change();
2751  
    vmBus_send chatBot_messageAdded(mc(), this, m);
2752  
  }
2753  
  
2754  
  int allCount() { ret archiveSize() + syncL(msgs); }
2755  
  int archiveSize() { ret syncLengthLevel2(oldDialogs) + syncL(oldDialogs); }
2756  
  L<Msg> allMsgs() { ret concatLists_syncIndividual(syncListPlus(oldDialogs, msgs)); }
2757  
  
2758  
  long lastMsgTime() { Msg m = last(msgs); ret m == null ? 0 : m.time; }
2759  
2760  
  void incReloadCounter() {
2761  
    cset(this, reloadCounter := reloadCounter+1);
2762  
  }
2763  
  
2764  
  void turnBotOff {
2765  
    cset(this, botOn := false);
2766  
    botMod().noteConversationChange();
2767  
  }
2768  
2769  
  void turnBotOn {  
2770  
    cset(this, botOn := true, worker := null);
2771  
    S backMsg = botMod().getCannedAnswer("#botBack", this);
2772  
    if (empty(msgs) || lastMessageIsFromUser() || !eq(last(msgs).text, backMsg))
2773  
      add(new Msg(backMsg, false));
2774  
    botMod().noteConversationChange();
2775  
  }
2776  
  
2777  
  bool lastMessageIsFromUser() {
2778  
    ret nempty(msgs) && last(msgs).fromUser;
2779  
  }
2780  
  
2781  
  void newDialog {
2782  
    if (syncNempty(msgs))
2783  
      syncAdd(oldDialogs, msgs);
2784  
    cset(this, msgs := new L);
2785  
    syncClear(answers);
2786  
    syncClear(stack);
2787  
    // TODO: clear scheduled actions?
2788  
    change();
2789  
    print("newDialog " + archiveSize() + "/" + allCount());
2790  
2791  
    vmBus_send chatBot_clearedSession(mc(), this);
2792  
    //botMod().addScheduledAction(new OnNewDialog(this));
2793  
  }
2794  
2795  
  void jumpTo(BotStep step) {
2796  
    syncPopLast(stack); // terminate inner script
2797  
    print("Jumping to " + step);
2798  
    noteEvent(EvtJumpTo(step));
2799  
    if (step == null) ret;
2800  
    syncAdd(stack, new ActiveSequence(step));
2801  
    change();
2802  
  }
2803  
2804  
  void callSubroutine(BotStep step) {
2805  
    if (step == null) ret;
2806  
    noteEvent(EvtCallSubroutine(step));
2807  
    print("Calling subroutine " + step);
2808  
    syncAdd(stack, new ActiveSequence(step));
2809  
    change();
2810  
  }
2811  
2812  
  void removeInputHandler(IInputHandler h) {
2813  
    if (inputHandler == h)
2814  
      cset(this, inputHandler := null);
2815  
  }
2816  
2817  
  void scheduleNextStep {
2818  
    botMod().addScheduledAction(new Action_NextStep(this));
2819  
  }
2820  
2821  
  void noteEvent(Concept event) {
2822  
    events.add(event);
2823  
  }
2824  
2825  
  bool isActive() {
2826  
    ret lastPing + botMod().activeConversationTimeout() >= now();
2827  
  }
2828  
2829  
  S botImg() {
2830  
    if (avatar.has())
2831  
      ret avatar->image->imageURL();
2832  
    else
2833  
      ret botMod().botImageForDomain(domainObj!);
2834  
  }
2835  
} // end of Conversation
2836  
2837  
abstract concept ScheduledAction implements Runnable {
2838  
  long time;
2839  
}
2840  
2841  
concept Action_NextStep > ScheduledAction {
2842  
  new Ref<Conversation> conv;
2843  
  int executedSteps = -1; // if >= 0, we verify against conv.executedSteps
2844  
2845  
  *() {}
2846  
  *(Conversation conv) { this.conv.set(conv); }
2847  
  *(Conversation conv, int *executedSteps) { this.conv.set(conv); }
2848  
2849  
  run {
2850  
    nextStep(conv!, executedSteps);
2851  
  }
2852  
}
2853  
2854  
concept OnNewDialog > ScheduledAction {
2855  
  new Ref<Conversation> conv;
2856  
   
2857  
  *() {}
2858  
  *(Conversation conv) { this.conv.set(conv); }
2859  
  
2860  
  run {
2861  
    botMod().startMainScript(conv!);
2862  
  }
2863  
}
2864  
2865  
svoid nextStep(Conversation conv, int expectedExecutedSteps default -1) {
2866  
  if (expectedExecutedSteps >= 0 && conv.executedSteps != expectedExecutedSteps) ret;
2867  
  
2868  
  while licensed {
2869  
    if (empty(conv.stack)) ret;
2870  
    ActiveSequence seq = syncLast(conv.stack);
2871  
    bool done = seq.done();
2872  
    printVars_str("Active sequence", +conv, +seq, +done, stackSize := syncL(conv.stack));
2873  
    if (done)
2874  
      continue with syncPopLast(conv.stack);
2875  
    BotStep step = seq.currentStep();
2876  
    seq.nextStep();
2877  
    ++conv.executedSteps;
2878  
    conv.change();
2879  
    if (!executeStep(step, conv)) {
2880  
      print("Step returned false: " + step);
2881  
      break;
2882  
    }
2883  
  }
2884  
}
2885  
2886  
sbool executeStep(BotStep step, Conversation conv) {
2887  
  if (step == null || conv == null) false;
2888  
  print("Executing step " + step + " in " + conv);
2889  
2890  
  ExecutedStep executed = null;
2891  
  if (botMod().recordExecutionsAndInputs)
2892  
    executed = cnew ExecutedStep(+step, conversation := conv);
2893  
    
2894  
  Msg lastMsg = syncLast(conv.msgs);
2895  
  bool result = step.run(conv);
2896  
  Msg newMsg = syncLast(conv.msgs);
2897  
  if (newMsg != null && !newMsg.fromUser && newMsg != lastMsg)
2898  
    cset(executed, msg := newMsg);
2899  
  ret result;
2900  
}
2901  
2902  
concept OnUserMessage > ScheduledAction {
2903  
  new Ref<Conversation> conv;
2904  
  
2905  
  *() {}
2906  
  *(Conversation conv) { this.conv.set(conv); }
2907  
2908  
  run {
2909  
    // TODO: handle multiple messages in case handling was delayed
2910  
    Msg msg = syncLast(conv->msgs);
2911  
    if (msg == null || !msg.fromUser) ret;
2912  
2913  
    print("OnUserMessage: " + msg);
2914  
2915  
    if (botMod().handleGeneralUserInput(conv!, msg, true))
2916  
      ret;
2917  
2918  
    IInputHandler inputHandler = conv->inputHandler;
2919  
    if (inputHandler != null) {
2920  
      InputHandled rec = null;
2921  
      S input = msg.text;
2922  
      if (botMod().recordExecutionsAndInputs)
2923  
        rec = cnew InputHandled(+inputHandler, conversation := conv!, +msg, +input);
2924  
      
2925  
      bool result = conv->inputHandler.handleInput(input, conv!);
2926  
      cset(rec, handled := result);
2927  
      if (result)
2928  
        ret;
2929  
    }
2930  
2931  
    botMod().handleGeneralUserInput(conv!, msg, false);
2932  
  }
2933  
}
2934  
2935  
// application-specific concepts
2936  
2937  
// a server we're running - either a domain name or domain.bla/path
2938  
concept Domain {
2939  
  S domainAndPath;
2940  
  S tenantID; // for external software
2941  
2942  
  // bot config - av
2943  
  new Ref<AbstractBotImage> botImage;
2944  
  S botName; // if empty, keep default
2945  
  new Ref<AbstractAvatar> avatar;
2946  
  
2947  
  S headerColorLeft, headerColorRight; // ditto
2948  
  Bool autoOpenBot;
2949  
  
2950  
  new Ref<BotStep> mainScript;
2951  
2952  
  S password = aGlobalID();
2953  
2954  
  sS _fieldOrder = "domainAndPath botImage mainScript";
2955  
  
2956  
  toString { ret domainAndPath; }
2957  
}
2958  
2959  
concept Form {
2960  
  S name;
2961  
  new RefL<BotStep> steps;
2962  
2963  
  toString { ret name; }
2964  
}
2965  
2966  
concept DeliveredDomain {
2967  
  S domain;
2968  
}
2969  
2970  
concept CannedAnswer {
2971  
  S hashTag, text;
2972  
}
2973  
2974  
sS text(CannedAnswer a) { ret a?.text; }
2975  
2976  
concept Lead {
2977  
  new Ref<Domain> domain;
2978  
  new Ref<Conversation> conversation;
2979  
  Timestamp date;
2980  
  SS answers;
2981  
}
2982  
2983  
concept ConversationFeedback {
2984  
  new Ref<Domain> domain;
2985  
  new Ref<Conversation> conversation;
2986  
  Timestamp date;
2987  
  SS answers;
2988  
}
2989  
2990  
concept Settings {
2991  
  S mainDomainName;
2992  
  double botTypingDelay;
2993  
  double botTypingDelayPerWord;
2994  
  double botTypingMaxDelay;
2995  
  S preferredCountryCodes; // dial codes, comma-separated
2996  
  bool multiLanguageMode;
2997  
  new Ref<UploadedSound> notificationSound;
2998  
  bool talkToBotOnlyWithAuth;
2999  
  bool enableWorkerChat;
3000  
  new Ref<Language> defaultLanguage;
3001  
  S mailLeadsTo; // where to mail leads
3002  
  S leadMailSubject = "A new lead occurred in your chat bot";
3003  
}
3004  
3005  
sinterface IInputHandler {
3006  
  // true if handled
3007  
  bool handleInput(S s, Conversation conv);
3008  
}
3009  
3010  
static DynNewBot2 botMod() {
3011  
  ret (DynNewBot2) db_mainConcepts().miscMapGet(DynNewBot2);
3012  
}

Author comment

Began life as a copy of #1029877

download  show line numbers  debug dex  old transpilations   

Travelled to 4 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, vouqrxazstgt

No comments. add comment

Snippet ID: #1030652
Snippet name: DynNewBot2 [backup before undo states]
Eternal ID of this version: #1030652/3
Text MD5: 612ba50dba6f47b0d53469da337deee9
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2021-08-31 18:08:04
Source code size: 103987 bytes / 3012 lines
Pitched / IR pitched: No / No
Views / Downloads: 186 / 198
Version history: 2 change(s)
Referenced in: [show references]