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

2679
LINES

< > BotCompany Repo | #1030497 // DynNewBot2 [backup before MsgRenderer]

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

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: #1030497
Snippet name: DynNewBot2 [backup before MsgRenderer]
Eternal ID of this version: #1030497/1
Text MD5: 66fa1281402928ad0a88cb6960565e9a
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-12-29 22:55:58
Source code size: 91671 bytes / 2679 lines
Pitched / IR pitched: No / No
Views / Downloads: 171 / 180
Referenced in: [show references]