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).

do not include class Msg.

// create/read password salt for program, or use legacy global salt
sS passwordSalt() {
  File f = programSecretFile("password-salt");
  if (fileExists(f)) ret loadTextFile(f);
  File global = javaxSecretDir("password-salt");
  if (fileExists(global)) ret loadTextFile(global);
  ret loadTextFileOrCreateWithRandomID(f);
}

concept PostReferenceable {}

// Note: If you change/add fields here, make sure to edit
// GazelleBEA.findOrCreateUserForLogin
concept User > PostReferenceable {
  S name;
  SecretValue<S> passwordMD5;
  S contact; // e.g. mail address
  bool isMaster;
  SecretValue<S> botToken = aSecretGlobalID/*UnlessLoading*/();
  long lastSeen;

  S name() { ret name; }
  toString { ret nempty(name) ? "User " + name : super.toString(); }
}

extend AuthedDialogID {
  new Ref<User> user; // who is logged in
  bool userMode; // show things as if user was not master
  
  User user() { ret user!; }
}

extend Conversation {
  new Ref<UserPost> mirrorPost;
  new Ref<UserPost> detailedMirrorPost;

  void change {
    super.change();
    ((DynGazelleRocks) botMod()).rstUpdateMirrorPosts.add(this);
  }

  void updateMirrorPost {
    if (isDeleted()) ret;

    // Simple mirror post with just texts and "User:" or "Bot:"
    if (!mirrorPost.has() && syncNempty(msgs))
      cset(Conversation.this, mirrorPost := cnew UserPost(
        title := "Conversation " + id,
        type := "Conversation Mirror",
        creator := ((DynGazelleRocks) botMod()).internalUser(), botInfo := "Conversation Mirror Bot"));
    cset(mirrorPost!, text := lines_rtrim(msgs));

    // Detailed mirror post with a lot of info
    if (!detailedMirrorPost.has() && syncNempty(msgs))
      cset(Conversation.this, detailedMirrorPost := cnew UserPost(
        title := "Conversation " + id + " with details",
        type := "Detailed Conversation Mirror",
        creator := ((DynGazelleRocks) botMod()).internalUser(), botInfo := "Conversation Mirror Bot"));
    cset(detailedMirrorPost!, text := indentedStructure(msgs));
  }

  void delete {
    cdelete(mirrorPost!);
    super.delete();
  }
}

concept UserCreatedObject > PostReferenceable {
  new Ref<User> creator;
  S botInfo; // info on how this was made. set to anything non-empty if made by bot
  //S nameInRepo; // unique by user, for URL
}

concept UserPost > UserCreatedObject {
  S type, title, text;
  bool isPublic = true, hidden;
  bool creating; // new post, still editing
  long xmodified; // modification timestamp that also includes replies
  long bumped;    // modification timestamp that includes "bumps" (re-posted identical bot reply)

  static LS fieldsToSaveOnDelete = ll("id", "text", "title", "type", "creatorID", "postRefs", "postRefTags", "hidden", "isPublic", "created", "_modified", "xmodified", "botInfo", "creating");

  // TODO: migrate to this eventually (but need to change CRUD too)
  //int flags;
  //static int F_PUBLIC = 1, F_HIDDEN = 2;

  S text() { ret text; }

  new RefL<PostReferenceable> postRefs;
  new LS postRefTags;

  sS _fieldOrder = "title text type postRefs isPublic";

  toString {
    S content = nempty(title) ? shorten(title) : escapeNewLines(shorten(text));
    //S _type = isReply() ? "reply" : "post";
    //if (hidden) _type += " (hidden)";
    //ret firstToUpper(_type) + " by " + author() + ": " + content;
    ret content + " [by " + author() + "]";
  }

  bool isReply() { ret nempty(postRefs); }
  bool isBotPost() { ret nempty(botInfo); }
  bool isMasterMade() { ret creator.has() && creator->isMaster; }

  S author() {
    S author = userName(creator!);
    if (isBotPost()) author += "'s bot " + quote(botInfo);
    ret author;
  }

  void change {
    super.change();
    ((DynGazelleRocks) botMod()).distributePostChanges(this);
  }

  LPair<PostReferenceable, S> postRefsWithTags() {
    ret zipTwoListsToPairs_lengthOfFirst(postRefs, postRefTags);
  }

  L<PostReferenceable> postRefsWithTag(S tag) {
    ret pairsAWhereB(postRefsWithTags(), t -> eqic_unnull(t, tag));
  }

  UserPost primaryRefPost() {
    ret optCast UserPost(first(postRefsWithTag("")));
  }

  void _setModified(long modified) {
    super._setModified(modified);
    setXModified(modified);
  }

  void bump {
    bumped = now();
    _change_withoutUpdatingModifiedField();
    for (UserPost p : syncInstancesOf UserPost(postRefs)) p.setXModified();
  }

  long modifiedOrBumped() { ret max(_modified, bumped); }

  // Notify of changes in replies
  void setXModified(long modified default now()) {
    bool changed = modified > xmodified;
    xmodified = modified;
    _change_withoutUpdatingModifiedField();
    if (changed)
      ((DynGazelleRocks) botMod()).distributePostChanges(this);
  }

  void _backRefsModified {
    super._backRefsModified();
    setXModified(now());
  }

  // -1 if no user post found in hierarchy
  int distanceToUserPost() {
    int n = 0;
    UserPost post = this;
    while (post != null && n < 100) {
      if (!post.isBotPost()) ret n;
      post = post.primaryRefPost();
    }
    ret -1;
  }

  bool isJavaXCode() {
    ret eqicOrSwicPlusSpace(type, "JavaX Code");
  }
} // end of UserPost

/*concept BotHandle > UserCreatedObject {
  S globalID = aGlobalIDUnlessLoading();
  S comment;
  SecretValue<S> token;
}*/

set flag NoNanoHTTPD.

