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

1478
LINES

< > BotCompany Repo | #1030607 // DynGazelleRocks

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

Libraryless. Click here for Pure Java version (50417L/367K).

1  
do not include class Msg.
2  
3  
// create/read password salt for program, or use legacy global salt
4  
sS passwordSalt() {
5  
  File f = programSecretFile("password-salt");
6  
  if (fileExists(f)) ret loadTextFile(f);
7  
  File global = javaxSecretDir("password-salt");
8  
  if (fileExists(global)) ret loadTextFile(global);
9  
  ret loadTextFileOrCreateWithRandomID(f);
10  
}
11  
12  
concept PostReferenceable {}
13  
14  
// Note: If you change/add fields here, make sure to edit
15  
// GazelleBEA.findOrCreateUserForLogin
16  
concept User > PostReferenceable {
17  
  S name;
18  
  SecretValue<S> passwordMD5;
19  
  S contact; // e.g. mail address
20  
  bool isMaster;
21  
  SecretValue<S> botToken = aSecretGlobalID/*UnlessLoading*/();
22  
  long lastSeen;
23  
24  
  S name() { ret name; }
25  
  toString { ret nempty(name) ? "User " + name : super.toString(); }
26  
}
27  
28  
extend AuthedDialogID {
29  
  new Ref<User> user; // who is logged in
30  
  bool userMode; // show things as if user was not master
31  
  
32  
  User user() { ret user!; }
33  
}
34  
35  
extend Conversation {
36  
  new Ref<UserPost> mirrorPost;
37  
  new Ref<UserPost> detailedMirrorPost;
38  
39  
  void change {
40  
    super.change();
41  
    ((DynGazelleRocks) botMod()).rstUpdateMirrorPosts.add(this);
42  
  }
43  
44  
  void updateMirrorPost {
45  
    if (isDeleted()) ret;
46  
47  
    // Simple mirror post with just texts and "User:" or "Bot:"
48  
    if (!mirrorPost.has() && syncNempty(msgs))
49  
      cset(Conversation.this, mirrorPost := cnew UserPost(
50  
        title := "Conversation " + id,
51  
        type := "Conversation Mirror",
52  
        creator := ((DynGazelleRocks) botMod()).internalUser(), botInfo := "Conversation Mirror Bot"));
53  
    cset(mirrorPost!, text := lines_rtrim(msgs));
54  
55  
    // Detailed mirror post with a lot of info
56  
    if (!detailedMirrorPost.has() && syncNempty(msgs))
57  
      cset(Conversation.this, detailedMirrorPost := cnew UserPost(
58  
        title := "Conversation " + id + " with details",
59  
        type := "Detailed Conversation Mirror",
60  
        creator := ((DynGazelleRocks) botMod()).internalUser(), botInfo := "Conversation Mirror Bot"));
61  
    cset(detailedMirrorPost!, text := indentedStructure(msgs));
62  
  }
63  
64  
  void delete {
65  
    cdelete(mirrorPost!);
66  
    super.delete();
67  
  }
68  
}
69  
70  
concept UserCreatedObject > PostReferenceable {
71  
  new Ref<User> creator;
72  
  S botInfo; // info on how this was made. set to anything non-empty if made by bot
73  
  //S nameInRepo; // unique by user, for URL
74  
}
75  
76  
concept UserPost > UserCreatedObject {
77  
  S type, title, text;
78  
  bool isPublic = true, hidden;
79  
  bool creating; // new post, still editing
80  
  long xmodified; // modification timestamp that also includes replies
81  
  long bumped;    // modification timestamp that includes "bumps" (re-posted identical bot reply)
82  
83  
  static LS fieldsToSaveOnDelete = ll("id", "text", "title", "type", "creatorID", "postRefs", "postRefTags", "hidden", "isPublic", "created", "_modified", "xmodified", "botInfo", "creating");
84  
85  
  // TODO: migrate to this eventually (but need to change CRUD too)
86  
  //int flags;
87  
  //static int F_PUBLIC = 1, F_HIDDEN = 2;
88  
89  
  S text() { ret text; }
90  
91  
  new RefL<PostReferenceable> postRefs;
92  
  new LS postRefTags;
93  
94  
  sS _fieldOrder = "title text type postRefs isPublic";
95  
96  
  toString {
97  
    S content = nempty(title) ? shorten(title) : escapeNewLines(shorten(text));
98  
    //S _type = isReply() ? "reply" : "post";
99  
    //if (hidden) _type += " (hidden)";
100  
    //ret firstToUpper(_type) + " by " + author() + ": " + content;
101  
    ret content + " [by " + author() + "]";
102  
  }
103  
104  
  bool isReply() { ret nempty(postRefs); }
105  
  bool isBotPost() { ret nempty(botInfo); }
106  
  bool isMasterMade() { ret creator.has() && creator->isMaster; }
107  
108  
  S author() {
109  
    S author = userName(creator!);
110  
    if (isBotPost()) author += "'s bot " + quote(botInfo);
111  
    ret author;
112  
  }
113  
114  
  void change {
115  
    super.change();
116  
    ((DynGazelleRocks) botMod()).distributePostChanges(this);
117  
  }
118  
119  
  LPair<PostReferenceable, S> postRefsWithTags() {
120  
    ret zipTwoListsToPairs_lengthOfFirst(postRefs, postRefTags);
121  
  }
122  
123  
  L<PostReferenceable> postRefsWithTag(S tag) {
124  
    ret pairsAWhereB(postRefsWithTags(), t -> eqic_unnull(t, tag));
125  
  }
126  
127  
  UserPost primaryRefPost() {
128  
    ret optCast UserPost(first(postRefsWithTag("")));
129  
  }
130  
131  
  void _setModified(long modified) {
132  
    super._setModified(modified);
133  
    setXModified(modified);
134  
  }
135  
136  
  void bump {
137  
    bumped = now();
138  
    _change_withoutUpdatingModifiedField();
139  
    for (UserPost p : syncInstancesOf UserPost(postRefs)) p.setXModified();
140  
  }
141  
142  
  long modifiedOrBumped() { ret max(_modified, bumped); }
143  
144  
  // Notify of changes in replies
145  
  void setXModified(long modified default now()) {
146  
    bool changed = modified > xmodified;
147  
    xmodified = modified;
148  
    _change_withoutUpdatingModifiedField();
149  
    if (changed)
150  
      ((DynGazelleRocks) botMod()).distributePostChanges(this);
151  
  }
152  
153  
  void _backRefsModified {
154  
    super._backRefsModified();
155  
    setXModified(now());
156  
  }
157  
158  
  // -1 if no user post found in hierarchy
159  
  int distanceToUserPost() {
160  
    int n = 0;
161  
    UserPost post = this;
162  
    while (post != null && n < 100) {
163  
      if (!post.isBotPost()) ret n;
164  
      post = post.primaryRefPost();
165  
    }
166  
    ret -1;
167  
  }
168  
169  
  bool isJavaXCode() {
170  
    ret eqicOrSwicPlusSpace(type, "JavaX Code");
171  
  }
172  
} // end of UserPost
173  
174  
/*concept BotHandle > UserCreatedObject {
175  
  S globalID = aGlobalIDUnlessLoading();
176  
  S comment;
177  
  SecretValue<S> token;
178  
}*/
179  
180  
set flag NoNanoHTTPD.
181  
182  
asclass DynGazelleRocks > DynNewBot2 {
183  
  transient bool inlineSearch; // show search form in nav links
184  
  switchable bool webPushEnabled = true;
185  
  switchable bool showPostStats;
186  
  switchable bool showSuggestorBot = true;
187  
  switchable bool showMetaBotOnEveryPage = true;
188  
  switchable int delayAfterSuggestion = 1000;
189  
  switchable int defaultBotPost = 238410;
190  
  switchable long teamPostID;
191  
  switchable long favIconID; // NB: it's an UploadedFile id in this DB
192  
  switchable S defaultFavIconSnippet = gazelleFavIconSnippet();
193  
  
194  
  switchable S gazelleBotURL = null; //"https://gazellebot.botcompany.de";
195  
  switchable S salterService;
196  
197  
  transient double systemLoad;
198  
  transient long processSize;
199  
200  
  void init {
201  
    dm_require("#1017856/SystemLoad");
202  
    dm_vmBus_onMessage systemLoad(voidfunc(double load) {
203  
      if (setField(systemLoad := load))
204  
        distributeDivChanges("serverLoadRightHemi");
205  
    });
206  
    dm_vmBus_onMessage processSize(voidfunc(long processSize) {
207  
      if (setField(+processSize))
208  
        distributeDivChanges("memRightHemi");
209  
    });
210  
211  
    botName = heading = adminName = "gazelle.rocks";
212  
    templateID = #1030086;
213  
    cssID = /*#1030233*/"237267"; // now a post ID
214  
    set enableUsers;
215  
    set useWebSockets;
216  
    set showRegisterLink;
217  
    unset showTalkToBotLink;
218  
    set alwaysRedirectToHttps;
219  
    set redirectOnLogout;
220  
    set showFullErrors;
221  
    set inlineSearch;
222  
    unset lockWhileDoingBotActions; // fix the _gazelle_text bug?
223  
    unset showMailSenderInfo;
224  
    if (empty(salterService))
225  
      passwordSalt(); // make early
226  
  }
227  
228  
  void makeIndices :: after {    
229  
    indexConceptFieldCI(User, "name");
230  
    indexConceptFieldDesc(UserPost, "xmodified");
231  
    indexConceptFieldDesc(UserPost, "_modified");
232  
    indexConceptFieldCI(UserPost, "botInfo");
233  
  }
234  
  
235  
  void start {
236  
    init();
237  
    super.start();
238  
    db_mainConcepts().modifyOnCreate = true;
239  
    
240  
    printConceptIndices();
241  
242  
    onConceptsChange(new Runnable {
243  
      int postCount = countConcepts(UserPost);
244  
      
245  
      run {
246  
        temp enter();
247  
        int count = countConcepts(UserPost);
248  
        if (count != postCount) {
249  
          postCount = count;
250  
          distributeDivChanges("postCount");
251  
        }
252  
      }
253  
    });
254  
255  
    internalUser(); 
256  
    
257  
    // legacy clean-up
258  
    /*for (UserPost post) {
259  
      for i to 5:
260  
        cset(post, "postRefTags_" + i, null);
261  
    }*/
262  
  }
263  
264  
  // web socket stuff
265  
266  
  void requestServed {
267  
    distributeDivChanges("webRequestsRightHemi");
268  
  }
269  
270  
  transient class WebSocketInfo extends Meta is AutoCloseable {
271  
    S uri;
272  
    SS params;
273  
    Req req;
274  
    WeakReference<virtual WebSocket> webSocket;
275  
    Set<IVF1<S>> messageHandlers = syncLinkedHashSet();
276  
    Set<IVF1<byte[]>> binMessageHandlers = syncLinkedHashSet();
277  
    Set<Runnable> closeHandlers = syncLinkedHashSet();
278  
    
279  
    // Any helper can store things here like jqueryLoaded := true
280  
    Map misc = syncMap();
281  
282  
    *(virtual WebSocket webSocket) {
283  
      this.webSocket = weakRef(webSocket);
284  
      setFieldToIVF1Proxy(webSocket, onMessage := msg -> {
285  
        temp enter(); pcall {
286  
          O opCode = call(msg, "getOpCode");
287  
          byte opCodeValue = cast call(opCode, "getValue");
288  
          bool isBinary = opCodeValue == 2; // isso!
289  
          
290  
          if (isBinary) {
291  
            byte[] data = cast rcall getBinaryPayload(msg);
292  
            pcallFAll(binMessageHandlers, data);
293  
          } else {
294  
            S data = rcall_string getTextPayload(msg);
295  
            pcallFAll(messageHandlers, data);
296  
          }
297  
        }
298  
      });
299  
    }
300  
    
301  
    S subURI aka subUri() { ret req.subURI(); }
302  
    S uri() { ret uri; }
303  
    SS params() { ret params; }
304  
    S get(S param) { ret mapGet(params, param); }
305  
    
306  
    void setParams(SS params) {
307  
      this.params = params;
308  
      req.params = params;
309  
    }
310  
311  
    O dbRepresentation;
312  
    new Set<Pair<S, O>> liveDivs; // id/content info
313  
    
314  
    void eval(S jsCode, O... _ default null) {
315  
      jsCode = jsDollarVars(jsCode, _);
316  
      if (empty(jsCode)) ret;
317  
      dm_call(webSocket!, "send", jsonEncode(litmap(eval := jsCode)));
318  
    }
319  
    
320  
    void send(S jsonData) {
321  
      if (empty(jsonData)) ret;
322  
      dm_call(webSocket!, "send", jsonData);
323  
    }
324  
    
325  
    AutoCloseable onStringMessage(IVF1<S> onMsg) {
326  
      ret tempAdd(messageHandlers, onMsg);
327  
    }
328  
    
329  
    AutoCloseable onBinaryMessage(IVF1<byte[]> onMsg) {
330  
      ret tempAdd(binMessageHandlers, onMsg);
331  
    }
332  
    
333  
    AutoCloseable onClose(Runnable r) {
334  
      ret tempAdd(closeHandlers, r);
335  
    }
336  
    
337  
    void callCloseHandlers {
338  
      pcallFAll(closeHandlers);
339  
    }
340  
    
341  
    public void close {
342  
      cleanUp(webSocket!);
343  
    }
344  
  }
345  
  
346  
  transient Map<virtual WebSocket, WebSocketInfo> webSockets = syncWeakHashMap();
347  
348  
  void cleanMeUp_webSockets {
349  
    closeAllKeysAndClear((Map) webSockets);
350  
  }
351  
352  
  // funny: useWebSockets exists in DynNewBot2,
353  
  // but only in this subclass do we define handleWebSocket
354  
  void handleWebSocket(virtual WebSocket ws) {
355  
    set(ws, onClose := r {
356  
      var info = webSockets.get(ws);
357  
      if (info != null) pcall { onWebSocketClose(info); }
358  
      webSockets.remove(ws);
359  
    });
360  
    
361  
    set(ws, onOpen := rEnter {  
362  
      S uri = cast rcall getUri(ws);
363  
      SS params = cast rcall getParms(ws);
364  
      print("WebSocket opened! uri: " + uri + ", params: " + params);
365  
      WebSocketInfo info = new(ws);
366  
      IWebRequest webRequest = proxy IWebRequest(call(ws, "webRequest"));
367  
      info.req = webRequestToReq(webRequest);
368  
      S cookie = cookieFromWebRequest(webRequest);
369  
      AuthedDialogID auth = authObject(cookie);
370  
      fillReqAuthFromCookie(info.req, cookie, auth);
371  
      temp tempSetTL(currentReq, info.req); // why not
372  
      
373  
      info.uri = uri;
374  
      info.params = params;
375  
      webSockets.put(ws, info);
376  
      pcall { onNewWebSocket(info); }
377  
      long objectID = toLong(params.get("objectID"));
378  
      long modified = toLong(params.get("modified"));
379  
      if (objectID != 0) {
380  
        UserPost c = getConceptOpt UserPost(objectID);
381  
        print("Modification: " + c.xmodified + " / " + c._modified + " / " + modified);
382  
        if (c != null && c.xmodified > modified)
383  
          reloadBody(ws);
384  
      }
385  
    });
386  
387  
    setFieldToIVF1Proxy(ws, onMessage := msg -> { temp enter(); pcall {
388  
      WebSocketInfo info = webSockets.get(ws);
389  
      if (info == null) ret;
390  
      S data = rcall_string getTextPayload(msg);
391  
      Map map = jsonDecodeMap(data);
392  
      O div = map.get("liveDiv");
393  
      if (div cast S) {
394  
        S contentDesc = div;
395  
        syncAdd(info.liveDivs, pair(div, (O) contentDesc));
396  
        reloadDiv(ws, div, calcDivContent(contentDesc));
397  
      }
398  
    }});
399  
  }
400  
401  
  S serveRegisterForm(SS params) {
402  
    S user = trim(params.get("user"));
403  
    S pw = trim(params.get("f_pw"));
404  
    S pw2 = trim(params.get("pw2"));
405  
    S redirect = params.get("redirect");
406  
    S contact = trim(params.get("contact"));
407  
    
408  
    redirect = dropParamFromURL(redirect, "logout"); // don't log us out right again
409  
    if (empty(redirect)) redirect = baseLink + "/";
410  
411  
    new LS msgs;
412  
    if (nempty(user) || nempty(pw)) {
413  
      lock dbLock();
414  
      if (empty(user)) msgs.add("Please enter a user name");
415  
        else if (l(user) < 4) msgs.add("Minimum user name length: 4 characters");
416  
        else if (l(user) > 30) msgs.add("Maximum user name length: 30 characters");
417  
        else if (hasConceptIC User(name := user))
418  
          msgs.add("This user exists, please choose a different name");
419  
      if (regexpContainsIC("[^a-z0-9\\-\\.]", user))
420  
        msgs.add("Bad characters in user name (please use only a-z, 0-9, - and .)");
421  
      if (empty(pw)) msgs.add("Please enter a password. Don't use one of your regular (important) passwords! Just make one up and let the browser save it.");
422  
        else if (l(pw) < 6) msgs.add("Minimum password length: 6 characters");
423  
      if (neq(pw, pw2)) msgs.add("Passwords don't match");
424  
425  
      if (empty(msgs)) {
426  
        User userObj = cnew User(name := user, passwordMD5 := SecretValue(hashPW(pw)), +contact);
427  
        vmBus_send userCreated(userObj);
428  
        ret hrefresh(5.0, redirect) + "User " + user + " created! Redirecting...";
429  
      }
430  
    }
431  
    
432  
    ret hhtml(hhead(htitle("Register new user")
433  
      + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter(
434  
      h3_htmlEncode(adminName + " | Register new user")
435  
      + hpostform(
436  
          hhidden(+redirect)
437  
          + tag table(
438  
            (empty(msgs) ? "" : tr(td() + td() + td(htmlEncode2_nlToBr(lines_rtrim(msgs)))))
439  
          + tr(td_valignTop("Choose a user name:")
440  
            + td_valignTop(hinputfield(+user) + "<br>" + "(Minimum length 4. Characters allowed: a-z, 0-9, - and .)"))
441  
          + tr(td_valignTop("Choose a password:")
442  
            + td_valignTop(hpassword(f_pw := pw) + "<br>" + "(Minimum length 6 characters)"))
443  
          + tr(td_valignTop("Repeat the password please:") + td(hpassword(+pw2)))
444  
          + tr(td_valignTop("Way to contact you (e.g. e-mail) - optional:") + td(hinputfield(+contact)))
445  
          + tr(td() + td(hsubmit("Register"))), class := "responstable"),
446  
        action := baseLink + "/register")
447  
      )));
448  
  }
449  
450  
  bool calcMasterAuthed(Req req) {
451  
    ret super.calcMasterAuthed(req)
452  
      || req.auth != null && req.auth.user.has() && req.auth.user->isMaster;
453  
  }
454  
  
455  
  O serveOtherPage(Req req) {
456  
    S uri = req.uri;
457  
    new Matches m;
458  
    
459  
    if (eq(uri, "/register"))
460  
      ret serveRegisterForm(req.params);
461  
462  
    if (eq(uri, "/becomeMaster") && req.auth != null && req.auth.user != null) {
463  
      S pw = trim(req.params.get("masterPW"));
464  
      if (eq(pw, realPW())) {
465  
        cset(req.auth.user, isMaster := true);
466  
        ret "You are master, " + req.auth.user + "!";
467  
      }
468  
      if (nempty(pw))
469  
        ret "Bad master PW";
470  
      ret hhtml(hhead(htitle("Become master")
471  
        + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter(
472  
      h3_htmlEncode(adminName + " | Become master")
473  
      + hpostform(
474  
            tag table(
475  
            tr(td_valignTop("You are:")
476  
            + td_valignTop(htmlEncode2(req.auth.user->name)))
477  
          + tr(td_valignTop("Enter the master password:")
478  
            + td_valignTop(hpassword(masterPW := pw)))
479  
          + tr(td() + td(hsubmit("Become master user"))), class := "responstable"),
480  
        action := baseLink + "/becomeMaster")
481  
      )));
482  
    }
483  
484  
    if (swic(uri, "/text/", m)) {
485  
      long id = parseLong(m.rest());
486  
      UserPost post = getConceptOpt UserPost(id);
487  
      if (post == null) ret serve404("Post with ID " + id + " not found");
488  
      if (!post.isPublic) ret serve404("Post is not public");
489  
      S ct = req.params.get("ct");
490  
      O response = nempty(ct)
491  
        ? serveWithContentType(post.text(), ct)
492  
        : serveText(post.text());
493  
      if (eq(ct, "text/javascript"))
494  
        addHeader("Service-Worker-Allowed", "/", response);
495  
      ret response;
496  
    }
497  
498  
    if (swic(uri, "/postRefs/", m)) {
499  
      long id = parseLong(m.rest());
500  
      UserPost post = getConceptOpt UserPost(id);
501  
      if (post == null) ret serve404("Post with ID " + id + " not found");
502  
      if (!post.isPublic) ret serve404("Post is not public");
503  
      ret serveJSON(lmap conceptID(post.postRefs));
504  
    }
505  
506  
    if (swic(uri, "/postRefsWithTags/", m)) {
507  
      long id = parseLong(m.rest());
508  
      UserPost post = getConceptOpt UserPost(id);
509  
      if (post == null) ret serve404("Post with ID " + id + " not found");
510  
      if (!post.isPublic) ret serve404("Post is not public");
511  
      ret serveJSON(map(post.postRefsWithTags(), p -> litorderedmap(id := conceptID(p.a), as := p.b)));
512  
    }
513  
514  
    // Serve a post
515  
    
516  
    S uri2 = dropSlashPrefix(uri);
517  
    if (isInteger(uri2)) {
518  
      long id = parseLong(uri2);
519  
      try object serveIntegerLink(req, id);
520  
    }
521  
522  
    try object serveOtherPage2(req);
523  
      
524  
    ret super.serveOtherPage(req);
525  
  }
526  
527  
  !include #1029962 // serveOtherPage2
528  
529  
  O servePost(UserPost post, Req req) {
530  
    if (!post.isPublic) ret serve404("Post is not public");
531  
    HTMLFramer1 framer = framer();
532  
    
533  
    framer.add(hjs("webSocketQuery = " + jsQuote("?objectID=" + post.id + "&modified=" + post.xmodified) + ";\n"
534  
      + [[if (ws != null) ws.url = ws.url.split("?")[0] + webSocketQuery;]]));
535  
    framer.title = "[" + post.id + "] " + or2(post.title, shorten(post.text));
536  
    framer.add(p("By " + htmlEncode2(post.author()) + ". " + renderConceptDate(post)));
537  
    if (nempty(post.type))
538  
      framer.add(p("Post type: "+ htmlEncode2(post.type)));
539  
540  
    new LS actions;
541  
542  
    // show render link
543  
    if (eqic(post.type, "HTML"))
544  
      actions.add(targetBlank("/html/" + post.id, "Show as HTML page"));
545  
546  
    if (eqic(post.type, "HTML (embedded)"))
547  
      actions.add(targetBlank("/htmlEmbedded/" + post.id, "Show as HTML page"));
548  
549  
    if (eqicOneOf(post.type, "JavaX Code (HTML Bot)", "JavaX Code (Live Home Page)"))
550  
      actions.add(targetBlank(htmlBotURL(post.id), "Show as HTML page"));
551  
      
552  
    if (eqic(post.type, "Conversation HTML"))
553  
      actions.add(targetBlank("/demo?cookie=htmlDemo&_autoOpenBot=1&chatContentsPost=" + post.id, "Show in chat box"));
554  
555  
    // show edit link
556  
    if (canEditPost(post)) {
557  
      actions.add(ahref(conceptEditLink(post), "Edit post"));
558  
      actions.add(ahref(touchLink(post), "Touch post"));
559  
      actions.add(post.hidden ? ahref(unhideLink(post), "Unhide post") : ahref(hideLink(post), "Hide post"));
560  
    }
561  
562  
    // show reply link etc.
563  
    actions.add(ahref(replyLink(post), "Reply"));
564  
    actions.add(ahref(conceptDuplicateLink(post), "Duplicate"));
565  
    actions.add(ahref(conceptEditLink(post, onlyFields := "title"), "Rename"));
566  
    if (postHasHistory(post))
567  
      actions.add(ahref("/history/" + post.id, "History"));
568  
    actions.add(ahref(baseLink + "/text/" + post.id, "Raw Text"));
569  
570  
    // show "Talk to this bot"
571  
    if (post.isJavaXCode()) {
572  
      actions.add(targetBlank(simulateBotLink(post), "Talk to this bot"));
573  
      actions.add(ahref(gazelleBotURL + "/transpilation/" + post.id, "Show Java transpilation"));
574  
    }
575  
576  
    framer.add(p_vbar(actions));
577  
      
578  
    // show post refs
579  
    L<PostReferenceable> postRefs = cloneList(post.postRefs);
580  
    if (nempty(postRefs)) {
581  
      framer.add(p("In reference to:"));
582  
      LPair<PostReferenceable, S> l = post.postRefsWithTags();
583  
      framer.add(ul(mapPairsToList(l, (ref, as) ->
584  
        htmlEncode2(as) + " " + objectToHTML(ref))));
585  
    }
586  
587  
    S text = post.text;
588  
    S html = null;
589  
    if (post.isJavaXCode()) pcall {
590  
      new JavaXHyperlinker hl;
591  
      hl.targetBlank = true;
592  
      html = hl.codeToHTML(text);
593  
    }
594  
    if (html == null)
595  
      html = htmlEncodeWithLinking(post.text);
596  
597  
    if (eq(req.params.get("showLineFeeds"), "1"))
598  
      html = html_showLineFeedsForPRE(html);
599  
600  
    // render the actual text
601  
    framer.add(div(sourceCodeToHTML_noEncode(html), style := hstyle_sourceCodeLikeInRepo()));
602  
603  
    // show referencing posts
604  
    Cl<UserPost> refs = referencingPosts(post);
605  
    refs = reversed(refs); // latest on top
606  
    Pair<L<UserPost>> p = filterAntiFilter(refs, post2 -> !post2.hidden);
607  
    if (nempty(p.a)) {
608  
      framer.add(p("Referenced by posts (latest first):"));
609  
      UserPost latestCodeSafetyPost = highestBy(p2 -> p2.modifiedOrBumped(), filter(p.a, p2 -> eqic(p2.botInfo, "Code Safety Checker")));
610  
      framer.add(ul(map(p.a, p2 -> {
611  
        S html2 = htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2);
612  
        if (p2 == latestCodeSafetyPost && cic(p2.text, "Unknown identifiers:"))
613  
          html2 += " " + hbuttonLink("/markSafe/" + p2.id, htmlEncode2("Mark safe"));
614  
        ret html2;
615  
      })));
616  
    }
617  
    if (nempty(p.b)) {
618  
      framer.add(p("Referenced by hidden posts:"));
619  
      framer.add(ul(map(p.b, p2 -> htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2))));
620  
    }
