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

3438
LINES

< > BotCompany Repo | #1029877 // DynNewBot2 [LIVE]

JavaX fragment (include) [tags: use-pretranspiled]

Transpiled version (56984L) is out of date.

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

Author comment

Began life as a copy of #1029672

download  show line numbers  debug dex  old transpilations   

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

No comments. add comment

Snippet ID: #1029877
Snippet name: DynNewBot2 [LIVE]
Eternal ID of this version: #1029877/634
Text MD5: 57e63ee380ef4b57b47476ecf11aa159
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2022-10-16 16:07:03
Source code size: 116568 bytes / 3438 lines
Pitched / IR pitched: No / No
Views / Downloads: 939 / 3639
Version history: 633 change(s)
Referenced in: [show references]