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

323
LINES

< > BotCompany Repo | #1028434 // WorkerChat + concept Worker [Include for web chat bots]

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

// a human who answers to clients
concept Worker {
  S loginName, displayName;
  //S password;
  bool available;
  long lastOnline; // recent timestamp if online
  
  sS _fieldOrder = "loginName displayName";
  
  S renderAsHTML() {
    ret htmlEncode2(loginName + " (display name: " + displayName + ")");
  }
}

sclass WorkerChat {
  int workerLongPollTick = 200;
  int workerLongPollMaxWait = 1000*30;
  long lastWorkerRequested; // timestamp for notification sound

  O html(S uri, SS params, Conversation conv, AuthedDialogID auth) null {
    S uri2 = appendSlash(uri);
    bool requestAuthed = auth != null;
  
    if (startsWith(uri2, "/workers-admin/")) {
      if (!requestAuthed) ret serveAuthForm(params.get('uri));
      ret serveWorkersAdmin(uri, params);
    }
  
    if (startsWith(uri2, "/worker/")) {
      if (!subBot_isHttps())
        ret subBot_serveRedirect("https://" + domain() + fullSelfLink(params));
  
      if (!requestAuthed) ret serveAuthForm(params.get('uri));
      
      if (nempty(params.get("turnBotOn")))
        conv.turnBotOn();
      
      ret serveWorkerPage(auth, conv, uri, params);
    }
  }
  
  // TODO: handle deletion properly
  S serveWorkersAdmin(S uri, SS params) {
    S nav = p(ahref(rawBotLink(dbBotID), "Main admin") + " | " + ahref(baseLink + "/workers-admin", "Workers admin"));
    
    if (eq(uri, "/workers-admin/change-image")) {
      long id = parseLong(params.get("id"));
      Worker worker = getConcept Worker(id);
      File f = workerImageFile(id);
      S content;
      
      S b64 = params.get("base64");
      if (nempty(b64))
        saveFile(f, decodeBASE64(b64));
  
      if (worker == null) content = "Worker not found";
      else
        content =
          nav +
          hscript([[
            function submitIt() {
              var file = $('#myUpload')[0].files[0];
              var reader = new FileReader();
  
              reader.onloadend = function () {
                var b64 = reader.result.replace(/^data:.+;base64,/, '');
                $("#base64").val(b64);
                console.log("Got base64 data: " + b64.length);
                $("#submitForm").submit();
              };
  
              reader.readAsDataURL(file);
              return false;
            }
          ]]) +
          h2("Worker image for: " + worker.renderAsHTML())
          + p(!fileExists(f) ? "No image set" : himgsrc(rawLink("worker-image/" + id)))
          + hpostform(
            hhiddenWithIDAndName("base64")
            + "Choose image: " + hfileupload("accept", "image/png,image/jpeg,image/gif", id := "myUpload") + "<br><br>"
            + "Note: Image should be square. Will be scaled to 40x40px in the chat"
      + hhidden(+id), action := rawLink("/workers-admin/change-image"), id := "submitForm")
      + hbuttonOnClick_returnFalse("Upload", "submitIt()");
      
      ret hhtml(hhead_title("Change worker image")
        + hsansserif()
        + hbody(loadJQuery()
        + content));
    }
    
    HCRUD_Concepts<Worker> data = new HCRUD_Concepts<Worker>(Worker);
    
    HCRUD crud = new(rawLink("workers-admin"), data) {
      S frame(S title, S contents) {
        ret hhtml(hhead_title_htmldecode(title) + hbody(
          nav + h1(title) + contents));
      }
    };
    crud.unshownFields = litset("available", "lastOnline", "away");

    crud.postProcessTableRow = (item, rendered) -> {
      printStruct(+item);
      long id = parseLong(item.get("id"));
      File f = workerImageFile(id);
      ret mapPlus(rendered, "Image" :=
        f == null ? "???" : !fileExists(f) ? "-" : himgsrc(rawLink("worker-image/" + id)));
    };
    crud.renderCmds = item -> crud.renderCmds_base(item) + " | " + ahref(rawLink("workers-admin/change-image" + hquery(id := item.get("id"))), "Change image...");
    crud.tableClass = "responstable";
    ret hsansserif() + hcss_responstable()
      + crud.renderPage(params);
  }
  
  // auth is tested before we get here
  S serveWorkerPage(AuthedDialogID auth, Conversation conv, S uri, SS params) {
    S cookie = conv.cookie;
    S uri2 = afterLastSlash(uri);
    
    if (eq(uri2, "availableWorkers"))
      ret "Available workers: " + or2(joinWithComma(map(workersAvailable(), w -> w.renderAsHTML())), "-");
    
    if (nempty(params.get('workerLogOut)))
      cset(auth, loggedIn := null);
      
    if (auth.loggedIn != null && nempty(params.get('workerAvailableBox)))
      if (cset_trueIfChanged(auth.loggedIn, available := nempty(params.get('workerAvailable))))
        noteConversationChange(); // update list of available workers

    if (nempty(params.get("acceptConversation"))) {
      if (conv.worker == null) { // only if not accepted by anyone
        cset(conv, worker := auth.loggedIn);
        conv.turnBotOff();
      }
    }
      
    S loginID = params.get('workerLogIn);
    if (nempty(loginID))
      cset(auth, loggedIn := getConcept Worker(parseLong(loginID)));
  
    Map map = prependEmptyOptionForHSelect(mapToOrderedMap(conceptsSortedByFieldCI(Worker, 'loginName),
      w -> pair(w.id, w.loginName)));
    if (auth.loggedIn == null)
      ret hsansserif() + p("You are not logged in as a worker")
        + hpostform(
          "Log in as: " + hselect("workerLogIn", map, conceptID(auth.loggedIn)) + " " + hsubmit("OK"), action := rawLink("worker"));
          
    // We are logged in
    
    if (eq(uri2, "conversation")) {
      if (conv == null) ret "Conversation not found";
      // Serve the checkbox & the JavaScript
      S onOffURL = rawLink("worker/botOnOff" + hquery(+cookie) + "&on=");
      ret
         hsansserif() + loadJQuery()
       + hhidden(cookie := conv.cookie) // for other frame
       + hpostform(
         hhidden(+cookie) +
         p(renderBotStatus(conv))
          + p(conv.botOn
            ? hsubmit("Accept conversation", name := "acceptConversation")
            : hsubmit("Turn bot back on", name := "turnBotOn"))
          , action := rawLink("worker/innerFrameSet"), target := "innerFrameSet")

       // include bot
       + hscriptsrc(rawLink(hquery(workerMode := 1, cookie := conv.cookie)));
    }
    
    if (eq(uri2, "conversations")) {
      cset(auth.loggedIn, lastOnline := now());
      bool poll = eq("1", params.get("poll")); // poll mode?
      S content = "";
      
      if (poll) {
        long seenChange = parseLong(params.get("lastChange"));
        vmBus_send chatBot_startingWorkerPoll(mc(), conv);
      
        long start = sysNow();
        L msgs;
        bool first = true;
        while (licensed() && sysNow() < start+workerLongPollMaxWait
          && lastConversationChange == seenChange)
          sleep(workerLongPollTick);
          
        printVars_str(+lastWorkerRequested, +seenChange);
        if (lastWorkerRequested > seenChange)
          content = hscript([[
            window.parent.parent.frames[0].sendDesktopNotification("A worker is requested!", { action: function() { window.focus(); } });
            window.parent.parent.frames[0].playWorkerRequestedSound();
          ]]);
          
        // if poll times out, send update anyway to update time calculations
      }
  
      long pingThreshold = now()-activeConversationTimeout();
      L<Conversation> convos = sortByCalculatedFieldDesc(c -> c.lastMsgTime(),
        conceptsWithFieldGreaterThan Conversation(lastPing := pingThreshold));
        
      content +=
          hhiddenWithID(+lastConversationChange) + tag table(
          hsimpletableheader("IP", "Country", "Bot/worker status", "Last change", "Last messages")
          + mapToLines(convos, c -> {
            L<Msg> lastMsgs = lastTwo(c.msgs);
            S style = c == conv ? "background: #90EE90" : null;
            S convLink = rawLink("worker/innerFrameSet" + hquery(cookie := c.cookie));
            ret tag tr(
              td(ahref(convLink, c.ip, target := "innerFrameSet")) +
              td(getCountry(c)) +
              td(renderBotStatus(c)) +
              td(renderHowLongAgo(c.lastMsgTime())) +
              td(ahref(convLink, hparagraphs(lambdaMap renderMsgForWorkerChat(lastMsgs)), target := "innerFrameSet", style := "text-decoration: none")), +style, /*class := "clickable-row", "data-href" := convLink*/);
          }), class := "responstable");
  
      if (poll) ret content;
  
      S incrementalURL = rawLink("worker/conversations?poll=1&lastChange=");
      
      ret hhtml(
          hhead(hsansserif() + loadJQuery()
        + hscript_clickableRows())
        + hbody(h3(botName)
        + hpostform(
            "Logged in as "
          + htmlEncode2(auth.loggedIn.loginName)
          + " (display name: " + htmlEncode2(auth.loggedIn.displayName) + ")"
          + hhidden(workerAvailableBox := 1)
          + " &nbsp; "
          + hcheckboxWithText("workerAvailable", "I am available", auth.loggedIn.available, onclick := "form.submit()")
          + " &nbsp; "
          + hsubmit("Log out", name := "workerLogOut"),
          target := "innerFrameSet", action := rawLink("worker/innerFrameSet"))
        + p("Available workers: " + b(or2(joinWithComma(
          map(workersAvailable(), w -> w.displayName)), "none")))
        + h3("Active conversations")
        + hcss_responstable()
        + hdivWithID("contentArea", content)
        + hscript([[
          function poll_start() {
            var lastChange = $("#lastConversationChange").val();
            if (!lastChange)
              setTimeout(poll_start, 1000);
            else {
              var url = "#INCREMENTALURL#" + lastChange;
              console.log("Loading " + url);
              $.get(url, function(src) {
                if (src.match(/^ERROR/)) console.log(src);
                else {
                  console.log("Loaded " + src.length + " chars");
                  $("#contentArea").html(src);
                }
                setTimeout(poll_start, 1000);
              }, 'text')
                .fail(function() {
                  console.log("Rescheduling after fail");
                  setTimeout(poll_start, 1000);
                });
            }
          }
          poll_start();
        ]].replace("#INCREMENTALURL#", incrementalURL)
        ));
    } // end of worker/conversations part
    
    if (eq(uri2, "notificationArea"))
      ret hhtml(
          hhead(hsansserif() + loadJQuery())
        + hbody(hdesktopNotifications()
        + div(small(
            span(hbutton("CLICK HERE to enable notification sounds!"), id := "enableSoundsBtn")
          + " | "
          + span("", id := "notiStatus")), style := "float: right")
        + hscript([[
          function enableSounds() {
            document.removeEventListener('click', enableSounds);
            $("#enableSoundsBtn").html("Notification sounds enabled");
          }
          document.addEventListener('click', enableSounds);
          
          if (window.workerRequestedSound == null) {
            console.log("Loading worker requested sound");
            window.workerRequestedSound = new Audio("https://botcompany.de/files/1400404/worker-requested.mp3");
          }
          
          function playWorkerRequestedSound() {
            console.log("Playing worker requested sound");
            window.workerRequestedSound.play();
          }
          window.playWorkerRequestedSound = playWorkerRequestedSound;

        ]])));
    
    if (eq(uri2, "innerFrameSet"))
      // serve frame set 2
      ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]")
        + hframeset_cols("*,*", 
          tag frame("", name := "conversations", src := rawLink("worker/conversations" + hquery(+cookie))) +
          tag frame("", name := "conversation", src := conv == null ? null : rawLink("worker/conversation" + hquery(+cookie)))));
          
    // serve frame set 1
    ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]")
      + hframeset_rows("50,*", 
        tag frame("", name := "notificationArea", src := rawLink("worker/notificationArea")) +
        tag frame("", name := "innerFrameSet", src := conv == null ? null : rawLink("worker/innerFrameSet" + hquery(+cookie)))));
        
  }
  
  S renderMsgForWorkerChat(Msg msg) {
    ret (msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : msg.fromUser ? "User" : "Bot") + ": " + b(htmlEncode2If(shouldHtmlEncodeMsg(msg), msg.text));
  }
  
  Cl<Worker> workersAvailable() {
    long timestamp = now()-workerLongPollMaxWait-10000;
    ret filter(list(Worker), w -> w.available && w.lastOnline >= timestamp);
  }

  bool anyWorkersAvailable() {
    ret nempty(workersAvailable());
  }
  
  S renderBotStatus(Conversation conv) {
   ret "Bot is " + b(conv.botOn ? "on" : "off") + "<br>"
     + "Assigned worker: " + b(conv.worker == null ? "none" : conv.worker.displayName);
  }
}

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: #1028434
Snippet name: WorkerChat + concept Worker [Include for web chat bots]
Eternal ID of this version: #1028434/67
Text MD5: 5a6f64bc62bd8cbbfc8c46f660765760
Author: stefan
Category: javax / web chat bots
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-07-13 21:00:33
Source code size: 13125 bytes / 323 lines
Pitched / IR pitched: No / No
Views / Downloads: 202 / 1644
Version history: 66 change(s)
Referenced in: [show references]