1 | switchable double bot_maxPollSeconds = 600; |
2 | switchable int bot_pollInterval = 500; |
3 | |
4 | Class<? extends Concept> defaultCRUDClass() { |
5 | ret Sequence; |
6 | } |
7 | |
8 | O serveOtherPage2(Req req) null { |
9 | new Matches m; |
10 | |
11 | S uri = dropTrailingSlashIfNemptyAfterwards(req.uri); |
12 | |
13 | if (eq(uri, "/admin")) |
14 | ret hrefresh(baseLink + "/crud/" + shortClassName(defaultCRUDClass())); |
15 | |
16 | if (eq(uri, "/latestPost")) { |
17 | UserPost post = highestConceptByField UserPost("created"); |
18 | if (post == null) ret serve404("No posts in database"); |
19 | ret servePost(post, req); |
20 | } |
21 | |
22 | if (eq(uri, "/latestModifiedPost")) { |
23 | UserPost post = highestConceptByField UserPost("_modified"); |
24 | if (post == null) ret serve404("No posts in database"); |
25 | ret servePost(post, req); |
26 | } |
27 | |
28 | if (eq(uri, "/rootPosts")) { |
29 | Cl<UserPost> posts = sortedByConceptIDDesc(filter(list(UserPost), post -> empty(post.postRefs))); |
30 | framer().navBeforeTitle = true; |
31 | framer().title = "Root Posts"; |
32 | framer().add(ul(lmap postToHTMLWithDate(posts))); |
33 | ret framer().render(); |
34 | } |
35 | |
36 | if (eq(uri, "/allPosts")) { |
37 | Cl<UserPost> posts = sortedByConceptIDDesc(list(UserPost)); |
38 | framer().navBeforeTitle = true; |
39 | framer().title = "All Posts"; |
40 | framer().add(ul(lmap postToHTMLWithDate(posts))); |
41 | ret framer().render(); |
42 | } |
43 | |
44 | if (eq(uri, "/latestPosts")) { |
45 | Cl<UserPost> posts = takeFirst(50, sortedByConceptIDDesc(list(UserPost))); |
46 | framer().navBeforeTitle = true; |
47 | framer().title = "Latest Posts & Replies"; |
48 | framer().add(ul(lmap postToHTMLWithDate(posts))); |
49 | ret framer().render(); |
50 | } |
51 | |
52 | if (eq(uri, "/latestModifiedPosts")) { |
53 | Cl<UserPost> posts = takeFirst(50, sortedByFieldDesc _modified(list(UserPost))); |
54 | framer().navBeforeTitle = true; |
55 | framer().title = "Latest Modified Posts & Replies"; |
56 | framer().add(ul(lmap postToHTMLWithDate(posts))); |
57 | ret framer().render(); |
58 | } |
59 | |
60 | if (eq(uri, "/mainPosts")) { |
61 | Cl<UserPost> posts = takeFirst(50, sortedByConceptID(conceptsWhereCI UserPost(type := "Main"))); |
62 | framer().navBeforeTitle = true; |
63 | framer().title = "Main Posts"; |
64 | framer().add(ul(lmap postToHTMLWithDate(posts))); |
65 | ret framer().render(); |
66 | } |
67 | |
68 | if (swic(uri, "/html/", m) && isInteger(m.rest())) { |
69 | long id = parseLong(m.rest()); |
70 | ret serveHTMLPost(id); |
71 | } |
72 | |
73 | if (swic(uri, "/htmlEmbedded/", m) && isInteger(m.rest())) { |
74 | long id = parseLong(m.rest()); |
75 | UserPost post = getConcept UserPost(id); |
76 | if (post == null) ret serve404("Post not found"); |
77 | |
78 | HTMLFramer1 framer = framer(); |
79 | framer.title = "[" + post.id + "] " + or2(post.title, shorten(post.text)); |
80 | framer.add(hcomment("Post begins here") + "\n" + post.text + "\n" + hcomment("Post ends here")); |
81 | |
82 | ret framer.render(); |
83 | } |
84 | |
85 | if (swic(uri, "/css/", m) && isInteger(m.rest())) { |
86 | long id = parseLong(m.rest()); |
87 | UserPost post = getConcept UserPost(id); |
88 | if (post == null) ret serve404("Post not found"); |
89 | ret serveWithContentType(post.text, "text/css"); |
90 | } |
91 | |
92 | if (eq(uri, "/search")) { |
93 | framer().navBeforeTitle = true; |
94 | S q = trim(req.params.get("q")); |
95 | framer().title = joinNemptiesWithColon("Search", q); |
96 | framer().add(hcomment("cookie: " + req.webRequest.cookie())); |
97 | framer().add(hform(hinputfield(+q, autofocus := true) + " " + hsubmit("Search"))); |
98 | 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."))); |
99 | |
100 | if (nempty(q)) { |
101 | ScoredSearcher<UserPost> searcher = new(q); |
102 | long id = parseLongOpt_pcall(q); |
103 | for (UserPost post) |
104 | searcher.add(post, (post.id == id ? 4 : 0) |
105 | + searcher.score(post.title)*3 |
106 | + searcher.score(post.text)*2 |
107 | + searcher.score(post.type) |
108 | + searcher.score(joinWithSpace(post.postRefTags))*0.5); |
109 | L<UserPost> posts = searcher!; |
110 | framer().add(p(b(addPlusToCount(searcher.maxResults, l(posts), nPosts(posts)) |
111 | + " found for " + htmlEncode2(q)))); |
112 | framer().add(ul(lmap postToHTMLWithDate(posts))); |
113 | } |
114 | |
115 | ret framer().render(); |
116 | } |
117 | |
118 | // serve replies for JSTree AJAX call |
119 | if (eq(uri, "/jstree/replies")) { |
120 | UserPost post = getConcept UserPost(parseLong(req.params.get("post"))); |
121 | if (post == null) ret serveJSON(ll()); |
122 | Cl<UserPost> refs = referencingPosts(post); |
123 | /*[ |
124 | { "id" : "demo_root_1", "text" : "Root 1", "children" : true, "type" : "root" }, |
125 | { "id" : "demo_root_2", "text" : "Root 2", "type" : "root" } |
126 | ]*/ |
127 | ret serveJSON(map(refs, p -> litorderedmap( |
128 | id := p.id, |
129 | text := postToHTMLWithDate(post), |
130 | children := nempty(referencingPosts(p)) ? true : null, |
131 | type := "root" // ? |
132 | ))); |
133 | } |
134 | |
135 | if (startsWith(uri, "/touchPost/", m)) { |
136 | long id = parseLong(m.rest()); |
137 | UserPost post = getConcept UserPost(id); |
138 | if (post == null) ret serve404("Post not found"); |
139 | if (!canEditPost(post)) ret "You are not authorized to do this"; |
140 | touchConcept(post); |
141 | ret hscript([[setTimeout('history.go(-1)', 1000);]]) + "Post " + post.id + " touched"; |
142 | } |
143 | |
144 | if (startsWith(uri, "/hidePost/", m)) { |
145 | long id = parseLong(m.rest()); |
146 | UserPost post = getConcept UserPost(id); |
147 | if (post == null) ret serve404("Post not found"); |
148 | if (!canEditPost(post)) ret "You are not authorized to do this"; |
149 | cset(post, hidden := true); |
150 | ret hrefresh(postLink(post)); |
151 | } |
152 | |
153 | if (startsWith(uri, "/unhidePost/", m)) { |
154 | long id = parseLong(m.rest()); |
155 | UserPost post = getConcept UserPost(id); |
156 | if (post == null) ret serve404("Post not found"); |
157 | if (!canEditPost(post)) ret "You are not authorized to do this"; |
158 | cset(post, hidden := false); |
159 | ret hrefresh(postLink(post)); |
160 | } |
161 | |
162 | if (startsWith(uri, "/markSafe/", m)) { |
163 | long id = parseLong(m.rest()); |
164 | UserPost post = getConcept UserPost(id); |
165 | if (post == null) ret serve404("Post not found"); |
166 | UserPost codePost = optCast UserPost(first(post.postRefs)); |
167 | if (codePost == null) ret "Code post not found"; |
168 | if (!canEditPost(codePost)) ret "You are not authorized to do this"; |
169 | |
170 | // create "Mark safe" post |
171 | uniqCI UserPost(creator := currentUser(), text := "Mark safe", postRefs := ll(post)); |
172 | |
173 | sleepSeconds(6); // allow bot to react |
174 | |
175 | // touch code post |
176 | touchConcept(codePost); |
177 | ret hrefresh(postLink(codePost)); |
178 | } |
179 | |
180 | if (eq(uri, "/mirrorAllConversations")) { |
181 | if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); |
182 | for (Conversation c) |
183 | rstUpdateMirrorPosts.add(c); |
184 | ret "OK"; |
185 | } |
186 | |
187 | if (startsWith(uri, "/mirrorConversation/", m)) { |
188 | if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); |
189 | long id = parseLong(m.rest()); |
190 | Conversation conv = getConcept Conversation(id); |
191 | if (conv == null) ret "Conversation not found"; |
192 | conv.updateMirrorPost(); |
193 | ret "Mirror post updated (" + htmlEncode2(str(conv.mirrorPost!)) + ")"; |
194 | } |
195 | |
196 | if (eqic(uri, "/favicon.ico")) |
197 | ret serveFavIcon(); |
198 | |
199 | // start of /bot commands |
200 | |
201 | if (startsWith(uri, "/bot/", m)) { |
202 | req.subURI = m.rest(); |
203 | S json = req.params.get("json"); |
204 | new Map data; |
205 | if (nempty(json)) data = jsonDecodeMap(json); |
206 | data.putAll(withoutKey("json", req.params)); |
207 | |
208 | User user; |
209 | S userName = cast data.get("_user"); |
210 | if (nempty(userName)) { |
211 | S botToken = cast data.get("_botToken"); |
212 | if (botToken == null) ret serveJSON(error := "Need _botToken"); |
213 | user = conceptWhereIC User(name := userName); |
214 | if (user == null) ret serveJSON(error := "User not found"); |
215 | if (!eq(botToken, getVar(user.botToken))) ret serveJSON(error := "Wrong bot token"); |
216 | } else |
217 | user = user(req); |
218 | |
219 | S function = beforeSlashOrAll(req.subURI); |
220 | req.subURI = substring(req.subURI, l(function)+1); |
221 | req.webRequest.noSpam(); // might want to change this |
222 | |
223 | try object servePossiblyUserlessBotFunction(req, function, data, user); |
224 | |
225 | if (user == null) ret serveJSON(error := "Need _user"); |
226 | |
227 | if (eq(function, "getCookie")) |
228 | ret serveJSON(cookie := req.cookie()); |
229 | |
230 | if (eq(function, "authTest")) |
231 | ret serveJSON(status := "You are authorized as " + user.name + " " + roundBracket(user.isMaster ? "master user" : "non-master user")); |
232 | |
233 | if (eq(function, "postCount")) |
234 | ret serveJSON(result := countConcepts(UserPost)); |
235 | |
236 | if (eq(function, "listPosts")) { |
237 | LS fields = unnull(stringToStringListOpt tok_identifiersInOrder(data.get("fields"))); |
238 | if (!fields.contains("id")) fields.add(0, "id"); |
239 | long changedAfter = toLong(data.get("changedAfter")); |
240 | long repliesTo = toLong(data.get("repliesTo")); |
241 | double pollFor = min(bot_maxPollSeconds, toLong(data.get("pollFor"))); // how long to poll (seconds) |
242 | long startTime = sysNow(); |
243 | |
244 | // We're super-anal about catching all changes. This will probably never trigger |
245 | if (changedAfter > 0 && changedAfter == now()) sleep(1); |
246 | |
247 | Cl<UserPost> posts; |
248 | while true { |
249 | if (repliesTo != 0) { |
250 | posts = referencingPosts(getConcept UserPost(repliesTo)); |
251 | if (changedAfter != 0) posts = objectsWhereFieldGreaterThan(posts, _modified := changedAfter); |
252 | } else { |
253 | posts = changedAfter == 0 ? list(UserPost) |
254 | : conceptsWithFieldGreaterThan_sorted(UserPost, _modified := changedAfter); |
255 | } |
256 | |
257 | // return when there are results, no polling or poll expired |
258 | if (nempty(posts) || pollFor == 0 || elapsedSeconds_sysNow(startTime) >= pollFor) |
259 | ret serveJSON_breakAtLevels(2, result := map(posts, post -> |
260 | mapToValues(fields, field -> getPostFieldForBot(post, field)) |
261 | )); |
262 | |
263 | // sleep and try again |
264 | sleep(bot_pollInterval); |
265 | } |
266 | } |
267 | |
268 | if (eq(function, "createPost")) { |
269 | Either<O[], S> e = createPostArgs(user, data); |
270 | if (e.isB()) |
271 | ret serveJSON(error := e.b()); |
272 | |
273 | Pair<UserPost, Bool> post = uniq2 UserPost(e.a()); |
274 | if (!post.b) post.a.bump(); // bump if exact same post exists |
275 | ret serveJSON(status := post.b ? "Post created" : "Post existed already, bumped", postID := post.a.id); |
276 | } |
277 | |
278 | if (eq(function, "editPost")) { |
279 | long postID = toLong(data.get("postID")); |
280 | UserPost post = getConcept UserPost(postID); |
281 | if (post == null) |
282 | ret serveJSON(error := "Post " + postID + " not found"); |
283 | |
284 | Either<O[], S> e = createPostArgs(user, data); |
285 | if (e.isB()) |
286 | ret serveJSON(error := e.b()); |
287 | |
288 | int changes = cset(post, e.a()); |
289 | // TODO: bump if no changes? |
290 | ret serveJSON(changes > 0 ? "Post updated" : "Post updated, no changes", +postID); |
291 | } |
292 | |
293 | if (eq(function, "deletePosts")) { |
294 | L<Long> ids = allToLong(tok_integersInOrder((S) data.get("ids"))); |
295 | new LS results; |
296 | new LS errors; |
297 | fOr (long id : ids) { |
298 | UserPost post = getConceptOpt UserPost(id); |
299 | if (post == null) errors.add("Post " + id + " not found"); |
300 | else { |
301 | if (!user.isMaster && neq(post.creator!, user)) |
302 | errors.add("Can't delete post " + id + " from other user"); |
303 | else { |
304 | deletePost(post); |
305 | results.add("Post " + id + " deleted"); |
306 | } |
307 | } |
308 | } |
309 | ret serveJSON(litorderedmap(+results, +errors)); |
310 | } |
311 | |
312 | ret serveBotFunction(req, function, data, user); |
313 | } |
314 | |
315 | if (teamPostID != 0 && eq(uri, "/team")) |
316 | ret serveHTMLPost(teamPostID); |
317 | |
318 | if (startsWith(uri, "/history/", m)) { |
319 | long id = parseLong(m.rest()); |
320 | UserPost post = getConcept UserPost(id); |
321 | if (post == null) ret serve404("Post not found"); |
322 | ret servePostHistory(post); |
323 | } |
324 | |
325 | if (startsWith(uri, "/htmlBot/", m)) { |
326 | long id = parseLong(beforeSlashOrAll(m.rest())); |
327 | UserPost post = getConcept UserPost(id); |
328 | if (post == null) ret serve404("Post not found"); |
329 | ret doPost(htmlBotURLOnBotServer(id), req.params()); |
330 | } |
331 | |
332 | if (eq(uri, "/deletedPosts")) { |
333 | temp CloseableItIt<S> lines = linesFromFile(deletedPostsFile()); |
334 | new L<Map> posts; |
335 | while (lines.hasNext()) { |
336 | S line = lines.next(); |
337 | if (isProperlyQuoted(line)) { |
338 | Map map = safeUnstructureMap(unquote(line)); |
339 | posts.add(onlyKeys(map, "id", "title", "type")); |
340 | } |
341 | } |
342 | // TODO: show more info, allow restoring posts |
343 | ret serveText("Deleted posts:\n" |
344 | + lines(lmap struct(posts))); |
345 | } |
346 | |
347 | if (eq(uri, "/formToPost")) { |
348 | User user = currentUser(); |
349 | if (user == null) serve500("Please log in first"); |
350 | SS params = cloneMap(req.params); |
351 | S type = or2(getAndRemove(params, "_type"), "Form Input"); |
352 | S title = getAndRemove(params, "_title"); |
353 | L<UserPost> postRefs = map(id -> getConceptOpt UserPost(parseLong(id)), tok_integersInOrder(getAndRemove(params, "_postRefs"))); |
354 | LS postRefTags = lines(getAndRemove(params, "_postRefTags")); |
355 | S text = sortLinesAlphaNumIC(mapToLines(params, (k, v) -> urlencode(k) + "=" + urlencode(v))); |
356 | Pair<UserPost,Bool> p = uniq2 UserPost(creator := user, |
357 | +text, +type, +title, +postRefs, +postRefTags); |
358 | ret (p.b ? "Post " + p.a.id + " created" : "Post " + p.a.id + " exists") + hrefresh(2.0, "/" + p.a.id); |
359 | } |
360 | |
361 | if (eq(uri, "/webPushSubscribe")) { |
362 | MapSO data = jsonDecodeMap(mapGet(req.webRequest.files(), "postData")); |
363 | printVars_str("webPushSubscribe", +data); |
364 | cnew(WebPushSubscription, +data, clientIP := req.webRequest.clientIP()); |
365 | ret serveJSON(litmap(message := "success")); |
366 | } |
367 | |
368 | if (eq(uri, "/changePassword")) { |
369 | if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); |
370 | S name = assertNempty(req.get("user")); |
371 | S newPW = assertNempty(req.get("newPW")); |
372 | User user = conceptWhereCI User(+name); |
373 | if (user == null) ret "User not found"; |
374 | cset(user, passwordMD5 := SecretValue(hashPW(newPW))); |
375 | ret "PW updated"; |
376 | } |
377 | |
378 | if (eq(uri, "/webPushNotify")) { |
379 | if (!req.masterAuthed) ret serveAuthForm(rawLink(uri)); |
380 | S msg = or2(req.params.get("msg"), "Hello user. It is " + localTimeWithSeconds() + " on the server"); |
381 | WebPushSubscription sub = getConcept WebPushSubscription(toLong(req.params.get("webPushSubID"))); |
382 | if (sub == null) ret serve404("webPushSubID not found"); |
383 | |
384 | S mod = dm_require("#1030463/WebPushKeyManager"); |
385 | dm_call(mod, "sendNotification", sub.data, msg); |
386 | |
387 | ret "Push message sent"; |
388 | } |
389 | |
390 | if (eq(uri, "/hashPW") && req.masterAuthed) |
391 | ret hashPW(req.params.get("pw")); |
392 | } // end of serveOtherPage2 |
393 | |
394 | O servePostHistory(UserPost post) { |
395 | ret serveText(unquoteAllLines(loadTextFile(postHistoryFile(post)))); |
396 | } |
397 | |
398 | // helper for bot functions. return params or error |
399 | Either<O[], S> createPostArgs(User user, Map data) { |
400 | S text = cast data.get("text"); |
401 | S type = cast data.get("type"); |
402 | S title = cast data.get("title"); |
403 | S botInfo = or2((S) data.get("botInfo"), "Made by bot"); |
404 | LS postRefTags = unnull(lines((S) data.get("refTags"))); |
405 | new L<UserPost> postRefs; |
406 | O _refs = data.get("refs"); |
407 | if (_refs cast S) |
408 | for (S s : tok_integersInOrder(_refs)) { |
409 | UserPost ref = getConcept UserPost(parseLong(s)); |
410 | if (ref == null) ret eitherB("Post " + s + " not found"); |
411 | postRefs.add(ref); |
412 | } |
413 | if (empty(text) && empty(title)) |
414 | ret eitherB("Need either a text or a title"); |
415 | |
416 | bool isPublic = eqOneOf(data.get("isPublic"), null, true, "1", "t", "true"); |
417 | ret eitherA(litparams(creator := user, |
418 | +text, +type, +title, +isPublic, +botInfo, +postRefs, +postRefTags)); |
419 | } |
420 | |
421 | O serveBotFunction(Req req, S function, Map data, User user) { |
422 | ret serveJSON(error := "You are logged in correctly but function is unknown: " + function); |
423 | } |
424 | |
425 | O servePossiblyUserlessBotFunction(Req req, S function, Map data, User user) { |
426 | null; |
427 | } |
download show line numbers debug dex old transpilations
Travelled to 5 computer(s): bhatertpkbcr, mqqgnosmbjvj, onxytkatvevr, pyentgdyhuwx, vouqrxazstgt
No comments. add comment
Snippet ID: | #1029962 |
Snippet name: | serveOtherPage2 [gazelle.rocks Include] |
Eternal ID of this version: | #1029962/153 |
Text MD5: | 60137e3ed452a20e0ae7aed04d5139d9 |
Author: | stefan |
Category: | javax |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-10-16 17:18:44 |
Source code size: | 16189 bytes / 427 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 280 / 2912 |
Version history: | 152 change(s) |
Referenced in: | [show references] |