switchable double bot_maxPollSeconds = 600; switchable int bot_pollInterval = 500; Class defaultCRUDClass() { ret Sequence; } O serveOtherPage2(Req req) null { new Matches m; S uri = dropTrailingSlashIfNemptyAfterwards(req.uri); if (eq(uri, "/admin")) ret hrefresh(baseLink + "/crud/" + shortClassName(defaultCRUDClass())); if (eq(uri, "/latestPost")) { UserPost post = highestConceptByField UserPost("created"); if (post == null) ret serve404("No posts in database"); ret servePost(post, req); } if (eq(uri, "/latestModifiedPost")) { UserPost post = highestConceptByField UserPost("_modified"); if (post == null) ret serve404("No posts in database"); ret servePost(post, req); } if (eq(uri, "/rootPosts")) { Cl posts = sortedByConceptIDDesc(filter(list(UserPost), post -> empty(post.postRefs))); framer().navBeforeTitle = true; framer().title = "Root Posts"; framer().add(ul(lmap postToHTMLWithDate(posts))); ret framer().render(); } if (eq(uri, "/allPosts")) { Cl posts = sortedByConceptIDDesc(list(UserPost)); framer().navBeforeTitle = true; framer().title = "All Posts"; framer().add(ul(lmap postToHTMLWithDate(posts))); ret framer().render(); } if (eq(uri, "/latestPosts")) { Cl posts = takeFirst(50, sortedByConceptIDDesc(list(UserPost))); framer().navBeforeTitle = true; framer().title = "Latest Posts & Replies"; framer().add(ul(lmap postToHTMLWithDate(posts))); ret framer().render(); } if (eq(uri, "/latestModifiedPosts")) { Cl posts = takeFirst(50, sortedByFieldDesc _modified(list(UserPost))); framer().navBeforeTitle = true; framer().title = "Latest Modified Posts & Replies"; framer().add(ul(lmap postToHTMLWithDate(posts))); ret framer().render(); } if (eq(uri, "/mainPosts")) { Cl posts = takeFirst(50, sortedByConceptID(conceptsWhereCI UserPost(type := "Main"))); framer().navBeforeTitle = true; framer().title = "Main Posts"; framer().add(ul(lmap postToHTMLWithDate(posts))); ret framer().render(); } if (swic(uri, "/html/", m) && isInteger(m.rest())) { long id = parseLong(m.rest()); ret serveHTMLPost(id); } if (swic(uri, "/htmlEmbedded/", m) && isInteger(m.rest())) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); HTMLFramer1 framer = framer(); framer.title = "[" + post.id + "] " + or2(post.title, shorten(post.text)); framer.add(hcomment("Post begins here") + "\n" + post.text + "\n" + hcomment("Post ends here")); ret framer.render(); } if (swic(uri, "/css/", m) && isInteger(m.rest())) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); ret serveWithContentType(post.text, "text/css"); } if (eq(uri, "/search")) { framer().navBeforeTitle = true; S q = trim(req.params.get("q")); framer().title = joinNemptiesWithColon("Search", q); framer().add(hcomment("cookie: " + req.webRequest.cookie())); framer().add(hform(hinputfield(+q, autofocus := true) + " " + hsubmit("Search"))); framer().add(p(small("Search help: If you search for multiple words, they can appear in any order in the text. An underscore matches any character. Plus means space. Post titles, texts and types are searched."))); if (nempty(q)) { ScoredSearcher searcher = new(q); long id = parseLongOpt_pcall(q); for (UserPost post) searcher.add(post, (post.id == id ? 4 : 0) + searcher.score(post.title)*3 + searcher.score(post.text)*2 + searcher.score(post.type) + searcher.score(joinWithSpace(post.postRefTags))*0.5); L posts = searcher!; framer().add(p(b(addPlusToCount(searcher.maxResults, l(posts), nPosts(posts)) + " found for " + htmlEncode2(q)))); framer().add(ul(lmap postToHTMLWithDate(posts))); } ret framer().render(); } // serve replies for JSTree AJAX call if (eq(uri, "/jstree/replies")) { UserPost post = getConcept UserPost(parseLong(req.params.get("post"))); if (post == null) ret serveJSON(ll()); Cl refs = referencingPosts(post); /*[ { "id" : "demo_root_1", "text" : "Root 1", "children" : true, "type" : "root" }, { "id" : "demo_root_2", "text" : "Root 2", "type" : "root" } ]*/ ret serveJSON(map(refs, p -> litorderedmap( id := p.id, text := postToHTMLWithDate(post), children := nempty(referencingPosts(p)) ? true : null, type := "root" // ? ))); } if (startsWith(uri, "/touchPost/", m)) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); if (!canEditPost(post)) ret "You are not authorized to do this"; touchConcept(post); ret hscript([[setTimeout('history.go(-1)', 1000);]]) + "Post " + post.id + " touched"; } if (startsWith(uri, "/hidePost/", m)) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); if (!canEditPost(post)) ret "You are not authorized to do this"; cset(post, hidden := true); ret hrefresh(postLink(post)); } if (startsWith(uri, "/unhidePost/", m)) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); if (!canEditPost(post)) ret "You are not authorized to do this"; cset(post, hidden := false); ret hrefresh(postLink(post)); } if (startsWith(uri, "/markSafe/", m)) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); UserPost codePost = optCast UserPost(first(post.postRefs)); if (codePost == null) ret "Code post not found"; if (!canEditPost(codePost)) ret "You are not authorized to do this"; // create "Mark safe" post uniqCI UserPost(creator := currentUser(), text := "Mark safe", postRefs := ll(post)); sleepSeconds(6); // allow bot to react // touch code post touchConcept(codePost); ret hrefresh(postLink(codePost)); } if (eq(uri, "/mirrorAllConversations")) { if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); for (Conversation c) rstUpdateMirrorPosts.add(c); ret "OK"; } if (startsWith(uri, "/mirrorConversation/", m)) { if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); long id = parseLong(m.rest()); Conversation conv = getConcept Conversation(id); if (conv == null) ret "Conversation not found"; conv.updateMirrorPost(); ret "Mirror post updated (" + htmlEncode2(str(conv.mirrorPost!)) + ")"; } if (eqic(uri, "/favicon.ico")) ret serveFavIcon(); // start of /bot commands if (startsWith(uri, "/bot/", m)) { req.subURI = m.rest(); S json = req.params.get("json"); new Map data; if (nempty(json)) data = jsonDecodeMap(json); data.putAll(withoutKey("json", req.params)); User user; S userName = cast data.get("_user"); if (nempty(userName)) { S botToken = cast data.get("_botToken"); if (botToken == null) ret serveJSON(error := "Need _botToken"); user = conceptWhereIC User(name := userName); if (user == null) ret serveJSON(error := "User not found"); if (!eq(botToken, getVar(user.botToken))) ret serveJSON(error := "Wrong bot token"); } else user = user(req); S function = beforeSlashOrAll(req.subURI); req.subURI = substring(req.subURI, l(function)+1); req.webRequest.noSpam(); // might want to change this try object servePossiblyUserlessBotFunction(req, function, data, user); if (user == null) ret serveJSON(error := "Need _user"); if (eq(function, "getCookie")) ret serveJSON(cookie := req.cookie()); if (eq(function, "authTest")) ret serveJSON(status := "You are authorized as " + user.name + " " + roundBracket(user.isMaster ? "master user" : "non-master user")); if (eq(function, "postCount")) ret serveJSON(result := countConcepts(UserPost)); if (eq(function, "listPosts")) { LS fields = unnull(stringToStringListOpt tok_identifiersInOrder(data.get("fields"))); if (!fields.contains("id")) fields.add(0, "id"); long changedAfter = toLong(data.get("changedAfter")); long repliesTo = toLong(data.get("repliesTo")); double pollFor = min(bot_maxPollSeconds, toLong(data.get("pollFor"))); // how long to poll (seconds) long startTime = sysNow(); // We're super-anal about catching all changes. This will probably never trigger if (changedAfter > 0 && changedAfter == now()) sleep(1); Cl posts; while true { if (repliesTo != 0) { posts = referencingPosts(getConcept UserPost(repliesTo)); if (changedAfter != 0) posts = objectsWhereFieldGreaterThan(posts, _modified := changedAfter); } else { posts = changedAfter == 0 ? list(UserPost) : conceptsWithFieldGreaterThan_sorted(UserPost, _modified := changedAfter); } // return when there are results, no polling or poll expired if (nempty(posts) || pollFor == 0 || elapsedSeconds_sysNow(startTime) >= pollFor) ret serveJSON_breakAtLevels(2, result := map(posts, post -> mapToValues(fields, field -> getPostFieldForBot(post, field)) )); // sleep and try again sleep(bot_pollInterval); } } if (eq(function, "createPost")) { Either e = createPostArgs(user, data); if (e.isB()) ret serveJSON(error := e.b()); Pair post = uniq2 UserPost(e.a()); if (!post.b) post.a.bump(); // bump if exact same post exists ret serveJSON(status := post.b ? "Post created" : "Post existed already, bumped", postID := post.a.id); } if (eq(function, "editPost")) { long postID = toLong(data.get("postID")); UserPost post = getConcept UserPost(postID); if (post == null) ret serveJSON(error := "Post " + postID + " not found"); Either e = createPostArgs(user, data); if (e.isB()) ret serveJSON(error := e.b()); int changes = cset(post, e.a()); // TODO: bump if no changes? ret serveJSON(changes > 0 ? "Post updated" : "Post updated, no changes", +postID); } if (eq(function, "deletePosts")) { L ids = allToLong(tok_integersInOrder((S) data.get("ids"))); new LS results; new LS errors; fOr (long id : ids) { UserPost post = getConceptOpt UserPost(id); if (post == null) errors.add("Post " + id + " not found"); else { if (!user.isMaster && neq(post.creator!, user)) errors.add("Can't delete post " + id + " from other user"); else { deletePost(post); results.add("Post " + id + " deleted"); } } } ret serveJSON(litorderedmap(+results, +errors)); } ret serveBotFunction(req, function, data, user); } if (teamPostID != 0 && eq(uri, "/team")) ret serveHTMLPost(teamPostID); if (startsWith(uri, "/history/", m)) { long id = parseLong(m.rest()); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); ret servePostHistory(post); } if (startsWith(uri, "/htmlBot/", m)) { long id = parseLong(beforeSlashOrAll(m.rest())); UserPost post = getConcept UserPost(id); if (post == null) ret serve404("Post not found"); ret doPost(htmlBotURLOnBotServer(id), req.params()); } if (eq(uri, "/deletedPosts")) { temp CloseableItIt lines = linesFromFile(deletedPostsFile()); new L posts; while (lines.hasNext()) { S line = lines.next(); if (isProperlyQuoted(line)) { Map map = safeUnstructureMap(unquote(line)); posts.add(onlyKeys(map, "id", "title", "type")); } } // TODO: show more info, allow restoring posts ret serveText("Deleted posts:\n" + lines(lmap struct(posts))); } if (eq(uri, "/formToPost")) { User user = currentUser(); if (user == null) serve500("Please log in first"); SS params = cloneMap(req.params); S type = or2(getAndRemove(params, "_type"), "Form Input"); S title = getAndRemove(params, "_title"); L postRefs = map(id -> getConceptOpt UserPost(parseLong(id)), tok_integersInOrder(getAndRemove(params, "_postRefs"))); LS postRefTags = lines(getAndRemove(params, "_postRefTags")); S text = sortLinesAlphaNumIC(mapToLines(params, (k, v) -> urlencode(k) + "=" + urlencode(v))); Pair p = uniq2 UserPost(creator := user, +text, +type, +title, +postRefs, +postRefTags); ret (p.b ? "Post " + p.a.id + " created" : "Post " + p.a.id + " exists") + hrefresh(2.0, "/" + p.a.id); } if (eq(uri, "/webPushSubscribe")) { MapSO data = jsonDecodeMap(mapGet(req.webRequest.files(), "postData")); printVars_str("webPushSubscribe", +data); cnew(WebPushSubscription, +data, clientIP := req.webRequest.clientIP()); ret serveJSON(litmap(message := "success")); } if (eq(uri, "/changePassword")) { if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); S name = assertNempty(req.get("user")); S newPW = assertNempty(req.get("newPW")); User user = conceptWhereCI User(+name); if (user == null) ret "User not found"; cset(user, passwordMD5 := SecretValue(hashPW(newPW))); ret "PW updated"; } if (eq(uri, "/webPushNotify")) { if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); S msg = or2(req.params.get("msg"), "Hello user. It is " + localTimeWithSeconds() + " on the server"); WebPushSubscription sub = getConcept WebPushSubscription(toLong(req.params.get("webPushSubID"))); if (sub == null) ret serve404("webPushSubID not found"); S mod = dm_require("#1030463/WebPushKeyManager"); dm_call(mod, "sendNotification", sub.data, msg); ret "Push message sent"; } if (eq(uri, "/hashPW") && req.masterAuthed) ret hashPW(req.params.get("pw")); } // end of serveOtherPage2 O servePostHistory(UserPost post) { ret serveText(unquoteAllLines(loadTextFile(postHistoryFile(post)))); } // helper for bot functions. return params or error Either createPostArgs(User user, Map data) { S text = cast data.get("text"); S type = cast data.get("type"); S title = cast data.get("title"); S botInfo = or2((S) data.get("botInfo"), "Made by bot"); LS postRefTags = unnull(lines((S) data.get("refTags"))); new L postRefs; O _refs = data.get("refs"); if (_refs cast S) for (S s : tok_integersInOrder(_refs)) { UserPost ref = getConcept UserPost(parseLong(s)); if (ref == null) ret eitherB("Post " + s + " not found"); postRefs.add(ref); } if (empty(text) && empty(title)) ret eitherB("Need either a text or a title"); bool isPublic = eqOneOf(data.get("isPublic"), null, true, "1", "t", "true"); ret eitherA(litparams(creator := user, +text, +type, +title, +isPublic, +botInfo, +postRefs, +postRefTags)); } O serveBotFunction(Req req, S function, Map data, User user) { ret serveJSON(error := "You are logged in correctly but function is unknown: " + function); } O servePossiblyUserlessBotFunction(Req req, S function, Map data, User user) { null; }