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 passwordMD5; S contact; // e.g. mail address bool isMaster; SecretValue botToken = aSecretGlobalID/*UnlessLoading*/(); long lastSeen; S name() { ret name; } 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 User user() { ret user!; } } extend Conversation { new Ref mirrorPost; new Ref 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 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 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 postRefsWithTags() { ret zipTwoListsToPairs_lengthOfFirst(postRefs, postRefTags); } L 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 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 webSocket; Set> messageHandlers = syncLinkedHashSet(); Set> binMessageHandlers = syncLinkedHashSet(); Set 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> 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 onMsg) { ret tempAdd(messageHandlers, onMsg); } AutoCloseable onBinaryMessage(IVF1 onMsg) { ret tempAdd(binMessageHandlers, onMsg); } AutoCloseable onClose(Runnable r) { ret tempAdd(closeHandlers, r); } void callCloseHandlers { pcallFAll(closeHandlers); } public void close { cleanUp(webSocket!); } } transient Map 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) + "
" + "(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(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 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 { 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 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))); } L 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 + "/"; } HCRUD_Concepts crudData(Class c, Req req) { HCRUD_Concepts 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 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; } HCRUD makeCRUD(Class 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 botCmdClasses() { ret ll(); } L crudClasses(Req req) { L 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 cruddableClasses(Req req) { ret addAllAndReturnCollection(super.cruddableClasses(req), WebPushSubscription); } Set 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 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 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 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, 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 = /([\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 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 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 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 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"); } S makeClassNavItem(Class 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), "^\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; }