asclass DynGazelleRocks > DynNewBot2 {
  transient bool inlineSearch; // show search form in nav links
  switchable bool webPushEnabled = true;
  switchable bool showPostStats;
  switchable bool showSuggestorBot = true;
  switchable bool showMetaBotOnEveryPage = true;
  switchable int delayAfterSuggestion = 1000;
  switchable int defaultBotPost = 238410;
  switchable long teamPostID;
  switchable long favIconID; // NB: it's an UploadedFile id in this DB
  switchable S defaultFavIconSnippet = gazelleFavIconSnippet();
  
  switchable S gazelleBotURL = null; //"https://gazellebot.botcompany.de";
  switchable S salterService;

  transient double systemLoad;
  transient long processSize;

  void init {
    dm_require("#1017856/SystemLoad");
    dm_vmBus_onMessage systemLoad(voidfunc(double load) {
      if (setField(systemLoad := load))
        distributeDivChanges("serverLoadRightHemi");
    });
    dm_vmBus_onMessage processSize(voidfunc(long processSize) {
      if (setField(+processSize))
        distributeDivChanges("memRightHemi");
    });

    botName = heading = adminName = "gazelle.rocks";
    templateID = #1030086;
    cssID = /*#1030233*/"237267"; // now a post ID
    set enableUsers;
    set useWebSockets;
    set showRegisterLink;
    unset showTalkToBotLink;
    set alwaysRedirectToHttps;
    set redirectOnLogout;
    set showFullErrors;
    set inlineSearch;
    unset lockWhileDoingBotActions; // fix the _gazelle_text bug?
    unset showMailSenderInfo;
    if (empty(salterService))
      passwordSalt(); // make early
  }

  void makeIndices :: after {    
    indexConceptFieldCI(User, "name");
    indexConceptFieldDesc(UserPost, "xmodified");
    indexConceptFieldDesc(UserPost, "_modified");
    indexConceptFieldCI(UserPost, "botInfo");
  }
  
  void start {
    init();
    super.start();
    db_mainConcepts().modifyOnCreate = true;
    
    printConceptIndices();

    onConceptsChange(new Runnable {
      int postCount = countConcepts(UserPost);
      
      run {
        temp enter();
        int count = countConcepts(UserPost);
        if (count != postCount) {
          postCount = count;
          distributeDivChanges("postCount");
        }
      }
    });

    internalUser(); 
    
    // legacy clean-up
    /*for (UserPost post) {
      for i to 5:
        cset(post, "postRefTags_" + i, null);
    }*/
  }

  // web socket stuff

  void requestServed {
    distributeDivChanges("webRequestsRightHemi");
  }

  transient class WebSocketInfo extends Meta is AutoCloseable {
    S uri;
    SS params;
    Req req;
    WeakReference<virtual WebSocket> webSocket;
    Set<IVF1<S>> messageHandlers = syncLinkedHashSet();
    Set<IVF1<byte[]>> binMessageHandlers = syncLinkedHashSet();
    Set<Runnable> closeHandlers = syncLinkedHashSet();
    
    // Any helper can store things here like jqueryLoaded := true
    Map misc = syncMap();

    *(virtual WebSocket webSocket) {
      this.webSocket = weakRef(webSocket);
      setFieldToIVF1Proxy(webSocket, onMessage := msg -> {
        temp enter(); pcall {
          O opCode = call(msg, "getOpCode");
          byte opCodeValue = cast call(opCode, "getValue");
          bool isBinary = opCodeValue == 2; // isso!
          
          if (isBinary) {
            byte[] data = cast rcall getBinaryPayload(msg);
            pcallFAll(binMessageHandlers, data);
          } else {
            S data = rcall_string getTextPayload(msg);
            pcallFAll(messageHandlers, data);
          }
        }
      });
    }
    
    S subURI aka subUri() { ret req.subURI(); }
    S uri() { ret uri; }
    SS params() { ret params; }
    S get(S param) { ret mapGet(params, param); }
    
    void setParams(SS params) {
      this.params = params;
      req.params = params;
    }

    O dbRepresentation;
    new Set<Pair<S, O>> liveDivs; // id/content info
    
    void eval(S jsCode, O... _ default null) {
      jsCode = jsDollarVars(jsCode, _);
      if (empty(jsCode)) ret;
      dm_call(webSocket!, "send", jsonEncode(litmap(eval := jsCode)));
    }
    
    void send(S jsonData) {
      if (empty(jsonData)) ret;
      dm_call(webSocket!, "send", jsonData);
    }
    
    AutoCloseable onStringMessage(IVF1<S> onMsg) {
      ret tempAdd(messageHandlers, onMsg);
    }
    
    AutoCloseable onBinaryMessage(IVF1<byte[]> onMsg) {
      ret tempAdd(binMessageHandlers, onMsg);
    }
    
    AutoCloseable onClose(Runnable r) {
      ret tempAdd(closeHandlers, r);
    }
    
    void callCloseHandlers {
      pcallFAll(closeHandlers);
    }
    
    public void close {
      cleanUp(webSocket!);
    }
  }
  
  transient Map<virtual WebSocket, WebSocketInfo> webSockets = syncWeakHashMap();

  void cleanMeUp_webSockets {
    closeAllKeysAndClear((Map) webSockets);
  }

  // funny: useWebSockets exists in DynNewBot2,
  // but only in this subclass do we define handleWebSocket
  void handleWebSocket(virtual WebSocket ws) {
    set(ws, onClose := r {
      var info = webSockets.get(ws);
      if (info != null) pcall { onWebSocketClose(info); }
      webSockets.remove(ws);
    });
    
    set(ws, onOpen := rEnter {  
      S uri = cast rcall getUri(ws);
      SS params = cast rcall getParms(ws);
      print("WebSocket opened! uri: " + uri + ", params: " + params);
      WebSocketInfo info = new(ws);
      IWebRequest webRequest = proxy IWebRequest(call(ws, "webRequest"));
      info.req = webRequestToReq(webRequest);
      S cookie = cookieFromWebRequest(webRequest);
      AuthedDialogID auth = authObject(cookie);
      fillReqAuthFromCookie(info.req, cookie, auth);
      temp tempSetTL(currentReq, info.req); // why not
      
      info.uri = uri;
      info.params = params;
      webSockets.put(ws, info);
      pcall { onNewWebSocket(info); }
      long objectID = toLong(params.get("objectID"));
      long modified = toLong(params.get("modified"));
      if (objectID != 0) {
        UserPost c = getConceptOpt UserPost(objectID);
        print("Modification: " + c.xmodified + " / " + c._modified + " / " + modified);
        if (c != null && c.xmodified > modified)
          reloadBody(ws);
      }
    });

    setFieldToIVF1Proxy(ws, onMessage := msg -> { temp enter(); pcall {
      WebSocketInfo info = webSockets.get(ws);
      if (info == null) ret;
      S data = rcall_string getTextPayload(msg);
      Map map = jsonDecodeMap(data);
      O div = map.get("liveDiv");
      if (div cast S) {
        S contentDesc = div;
        syncAdd(info.liveDivs, pair(div, (O) contentDesc));
        reloadDiv(ws, div, calcDivContent(contentDesc));
      }
    }});
  }

  S serveRegisterForm(SS params) {
    S user = trim(params.get("user"));
    S pw = trim(params.get("f_pw"));
    S pw2 = trim(params.get("pw2"));
    S redirect = params.get("redirect");
    S contact = trim(params.get("contact"));
    
    redirect = dropParamFromURL(redirect, "logout"); // don't log us out right again
    if (empty(redirect)) redirect = baseLink + "/";

    new LS msgs;
    if (nempty(user) || nempty(pw)) {
      lock dbLock();
      if (empty(user)) msgs.add("Please enter a user name");
        else if (l(user) < 4) msgs.add("Minimum user name length: 4 characters");
        else if (l(user) > 30) msgs.add("Maximum user name length: 30 characters");
        else if (hasConceptIC User(name := user))
          msgs.add("This user exists, please choose a different name");
      if (regexpContainsIC("[^a-z0-9\\-\\.]", user))
        msgs.add("Bad characters in user name (please use only a-z, 0-9, - and .)");
      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.");
        else if (l(pw) < 6) msgs.add("Minimum password length: 6 characters");
      if (neq(pw, pw2)) msgs.add("Passwords don't match");

      if (empty(msgs)) {
        User userObj = cnew User(name := user, passwordMD5 := SecretValue(hashPW(pw)), +contact);
        vmBus_send userCreated(userObj);
        ret hrefresh(5.0, redirect) + "User " + user + " created! Redirecting...";
      }
    }
    
    ret hhtml(hhead(htitle("Register new user")
      + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter(
      h3_htmlEncode(adminName + " | Register new user")
      + hpostform(
          hhidden(+redirect)
          + tag table(
            (empty(msgs) ? "" : tr(td() + td() + td(htmlEncode2_nlToBr(lines_rtrim(msgs)))))
          + tr(td_valignTop("Choose a user name:")
            + td_valignTop(hinputfield(+user) + "<br>" + "(Minimum length 4. Characters allowed: a-z, 0-9, - and .)"))
          + tr(td_valignTop("Choose a password:")
            + td_valignTop(hpassword(f_pw := pw) + "<br>" + "(Minimum length 6 characters)"))
          + tr(td_valignTop("Repeat the password please:") + td(hpassword(+pw2)))
          + tr(td_valignTop("Way to contact you (e.g. e-mail) - optional:") + td(hinputfield(+contact)))
          + tr(td() + td(hsubmit("Register"))), class := "responstable"),
        action := baseLink + "/register")
      )));
  }

  bool calcMasterAuthed(Req req) {
    ret super.calcMasterAuthed(req)
      || req.auth != null && req.auth.user.has() && req.auth.user->isMaster;
  }
  
  O serveOtherPage(Req req) {
    S uri = req.uri;
    new Matches m;
    
    if (eq(uri, "/register"))
      ret serveRegisterForm(req.params);

    if (eq(uri, "/becomeMaster") && req.auth != null && req.auth.user != null) {
      S pw = trim(req.params.get("masterPW"));
      if (eq(pw, realPW())) {
        cset(req.auth.user, isMaster := true);
        ret "You are master, " + req.auth.user + "!";
      }
      if (nempty(pw))
        ret "Bad master PW";
      ret hhtml(hhead(htitle("Become master")
        + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter(
      h3_htmlEncode(adminName + " | Become master")
      + hpostform(
            tag table(
            tr(td_valignTop("You are:")
            + td_valignTop(htmlEncode2(req.auth.user->name)))
          + tr(td_valignTop("Enter the master password:")
            + td_valignTop(hpassword(masterPW := pw)))
          + tr(td() + td(hsubmit("Become master user"))), class := "responstable"),
        action := baseLink + "/becomeMaster")
      )));
    }

    if (swic(uri, "/text/", m)) {
      long id = parseLong(m.rest());
      UserPost post = getConceptOpt UserPost(id);
      if (post == null) ret serve404("Post with ID " + id + " not found");
      if (!post.isPublic) ret serve404("Post is not public");
      S ct = req.params.get("ct");
      O response = nempty(ct)
        ? serveWithContentType(post.text(), ct)
        : serveText(post.text());
      if (eq(ct, "text/javascript"))
        addHeader("Service-Worker-Allowed", "/", response);
      ret response;
    }

    if (swic(uri, "/postRefs/", m)) {
      long id = parseLong(m.rest());
      UserPost post = getConceptOpt UserPost(id);
      if (post == null) ret serve404("Post with ID " + id + " not found");
      if (!post.isPublic) ret serve404("Post is not public");
      ret serveJSON(lmap conceptID(post.postRefs));
    }

    if (swic(uri, "/postRefsWithTags/", m)) {
      long id = parseLong(m.rest());
      UserPost post = getConceptOpt UserPost(id);
      if (post == null) ret serve404("Post with ID " + id + " not found");
      if (!post.isPublic) ret serve404("Post is not public");
      ret serveJSON(map(post.postRefsWithTags(), p -> litorderedmap(id := conceptID(p.a), as := p.b)));
    }

    // Serve a post
    
    S uri2 = dropSlashPrefix(uri);
    if (isInteger(uri2)) {
      long id = parseLong(uri2);
      try object serveIntegerLink(req, id);
    }

    try object serveOtherPage2(req);
      
    ret super.serveOtherPage(req);
  }

  !include #1029962 // serveOtherPage2

  O servePost(UserPost post, Req req) {
    if (!post.isPublic) ret serve404("Post is not public");
    HTMLFramer1 framer = framer();
    
    framer.add(hjs("webSocketQuery = " + jsQuote("?objectID=" + post.id + "&modified=" + post.xmodified) + ";\n"
      + [[if (ws != null) ws.url = ws.url.split("?")[0] + webSocketQuery;]]));
    framer.title = "[" + post.id + "] " + or2(post.title, shorten(post.text));
    framer.add(p("By " + htmlEncode2(post.author()) + ". " + renderConceptDate(post)));
    if (nempty(post.type))
      framer.add(p("Post type: "+ htmlEncode2(post.type)));

    new LS actions;

    // show render link
    if (eqic(post.type, "HTML"))
      actions.add(targetBlank("/html/" + post.id, "Show as HTML page"));

    if (eqic(post.type, "HTML (embedded)"))
      actions.add(targetBlank("/htmlEmbedded/" + post.id, "Show as HTML page"));

    if (eqicOneOf(post.type, "JavaX Code (HTML Bot)", "JavaX Code (Live Home Page)"))
      actions.add(targetBlank(htmlBotURL(post.id), "Show as HTML page"));
      
    if (eqic(post.type, "Conversation HTML"))
      actions.add(targetBlank("/demo?cookie=htmlDemo&_autoOpenBot=1&chatContentsPost=" + post.id, "Show in chat box"));

    // show edit link
    if (canEditPost(post)) {
      actions.add(ahref(conceptEditLink(post), "Edit post"));
      actions.add(ahref(touchLink(post), "Touch post"));
      actions.add(post.hidden ? ahref(unhideLink(post), "Unhide post") : ahref(hideLink(post), "Hide post"));
    }

    // show reply link etc.
    actions.add(ahref(replyLink(post), "Reply"));
    actions.add(ahref(conceptDuplicateLink(post), "Duplicate"));
    actions.add(ahref(conceptEditLink(post, onlyFields := "title"), "Rename"));
    if (postHasHistory(post))
      actions.add(ahref("/history/" + post.id, "History"));
    actions.add(ahref(baseLink + "/text/" + post.id, "Raw Text"));

    // show "Talk to this bot"
    if (post.isJavaXCode()) {
      actions.add(targetBlank(simulateBotLink(post), "Talk to this bot"));
      actions.add(ahref(gazelleBotURL + "/transpilation/" + post.id, "Show Java transpilation"));
    }

    framer.add(p_vbar(actions));
      
    // show post refs
    L<PostReferenceable> postRefs = cloneList(post.postRefs);
    if (nempty(postRefs)) {
      framer.add(p("In reference to:"));
      LPair<PostReferenceable, S> l = post.postRefsWithTags();
      framer.add(ul(mapPairsToList(l, (ref, as) ->
        htmlEncode2(as) + " " + objectToHTML(ref))));
    }

    S text = post.text;
    S html = null;
    if (post.isJavaXCode()) pcall {
      new JavaXHyperlinker hl;
      hl.targetBlank = true;
      html = hl.codeToHTML(text);
    }
    if (html == null)
      html = htmlEncodeWithLinking(post.text);

    if (eq(req.params.get("showLineFeeds"), "1"))
      html = html_showLineFeedsForPRE(html);

    // render the actual text
    framer.add(div(sourceCodeToHTML_noEncode(html), style := hstyle_sourceCodeLikeInRepo()));

    // show referencing posts
    Cl<UserPost> refs = referencingPosts(post);
    refs = reversed(refs); // latest on top
    Pair<L<UserPost>> p = filterAntiFilter(refs, post2 -> !post2.hidden);
    if (nempty(p.a)) {
      framer.add(p("Referenced by posts (latest first):"));
      UserPost latestCodeSafetyPost = highestBy(p2 -> p2.modifiedOrBumped(), filter(p.a, p2 -> eqic(p2.botInfo, "Code Safety Checker")));
      framer.add(ul(map(p.a, p2 -> {
        S html2 = htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2);
        if (p2 == latestCodeSafetyPost && cic(p2.text, "Unknown identifiers:"))
          html2 += " " + hbuttonLink("/markSafe/" + p2.id, htmlEncode2("Mark safe"));
        ret html2;
      })));
    }
    if (nempty(p.b)) {
      framer.add(p("Referenced by hidden posts:"));
      framer.add(ul(map(p.b, p2 -> htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2))));
    }

    if (showPostStats) {
      S stats = nLines(countLines(post.text)) + ", distance to user post: " + post.distanceToUserPost();
      framer.add(p_alignRight(small(span_title(stats, "Post stats"))));
    }
    ret framer.render();
  }

  S getPostRefTag(UserPost a, UserPost b) {
    ret get(a.postRefTags, indexOf(a.postRefs, b));
  }

  // turn "post 123" into a link to post 123
  S htmlEncodeWithLinking(S text) {
    S html = htmlEncode2(text);
    ret regexpReplace(html, gazelle_postMentionRegexp(),
      matcher -> {
        UserPost post = getConceptOpt UserPost(parseLong(matcher.group(1)));
        ret post == null ? matcher.group()
          : ahref(postLink(post), matcher.group());
      });
  }

  L<UserPost> referencingPosts(UserPost post) {
    ret sortedByCalculatedField(p -> p.modifiedOrBumped(), instancesOf UserPost(allBackRefs(post)));
  }

  L<UserPost> referencingPostsWithTag(UserPost post, S tag) {
    ret filter(referencingPosts(post), p -> eqic_unnull(getPostRefTag(p, post), tag));
  }


  S serveAuthForm(S redirect) {
    ret hAddToHead(super.serveAuthForm(redirect), hInitWebSocket());
  }

  S authFormMoreContent() {
    ret navDiv();
  }

  S navDiv() {
    ret div_vbar(navLinks(), style := "margin-bottom: 0.5em");
  }
  
  // handle user log-in (create/update AuthedDialogID concept)
  O handleAuth(Req req, S cookie) null {
    S name = trim(req.params.get('user)), pw = trim(req.params.get("pw"));
    if (nempty(name) && nempty(pw) && nempty(cookie)) {
      Domain authDomain;
      
      User user = findOrCreateUserForLogin(name, pw);
      if (user == null)
        ret errorMsg("User " + htmlEncode2(name) + " not found");
      if (!eq(getVar(user.passwordMD5), hashPW(pw)))
        ret errorMsg("Bad password, please try again");

      // auth
      cset(uniq AuthedDialogID(+cookie), restrictedToDomain := null, master := user.isMaster, +user);

      S redirect = req.params.get('redirect);
      if (nempty(redirect))
        ret hrefresh(redirect);
    }
  }

  // e.g. grab from central database
  // does not have to check the PW
  User findOrCreateUserForLogin(S name, S pw) {
    ret conceptWhereCI User(+name);
  }


  O serve2(Req req) {
    /*S userMode = req.params.get("userMode");
    if (nempty(userMode) && req.auth != null)
      req.auth.userMode = eq(userMode, "1");*/
      
    if (req.auth != null) {
      cset(req.auth, userMode := false);
    
      if (req.auth.user! != null)
        cset(req.auth.user!, lastSeen := now());
    }

    ret super.serve2(req);
  }

  S hashPW(S pw) {
    if (nempty(salterService))
      ret assertMD5(postPage(salterService, +pw));
    else
      ret md5(pw + passwordSalt());
  }
  
  Req currentReq() {
    ret currentReq!;
  }
  
  bool inMasterMode aka isMasterAuthed aka masterAuthed() {
    ret inMasterMode(currentReq());
  }
  
  bool inMasterMode aka isMasterAuthed aka masterAuthed(Req req) {
    ret req.masterAuthed && !(req.auth != null && req.auth.userMode);
  }

  MapSO filtersForClass(Class c, Req req) {
    if (c == UserPost) {
      if (req.auth != null && !inMasterMode(req))
        ret litmap(creator := req.auth.user!);
    }
    ret super.filtersForClass(c, req);
  }
  
  S loggedInUserDesc_html(Req req) {
    // no auth
    if (req.auth == null)
      ret ahref(loginLink(), "not logged in");
      
    // old school nameless master auth
    if (req.auth.user! == null)
      ret req.masterAuthed ? "master user" : "anonymous user " + req.auth.id;

    // actual Gazelle user
    ret "user " + req.auth.user->name;
  }

  S loginLink() { ret baseLink + "/"; }

  <A extends Concept> HCRUD_Concepts<A> crudData(Class<A> c, Req req) {
    HCRUD_Concepts<A> cc = super.crudData(c, req);
    
    if (c == UserPost) {
      cc.useDynamicComboBoxes = true;
      cc.lsMagic = true;
      cc.itemName = () -> "Post";

      cc.dropEmptyListValues = false;
      //cc.verbose = true;
      
      if (eq(req.params.get("noace"), "1"))
        cc.addRenderer("text", new HCRUD_Data.TextArea(80, 20));
      else
        cc.addRenderer("text", new HCRUD_Data.AceEditor(80, 20));

      cc.addRenderer("postRefTags", new HCRUD_Data.FlexibleLengthList(new HCRUD_Data.TextField(20)));
      cc.addRenderer("type", new HCRUD_Data.ComboBox(true, itemPlus("", userPostTypesByPopularity())));
      
      cc.fieldHelp(
        type := "Type of post (any format, optional)",
        title := "Title for this post (any format, optional)",
        text := "Text contents of this post (any format)",
        nameInRepo := "Name in repository (not really used yet)",
        botInfo := "Info on which bot made this post (if any)");
        
      cc.massageItemMapForList = (item, map) -> {
        applyFunctionToValue shorten(map, "text", "title", "type", "nameInRepo");
        L<PostReferenceable> refs = item/UserPost.postRefs;
        map.put("postRefs", HTML(joinWithBR(lmap objectToHTML(refs))));
      };
        
      cc.getObject = id -> {
        MapSO map = cc.getObject_base(id);
        L refs = cast map.get("postRefs");
        L tags = cast map.get("postRefTags");
        print("counts:" + l(refs) + "/" + l(tags));
        map.put("postRefTags", padList(tags, l(refs), ""));
        ret map;
      };
      
      cc.emptyObject = () -> {
        MapSO item = cc.emptyObject_base();
        item.put(creator := req.auth.user!); // set default creator to current user
        ret item;
      };

      cc.getObjectForDuplication = id -> {
        MapSO item = cc.getObjectForDuplication_base(id);
        item.put(creator := req.auth.user!); // set default creator to current user

        // get post and refs
        
        UserPost post = getConcept UserPost(toLong(id));
        L postRefs = cloneList((L) item.get("postRefs"));
        LS tags = cloneList((L) item.get("postRefTags"));
        
        // drop old "created from" references
        for i over tags:
          if (eqic(tags.get(i), "created from")) {
            remove(postRefs, i);
            tags.remove(i--);
          }
        
        // add "created from" reference
        int idx = l(postRefs);
        postRefs.add(post);
        listSet(tags, idx, "created from");

        // store refs
        item.put("postRefs", postRefs);
        item.put("postRefTags", tags);
        print(+tags);
        
        ret item;
      };

      cc.fieldsToHideInCreationForm = litset("hidden", "creating");

      cc.lockDB = true;
      cc.afterUpdate = (post, oldValues) -> {
        MapSO newValues = cgetAll_cloneLists(post, keys(oldValues));
        for (S field, O newVal : newValues) {
          O oldVal = oldValues.get(field);
          if (eq(oldVal, newVal)) {
            printVars_str(+field, +oldVal);
            continue;
          }
          IF1 f = o -> o instanceof Concept ? o/Concept.id : null;
          O oldValForLog = defaultMetaTransformer().transform(f, oldVal);
          O newValForLog = defaultMetaTransformer().transform(f, newVal);
          Map logEntry = litorderedmap(date := now(), +field, oldVal := oldValForLog, newVal := newValForLog);
          printStruct(+logEntry);
          logStructure(postHistoryFile(post/UserPost), logEntry);
        }
      };

      cc.actuallyDeleteConcept = post -> deletePost(post/UserPost);
    }
    ret cc;
  }

  <A extends Concept> HCRUD makeCRUD(Class<A> c, Req req) {
    HCRUD crud = super.makeCRUD(c, req);
    if (c == UserPost) {
      crud.refreshAfterCommand = (params, msgs) -> {
        UserPost post = getConcept UserPost(toLong(crud.objectIDToHighlight));
        ret post != null ? hrefresh(postLink(post)) : crud.refreshAfterCommand_base(params, msgs);
      };
      
      crud.renderCmds = map -> {
        UserPost post = getConcept UserPost(toLong(crud.itemID(map)));
        ret joinNemptiesWithVBar(crud.renderCmds_base(map),
          ahref(postLink(post), "Show post"),
          ahref(touchLink(post), "Touch", title := "Mark post as modified so it is looked at by bots again"));
      };
      crud.uneditableFields = litset("xmodified", "creating", "bumped");

      if (showSuggestorBot)
        crud.moreSelectizeOptions = name -> [[, onChange: function() { console.log("selectize onChange"); sugTrigger(); }]];

      crud.showQuickSaveButton = true;
      crud.massageFormMatrix = (map, matrix) -> {
        /*if (map.containsKey(crud.idField())) // editing, not creating
          matrix.add(0, ll("", hbuttonOnClick_noSubmit("Save & keep editing", [[
            $.ajax({
              type: 'POST',
              url: $('#crudForm').attr('action'),
              data: $('#crudForm').serialize(), 
              success: function(response) { successNotification("Saved"); },
            }).error(function() { errorNotification("Couldn't save"); });
          ]])));*/
            
        // mergeTables businesss broken somehow
        /*int idx1 = indexOfPred(matrix, row -> cic(first(row), "Post Refs"));
        int idx2 = indexOfPred(matrix, row -> cic(first(row), "Post Ref Tags"));
        if (idx1 < 0 || idx2 < 0) ret; // reduced form (e.g. rename)
        LS row1 = matrix.get(idx1), row2 = matrix.get(idx2);
        row1.set(1, hcrud_mergeTables(row1.get(1), row2.get(1), "as"));
        matrix.remove(idx2);*/

        // TODO: handle changes in AceEditor
        if (showSuggestorBot) {
          LS entries = ((HCRUD_Concepts) crud.data).comboBoxItems(
            conceptsWhereIC(UserPost, type := "JavaX Code (Post Edit Suggestor)"));
          S js = hscript(replaceVars([[
            const suggestorSelector = $("[name=suggestorID]");
            var sugLoading = false, sugTriggerAgain = false;

            function sugTrigger() {
              //console.log("sugTrigger");
              if (sugLoading) { sugTriggerAgain = true; return; }

              const sugText = suggestorSelector.text();
              //console.log("sugText=" + sugText);
              const sugMatch = sugText.match(/\d+/);
              if (!sugMatch) {
                //$("#suggestorResult").html("");
                $("#suggestorResult").hide();
                return;
              }
              const suggestorID = sugMatch[0];

              // get form data as JSON
              
              var data = {};
              $(suggestorSelector).closest("form").serializeArray().map(function(x){data[x.name] = x.value;});
              const json = JSON.stringify(data);
              console.log("JSON: " + json);
              
              const url = "]] + gazelleBotURL + [[/chatBotReply/" + suggestorID;
              console.log("Loading " + url);
              sugLoading = true;
              $.post(url, {q : json},
                function(result) {
                  console.log("Suggestor result: " + result);
                  const answer = !result ? "" : JSON.parse(result).answer;
                  if (answer) {
                    $("#suggestorResult .sectionContents").html(answer);
                    $("#suggestorResult").show();
                  } else
                    $("#suggestorResult").hide();
                  //$("#suggestorResult").html(answer ? "Suggestor says: " + answer : "");
                }
              ).always(function() {
                console.log("sug loading done");
                setTimeout(function() {
                  sugLoading = false;
                  if (sugTriggerAgain) { sugTriggerAgain = false; sugTrigger(); }
                }, delayAfterSuggestion);
              });
            }

            $(document).ready(function() {
              $("input[type=text], input[type=hidden], textarea, select").on('input propertychange change', sugTrigger);
              sugTrigger();
            });
            suggestorSelector.change(sugTrigger);
          ]], +delayAfterSuggestion));

          long defaultSuggestor = parseFirstLong(userDefaults(req.auth.user!).get("Default Suggestor Bot"));
          S selectedSuggestor = firstWhereFirstLongIs(entries, defaultSuggestor);
          
          matrix.add(0, ll("Suggestor Bot", crud.addHelpText(
            "Choose a bot to assist you in editing this post [feature in development]",
              p_alignRight(crud.renderComboBox("suggestorID", selectedSuggestor, entries, false)))
            //+ hdiv("", id := "suggestorResult")
            + htitledSectionWithDiv("Suggestion", "",
              id := "suggestorResult", style := "display: none",
              innerDivStyle := "max-height: 150px; overflow: auto")
            + js));
        }
      };

      crud.flexibleLengthListLeeway = 3;
    }
    
    if (c == User) {
      crud.allowCreate = false;
      //crud.uneditableFields = litset("passwordMD5");
    }

    ret crud;
  }
  
  S authFormHeading() {
    ret h3_htmlEncode("Welcome to the Gazelle AI System")
      + p(hsnippetimg_scaleToWidth(200, #1101482, 425, 257, title := "Gazelle"));
  }

  void makeFramer(Req req) {
    super.makeFramer(req);
    /*if (req.masterAuthed)
      req.framer.add(p_vbar(
        ahrefIf(req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 0), "Master mode", title := "View pages as master"),
        ahrefIf(!req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 1), "User mode", title := "View pages as user")));*/
    req.framer.renderTitle = () -> h1(ahref(baseLink + "/", himgsnippet(#1102967, style := "height: 1em; vertical-align: bottom", title := "Gazelle.rocks Logo")) 
      + " " + req.framer.encodedTitle());
      
    req.framer.add(() -> navDiv()); // calculate on completeFrame
    
    if (showMetaBotOnEveryPage && !eq(req.params.get("_reloading"), "1"))
      req.framer.willRender.add(r {
        req.framer.add(hscript([[var botConfig = "codePost=]] + defaultBotPost + [["; ]]) + hjssrc(baseLink + "/script"));
      });
  }

  HTMLFramer1 framer(Req req default currentReq!) {
    if (req.framer == null) makeFramer(req);
    ret req.framer;
  }

  // link to show post
  S postLink(UserPost post) {
    ret objectLink(post);
  }

  S postLink(long postID) {
    ret objectLink(getConcept UserPost(postID));
  }

  S touchLink(UserPost post) { ret baseLink + "/touchPost/" + post.id; }
  S hideLink(UserPost post) { ret baseLink + "/hidePost/" + post.id; }
  S unhideLink(UserPost post) { ret baseLink + "/unhidePost/" + post.id; }
  
  S objectLink(Concept c) {
    ret baseLink + "/" + c.id;
  }

  S objectToHTML(Concept c) {
    ret c == null ? "-" : ahref(objectLink(c), htmlEncode2(str(c)));
  }

  S postToHTMLWithDate(UserPost post) {
    ret post == null ? "" : objectToHTML(post)
      + " " + span(htmlEncode2(
        renderHowLongAgoPlusModified(post.created, post._modified)), style := "color: #888");
  }

  bool canEditPost(UserPost post) {
    User user = currentUser();
    ret user != null && (user.isMaster || user == post.creator!);
  }

  User currentUser() {
    Req req = currentReq!;
    ret req != null && req.auth != null ? req.auth.user! : null;
  }

  S replyLink(UserPost post) {
    ret appendQueryToURL(crudLink(UserPost), cmd := "new", fList_postRefs_0 := post?.id);
  }

  S newPostLink() {
    ret replyLink(null);
  }

  LS navLinks(O... _) {
    ret gazelle_navLinks(baseLink, inlineSearch && !eq(currentReq->uri, "/search") ? "" : null,
      paramsPlus(_, withTeam := teamPostID != 0));
  }

  L<Class> botCmdClasses() {
    ret ll();
  }
  
  L<Class> crudClasses(Req req) {
    L<Class> l = super.crudClasses(req);
    l = listMinusSet(l, hiddenCrudClasses());
    l.add(UserPost);
    l.add(UploadedImage);
    if (req.masterAuthed) {
      l.add(User);
      
      // currently not putting in CRUD list
      // (but can be viewed with /crud/WebPushSubscription)
      //if (webPushEnabled) l.add(WebPushSubscription);
    }
    ret l;
  }
  
  @Override Cl<Class> cruddableClasses(Req req) {
    ret addAllAndReturnCollection(super.cruddableClasses(req), WebPushSubscription);
  }

  Set<Class> hiddenCrudClasses() {
    ret litset(Lead, ConversationFeedback, Domain, UserKeyword, UploadedSound, Settings);
  }

  O getPostFieldForBot(UserPost post, S field) {
    if (eq(field, "creatorID")) ret post.creator->id;
    if (eq(field, "creatorName")) ret post.creator->name;
    if (eq(field, "creatorIsMaster")) ret post.creator->isMaster;
    if (eq(field, "postRefs")) ret lmap conceptID(post.postRefs);
    ret cget(post, field);
  }

  void deletePost(UserPost post) {
    if (post == null) ret;
    print("Deleting post " + post);
    lock dbLock();
    long filePos = l(deletedPostsFile());
    logStructure(deletedPostsFile(),
      mapToValues(UserPost.fieldsToSaveOnDelete,
        field -> getPostFieldForBot(post, field)));
    for (Pair<PostReferenceable, S> p : post.postRefsWithTags())
      if (p.a instanceof UserPost)
        logStructure(deletedRepliesFile((UserPost) p.a), litorderedmap(tag := p.b, id := post.id, +filePos, type := post.type));
    cdelete(post);
  }

  File deletedPostsFile() {
    ret programFile("deleted-posts.log");
  }

  O serveHTMLPost(long id) {
    UserPost post = getConcept UserPost(id);
    if (post == null) ret serve404("Post " + id + " not found");
    ret post.text;
  }

  transient ReliableSingleThread_Multi<UserPost> rstDistributePostChanges = new(1000, lambda1 distributePostChanges_impl);

  void distributePostChanges(UserPost post) {
    rstDistributePostChanges.add(post);
  }

  // This is also called when replies change
  void distributePostChanges_impl(UserPost post) enter {
    //print("distributePostChanges_impl " + post);
    
    S uri = "/" + post.id;
    for (Pair<virtual WebSocket, WebSocketInfo> p : syncMapToPairs(webSockets)) {
      if (eq(p.b.uri, uri))
        reloadBody(p.a);
    }

    if (eqic(post.type, "Line Labels")) {
      UserPost parent = post.primaryRefPost();
      print("Line labels post " + post.id + ", parent=" + parent);
      updateLineLabels(parent);
    }
  }

  void updateLineLabels(UserPost post) pcall {
    if (post == null || !eqic(post.type, "Detailed Conversation Mirror")) ret;
    print("Looking at conversation mirror " + post.id + " - " + post.title);
    long convID = toLong(regexpFirstGroupIC("Conversation (\\d+) with details", post.title));
    Conversation conv = getConcept Conversation(convID);
    if (conv == null) ret;
    print("updateLineLabels " + convID + " / " + post.id);

    new MultiMap<Pair<Long, S>, S> labels;
    for (UserPost p : referencingPostsWithTag(post, ""))
      if (eqic(p.type, "Line Labels")) pcall {
        Map map = safeUnstructMapAllowingClasses(p.text(), Pair);
        print("Got labels map: " + map);
        putAll(labels, map);
      }

    bool change;
    for (Msg msg : cloneList(conv.msgs)) {
      LS lbls = uniquifyAndSortAlphaNum(allToUpper(labels.get(pair(msg.time, msg.text))));
      if (nempty(lbls))
        print("Got labels: " + lbls);
      if (!eq(msg.labels, lbls)) {
        msg.labels = lbls; set change;
      }
    }

    if (change)
      conv.incReloadCounter();
  }

  void reloadBody(virtual WebSocket ws) {
    print("Reloading body through WebSocket");
    S jsCode = [[ {
      const loc = new URL(document.location);
      const params = new URLSearchParams(loc.search);
      params.set('_reloading', '1');
      loc.search = params;
      console.log("Getting: " + loc);
      $.get(loc, function(html) {
        var bodyHtml = /<body.*?>([\s\S]*)<\/body>/.exec(html)[1];
        if (bodyHtml) {
          //$("body").html(bodyHtml);
          $('body > *:not(.chatbot)').remove();
          $("body").prepend(bodyHtml);
        }
      });
    } ]];
    dm_call(ws, "send", jsonEncode(litmap(eval := jsCode)));
  }

  transient ReliableSingleThread_Multi<S> rstDistributeDivChanges = new(1000, lambda1 distributeDivChanges_impl, func -> AutoCloseable { enter() });

  void distributeDivChanges(S contentDesc) {
    rstDistributeDivChanges.add(contentDesc);
  }

  void distributeDivChanges_impl(S contentDesc) enter {
    //print("distributeDivChanges_impl " + contentDesc);
    S content = null;
    for (Pair<virtual WebSocket, WebSocketInfo> p : syncMapToPairs(webSockets)) {
      for (S div : asForPairsWithB(p.b.liveDivs, contentDesc)) {
        if (content == null) content = calcDivContent(contentDesc);
        if (content == null) { print("No content for " + contentDesc); ret; }
        reloadDiv(p.a, div, content);
      }
    }
  }

  void reloadDiv(virtual WebSocket ws, S div, S content) {
    //print("Reloading div " + div + " through WebSocket");
    S jsCode = replaceDollarVars(
      [[ $("#" + $div).html($content);]],
      div := jsQuote(div), content := jsQuote(content));
    dm_call(ws, "send", jsonEncode(litmap(eval := jsCode)));
  }

  S calcDivContent(S contentDesc) {
    if (eq(contentDesc, "postCount"))
      ret nPosts(countConcepts(UserPost));

    if (eq(contentDesc, "webRequestsRightHemi"))
      ret n2(requestsServed);
      
    if (eq(contentDesc, "serverLoadRightHemi"))
      ret formatDoubleX(systemLoad, 1);
     
    if (eq(contentDesc, "memRightHemi"))
      ret str_toM(processSize);

    null;
  }

  int countUserPosts() { ret countConcepts(UserPost, botInfo := null) + countConcepts(UserPost, botInfo := ""); }
  int countBotPosts() { ret countConcepts(UserPost)-countUserPosts(); }
  
  Cl<UserPost> allUserPosts() { ret filter(list(UserPost), p -> !p.isBotPost()); }

  LS userPostTypesByPopularity() {
    ret listToTopTenCI(map(allUserPosts(), p -> p.type));
  }

  void startMainScript(Conversation conv) {
    UserPost post = getConceptOpt UserPost(parseLong(mapGet(conv.botConfig, "codePost")));
    if (post != null) {
      CustomBotStep step = uniq CustomBotStep(codePostID := post.id);
      if (executeStep(step, conv))
        nextStep(conv);
    } else
      super.startMainScript(conv);
  }

  S simulateBotLink(UserPost post) {
    ret appendQueryToURL(baseLink + "/demo", _botConfig := makePostData(codePost := post.id),
      _newConvCookie := 1,
      _autoOpenBot := 1);
  }  

  @Override
  bool isRequestFromBot(Req req) {
    //print("isRequestFromBot: " + req.uri);
    ret startsWith(req.uri, "/bot/");
  }

  User internalUser() {
    ret uniqCI(User, name := "internal");
  }

  transient ReliableSingleThread_Multi<Conversation> rstUpdateMirrorPosts = new(100, c -> c.updateMirrorPost());

  File postHistoryFile(UserPost post) {
    ret programFile("Post Histories/" + post.id + ".log");
  }

  bool postHasHistory(UserPost post) {
    ret fileNempty(postHistoryFile(post));
  }
  
  File deletedRepliesFile(UserPost post) {
    ret programFile("Deleted Replies/" + post.id + ".log");
  }

  <A extends Concept> S makeClassNavItem(Class<A> c, Req req) {
    S html = super.makeClassNavItem(c, req);
    if (c == UserPost) {
      int count1 = countUserPosts(), count2 = countBotPosts();
      if (count1 != 0 || count2 != 0)
        html += " " + roundBracket(n2(count1) + " from users, "
          + n2(count2) + " from bots");
    }
    ret html;
  }

  @Override
  S modifyTemplateBeforeDelivery(S html, Req req) {
    // for design testing
    S contentsPostID = req.params.get("chatContentsPost");
    if (nempty(contentsPostID)) {
      html = html.replace("#N#", "99999");
      html = html.replace("chatBot_interval = 1000", "chatBot_interval = 3600*1000");
      html = html.replace("#INCREMENTALURL#", baseLink + "/text/" + contentsPostID + "?");
    }
    ret html;
  }

  S headingForReq(Req req) {
    S codePost = decodeHQuery(req.params.get("_botConfig")).get("codePost");
    if (nempty(codePost)) {
      UserPost post = getConcept UserPost(parseLong(codePost));
      ret post?.title;
    }
    null;
  }

  UserPost homePagePost() {
    ret firstThat(p -> p.isMasterMade(), conceptsWhereIC UserPost(type := "JavaX Code (Live Home Page)"));
  }

  O serveHomePage() {
    pcall {
      UserPost post = homePagePost();
      if (post != null)
        ret loadPage(htmlBotURLOnBotServer(post.id));
    }
    null;
  }
  
  S callHtmlBot(long id, SS params) {
    ret id == 0 ? "" : doPost(htmlBotURLOnBotServer(id), params);
  }
  
  S callHtmlBot_dropMadeByComment(long id, SS params) {
    ret regexpReplace_direct(callHtmlBot(id, params), "^<!-- Made by (.*?) -->\n", "");
  }

  S htmlBotURL(long postID) {
    ret baseLink + "/htmlBot/" + postID;
  }

  S htmlBotURLOnBotServer(long postID) {
    ret gazelleBotURL + "/htmlBot/" + postID;
  }

  UserPost userDefaultsPost(User user) {
    ret conceptWhereIC UserPost(creator := user, type := "My Defaults");
  }

  SS userDefaults(User user) {
    UserPost post = userDefaultsPost(user);
    ret post == null ? emptyMap() : parseColonPropertyCIMap(post.text);
  }
  
  double favIconCacheHours() {
    ret 24;
  }

  O favIconHeaders(O response) {  
    call(response, "addHeader", "Cache-Control", "public, max-age=" + iround(hoursToSeconds(favIconCacheHours())));
    ret response;
  }

  O serveFavIcon() {
    if (favIconID != 0) {
      UploadedFile f = getConcept UploadedFile(favIconID);
      if (f != null)
        ret favIconHeaders(subBot_serveFile(f.theFile(), faviconMimeType()));
    }
  
    ret favIconHeaders(subBot_serveFile(loadLibrary(defaultFavIconSnippet), faviconMimeType()));
  }

  S cssURL() {
    ret gazelle_textURL(parseLong(cssID));
  }

  S getText(long postID) {
    UserPost post = getConcept UserPost(postID);
    ret post?.text();
  }
  
  @Override
  void addThingsAboveCRUDTable(Req req, Class c, LS aboveTable) {
    if (c == User)
      aboveTable.add(p(ahref(baseLink + "/register", "Register new user")));
  }
  
  User user(Req req) {
    if (req == null) null;
    AuthedDialogID auth = req.auth;
    ret auth?.user();
  }

  O serveIntegerLink(Req req, long id) {
    UserPost post = getConceptOpt UserPost(id);
    if (post == null) null; //ret serve404("Post with ID " + id + " not found");
    ret servePost(post, req);
  }
  
  void onNewWebSocket(WebSocketInfo ws) {}
  void onWebSocketClose(WebSocketInfo ws) {
    ws.callCloseHandlers();
  }
  
  S cookieFromWebRequest(IWebRequest req) {
    if (req == null) null;
    try answer req.cookie();
    ret afterLastEquals(req.headers().get("cookie"));
  }
  
  void makeMaster {
    var users = listMinus(list(User), internalUser());
    if (empty(users))
      ret with infoMessage("Please first register a user in the web interface!");
      
    new ShowComboBoxForm cb;
    cb.desc = "Select user to upgrade";
    cb.itemDesc = "User to make master";
    cb.items = map(users, user -> user.id + " " + user.name);
    cb.action = user -> thread {
      enter();
      long userID = parseFirstLong(user);
      User userObj = getConcept User(userID);
      printVars(+user, +userID, +userObj);
      makeMaster(userObj);
    };
    cb.show();
  }
  
  void makeMaster(User user) {
    if (user == null) ret;
    if (!swingConfirm("Turn " + user + " into a master user?")) ret;
    cset(user, isMaster := true);
    infoMessage(user + " is now my master!!");
  }
  
  O[] popDownButtonEntries() {
    ret objectArrayPlus(super.popDownButtonEntries(),
      "Make master user..." := rThreadEnter makeMaster
    );
  }
} // end of module

// Talk to a bot that is implemented in a Gazelle post
concept CustomBotStep > BotStep implements IInputHandler {
  long codePostID;

  toString { ret "CustomBotStep " + codePostID; }

  bool run(Conversation conv) {
    S answer;
    try {
      Map result = cast postJSONPage(replyURL(), initial := 1, cookie := conv.cookie);
      answer = (S) result.get("answer");
    } catch print e {
      answer = "Error: " + e.getMessage();
    }
    conv.add(new Msg(false, answer));
    cset(conv, inputHandler := this);
    false;
  }

  S replyURL() {
    ret ((DynGazelleRocks) botMod()).gazelleBotURL + "/chatBotReply/" + codePostID;
  }

  public bool handleInput(S s, Conversation conv) {
    S answer;
    try {
      Map result = cast postJSONPage(replyURL(), q := s, cookie := conv.cookie);
      answer = (S) result.get("answer");
    } catch print e {
      answer = "Error: " + e.getMessage();
    }
    conv.add(new Msg(false, answer));
    true; // acknowledge that we handled the input
  }
} // end of CustomBotStep

sS userName(User user) {
  ret user != null ? user.name : "?";
}

concept WebPushSubscription {
  S clientIP;
  MapSO data;
}

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: 606 / 1177
Version history: 102 change(s)
Referenced in: #1034167 - Standard Classes + Interfaces (LIVE, continuation of #1003674)