621  
622  
    if (showPostStats) {
623  
      S stats = nLines(countLines(post.text)) + ", distance to user post: " + post.distanceToUserPost();
624  
      framer.add(p_alignRight(small(span_title(stats, "Post stats"))));
625  
    }
626  
    ret framer.render();
627  
  }
628  
629  
  S getPostRefTag(UserPost a, UserPost b) {
630  
    ret get(a.postRefTags, indexOf(a.postRefs, b));
631  
  }
632  
633  
  // turn "post 123" into a link to post 123
634  
  S htmlEncodeWithLinking(S text) {
635  
    S html = htmlEncode2(text);
636  
    ret regexpReplace(html, gazelle_postMentionRegexp(),
637  
      matcher -> {
638  
        UserPost post = getConceptOpt UserPost(parseLong(matcher.group(1)));
639  
        ret post == null ? matcher.group()
640  
          : ahref(postLink(post), matcher.group());
641  
      });
642  
  }
643  
644  
  L<UserPost> referencingPosts(UserPost post) {
645  
    ret sortedByCalculatedField(p -> p.modifiedOrBumped(), instancesOf UserPost(allBackRefs(post)));
646  
  }
647  
648  
  L<UserPost> referencingPostsWithTag(UserPost post, S tag) {
649  
    ret filter(referencingPosts(post), p -> eqic_unnull(getPostRefTag(p, post), tag));
650  
  }
