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

297
LINES

< > BotCompany Repo | #1028566 // WorkerChat + concept Worker [backup with single frameset]

JavaX fragment (include)

1  
// a human who answers to clients
2  
concept Worker {
3  
  S loginName, displayName;
4  
  //S password;
5  
  bool available;
6  
  long lastOnline; // recent timestamp if online
7  
  
8  
  sS _fieldOrder = "loginName displayName";
9  
  
10  
  S renderAsHTML() {
11  
    ret htmlEncode2(loginName + " (display name: " + displayName + ")");
12  
  }
13  
}
14  
15  
sclass WorkerChat {
16  
  int workerLongPollTick = 200;
17  
  int workerLongPollMaxWait = 1000*30;
18  
  long lastWorkerRequested; // timestamp for notification sound
19  
20  
  O html(S uri, SS params, Conversation conv, AuthedDialogID auth) null {
21  
    S uri2 = appendSlash(uri);
22  
    bool requestAuthed = auth != null;
23  
  
24  
    if (startsWith(uri2, "/workers-admin/")) {
25  
      if (!requestAuthed) ret serveAuthForm(params.get('uri));
26  
      ret serveWorkersAdmin(uri, params);
27  
    }
28  
  
29  
    if (startsWith(uri2, "/worker/")) {
30  
      if (!requestAuthed) ret serveAuthForm(params.get('uri));
31  
      
32  
      if (nempty(params.get("turnBotOn")))
33  
        conv.turnBotOn();
34  
      
35  
      ret serveWorkerPage(auth, conv, uri, params);
36  
    }
37  
  }
38  
  
39  
  // TODO: handle deletion properly
40  
  S serveWorkersAdmin(S uri, SS params) {
41  
    S nav = p(ahref(rawBotLink(dbBotID), "Main admin") + " | " + ahref(baseLink + "/workers-admin", "Workers admin"));
42  
    
43  
    if (eq(uri, "/workers-admin/change-image")) {
44  
      long id = parseLong(params.get("id"));
45  
      Worker worker = getConcept Worker(id);
46  
      File f = workerImageFile(id);
47  
      S content;
48  
      
49  
      S b64 = params.get("base64");
50  
      if (nempty(b64))
51  
        saveFile(f, decodeBASE64(b64));
52  
  
53  
      if (worker == null) content = "Worker not found";
54  
      else
55  
        content =
56  
          nav +
57  
          hscript([[
58  
            function submitIt() {
59  
              var file = $('#myUpload')[0].files[0];
60  
              var reader = new FileReader();
61  
  
62  
              reader.onloadend = function () {
63  
                var b64 = reader.result.replace(/^data:.+;base64,/, '');
64  
                $("#base64").val(b64);
65  
                console.log("Got base64 data: " + b64.length);
66  
                $("#submitForm").submit();
67  
              };
68  
  
69  
              reader.readAsDataURL(file);
70  
              return false;
71  
            }
72  
          ]]) +
73  
          h2("Worker image for: " + worker.renderAsHTML())
74  
          + p(!fileExists(f) ? "No image set" : himgsrc(rawLink("worker-image/" + id)))
75  
          + hpostform(
76  
            hhiddenWithIDAndName("base64")
77  
            + "Choose image: " + hfileupload("accept", "image/png,image/jpeg,image/gif", id := "myUpload") + " "
78  
      + hhidden(+id), action := rawLink("/workers-admin/change-image"), id := "submitForm")
79  
      + hbuttonOnClick_returnFalse("Upload", "submitIt()");
80  
      
81  
      ret hhtml(hhead_title("Change worker image")
82  
        + hsansserif()
83  
        + hbody(loadJQuery()
84  
        + content));
85  
    }
86  
    
87  
    HCRUD_Concepts<Worker> data = new HCRUD_Concepts<Worker>(Worker);
88  
    
89  
    HCRUD crud = new(rawLink("workers-admin"), data) {
90  
      S frame(S title, S contents) {
91  
        ret hhtml(hhead_title_htmldecode(title) + hbody(
92  
          nav + h1(title) + contents));
93  
      }
94  
    };
95  
    crud.uneditableFields = litset("available", "lastOnline");
96  
    
97  
    crud.postProcessTableRow = (item, rendered) -> {
98  
      printStruct(+item);
99  
      long id = parseLong(item.get("id"));
100  
      File f = workerImageFile(id);
101  
      ret mapPlus(rendered, "Image" :=
102  
        f == null ? "???" : !fileExists(f) ? "-" : himgsrc(rawLink("worker-image/" + id)));
103  
    };
104  
    crud.renderCmds = item -> crud.renderCmds_base(item) + " | " + ahref(rawLink("workers-admin/change-image" + hquery(id := item.get("id"))), "Change image...");
105  
    crud.tableClass = "responstable";
106  
    ret hsansserif() + hcss_responstable()
107  
      + crud.renderPage(params);
108  
  }
109  
  
110  
  // auth is tested before we get here
111  
  S serveWorkerPage(AuthedDialogID auth, Conversation conv, S uri, SS params) {
112  
    S cookie = conv.cookie;
113  
    
114  
    if (eq(afterLastSlash(uri), "availableWorkers"))
115  
      ret "Available workers: " + or2(joinWithComma(map(workersAvailable(), w -> w.renderAsHTML())), "-");
116  
    
117  
    if (nempty(params.get('workerLogOut)))
118  
      cset(auth, loggedIn := null);
119  
      
120  
    if (auth.loggedIn != null && nempty(params.get('workerAvailableBox)))
121  
      if (cset_trueIfChanged(auth.loggedIn, available := nempty(params.get('workerAvailable))))
122  
        noteConversationChange(); // update list of available workers
123  
124  
    if (nempty(params.get("acceptConversation"))) {
125  
      cset(conv, worker := auth.loggedIn);
126  
      conv.turnBotOff();
127  
    }
128  
      
129  
    S loginID = params.get('workerLogIn);
130  
    if (nempty(loginID))
131  
      cset(auth, loggedIn := getConcept Worker(parseLong(loginID)));
132  
  
133  
    Map map = prependEmptyOptionForHSelect(mapToOrderedMap(conceptsSortedByFieldCI(Worker, 'loginName),
134  
      w -> pair(w.id, w.loginName)));
135  
    if (auth.loggedIn == null)
136  
      ret hsansserif() + p("You are not logged in as a worker")
137  
        + hpostform(
138  
          "Log in as: " + hselect("workerLogIn", map, conceptID(auth.loggedIn)) + " " + hsubmit("OK"), action := rawLink("worker"));
139  
          
140  
    // We are logged in
141  
    
142  
    if (eq(afterLastSlash(uri), "conversation")) {
143  
      if (conv == null) ret "Conversation not found";
144  
      // Serve the checkbox & the JavaScript
145  
      S onOffURL = rawLink("worker/botOnOff" + hquery(+cookie) + "&on=");
146  
      ret
147  
         hsansserif() + loadJQuery()
148  
       + hhidden(cookie := conv.cookie) // for other frame
149  
       + hpostform(
150  
         hhidden(+cookie) +
151  
         p(renderBotStatus(conv))
152  
          + p(conv.botOn
153  
            ? hsubmit("Accept conversation", name := "acceptConversation")
154  
            : hsubmit("Turn bot back on", name := "turnBotOn"))
155  
          , action := rawLink("worker/conversation"))
156  
157  
       // include bot
158  
       + hscriptsrc(rawLink(hquery(workerMode := 1, cookie := conv.cookie)));
159  
    }
160  
    
161  
    if (eq(afterLastSlash(uri), "conversations")) {
162  
      cset(auth.loggedIn, lastOnline := now());
163  
      bool poll = eq("1", params.get("poll")); // poll mode?
164  
      S content = "";
165  
      
166  
      if (poll) {
167  
        long seenChange = parseLong(params.get("lastChange"));
168  
        vmBus_send chatBot_startingWorkerPoll(mc(), conv);
169  
      
170  
        long start = sysNow();
171  
        L msgs;
172  
        bool first = true;
173  
        while (licensed() && sysNow() < start+workerLongPollMaxWait
174  
          && lastConversationChange == seenChange)
175  
          sleep(workerLongPollTick);
176  
          
177  
        printVars_str(+lastWorkerRequested, +seenChange);
178  
        if (lastWorkerRequested > seenChange)
179  
          content = hscript([[
180  
            sendDesktopNotification("A worker is requested!", { action: function() { window.focus(); } });
181  
            if (window.workerRequestedSound == null) {
182  
              console.log("Loading worker requested sound");
183  
              window.workerRequestedSound = new Audio("https://botcompany.de/files/1400404/worker-requested.mp3");
184  
            }
185  
            console.log("Playing worker requested sound");
186  
            window.workerRequestedSound.play();
187  
          ]]);
188  
          
189  
        // if poll times out, send update anyway to update time calculations
190  
      }
191  
  
192  
      long pingThreshold = now()-activeConversationTimeout();
193  
      L<Conversation> convos = sortByCalculatedFieldDesc(c -> c.lastMsgTime(),
194  
        conceptsWithFieldGreaterThan Conversation(lastPing := pingThreshold));
195  
        
196  
      content +=
197  
          hhiddenWithID(+lastConversationChange) + tag table(
198  
          hsimpletableheader("IP", "Country", "Bot/worker status", "Last change", "Last messages")
199  
          + mapToLines(convos, c -> {
200  
            L<Msg> lastMsgs = lastTwo(c.msgs);
201  
            S style = c == conv ? "background: #90EE90" : null;
202  
            S convLink = rawLink("worker" + hquery(cookie := c.cookie));
203  
            ret tag tr(
204  
              td(ahref(convLink, c.ip, target := "_top")) +
205  
              td(getCountry(c)) +
206  
              td(renderBotStatus(c)) +
207  
              td(renderHowLongAgo(c.lastMsgTime())) +
208  
              td(ahref(convLink, hparagraphs(lambdaMap renderMsgForWorkerChat(lastMsgs)), target := "_top", style := "text-decoration: none")), +style, /*class := "clickable-row", "data-href" := convLink*/);
209  
          }), class := "responstable");
210  
  
211  
      if (poll) ret content;
212  
  
213  
      S incrementalURL = rawLink("worker/conversations?poll=1&lastChange=");
214  
      
215  
      ret hhtml(
216  
          hhead(hsansserif() + loadJQuery()
217  
        + hscript_clickableRows())
218  
        + hdesktopNotifications()
219  
        + div(small(
220  
            span(hbutton("CLICK HERE to enable notification sounds!"), id := "enableSoundsBtn")
221  
          + " | "
222  
          + span("", id := "notiStatus")), style := "float: right")
223  
        + hbody(h3(botName)
224  
        + hscript([[
225  
          function enableSounds() {
226  
            document.removeEventListener('click', enableSounds);
227  
            $("#enableSoundsBtn").html("Notification sounds enabled");
228  
          }
229  
          document.addEventListener('click', enableSounds);
230  
        ]])
231  
        + hpostform(
232  
            "Logged in as "
233  
          + htmlEncode2(auth.loggedIn.loginName)
234  
          + " (display name: " + htmlEncode2(auth.loggedIn.displayName) + ")"
235  
          + hhidden(workerAvailableBox := 1)
236  
          + " &nbsp; "
237  
          + hcheckboxWithText("workerAvailable", "I am available", auth.loggedIn.available, onclick := "form.submit()")
238  
          + " &nbsp; "
239  
          + hsubmit("Log out", name := "workerLogOut"),
240  
          target := "_top", action := rawLink("worker"))
241  
        + p("Available workers: " + b(or2(joinWithComma(
242  
          map(workersAvailable(), w -> w.displayName)), "none")))
243  
        + h3("Active conversations")
244  
        + hcss_responstable()
245  
        + hdivWithID("contentArea", content)
246  
        + hscript([[
247  
          function poll_start() {
248  
            var lastChange = $("#lastConversationChange").val();
249  
            if (!lastChange)
250  
              setTimeout(poll_start, 1000);
251  
            else {
252  
              var url = "#INCREMENTALURL#" + lastChange;
253  
              console.log("Loading " + url);
254  
              $.get(url, function(src) {
255  
                if (src.match(/^ERROR/)) console.log(src);
256  
                else {
257  
                  console.log("Loaded " + src.length + " chars");
258  
                  $("#contentArea").html(src);
259  
                }
260  
                setTimeout(poll_start, 1000);
261  
              }, 'text')
262  
                .fail(function() {
263  
                  console.log("Rescheduling after fail");
264  
                  setTimeout(poll_start, 1000);
265  
                });
266  
            }
267  
          }
268  
          poll_start();
269  
        ]].replace("#INCREMENTALURL#", incrementalURL)
270  
        ));
271  
    } // end of worker/conversations part
272  
    
273  
    // serve frame set
274  
    ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]")
275  
      + hframeset_cols("*,*", 
276  
        tag frame("", name := "conversations", src := rawLink("worker/conversations" + hquery(+cookie))) +
277  
        tag frame("", name := "conversation", src := conv == null ? null : rawLink("worker/conversation" + hquery(+cookie)))));
278  
  }
279  
  
280  
  S renderMsgForWorkerChat(Msg msg) {
281  
    ret (msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : msg.fromUser ? "User" : "Bot") + ": " + b(htmlEncode2If(shouldHtmlEncodeMsg(msg), msg.text));
282  
  }
283  
  
284  
  Cl<Worker> workersAvailable() {
285  
    long timestamp = now()-workerLongPollMaxWait-10000;
286  
    ret filter(list(Worker), w -> w.available && w.lastOnline >= timestamp);
287  
  }
288  
289  
  bool anyWorkersAvailable() {
290  
    ret nempty(workersAvailable());
291  
  }
292  
  
293  
  S renderBotStatus(Conversation conv) {
294  
   ret "Bot is " + b(conv.botOn ? "on" : "off") + "<br>"
295  
     + "Assigned worker: " + b(conv.worker == null ? "none" : conv.worker.displayName);
296  
  }
297  
}

Author comment

Began life as a copy of #1028434

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: #1028566
Snippet name: WorkerChat + concept Worker [backup with single frameset]
Eternal ID of this version: #1028566/1
Text MD5: dd08c95b697770f7cfa266e84e41c4d3
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-06-27 15:31:46
Source code size: 11959 bytes / 297 lines
Pitched / IR pitched: No / No
Views / Downloads: 240 / 235
Referenced in: [show references]