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

394
LINES

< > BotCompany Repo | #1027629 // WebChatBot [LIVE]

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

Libraryless. Click here for Pure Java version (16847L/123K).

1  
!include once #1027630 // Msg + Conversation
2  
3  
sclass WebChatBot {
4  
  O thoughtBot;
5  
6  
  int longPollTick = 100;
7  
  int longPollMaxWait = 1000*30; // lowered to 30 seconds
8  
  
9  
  S templateID = #1027638;
10  
  S botName = "Chat Bot";
11  
  S heading = "Chat Bot";
12  
  S afterHeading;
13  
  S botImageID = #1102802;
14  
  S userImageID = #1102803;
15  
  S chatHeaderImageID = #1102802;
16  
  S cssID = #1026266;
17  
  S baseLink;
18  
  S jsOnMsgHTML; // operates on variable "src"
19  
  bool botOnRight = true;
20  
  S timeZone = germanTimeZone_string();
21  
  S moreStuff; // JS code to execute after init is done
22  
  S onBotShown;
23  
  bool forceCookie;
24  
  
25  
  Set<S> specialButtons = litciset("Cancel", "Back", "Abbrechen", "Zurück");
26  
  
27  
  void start {
28  
    db();
29  
    Class envType = fieldType(thoughtBot, "env");
30  
    if (envType != null)
31  
      setOpt(thoughtBot, "env", proxy(envType, (O) mc()));
32  
  }
33  
  
34  
  void sayAsync(S session, S text) {
35  
    lock dbLock();
36  
    Conversation conv = getConv(session);
37  
    conv.add(new Msg(false, text));
38  
    print("sayAsync " + session + ", new size=" + conv.allCount());
39  
  }
40  
  
41  
  Request newRequest(S uri, SS params) { ret new Request(uri, params); }
42  
  
43  
  O html(S uri, SS params, O... _) {
44  
    ret newRequest(uri, params).html(_);
45  
  }
46  
  
47  
  class Request {
48  
    S uri;
49  
    SS params;
50  
    S cookie;
51  
    S clientIP;
52  
    
53  
    *(S *uri, SS *params) {}
54  
55  
    O html(O... _) {
56  
      temp tempRegisterThread();
57  
      uri = dropTrailingSlashIfNemptyAfterwards(uri);
58  
      if (cookie == null) cookie = params.get("cookie");
59  
      Conversation conv = nempty(cookie) ? getConv(cookie) : null;
60  
      if (conv != null) {
61  
        if (clientIP == null) clientIP = clientIP();
62  
        cset(conv, ip := clientIP);
63  
      }
64  
      print("URI: " + uri + ", cookie: " + cookie + (conv == null ? "" : ", msgs: " + l(conv.msgs)));
65  
      
66  
      S pw = trim(params.get('pw));
67  
      if (nempty(pw)) {
68  
        S realPW = trim(loadSecretTextFile("password.txt"));
69  
        if (empty(realPW)) ret errorMsg("Administrator has not set a password");
70  
        if (neq(pw, realPW))
71  
          ret errorMsg("Bad password, please try again");
72  
        cset(conv, authed := true);
73  
        if (nempty(params.get('redirect)))
74  
          ret hrefresh(params.get('redirect));
75  
      }
76  
      
77  
      if (eq(uri, "/stats")) {
78  
        if (!conv.authed) ret serveAuthForm(rawLink(uri));
79  
        ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads()));
80  
      }
81  
          
82  
      if (eq(uri, "/logs")) {
83  
        if (!conv.authed) ret serveAuthForm(rawLink(uri));
84  
        ret webChatBotLogsHTML2(rawLink(uri), params);
85  
      }
86  
    
87  
      if (eq(uri, "/auth-only")) {
88  
        if (eq(params.get('logout), "1"))
89  
          cset(conv, authed := false);
90  
        if (!conv.authed) ret serveAuthForm(params.get('uri));
91  
        ret "";
92  
      }
93  
    
94  
      if (conv != null) {
95  
        lock dbLock();
96  
        
97  
        S message = trim(params.get("btn"));
98  
        if (empty(message)) message = trim(params.get("message"));
99  
        message = preprocess(message);
100  
        
101  
        if (match_vbar("new dialog|new dialogue", message)) {
102  
          conv.oldDialogs.add(conv.msgs);
103  
          cset(conv, msgs := new L);
104  
          conv.change();
105  
          callOpt(thoughtBot, "clearSession", conv.cookie);
106  
          vmBus_send chatBot_clearedSession(mc(), conv);
107  
          message = null;
108  
        }
109  
        
110  
        callOpt(thoughtBot, "setSession", cookie, params);
111  
        
112  
        if (empty(conv.msgs))
113  
          addReplyToConvo(conv, lambda0 initialMessage);
114  
          
115  
        if (nempty(message) && !lastUserMessageWas(conv, message)) {
116  
          conv.add(new Msg(true, message));
117  
          print("Added message: " + message + " to " + conv.id + ", l=" + l(conv.msgs));
118  
        }
119  
      
120  
        if (nempty(conv.msgs) && last(conv.msgs).fromUser)
121  
          addReplyToConvo(conv, () -> makeReply(last(conv.msgs).text));
122  
      } // locked
123  
      
124  
      if (eq(uri, "/msg")) ret withHeader("OK");
125  
      
126  
      if (eq(uri, "/grabFullLog")) {
127  
        if (conv == null) ret "No conversation for " + cookie;
128  
        L<Msg> msgs = conv.allMsgs();
129  
        ret jsonEncode(map(msgs, msg -> 
130  
          litorderedmap(
131  
            fromUser := msg.fromUser,
132  
            text := msg.text,
133  
            time := msg.time)));
134  
      }
135  
      
136  
      if (eq(uri, "/incremental")) {
137  
        vmBus_send chatBot_userPolling(mc(), conv);
138  
        int a = parseInt(params.get("a"));
139  
        
140  
        print("a=" + a + ", as=" + conv.archiveSize());
141  
142  
        long start = sysNow();
143  
        L msgs;
144  
        bool first = true;
145  
        while (licensed() && sysNow() < start+longPollMaxWait) {
146  
          int as = conv.archiveSize();
147  
          msgs = cloneSubList(conv.msgs, a-as);
148  
          bool newDialog = a <= as;
149  
          if (empty(msgs)) {
150  
            if (first) {
151  
              print("Long poll starting on " + cookie + ", " + a + "/" + a + ", id=" + conv.id);
152  
              first = false;
153  
            }
154  
            sleep(longPollTick);
155  
          } else {
156  
            if (first) print("Long poll ended.");
157  
            new StringBuilder buf;
158  
            renderMessages(buf, msgs);
159  
            ret withHeader("<!-- " + conv.allCount() + " " + (newDialog ? "NEW DIALOG " : "") + "-->\n"  + buf);
160  
          }
161  
        }
162  
        ret withHeader("");
163  
      }
164  
      
165  
      if (eq(uri, "/n")) ret str(conv.allCount());
166  
      
167  
      {
168  
        lock dbLock();
169  
        S html = templateHTML();
170  
        new StringBuilder buf;
171  
    
172  
        // incremental only    
173  
        //renderMessages(buf, conv.msgs);
174  
      
175  
        S langlinks = "<!-- langlinks here -->";
176  
        if (html.contains(langlinks))
177  
          html = html.replace(langlinks,
178  
            ahref(rawLink("eng"), "English") + " | " + ahref(rawLink("deu"), "German"));
179  
        
180  
        if (forceCookie && nempty(cookie))
181  
          html = html.replace([[localStorage.getItem('cookie')]], jsQuote(cookie));
182  
        html = html.replace("#BOTIMG#", imageSnippetURLOrEmptyGIF(chatHeaderImageID));
183  
        html = html.replace("#N#", "0" /*str(conv.allCount())*/);
184  
        html = html.replace("#INCREMENTALURL#", baseLink + "/incremental?a=");
185  
        html = html.replace("#MSGURL#", baseLink + "/msg?message=");
186  
        html = html.replace("#CSS_ID#", psI_str(cssID));
187  
        if (nempty(params.get("debug")))
188  
          html = html.replace("var showActions = false;", "var showActions = true;");
189  
      
190  
        html = html.replace("#AUTOOPEN#", jsBool(botAutoOpen());
191  
        html = html.replace("#BOT_ON#", jsBool(botOn());
192  
        html = html.replace("$HEADING", joinNemptiesWithSpace(heading, afterHeading));
193  
        html = html.replace("#ONMSGHTML#", unnull(jsOnMsgHTML));
194  
        html = html.replace("<!-- MSGS HERE -->", str(buf));
195  
        html = html.replace("//MORESTUFF//", unnull(moreStuff));
196  
        html = html.replace("//chatBotShown//", unnull(onBotShown));
197  
        html = hreplaceTitle(html, heading);
198  
        
199  
        if (eqGet(params, "_botDemo", "1"))
200  
          ret hhtml(hhead(
201  
            htitle(heading)
202  
            + loadJQuery()
203  
          ) + hbody(hjavascript(html)));
204  
        else {
205  
          optPar bool returnJS;
206  
          ret returnJS ? html : withHeader(serveJavaScript(html));
207  
        }
208  
      }
209  
    }
210  
  } // end of class Request
211  
  
212  
  void addReplyToConvo(Conversation conv, IF0<S> think) {
213  
    S reply = "";
214  
    MsgOut out = null;
215  
    pcall {
216  
      reply = trim(think!);
217  
      out = (MsgOut) quickImport(getThreadLocal(thoughtBot, "out"));
218  
    }
219  
    
220  
    // if no answer and no buttons, don't add message
221  
    if (empty(reply) && (out == null || out.isEmpty())) ret;
222  
    
223  
    Msg msg = new Msg(false, reply);
224  
    msg.out = out;
225  
    conv.add(msg);
226  
  }
227  
  
228  
  O withHeader(S html) {
229  
    ret withHeader(noCacheHeaders(serveHTML(html)));
230  
  }
231  
  
232  
  O withHeader(O response) {
233  
    call(response, 'addHeader, "Access-Control-Allow-Origin", "*");
234  
    ret response;
235  
  }
236  
  
237  
  S renderMessageText(S text) {
238  
    ret replace(htmlEncode2_nlToBr(trim(text)),
239  
      ":wave:", html_wavingHand());
240  
  }
241  
  
242  
  void renderMessages(StringBuilder buf, L<Msg> msgs) {
243  
    new Set<S> buttonsToSkip;
244  
    new LS buttonsHtml;
245  
    for (Msg m : msgs) {
246  
      if (!m.fromUser && eq(m.text, "-")) continue;
247  
      S html = renderMessageText(m.text);
248  
      // pull back & cancel buttons to beginning of msg
249  
      if (m == last(msgs) && m.out != null) {
250  
        fOr (S btn : m.out.buttons)
251  
          if (specialButtons.contains(btn)) {
252  
            buttonsToSkip.add(btn);
253  
            buttonsHtml.add(renderButtons(ll(btn)));
254  
          }
255  
      }
256  
      if (nempty(buttonsHtml) && l(m.out.buttons) == l(buttonsToSkip))
257  
        html += " " + hspan("&nbsp;&nbsp;", class := "chat-button-span") + lines(buttonsHtml);
258  
      else
259  
        buttonsToSkip.clear();
260  
      appendMsg(buf, m.fromUser ? defaultUserName() : botName, formatTime(m.time), html, !m.fromUser);
261  
    }
262  
        
263  
    appendButtons(buf, last(msgs).out, buttonsToSkip);
264  
  }
265  
  
266  
  void appendMsg(StringBuilder buf, S name, S time, S text, bool bot) {
267  
    // img now before text because of position: absolute
268  
    // removed from last div: <span class="direct-chat-timestamp pull-$LR">$TIME</span>
269  
    buf.append(([[<div class="direct-chat-msg doted-border">]]);
270  
    
271  
    S imgID = bot ? botImageID : userImageID;
272  
    new Matches m;
273  
    bool dontSay = bot && startsWith(text, "[don't say]", m);
274  
    if (dontSay) text = m.rest();
275  
    if (nempty(imgID))
276  
      buf.append([[<img alt="message user image" src="$IMG" class="direct-chat-img">]]
277  
        .replace("$IMG", snippetImgLink(imgID)));
278  
    
279  
    buf.append(([[
280  
      <div class="direct-chat-info clearfix">
281  
      <div style="float: right" class="direct-chat-timestamp">$TIME</div>
282  
      <span class="direct-chat-name pull-left">$NAME</span>
283  
      </div>
284  
      <div class="direct-chat-text pull-$LR clearfix]] + (bot ? " bot-utterance" + (dontSay ? " dont-say" : "") : "") + [[">$TEXT</div>
285  
      <div class="direct-chat-info clearfix"></div>
286  
    </div>
287  
    ]]).replace("$LR", bot != botOnRight ? "left" : "right")
288  
       .replace("$NAME", name)
289  
       .replace("$TIME", time)
290  
       .replace("$TEXT", text));
291  
  }
292  
  
293  
  S replaceButtonText(S s) {
294  
    if (eqicOneOf(s, "back", "zurück")) ret unicode_undoArrow();
295  
    if (eqicOneOf(s, "cancel", "Abbrechen")) ret unicode_crossProduct();
296  
    ret s;
297  
  }
298  
  
299  
  S renderButtons(LS buttons) {
300  
    new LS out;
301  
    for i over buttons: {
302  
      S code = buttons.get(i);
303  
      S text = replaceButtonText(code);
304  
      out.add(hbuttonOnClick_returnFalse(text, "submitAMsg(" + jsQuote(text) + ")", class := "chatbot-choice-button", title := eq(code, text) ? null : code));
305  
      if (!specialButtons.contains(code)
306  
        && i+1 < l(buttons) && specialButtons.contains(buttons.get(i+1)))
307  
        out.add("&nbsp;&nbsp;");
308  
    }
309  
    ret lines(out);
310  
  }
311  
  
312  
  void appendButtons(StringBuilder buf, MsgOut out, Set<S> buttonsToSkip) {
313  
    S placeholder = out == null ? "" : unnull(out.placeholder);
314  
    S defaultInput = out == null ? "" : unnull(out.defaultInput);
315  
    buf.append(hscript("chatBot_setInput(" + jsQuote(defaultInput) + ", " + jsQuote(placeholder) + ");"));
316  
    if (out == null) ret;
317  
    LS buttons = listMinusSet(out.buttons, buttonsToSkip);
318  
    if (empty(buttons)) ret;
319  
    S buttonsHtml = renderButtons(buttons);
320  
    buf.append([[<div class="direct-chat-msg doted-border">
321  
      <div class="direct-chat-buttons">
322  
        $BUTTONS
323  
      </div>
324  
    </div>
325  
      ]].replace("$BUTTONS", htmlEncode2_nlToBr(appendNewLineIfNempty(trim(out.buttonsIntro))) + buttonsHtml));
326  
  }
327  
  
328  
  void appendDate(StringBuilder buf, S date) {
329  
    buf.append([[
330  
    <div class="chat-box-single-line">
331  
      <abbr class="timestamp">DATE</abbr>
332  
    </div>]].replace("DATE", date));
333  
  }
334  
  
335  
  S initialMessage() {
336  
    ret (S) call(thoughtBot, "initialMessage"); 
337  
  }
338  
  
339  
  bool lastUserMessageWas(Conversation conv, S message) {
340  
    Msg m = last(conv.msgs);
341  
    ret m != null && m.fromUser && eq(m.text, message);
342  
  }
343  
  
344  
  S makeReply(S message) {
345  
    ret callStaticAnswerMethod(thoughtBot, message);
346  
  }
347  
  
348  
  S formatTime(long time) {
349  
    ret timeInTimeZoneWithOptionalDate_24(timeZone, time);
350  
  }
351  
  
352  
  S formatDialog(S id, L<Msg> msgs) {
353  
    new L<S> lc;
354  
    for (Msg m : msgs)
355  
      lc.add(htmlencode((m.fromUser ? "> " : "< ") + m.text));
356  
    ret id + ul(lc);
357  
  }
358  
  
359  
  Conversation getConv(fS cookie) {
360  
    ret uniq Conversation(+cookie);
361  
  }
362  
  
363  
  S serveAuthForm(S redirect) {
364  
    ret hhtml(hhead(htitle("Authorization required")) + hbody(hfullcenter(
365  
      h3_htmlEncode(heading + " Admin")
366  
      + hpostform(
367  
        hhidden(+redirect)
368  
      + "Password: " + hpassword('pw) + "<br><br>" + hsubmit()))));
369  
  }
370  
  
371  
  S errorMsg(S msg) {
372  
    ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "<br><br>" + ahref(jsBackLink(), "Back"))));
373  
  }
374  
  
375  
  S defaultUserName() {
376  
    ret de() ? "Sie" : "You";
377  
  }
378  
  
379  
  bool de() {
380  
    ret isTrue(callOpt(thoughtBot, "de"));
381  
  }
382  
  
383  
  bool botOn() { true; }
384  
385  
  bool botAutoOpen() {
386  
    ret !isFalse(callOpt(thoughtBot, "botAutoOpen"));
387  
  }
388  
  
389  
  swappable S templateHTML() {
390  
    ret loadSnippet(templateID); // TODO: cache
391  
  }
392  
  
393  
  swappable S preprocess(S message) { ret message; }
394  
}

Author comment

Began life as a copy of #1026250

download  show line numbers  debug dex  old transpilations   

Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv

No comments. add comment

Snippet ID: #1027629
Snippet name: WebChatBot [LIVE]
Eternal ID of this version: #1027629/59
Text MD5: fd0c6c243f0aecf02fa385248bc2a7c7
Transpilation MD5: 53770747c4cea9ac616813ab4608bf17
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-07-14 13:59:56
Source code size: 13455 bytes / 394 lines
Pitched / IR pitched: No / No
Views / Downloads: 304 / 886
Version history: 58 change(s)
Referenced in: [show references]