!7 do not include class Msg. sS passwordSalt() { ret loadTextFileOrCreateWithRandomID(javaxSecretDir("password-salt")); } concept PostReferenceable {} concept User > PostReferenceable { S name; SecretValue passwordMD5; S contact; // e.g. mail address bool isMaster; SecretValue botToken = aSecretGlobalID/*UnlessLoading*/(); toString { ret nempty(name) ? "User " + name : super.toString(); } } extend AuthedDialogID { new Ref user; // who is logged in bool userMode; // show things as if user was not master } extend Conversation { new Ref mirrorPost; void change { super.change(); ((GazelleExamples) botMod()).rstUpdateMirrorPosts.add(this); } void updateMirrorPost { if (isDeleted()) ret; if (!mirrorPost.has()) cset(Conversation.this, mirrorPost := cnew UserPost( title := "Conversation " + id, type := "Conversation Mirror", creator := ((GazelleExamples) botMod()).internalUser(), botInfo := "Conversation Mirror Bot")); cset(mirrorPost!, text := lines_rtrim(msgs)); } void delete { cdelete(mirrorPost!); super.delete(); } } concept UserCreatedObject > PostReferenceable { new Ref 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) // 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 postRefs; new LS postRefTags; sS _fieldOrder = "isPublic title text type postRefs"; toString { S content = nempty(title) ? shorten(title) : shorten(text); S _type = isReply() ? "reply" : "post"; if (hidden) _type += " (hidden)"; ret firstToUpper(_type) + " by " + author() + ": " + content; } bool isReply() { ret nempty(postRefs); } bool isBotPost() { ret nempty(botInfo); } S author() { S author = userName(creator!); if (isBotPost()) author += "'s bot " + quote(botInfo); ret author; } void change { super.change(); ((GazelleExamples) botMod()).distributePostChanges(this); } LPair postRefsWithTags() { ret zipTwoListsToPairs_lengthOfFirst(postRefs, postRefTags); } L postRefsWithTag(S tag) { ret pairsAWhereB(postRefsWithTags(), t -> eqic_unnull(t, tag)); } UserPost primaryRefPost() { ret optCast UserPost(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); } void setXModified(long modified default now()) { bool changed = modified > xmodified; xmodified = modified; _change_withoutUpdatingModifiedField(); if (changed) ((GazelleExamples) 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 token; }*/ set flag NoNanoHTTPD. cmodule2 GazelleExamples > DynNewBot2 { transient bool inlineSearch; // show search form in nav links switchable bool showPostStats; switchable bool showSuggestorBot = true; S gazelleBotURL = "https://gazellebot.botcompany.de"; void start { botName = heading = adminName = "gazelle.rocks"; templateID = #1030086; cssID = #1029713; set enableUsers; set useWebSockets; set showRegisterLink; unset showTalkToBotLink; set alwaysRedirectToHttps; set redirectOnLogout; set showFullErrors; set inlineSearch; unset lockWhileDoingBotActions; // fix the _gazelle_text bug? passwordSalt(); // make early super.start(); db_mainConcepts().modifyOnCreate = true; indexConceptFieldCI(User, "name"); indexConceptFieldDesc(UserPost, "xmodified"); indexConceptFieldCI(UserPost, "botInfo"); internalUser(); // legacy clean-up /*for (UserPost post) { for i to 5: cset(post, "postRefTags_" + i, null); }*/ } // web socket stuff class WebSocketInfo { S uri; SS params; } transient Map webSockets = syncWeakHashMap(); void cleanMeUp_webSockets { closeAllKeysAndClear((Map) webSockets); } void handleWebSocket(virtual WebSocket ws) { set(ws, onClose := r { 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); new WebSocketInfo info; info.uri = uri; info.params = params; webSockets.put(ws, 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); } }); } 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"); 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)) { cnew User(name := user, passwordMD5 := SecretValue(hashPW(pw)), +contact); 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) + "
" + "(Minimum length 4. Characters allowed: a-z, 0-9, - and .)")) + tr(td_valignTop("Choose a password:") + td_valignTop(hpassword(f_pw := pw) + "
" + "(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(hpassword(+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"); ret serveText(post.text()); } 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); UserPost post = getConceptOpt UserPost(id); if (post == null) ret serve404("Post with ID " + id + " not found"); ret servePost(post); } try object serveOtherPage2(req); ret super.serveOtherPage(req); } !include #1029962 // serveOtherPage2 O servePost(UserPost post) { 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(ahref("/html/" + post.id, "Show as HTML page")); // 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 actions.add(ahref(replyLink(post), "Reply")); actions.add(ahref(conceptDuplicateLink(post), "Duplicate")); // 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 postRefs = cloneList(post.postRefs); if (nempty(postRefs)) { framer.add(p("In reference to:")); LPair 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 { html = new JavaXHyperlinker().codeToHTML(text); } if (html == null) html = htmlEncodeWithLinking(post.text); framer.add(div(sourceCodeToHTML_noEncode(html), style := hstyle_sourceCodeLikeInRepo())); // show referencing posts Cl refs = referencingPosts(post); refs = reversed(refs); // latest on top Pair> 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 referencingPosts(UserPost post) { ret sortedByCalculatedField(p -> p.modifiedOrBumped(), instancesOf UserPost(allBackRefs(post))); } 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 = conceptWhereCI User(+name); 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); } } 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); ret super.serve2(req); } S hashPW(S pw) { ret md5(pw + passwordSalt()); } bool inMasterMode(Req req) { ret req.masterAuthed && !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(Req req) { ret req.auth == null ? "not logged in" : req.auth.user! == null ? (req.masterAuthed ? "master user" : "weird user") : "user " + req.auth.user->name; } HCRUD_Concepts crudData(Class c, Req req) { HCRUD_Concepts cc = super.crudData(c, req); if (c == UserPost) { cc.itemName = () -> "Post"; cc.dropEmptyListValues = false; //cc.verbose = true; cc.addRenderer("text", new HCRUD_Data.TextArea(80, 10)); cc.addRenderer("postRefTags", new HCRUD_Data.FlexibleLengthList(new HCRUD_Data.TextField(20))); cc.addRenderer("type", new HCRUD_Data.ComboBox(true, 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 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"); } ret cc; } HCRUD makeCRUD(Class c, Req req) { HCRUD crud = super.makeCRUD(c, req); crud.buttonsOnTop = true; 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"); crud.massageFormMatrix = (map, matrix) -> { 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) print("Can't massage matrix"); LS row1 = matrix.get(idx1), row2 = matrix.get(idx2); row1.set(1, hcrud_mergeTables(row1.get(1), row2.get(1), "as")); matrix.remove(idx2); if (showSuggestorBot) { LS entries = ((HCRUD_Concepts) crud.data).comboBoxItems( conceptsWhereIC(UserPost, type := "JavaX Code (Post Edit Suggestor)")); S js = hscript([[ const suggestorSelector = $("[name=suggestorID]"); var sugLoading = false, sugTriggerAgain = false; function sugTrigger() { if (sugLoading) { sugTriggerAgain = true; return; } const sugMatch = suggestorSelector.text().match(/\d+/); if (!sugMatch) return; const suggestorID = sugMatch[0]; const data = "bla"; const url = "]] + gazelleBotURL + [[/chatBotReply/" + suggestorID; console.log("Loading " + url); sugLoading = true; $.post(url, {q : data}, function(result) { console.log("Suggestor result: " + result); } ).always(function() { sugLoading = false; if (sugTriggerAgain) { sugTriggerAgain = false; sugTrigger(); } }); } $(document).ready(function() { $("input[type=text], textarea").change(sugTrigger); sugTrigger(); }); suggestorSelector.change(sugTrigger); ]]); 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", "", entries, false))) + 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(himgsnippet(#1102967, style := "height: 1em; vertical-align: bottom", title := "Gazelle.rocks Logo") + " " + htmlEncode2(req.framer.title)); req.framer.add(navDiv()); } HTMLFramer1 framer() { Req req = 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() { ret llNonNulls( //currentUser() != null ? ahref(crudLink(UserPost), "Admin") : null, ahref(newPostLink(), "New Post"), inlineSearch && !eq(currentReq->uri, "/search") ? hform(hform(hinputfield(+q, style := "width: 75px") + " " + hsubmit("Search")), style := "display: inline", action := "/search") : ahref("/search", "Search"), ahref("/rootPosts", "Root Posts"), ahref("/allPosts", "All Posts"), ahref("/latestPosts", "Latest Posts"), ahref("/latestModifiedPosts", "Latest Changes"), ahref("/mainPosts", "Main Posts"), ahref("/team", "Team")); } L botCmdClasses() { ret ll(); } L crudClasses(Req req) { L l = super.crudClasses(req); l = listMinusSet(l, litset(Lead, ConversationFeedback, Domain, UserKeyword, UploadedSound, Settings)); l.add(UserPost); l.add(UploadedImage); if (req.masterAuthed) l.add(User); ret l; } 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); logStructure(programFile("deleted-posts.log"), mapToValues(ll("id", "text", "title", "type", "creatorID", "postRefs", "postRefTags", "hidden", "isPublic", "created", "_modified", "xmodified", "botInfo", "creating"), field -> getPostFieldForBot(post, field))); cdelete(post); } O serveHTMLPost(long id) { UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post " + id + " not found"); ret post.text; } transient ReliableSingleThread_Multi rstDistributePostChanges = new(1000, lambda1 distributePostChanges_impl); void distributePostChanges(UserPost post) { rstDistributePostChanges.add(post); } void distributePostChanges_impl(UserPost post) enter { print("distributePostChanges_impl " + post); S uri = "/" + post.id; for (Pair p : syncMapToPairs(webSockets)) { if (eq(p.b.uri, uri)) reloadBody(p.a); } } void reloadBody(virtual WebSocket ws) { print("Reloading body through WebSocket"); S jsCode = [[ $.get(document.location, function(html) { var bodyHtml = /([\s\S]*)<\/body>/.exec(html)[1]; if (bodyHtml) $("body").html(bodyHtml); }); ]]; dm_call(ws, "send", jsonEncode(litmap(eval := jsCode))); } int countUserPosts() { ret countConcepts(UserPost, botInfo := null) + countConcepts(UserPost, botInfo := ""); } int countBotPosts() { ret countConcepts(UserPost)-countUserPosts(); } Cl 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), cookie := "test_" + aRandomID(), _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 rstUpdateMirrorPosts = new(100, c -> c.updateMirrorPost()); } // end of module concept CustomBotStep > BotStep implements IInputHandler { long codePostID; toString { ret "CustomBotStep " + codePostID; } bool run(Conversation conv) { conv.add(new Msg(false, "Bot " + codePostID + " ready")); cset(conv, inputHandler := this); false; } public bool handleInput(S s, Conversation conv) { Map result = cast postJSONPage(((GazelleExamples) botMod()).gazelleBotURL + "/chatBotReply/" + codePostID, q := s, cookie := conv.cookie); S answer = cast result.get("answer"); conv.add(new Msg(false, answer)); true; // acknowledge that we handled the input } } sS userName(User user) { ret user != null ? user.name : "?"; }