651  
652  
653  
  S serveAuthForm(S redirect) {
654  
    ret hAddToHead(super.serveAuthForm(redirect), hInitWebSocket());
655  
  }
656  
657  
  S authFormMoreContent() {
658  
    ret navDiv();
659  
  }
660  
661  
  S navDiv() {
662  
    ret div_vbar(navLinks(), style := "margin-bottom: 0.5em");
663  
  }
664  
  
665  
  // handle user log-in (create/update AuthedDialogID concept)
666  
  O handleAuth(Req req, S cookie) null {
667  
    S name = trim(req.params.get('user)), pw = trim(req.params.get("pw"));
668  
    if (nempty(name) && nempty(pw) && nempty(cookie)) {
669  
      Domain authDomain;
670  
      
671  
      User user = findOrCreateUserForLogin(name, pw);
672  
      if (user == null)
673  
        ret errorMsg("User " + htmlEncode2(name) + " not found");
674  
      if (!eq(getVar(user.passwordMD5), hashPW(pw)))
675  
        ret errorMsg("Bad password, please try again");
676  
677  
      // auth
678  
      cset(uniq AuthedDialogID(+cookie), restrictedToDomain := null, master := user.isMaster, +user);
679  
680  
      S redirect = req.params.get('redirect);
681  
      if (nempty(redirect))
682  
        ret hrefresh(redirect);
683  
    }
684  
  }
685  
686  
  // e.g. grab from central database
687  
  // does not have to check the PW
688  
  User findOrCreateUserForLogin(S name, S pw) {
689  
    ret conceptWhereCI User(+name);
690  
  }
691  
692  
693  
  O serve2(Req req) {
694  
    /*S userMode = req.params.get("userMode");
695  
    if (nempty(userMode) && req.auth != null)
696  
      req.auth.userMode = eq(userMode, "1");*/
697  
      
698  
    if (req.auth != null) {
699  
      cset(req.auth, userMode := false);
700  
    
701  
      if (req.auth.user! != null)
702  
        cset(req.auth.user!, lastSeen := now());
703  
    }
704  
705  
    ret super.serve2(req);
706  
  }
707  
708  
  S hashPW(S pw) {
709  
    if (nempty(salterService))
710  
      ret assertMD5(postPage(salterService, +pw));
711  
    else
712  
      ret md5(pw + passwordSalt());
713  
  }
714  
  
715  
  Req currentReq() {
716  
    ret currentReq!;
717  
  }
718  
  
719  
  bool inMasterMode aka isMasterAuthed aka masterAuthed() {
720  
    ret inMasterMode(currentReq());
721  
  }
722  
  
723  
  bool inMasterMode aka isMasterAuthed aka masterAuthed(Req req) {
724  
    ret req.masterAuthed && !(req.auth != null && req.auth.userMode);
725  
  }
726  
727  
  MapSO filtersForClass(Class c, Req req) {
728  
    if (c == UserPost) {
729  
      if (req.auth != null && !inMasterMode(req))
730  
        ret litmap(creator := req.auth.user!);
731  
    }
732  
    ret super.filtersForClass(c, req);
733  
  }
734  
  
735  
  S loggedInUserDesc_html(Req req) {
736  
    // no auth
737  
    if (req.auth == null)
738  
      ret ahref(loginLink(), "not logged in");
739  
      
740  
    // old school nameless master auth
741  
    if (req.auth.user! == null)
742  
      ret req.masterAuthed ? "master user" : "anonymous user " + req.auth.id;
743  
744  
    // actual Gazelle user
745  
    ret "user " + req.auth.user->name;
746  
  }
747  
748  
  S loginLink() { ret baseLink + "/"; }
749  
750  
  <A extends Concept> HCRUD_Concepts<A> crudData(Class<A> c, Req req) {
751  
    HCRUD_Concepts<A> cc = super.crudData(c, req);
752  
    
753  
    if (c == UserPost) {
754  
      cc.useDynamicComboBoxes = true;
755  
      cc.lsMagic = true;
756  
      cc.itemName = () -> "Post";
757  
758  
      cc.dropEmptyListValues = false;
759  
      //cc.verbose = true;
760  
      
761  
      if (eq(req.params.get("noace"), "1"))
762  
        cc.addRenderer("text", new HCRUD_Data.TextArea(80, 20));
763  
      else
764  
        cc.addRenderer("text", new HCRUD_Data.AceEditor(80, 20));
765  
766  
      cc.addRenderer("postRefTags", new HCRUD_Data.FlexibleLengthList(new HCRUD_Data.TextField(20)));
767  
      cc.addRenderer("type", new HCRUD_Data.ComboBox(true, itemPlus("", userPostTypesByPopularity())));
768  
      
769  
      cc.fieldHelp(
770  
        type := "Type of post (any format, optional)",
771  
        title := "Title for this post (any format, optional)",
772  
        text := "Text contents of this post (any format)",
773  
        nameInRepo := "Name in repository (not really used yet)",
774  
        botInfo := "Info on which bot made this post (if any)");
775  
        
776  
      cc.massageItemMapForList = (item, map) -> {
777  
        applyFunctionToValue shorten(map, "text", "title", "type", "nameInRepo");
778  
        L<PostReferenceable> refs = item/UserPost.postRefs;
779  
        map.put("postRefs", HTML(joinWithBR(lmap objectToHTML(refs))));
780  
      };
781  
        
782  
      cc.getObject = id -> {
783  
        MapSO map = cc.getObject_base(id);
784  
        L refs = cast map.get("postRefs");
785  
        L tags = cast map.get("postRefTags");
786  
        print("counts:" + l(refs) + "/" + l(tags));
787  
        map.put("postRefTags", padList(tags, l(refs), ""));
788  
        ret map;
789  
      };
790  
      
791  
      cc.emptyObject = () -> {
792  
        MapSO item = cc.emptyObject_base();
793  
        item.put(creator := req.auth.user!); // set default creator to current user
794  
        ret item;
795  
      };
796  
797  
      cc.getObjectForDuplication = id -> {
798  
        MapSO item = cc.getObjectForDuplication_base(id);
799  
        item.put(creator := req.auth.user!); // set default creator to current user
800  
801  
        // get post and refs
802  
        
803  
        UserPost post = getConcept UserPost(toLong(id));
804  
        L postRefs = cloneList((L) item.get("postRefs"));
805  
        LS tags = cloneList((L) item.get("postRefTags"));
806  
        
807  
        // drop old "created from" references
808  
        for i over tags:
809  
          if (eqic(tags.get(i), "created from")) {
810  
            remove(postRefs, i);
811  
            tags.remove(i--);
812  
          }
813  
        
814  
        // add "created from" reference
815  
        int idx = l(postRefs);
816  
        postRefs.add(post);
817  
        listSet(tags, idx, "created from");
818  
819  
        // store refs
820  
        item.put("postRefs", postRefs);
821  
        item.put("postRefTags", tags);
822  
        print(+tags);
823  
        
824  
        ret item;
825  
      };
826  
827  
      cc.fieldsToHideInCreationForm = litset("hidden", "creating");
828  
829  
      cc.lockDB = true;
830  
      cc.afterUpdate = (post, oldValues) -> {
831  
        MapSO newValues = cgetAll_cloneLists(post, keys(oldValues));
832  
        for (S field, O newVal : newValues) {
833  
          O oldVal = oldValues.get(field);
834  
          if (eq(oldVal, newVal)) {
835  
            printVars_str(+field, +oldVal);
836  
            continue;
837  
          }
838  
          IF1 f = o -> o instanceof Concept ? o/Concept.id : null;
839  
          O oldValForLog = defaultMetaTransformer().transform(f, oldVal);
840  
          O newValForLog = defaultMetaTransformer().transform(f, newVal);
841  
          Map logEntry = litorderedmap(date := now(), +field, oldVal := oldValForLog, newVal := newValForLog);
842  
          printStruct(+logEntry);
843  
          logStructure(postHistoryFile(post/UserPost), logEntry);
844  
        }
845  
      };
846  
847  
      cc.actuallyDeleteConcept = post -> deletePost(post/UserPost);
848  
    }
849  
    ret cc;
850  
  }
851  
852  
  <A extends Concept> HCRUD makeCRUD(Class<A> c, Req req) {
853  
    HCRUD crud = super.makeCRUD(c, req);
854  
    if (c == UserPost) {
855  
      crud.refreshAfterCommand = (params, msgs) -> {
856  
        UserPost post = getConcept UserPost(toLong(crud.objectIDToHighlight));
857  
        ret post != null ? hrefresh(postLink(post)) : crud.refreshAfterCommand_base(params, msgs);
858  
      };
859  
      
860  
      crud.renderCmds = map -> {
861  
        UserPost post = getConcept UserPost(toLong(crud.itemID(map)));
862  
        ret joinNemptiesWithVBar(crud.renderCmds_base(map),
863  
          ahref(postLink(post), "Show post"),
864  
          ahref(touchLink(post), "Touch", title := "Mark post as modified so it is looked at by bots again"));
865  
      };
866  
      crud.uneditableFields = litset("xmodified", "creating", "bumped");
867  
868  
      if (showSuggestorBot)
869  
        crud.moreSelectizeOptions = name -> [[, onChange: function() { console.log("selectize onChange"); sugTrigger(); }]];
870  
871  
      crud.showQuickSaveButton = true;
872  
      crud.massageFormMatrix = (map, matrix) -> {
873  
        /*if (map.containsKey(crud.idField())) // editing, not creating
874  
          matrix.add(0, ll("", hbuttonOnClick_noSubmit("Save & keep editing", [[
875  
            $.ajax({
876  
              type: 'POST',
877  
              url: $('#crudForm').attr('action'),
878  
              data: $('#crudForm').serialize(), 
879  
              success: function(response) { successNotification("Saved"); },
880  
            }).error(function() { errorNotification("Couldn't save"); });
881  
          ]])));*/
882  
            
883  
        // mergeTables businesss broken somehow
884  
        /*int idx1 = indexOfPred(matrix, row -> cic(first(row), "Post Refs"));
885  
        int idx2 = indexOfPred(matrix, row -> cic(first(row), "Post Ref Tags"));
886  
        if (idx1 < 0 || idx2 < 0) ret; // reduced form (e.g. rename)
887  
        LS row1 = matrix.get(idx1), row2 = matrix.get(idx2);
888  
        row1.set(1, hcrud_mergeTables(row1.get(1), row2.get(1), "as"));
889  
        matrix.remove(idx2);*/
890  
891  
        // TODO: handle changes in AceEditor
892  
        if (showSuggestorBot) {
893  
          LS entries = ((HCRUD_Concepts) crud.data).comboBoxItems(
894  
            conceptsWhereIC(UserPost, type := "JavaX Code (Post Edit Suggestor)"));
895  
          S js = hscript(replaceVars([[
896  
            const suggestorSelector = $("[name=suggestorID]");
897  
            var sugLoading = false, sugTriggerAgain = false;
898  
899  
            function sugTrigger() {
900  
              //console.log("sugTrigger");
901  
              if (sugLoading) { sugTriggerAgain = true; return; }
902  
903  
              const sugText = suggestorSelector.text();
904  
              //console.log("sugText=" + sugText);
905  
              const sugMatch = sugText.match(/\d+/);
906  
              if (!sugMatch) {
907  
                //$("#suggestorResult").html("");
908  
                $("#suggestorResult").hide();
909  
                return;
910  
              }
911  
              const suggestorID = sugMatch[0];
912  
913  
              // get form data as JSON
914  
              
915  
              var data = {};
916  
              $(suggestorSelector).closest("form").serializeArray().map(function(x){data[x.name] = x.value;});
917  
              const json = JSON.stringify(data);
918  
              console.log("JSON: " + json);
919  
              
920  
              const url = "]] + gazelleBotURL + [[/chatBotReply/" + suggestorID;
921  
              console.log("Loading " + url);
922  
              sugLoading = true;
923  
              $.post(url, {q : json},
924  
                function(result) {
925  
                  console.log("Suggestor result: " + result);
926  
                  const answer = !result ? "" : JSON.parse(result).answer;
927  
                  if (answer) {
928  
                    $("#suggestorResult .sectionContents").html(answer);
929  
                    $("#suggestorResult").show();
930  
                  } else
931  
                    $("#suggestorResult").hide();
932  
                  //$("#suggestorResult").html(answer ? "Suggestor says: " + answer : "");
933  
                }
934  
              ).always(function() {
935  
                console.log("sug loading done");
936  
                setTimeout(function() {
937  
                  sugLoading = false;
938  
                  if (sugTriggerAgain) { sugTriggerAgain = false; sugTrigger(); }
939  
                }, delayAfterSuggestion);
940  
              });
941  
            }
942  
943  
            $(document).ready(function() {
944  
              $("input[type=text], input[type=hidden], textarea, select").on('input propertychange change', sugTrigger);
945  
              sugTrigger();
946  
            });
947  
            suggestorSelector.change(sugTrigger);
948  
          ]], +delayAfterSuggestion));
949  
950  
          long defaultSuggestor = parseFirstLong(userDefaults(req.auth.user!).get("Default Suggestor Bot"));
951  
          S selectedSuggestor = firstWhereFirstLongIs(entries, defaultSuggestor);
952  
          
953  
          matrix.add(0, ll("Suggestor Bot", crud.addHelpText(
954  
            "Choose a bot to assist you in editing this post [feature in development]",
955  
              p_alignRight(crud.renderComboBox("suggestorID", selectedSuggestor, entries, false)))
956  
            //+ hdiv("", id := "suggestorResult")
957  
            + htitledSectionWithDiv("Suggestion", "",
958  
              id := "suggestorResult", style := "display: none",
959  
              innerDivStyle := "max-height: 150px; overflow: auto")
960  
            + js));
961  
        }
962  
      };
963  
964  
      crud.flexibleLengthListLeeway = 3;
965  
    }
966  
    
967  
    if (c == User) {
968  
      crud.allowCreate = false;
969  
      //crud.uneditableFields = litset("passwordMD5");
970  
    }
971  
972  
    ret crud;
973  
  }
974  
  
975  
  S authFormHeading() {
976  
    ret h3_htmlEncode("Welcome to the Gazelle AI System")
977  
      + p(hsnippetimg_scaleToWidth(200, #1101482, 425, 257, title := "Gazelle"));
978  
  }
979  
980  
  void makeFramer(Req req) {
981  
    super.makeFramer(req);
982  
    /*if (req.masterAuthed)
983  
      req.framer.add(p_vbar(
984  
        ahrefIf(req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 0), "Master mode", title := "View pages as master"),
985  
        ahrefIf(!req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 1), "User mode", title := "View pages as user")));*/
986  
    req.framer.renderTitle = () -> h1(ahref(baseLink + "/", himgsnippet(#1102967, style := "height: 1em; vertical-align: bottom", title := "Gazelle.rocks Logo")) 
987  
      + " " + req.framer.encodedTitle());
988  
      
989  
    req.framer.add(() -> navDiv()); // calculate on completeFrame
990  
    
991  
    if (showMetaBotOnEveryPage && !eq(req.params.get("_reloading"), "1"))
992  
      req.framer.willRender.add(r {
993  
        req.framer.add(hscript([[var botConfig = "codePost=]] + defaultBotPost + [["; ]]) + hjssrc(baseLink + "/script"));
994  
      });
995  
  }
996  
997  
  HTMLFramer1 framer(Req req default currentReq!) {
998  
    if (req.framer == null) makeFramer(req);
999  
    ret req.framer;
1000  
  }
1001  
1002  
  // link to show post
1003  
  S postLink(UserPost post) {
1004  
    ret objectLink(post);
1005  
  }
1006  
1007  
  S postLink(long postID) {
1008  
    ret objectLink(getConcept UserPost(postID));
1009  
  }
1010  
1011  
  S touchLink(UserPost post) { ret baseLink + "/touchPost/" + post.id; }
1012  
  S hideLink(UserPost post) { ret baseLink + "/hidePost/" + post.id; }
1013  
  S unhideLink(UserPost post) { ret baseLink + "/unhidePost/" + post.id; }
1014  
  
1015  
  S objectLink(Concept c) {
1016  
    ret baseLink + "/" + c.id;
1017  
  }
1018  
1019  
  S objectToHTML(Concept c) {
1020  
    ret c == null ? "-" : ahref(objectLink(c), htmlEncode2(str(c)));
1021  
  }
1022  
1023  
  S postToHTMLWithDate(UserPost post) {
1024  
    ret post == null ? "" : objectToHTML(post)
1025  
      + " " + span(htmlEncode2(
1026  
        renderHowLongAgoPlusModified(post.created, post._modified)), style := "color: #888");
1027  
  }
1028  
1029  
  bool canEditPost(UserPost post) {
1030  
    User user = currentUser();
1031  
    ret user != null && (user.isMaster || user == post.creator!);
1032  
  }
1033  
1034  
  User currentUser() {
1035  
    Req req = currentReq!;
1036  
    ret req != null && req.auth != null ? req.auth.user! : null;
1037  
  }
1038  
1039  
  S replyLink(UserPost post) {
1040  
    ret appendQueryToURL(crudLink(UserPost), cmd := "new", fList_postRefs_0 := post?.id);
1041  
  }
1042  
1043  
  S newPostLink() {
1044  
    ret replyLink(null);
1045  
  }
1046  
1047  
  LS navLinks(O... _) {
1048  
    ret gazelle_navLinks(baseLink, inlineSearch && !eq(currentReq->uri, "/search") ? "" : null,
1049  
      paramsPlus(_, withTeam := teamPostID != 0));
1050  
  }
1051  
1052  
  L<Class> botCmdClasses() {
1053  
    ret ll();
1054  
  }
1055  
  
1056  
  L<Class> crudClasses(Req req) {
1057  
    L<Class> l = super.crudClasses(req);
1058  
    l = listMinusSet(l, hiddenCrudClasses());
1059  
    l.add(UserPost);
1060  
    l.add(UploadedImage);
1061  
    if (req.masterAuthed) {
1062  
      l.add(User);
1063  
      
1064  
      // currently not putting in CRUD list
1065  
      // (but can be viewed with /crud/WebPushSubscription)
1066  
      //if (webPushEnabled) l.add(WebPushSubscription);
1067  
    }
1068  
    ret l;
1069  
  }
1070  
  
1071  
  @Override Cl<Class> cruddableClasses(Req req) {
1072  
    ret addAllAndReturnCollection(super.cruddableClasses(req), WebPushSubscription);
1073  
  }
1074  
1075  
  Set<Class> hiddenCrudClasses() {
1076  
    ret litset(Lead, ConversationFeedback, Domain, UserKeyword, UploadedSound, Settings);
1077  
  }
1078  
1079  
  O getPostFieldForBot(UserPost post, S field) {
1080  
    if (eq(field, "creatorID")) ret post.creator->id;
1081  
    if (eq(field, "creatorName")) ret post.creator->name;
1082  
    if (eq(field, "creatorIsMaster")) ret post.creator->isMaster;
1083  
    if (eq(field, "postRefs")) ret lmap conceptID(post.postRefs);
1084  
    ret cget(post, field);
1085  
  }
1086  
1087  
  void deletePost(UserPost post) {
1088  
    if (post == null) ret;
1089  
    print("Deleting post " + post);
1090  
    lock dbLock();
1091  
    long filePos = l(deletedPostsFile());
1092  
    logStructure(deletedPostsFile(),
1093  
      mapToValues(UserPost.fieldsToSaveOnDelete,
1094  
        field -> getPostFieldForBot(post, field)));
1095  
    for (Pair<PostReferenceable, S> p : post.postRefsWithTags())
1096  
      if (p.a instanceof UserPost)
1097  
        logStructure(deletedRepliesFile((UserPost) p.a), litorderedmap(tag := p.b, id := post.id, +filePos, type := post.type));
1098  
    cdelete(post);
1099  
  }
1100  
1101  
  File deletedPostsFile() {
1102  
    ret programFile("deleted-posts.log");
1103  
  }
1104  
1105  
  O serveHTMLPost(long id) {
1106  
    UserPost post = getConcept UserPost(id);
1107  
    if (post == null) ret serve404("Post " + id + " not found");
1108  
    ret post.text;
1109  
  }
1110  
1111  
  transient ReliableSingleThread_Multi<UserPost> rstDistributePostChanges = new(1000, lambda1 distributePostChanges_impl);
1112  
1113  
  void distributePostChanges(UserPost post) {
1114  
    rstDistributePostChanges.add(post);
1115  
  }
1116  
1117  
  // This is also called when replies change
1118  
  void distributePostChanges_impl(UserPost post) enter {
1119  
    //print("distributePostChanges_impl " + post);
1120  
    
1121  
    S uri = "/" + post.id;
1122  
    for (Pair<virtual WebSocket, WebSocketInfo> p : syncMapToPairs(webSockets)) {
1123  
      if (eq(p.b.uri, uri))
1124  
        reloadBody(p.a);
1125  
    }
1126  
1127  
    if (eqic(post.type, "Line Labels")) {
1128  
      UserPost parent = post.primaryRefPost();
1129  
      print("Line labels post " + post.id + ", parent=" + parent);
1130  
      updateLineLabels(parent);
1131  
    }
1132  
  }
1133  
1134  
  void updateLineLabels(UserPost post) pcall {
1135  
    if (post == null || !eqic(post.type, "Detailed Conversation Mirror")) ret;
1136  
    print("Looking at conversation mirror " + post.id + " - " + post.title);
1137  
    long convID = toLong(regexpFirstGroupIC("Conversation (\\d+) with details", post.title));
1138  
    Conversation conv = getConcept Conversation(convID);
1139  
    if (conv == null) ret;
1140  
    print("updateLineLabels " + convID + " / " + post.id);
1141  
1142  
    new MultiMap<Pair<Long, S>, S> labels;
1143  
    for (UserPost p : referencingPostsWithTag(post, ""))
1144  
      if (eqic(p.type, "Line Labels")) pcall {
1145  
        Map map = safeUnstructMapAllowingClasses(p.text(), Pair);
1146  
        print("Got labels map: " + map);
1147  
        putAll(labels, map);
1148  
      }
1149  
1150  
    bool change;
1151  
    for (Msg msg : cloneList(conv.msgs)) {
1152  
      LS lbls = uniquifyAndSortAlphaNum(allToUpper(labels.get(pair(msg.time, msg.text))));
1153  
      if (nempty(lbls))
1154  
        print("Got labels: " + lbls);
1155  
      if (!eq(msg.labels, lbls)) {
1156  
        msg.labels = lbls; set change;
1157  
      }
1158  
    }
1159  
1160  
    if (change)
1161  
      conv.incReloadCounter();
1162  
  }
1163  
1164  
  void reloadBody(virtual WebSocket ws) {
1165  
    print("Reloading body through WebSocket");
1166  
    S jsCode = [[ {
1167  
      const loc = new URL(document.location);
1168  
      const params = new URLSearchParams(loc.search);
1169  
      params.set('_reloading', '1');
1170  
      loc.search = params;
1171  
      console.log("Getting: " + loc);
1172  
      $.get(loc, function(html) {
1173  
        var bodyHtml = /<body.*?>([\s\S]*)<\/body>/.exec(html)[1];
1174  
        if (bodyHtml) {
1175  
          //$("body").html(bodyHtml);
1176  
          $('body > *:not(.chatbot)').remove();
1177  
          $("body").prepend(bodyHtml);
1178  
        }
1179  
      });
1180  
    } ]];
1181  
    dm_call(ws, "send", jsonEncode(litmap(eval := jsCode)));
1182  
  }
1183  
1184  
  transient ReliableSingleThread_Multi<S> rstDistributeDivChanges = new(1000, lambda1 distributeDivChanges_impl, func -> AutoCloseable { enter() });
1185  
1186  
  void distributeDivChanges(S contentDesc) {
1187  
    rstDistributeDivChanges.add(contentDesc);
1188  
  }
1189  
1190  
  void distributeDivChanges_impl(S contentDesc) enter {
1191  
    //print("distributeDivChanges_impl " + contentDesc);
1192  
    S content = null;
1193  
    for (Pair<virtual WebSocket, WebSocketInfo> p : syncMapToPairs(webSockets)) {
1194  
      for (S div : asForPairsWithB(p.b.liveDivs, contentDesc)) {
1195  
        if (content == null) content = calcDivContent(contentDesc);
1196  
        if (content == null) { print("No content for " + contentDesc); ret; }
1197  
        reloadDiv(p.a, div, content);
1198  
      }
1199  
    }
1200  
  }
1201  
1202  
  void reloadDiv(virtual WebSocket ws, S div, S content) {
1203  
    //print("Reloading div " + div + " through WebSocket");
1204  
    S jsCode = replaceDollarVars(
1205  
      [[ $("#" + $div).html($content);]],
1206  
      div := jsQuote(div), content := jsQuote(content));
1207  
    dm_call(ws, "send", jsonEncode(litmap(eval := jsCode)));
1208  
  }
1209  
1210  
  S calcDivContent(S contentDesc) {
1211  
    if (eq(contentDesc, "postCount"))
1212  
      ret nPosts(countConcepts(UserPost));
1213  
1214  
    if (eq(contentDesc, "webRequestsRightHemi"))
1215  
      ret n2(requestsServed);
1216  
      
1217  
    if (eq(contentDesc, "serverLoadRightHemi"))
1218  
      ret formatDoubleX(systemLoad, 1);
1219  
     
1220  
    if (eq(contentDesc, "memRightHemi"))
1221  
      ret str_toM(processSize);
1222  
1223  
    null;
1224  
  }
1225  
1226  
  int countUserPosts() { ret countConcepts(UserPost, botInfo := null) + countConcepts(UserPost, botInfo := ""); }
1227  
  int countBotPosts() { ret countConcepts(UserPost)-countUserPosts(); }
1228  
  
1229  
  Cl<UserPost> allUserPosts() { ret filter(list(UserPost), p -> !p.isBotPost()); }
1230  
1231  
  LS userPostTypesByPopularity() {
1232  
    ret listToTopTenCI(map(allUserPosts(), p -> p.type));
1233  
  }
1234  
1235  
  void startMainScript(Conversation conv) {
1236  
    UserPost post = getConceptOpt UserPost(parseLong(mapGet(conv.botConfig, "codePost")));
1237  
    if (post != null) {
1238  
      CustomBotStep step = uniq CustomBotStep(codePostID := post.id);
1239  
      if (executeStep(step, conv))
1240  
        nextStep(conv);
1241  
    } else
1242  
      super.startMainScript(conv);
1243  
  }
1244  
1245  
  S simulateBotLink(UserPost post) {
1246  
    ret appendQueryToURL(baseLink + "/demo", _botConfig := makePostData(codePost := post.id),
1247  
      _newConvCookie := 1,
1248  
      _autoOpenBot := 1);
1249  
  }  
1250  
1251  
  @Override
1252  
  bool isRequestFromBot(Req req) {
1253  
    //print("isRequestFromBot: " + req.uri);
1254  
    ret startsWith(req.uri, "/bot/");
1255  
  }
1256  
1257  
  User internalUser() {
1258  
    ret uniqCI(User, name := "internal");
1259  
  }
1260  
1261  
  transient ReliableSingleThread_Multi<Conversation> rstUpdateMirrorPosts = new(100, c -> c.updateMirrorPost());
1262  
1263  
  File postHistoryFile(UserPost post) {
1264  
    ret programFile("Post Histories/" + post.id + ".log");
1265  
  }
1266  
1267  
  bool postHasHistory(UserPost post) {
1268  
    ret fileNempty(postHistoryFile(post));
1269  
  }
1270  
  
1271  
  File deletedRepliesFile(UserPost post) {
1272  
    ret programFile("Deleted Replies/" + post.id + ".log");
1273  
  }
1274  
1275  
  <A extends Concept> S makeClassNavItem(Class<A> c, Req req) {
1276  
    S html = super.makeClassNavItem(c, req);
1277  
    if (c == UserPost) {
1278  
      int count1 = countUserPosts(), count2 = countBotPosts();
1279  
      if (count1 != 0 || count2 != 0)
1280  
        html += " " + roundBracket(n2(count1) + " from users, "
1281  
          + n2(count2) + " from bots");
1282  
    }
1283  
    ret html;
1284  
  }
1285  
1286  
  @Override
1287  
  S modifyTemplateBeforeDelivery(S html, Req req) {
1288  
    // for design testing
1289  
    S contentsPostID = req.params.get("chatContentsPost");
1290  
    if (nempty(contentsPostID)) {
1291  
      html = html.replace("#N#", "99999");
1292  
      html = html.replace("chatBot_interval = 1000", "chatBot_interval = 3600*1000");
1293  
      html = html.replace("#INCREMENTALURL#", baseLink + "/text/" + contentsPostID + "?");
1294  
    }
1295  
    ret html;
1296  
  }
1297  
1298  
  S headingForReq(Req req) {
1299  
    S codePost = decodeHQuery(req.params.get("_botConfig")).get("codePost");
1300  
    if (nempty(codePost)) {
1301  
      UserPost post = getConcept UserPost(parseLong(codePost));
1302  
      ret post?.title;
1303  
    }
1304  
    null;
1305  
  }
1306  
1307  
  UserPost homePagePost() {
1308  
    ret firstThat(p -> p.isMasterMade(), conceptsWhereIC UserPost(type := "JavaX Code (Live Home Page)"));
1309  
  }
1310  
1311  
  O serveHomePage() {
1312  
    pcall {
1313  
      UserPost post = homePagePost();
1314  
      if (post != null)
1315  
        ret loadPage(htmlBotURLOnBotServer(post.id));
1316  
    }
1317  
    null;
1318  
  }
1319  
  
1320  
  S callHtmlBot(long id, SS params) {
1321  
    ret id == 0 ? "" : doPost(htmlBotURLOnBotServer(id), params);
1322  
  }
1323  
  
1324  
  S callHtmlBot_dropMadeByComment(long id, SS params) {
1325  
    ret regexpReplace_direct(callHtmlBot(id, params), "^<!-- Made by (.*?) -->\n", "");
1326  
  }
1327  
1328  
  S htmlBotURL(long postID) {
1329  
    ret baseLink + "/htmlBot/" + postID;
1330  
  }
1331  
1332  
  S htmlBotURLOnBotServer(long postID) {
1333  
    ret gazelleBotURL + "/htmlBot/" + postID;
1334  
  }
1335  
1336  
  UserPost userDefaultsPost(User user) {
1337  
    ret conceptWhereIC UserPost(creator := user, type := "My Defaults");
1338  
  }
1339  
1340  
  SS userDefaults(User user) {
1341  
    UserPost post = userDefaultsPost(user);
1342  
    ret post == null ? emptyMap() : parseColonPropertyCIMap(post.text);
1343  
  }
1344  
  
1345  
  double favIconCacheHours() {
1346  
    ret 24;
1347  
  }
1348  
1349  
  O favIconHeaders(O response) {  
1350  
    call(response, "addHeader", "Cache-Control", "public, max-age=" + iround(hoursToSeconds(favIconCacheHours())));
1351  
    ret response;
1352  
  }
1353  
1354  
  O serveFavIcon() {
1355  
    if (favIconID != 0) {
1356  
      UploadedFile f = getConcept UploadedFile(favIconID);
1357  
      if (f != null)
1358  
        ret favIconHeaders(subBot_serveFile(f.theFile(), faviconMimeType()));
1359  
    }
1360  
  
1361  
    ret favIconHeaders(subBot_serveFile(loadLibrary(defaultFavIconSnippet), faviconMimeType()));
1362  
  }
1363  
1364  
  S cssURL() {
1365  
    ret gazelle_textURL(parseLong(cssID));
1366  
  }
1367  
1368  
  S getText(long postID) {
1369  
    UserPost post = getConcept UserPost(postID);
1370  
    ret post?.text();
1371  
  }
1372  
  
1373  
  @Override
1374  
  void addThingsAboveCRUDTable(Req req, Class c, LS aboveTable) {
1375  
    if (c == User)
1376  
      aboveTable.add(p(ahref(baseLink + "/register", "Register new user")));
1377  
  }
1378  
  
1379  
  User user(Req req) {
1380  
    if (req == null) null;
1381  
    AuthedDialogID auth = req.auth;
1382  
    ret auth?.user();
1383  
  }
1384  
1385  
  O serveIntegerLink(Req req, long id) {
1386  
    UserPost post = getConceptOpt UserPost(id);
1387  
    if (post == null) null; //ret serve404("Post with ID " + id + " not found");
1388  
    ret servePost(post, req);
1389  
  }
1390  
  
1391  
  void onNewWebSocket(WebSocketInfo ws) {}
1392  
  void onWebSocketClose(WebSocketInfo ws) {
1393  
    ws.callCloseHandlers();
1394  
  }
1395  
  
1396  
  S cookieFromWebRequest(IWebRequest req) {
1397  
    if (req == null) null;
1398  
    try answer req.cookie();
1399  
    ret afterLastEquals(req.headers().get("cookie"));
1400  
  }
1401  
  
1402  
  void makeMaster {
1403  
    var users = listMinus(list(User), internalUser());
1404  
    if (empty(users))
1405  
      ret with infoMessage("Please first register a user in the web interface!");
1406  
      
1407  
    new ShowComboBoxForm cb;
1408  
    cb.desc = "Select user to upgrade";
1409  
    cb.itemDesc = "User to make master";
1410  
    cb.items = map(users, user -> user.id + " " + user.name);
1411  
    cb.action = user -> thread {
1412  
      enter();
1413  
      long userID = parseFirstLong(user);
1414  
      User userObj = getConcept User(userID);
1415  
      printVars(+user, +userID, +userObj);
1416  
      makeMaster(userObj);
1417  
    };
1418  
    cb.show();
1419  
  }
1420  
  
1421  
  void makeMaster(User user) {
1422  
    if (user == null) ret;
1423  
    if (!swingConfirm("Turn " + user + " into a master user?")) ret;
1424  
    cset(user, isMaster := true);
1425  
    infoMessage(user + " is now my master!!");
1426  
  }
1427  
  
1428  
  O[] popDownButtonEntries() {
1429  
    ret objectArrayPlus(super.popDownButtonEntries(),
1430  
      "Make master user..." := rThreadEnter makeMaster
1431  
    );
1432  
  }
1433  
} // end of module
1434  
1435  
// Talk to a bot that is implemented in a Gazelle post
1436  
concept CustomBotStep > BotStep implements IInputHandler {
1437  
  long codePostID;
1438  
1439  
  toString { ret "CustomBotStep " + codePostID; }
1440  
1441  
  bool run(Conversation conv) {
1442  
    S answer;
1443  
    try {
1444  
      Map result = cast postJSONPage(replyURL(), initial := 1, cookie := conv.cookie);
1445  
      answer = (S) result.get("answer");
1446  
    } catch print e {
1447  
      answer = "Error: " + e.getMessage();
1448  
    }
1449  
    conv.add(new Msg(false, answer));
1450  
    cset(conv, inputHandler := this);
1451  
    false;
1452  
  }
1453  
1454  
  S replyURL() {
1455  
    ret ((DynGazelleRocks) botMod()).gazelleBotURL + "/chatBotReply/" + codePostID;
1456  
  }
1457  
1458  
  public bool handleInput(S s, Conversation conv) {
1459  
    S answer;
1460  
    try {
1461  
      Map result = cast postJSONPage(replyURL(), q := s, cookie := conv.cookie);
1462  
      answer = (S) result.get("answer");
1463  
    } catch print e {
1464  
      answer = "Error: " + e.getMessage();
1465  
    }
1466  
    conv.add(new Msg(false, answer));
1467  
    true; // acknowledge that we handled the input
1468  
  }
1469  
} // end of CustomBotStep
1470  
1471  
sS userName(User user) {
1472  
  ret user != null ? user.name : "?";
1473  
}
1474  
1475  
concept WebPushSubscription {
1476  
  S clientIP;
1477  
  MapSO data;
1478  
}

Author comment

Began life as a copy of #1029913

download  show line numbers  debug dex  old transpilations   

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

No comments. add comment

Snippet ID: #1030607
Snippet name: DynGazelleRocks
Eternal ID of this version: #1030607/103
Text MD5: a7c07e154b92c64bc47693a87c3a5d3a
Transpilation MD5: 6a806ec87525860fc18369d73478f31c
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2021-10-11 04:35:05
Source code size: 50750 bytes / 1478 lines
Pitched / IR pitched: No / No
Views / Downloads: 607 / 1179
Version history: 102 change(s)
Referenced in: [show references]