Libraryless. Click here for Pure Java version (50417L/367K).
1 | do not include class Msg. |
2 | |
3 | // create/read password salt for program, or use legacy global salt |
4 | sS passwordSalt() { |
5 | File f = programSecretFile("password-salt"); |
6 | if (fileExists(f)) ret loadTextFile(f); |
7 | File global = javaxSecretDir("password-salt"); |
8 | if (fileExists(global)) ret loadTextFile(global); |
9 | ret loadTextFileOrCreateWithRandomID(f); |
10 | } |
11 | |
12 | concept PostReferenceable {} |
13 | |
14 | // Note: If you change/add fields here, make sure to edit |
15 | // GazelleBEA.findOrCreateUserForLogin |
16 | concept User > PostReferenceable { |
17 | S name; |
18 | SecretValue<S> passwordMD5; |
19 | S contact; // e.g. mail address |
20 | bool isMaster; |
21 | SecretValue<S> botToken = aSecretGlobalID/*UnlessLoading*/(); |
22 | long lastSeen; |
23 | |
24 | S name() { ret name; } |
25 | toString { ret nempty(name) ? "User " + name : super.toString(); } |
26 | } |
27 | |
28 | extend AuthedDialogID { |
29 | new Ref<User> user; // who is logged in |
30 | bool userMode; // show things as if user was not master |
31 | |
32 | User user() { ret user!; } |
33 | } |
34 | |
35 | extend Conversation { |
36 | new Ref<UserPost> mirrorPost; |
37 | new Ref<UserPost> detailedMirrorPost; |
38 | |
39 | void change { |
40 | super.change(); |
41 | ((DynGazelleRocks) botMod()).rstUpdateMirrorPosts.add(this); |
42 | } |
43 | |
44 | void updateMirrorPost { |
45 | if (isDeleted()) ret; |
46 | |
47 | // Simple mirror post with just texts and "User:" or "Bot:" |
48 | if (!mirrorPost.has() && syncNempty(msgs)) |
49 | cset(Conversation.this, mirrorPost := cnew UserPost( |
50 | title := "Conversation " + id, |
51 | type := "Conversation Mirror", |
52 | creator := ((DynGazelleRocks) botMod()).internalUser(), botInfo := "Conversation Mirror Bot")); |
53 | cset(mirrorPost!, text := lines_rtrim(msgs)); |
54 | |
55 | // Detailed mirror post with a lot of info |
56 | if (!detailedMirrorPost.has() && syncNempty(msgs)) |
57 | cset(Conversation.this, detailedMirrorPost := cnew UserPost( |
58 | title := "Conversation " + id + " with details", |
59 | type := "Detailed Conversation Mirror", |
60 | creator := ((DynGazelleRocks) botMod()).internalUser(), botInfo := "Conversation Mirror Bot")); |
61 | cset(detailedMirrorPost!, text := indentedStructure(msgs)); |
62 | } |
63 | |
64 | void delete { |
65 | cdelete(mirrorPost!); |
66 | super.delete(); |
67 | } |
68 | } |
69 | |
70 | concept UserCreatedObject > PostReferenceable { |
71 | new Ref<User> creator; |
72 | S botInfo; // info on how this was made. set to anything non-empty if made by bot |
73 | //S nameInRepo; // unique by user, for URL |
74 | } |
75 | |
76 | concept UserPost > UserCreatedObject { |
77 | S type, title, text; |
78 | bool isPublic = true, hidden; |
79 | bool creating; // new post, still editing |
80 | long xmodified; // modification timestamp that also includes replies |
81 | long bumped; // modification timestamp that includes "bumps" (re-posted identical bot reply) |
82 | |
83 | static LS fieldsToSaveOnDelete = ll("id", "text", "title", "type", "creatorID", "postRefs", "postRefTags", "hidden", "isPublic", "created", "_modified", "xmodified", "botInfo", "creating"); |
84 | |
85 | // TODO: migrate to this eventually (but need to change CRUD too) |
86 | //int flags; |
87 | //static int F_PUBLIC = 1, F_HIDDEN = 2; |
88 | |
89 | S text() { ret text; } |
90 | |
91 | new RefL<PostReferenceable> postRefs; |
92 | new LS postRefTags; |
93 | |
94 | sS _fieldOrder = "title text type postRefs isPublic"; |
95 | |
96 | toString { |
97 | S content = nempty(title) ? shorten(title) : escapeNewLines(shorten(text)); |
98 | //S _type = isReply() ? "reply" : "post"; |
99 | //if (hidden) _type += " (hidden)"; |
100 | //ret firstToUpper(_type) + " by " + author() + ": " + content; |
101 | ret content + " [by " + author() + "]"; |
102 | } |
103 | |
104 | bool isReply() { ret nempty(postRefs); } |
105 | bool isBotPost() { ret nempty(botInfo); } |
106 | bool isMasterMade() { ret creator.has() && creator->isMaster; } |
107 | |
108 | S author() { |
109 | S author = userName(creator!); |
110 | if (isBotPost()) author += "'s bot " + quote(botInfo); |
111 | ret author; |
112 | } |
113 | |
114 | void change { |
115 | super.change(); |
116 | ((DynGazelleRocks) botMod()).distributePostChanges(this); |
117 | } |
118 | |
119 | LPair<PostReferenceable, S> postRefsWithTags() { |
120 | ret zipTwoListsToPairs_lengthOfFirst(postRefs, postRefTags); |
121 | } |
122 | |
123 | L<PostReferenceable> postRefsWithTag(S tag) { |
124 | ret pairsAWhereB(postRefsWithTags(), t -> eqic_unnull(t, tag)); |
125 | } |
126 | |
127 | UserPost primaryRefPost() { |
128 | ret optCast UserPost(first(postRefsWithTag(""))); |
129 | } |
130 | |
131 | void _setModified(long modified) { |
132 | super._setModified(modified); |
133 | setXModified(modified); |
134 | } |
135 | |
136 | void bump { |
137 | bumped = now(); |
138 | _change_withoutUpdatingModifiedField(); |
139 | for (UserPost p : syncInstancesOf UserPost(postRefs)) p.setXModified(); |
140 | } |
141 | |
142 | long modifiedOrBumped() { ret max(_modified, bumped); } |
143 | |
144 | // Notify of changes in replies |
145 | void setXModified(long modified default now()) { |
146 | bool changed = modified > xmodified; |
147 | xmodified = modified; |
148 | _change_withoutUpdatingModifiedField(); |
149 | if (changed) |
150 | ((DynGazelleRocks) botMod()).distributePostChanges(this); |
151 | } |
152 | |
153 | void _backRefsModified { |
154 | super._backRefsModified(); |
155 | setXModified(now()); |
156 | } |
157 | |
158 | // -1 if no user post found in hierarchy |
159 | int distanceToUserPost() { |
160 | int n = 0; |
161 | UserPost post = this; |
162 | while (post != null && n < 100) { |
163 | if (!post.isBotPost()) ret n; |
164 | post = post.primaryRefPost(); |
165 | } |
166 | ret -1; |
167 | } |
168 | |
169 | bool isJavaXCode() { |
170 | ret eqicOrSwicPlusSpace(type, "JavaX Code"); |
171 | } |
172 | } // end of UserPost |
173 | |
174 | /*concept BotHandle > UserCreatedObject { |
175 | S globalID = aGlobalIDUnlessLoading(); |
176 | S comment; |
177 | SecretValue<S> token; |
178 | }*/ |
179 | |
180 | set flag NoNanoHTTPD. |
181 | |
182 | asclass DynGazelleRocks > DynNewBot2 { |
183 | transient bool inlineSearch; // show search form in nav links |
184 | switchable bool webPushEnabled = true; |
185 | switchable bool showPostStats; |
186 | switchable bool showSuggestorBot = true; |
187 | switchable bool showMetaBotOnEveryPage = true; |
188 | switchable int delayAfterSuggestion = 1000; |
189 | switchable int defaultBotPost = 238410; |
190 | switchable long teamPostID; |
191 | switchable long favIconID; // NB: it's an UploadedFile id in this DB |
192 | switchable S defaultFavIconSnippet = gazelleFavIconSnippet(); |
193 | |
194 | switchable S gazelleBotURL = null; //"https://gazellebot.botcompany.de"; |
195 | switchable S salterService; |
196 | |
197 | transient double systemLoad; |
198 | transient long processSize; |
199 | |
200 | void init { |
201 | dm_require("#1017856/SystemLoad"); |
202 | dm_vmBus_onMessage systemLoad(voidfunc(double load) { |
203 | if (setField(systemLoad := load)) |
204 | distributeDivChanges("serverLoadRightHemi"); |
205 | }); |
206 | dm_vmBus_onMessage processSize(voidfunc(long processSize) { |
207 | if (setField(+processSize)) |
208 | distributeDivChanges("memRightHemi"); |
209 | }); |
210 | |
211 | botName = heading = adminName = "gazelle.rocks"; |
212 | templateID = #1030086; |
213 | cssID = /*#1030233*/"237267"; // now a post ID |
214 | set enableUsers; |
215 | set useWebSockets; |
216 | set showRegisterLink; |
217 | unset showTalkToBotLink; |
218 | set alwaysRedirectToHttps; |
219 | set redirectOnLogout; |
220 | set showFullErrors; |
221 | set inlineSearch; |
222 | unset lockWhileDoingBotActions; // fix the _gazelle_text bug? |
223 | unset showMailSenderInfo; |
224 | if (empty(salterService)) |
225 | passwordSalt(); // make early |
226 | } |
227 | |
228 | void makeIndices :: after { |
229 | indexConceptFieldCI(User, "name"); |
230 | indexConceptFieldDesc(UserPost, "xmodified"); |
231 | indexConceptFieldDesc(UserPost, "_modified"); |
232 | indexConceptFieldCI(UserPost, "botInfo"); |
233 | } |
234 | |
235 | void start { |
236 | init(); |
237 | super.start(); |
238 | db_mainConcepts().modifyOnCreate = true; |
239 | |
240 | printConceptIndices(); |
241 | |
242 | onConceptsChange(new Runnable { |
243 | int postCount = countConcepts(UserPost); |
244 | |
245 | run { |
246 | temp enter(); |
247 | int count = countConcepts(UserPost); |
248 | if (count != postCount) { |
249 | postCount = count; |
250 | distributeDivChanges("postCount"); |
251 | } |
252 | } |
253 | }); |
254 | |
255 | internalUser(); |
256 | |
257 | // legacy clean-up |
258 | /*for (UserPost post) { |
259 | for i to 5: |
260 | cset(post, "postRefTags_" + i, null); |
261 | }*/ |
262 | } |
263 | |
264 | // web socket stuff |
265 | |
266 | void requestServed { |
267 | distributeDivChanges("webRequestsRightHemi"); |
268 | } |
269 | |
270 | transient class WebSocketInfo extends Meta is AutoCloseable { |
271 | S uri; |
272 | SS params; |
273 | Req req; |
274 | WeakReference<virtual WebSocket> webSocket; |
275 | Set<IVF1<S>> messageHandlers = syncLinkedHashSet(); |
276 | Set<IVF1<byte[]>> binMessageHandlers = syncLinkedHashSet(); |
277 | Set<Runnable> closeHandlers = syncLinkedHashSet(); |
278 | |
279 | // Any helper can store things here like jqueryLoaded := true |
280 | Map misc = syncMap(); |
281 | |
282 | *(virtual WebSocket webSocket) { |
283 | this.webSocket = weakRef(webSocket); |
284 | setFieldToIVF1Proxy(webSocket, onMessage := msg -> { |
285 | temp enter(); pcall { |
286 | O opCode = call(msg, "getOpCode"); |
287 | byte opCodeValue = cast call(opCode, "getValue"); |
288 | bool isBinary = opCodeValue == 2; // isso! |
289 | |
290 | if (isBinary) { |
291 | byte[] data = cast rcall getBinaryPayload(msg); |
292 | pcallFAll(binMessageHandlers, data); |
293 | } else { |
294 | S data = rcall_string getTextPayload(msg); |
295 | pcallFAll(messageHandlers, data); |
296 | } |
297 | } |
298 | }); |
299 | } |
300 | |
301 | S subURI aka subUri() { ret req.subURI(); } |
302 | S uri() { ret uri; } |
303 | SS params() { ret params; } |
304 | S get(S param) { ret mapGet(params, param); } |
305 | |
306 | void setParams(SS params) { |
307 | this.params = params; |
308 | req.params = params; |
309 | } |
310 | |
311 | O dbRepresentation; |
312 | new Set<Pair<S, O>> liveDivs; // id/content info |
313 | |
314 | void eval(S jsCode, O... _ default null) { |
315 | jsCode = jsDollarVars(jsCode, _); |
316 | if (empty(jsCode)) ret; |
317 | dm_call(webSocket!, "send", jsonEncode(litmap(eval := jsCode))); |
318 | } |
319 | |
320 | void send(S jsonData) { |
321 | if (empty(jsonData)) ret; |
322 | dm_call(webSocket!, "send", jsonData); |
323 | } |
324 | |
325 | AutoCloseable onStringMessage(IVF1<S> onMsg) { |
326 | ret tempAdd(messageHandlers, onMsg); |
327 | } |
328 | |
329 | AutoCloseable onBinaryMessage(IVF1<byte[]> onMsg) { |
330 | ret tempAdd(binMessageHandlers, onMsg); |
331 | } |
332 | |
333 | AutoCloseable onClose(Runnable r) { |
334 | ret tempAdd(closeHandlers, r); |
335 | } |
336 | |
337 | void callCloseHandlers { |
338 | pcallFAll(closeHandlers); |
339 | } |
340 | |
341 | public void close { |
342 | cleanUp(webSocket!); |
343 | } |
344 | } |
345 | |
346 | transient Map<virtual WebSocket, WebSocketInfo> webSockets = syncWeakHashMap(); |
347 | |
348 | void cleanMeUp_webSockets { |
349 | closeAllKeysAndClear((Map) webSockets); |
350 | } |
351 | |
352 | // funny: useWebSockets exists in DynNewBot2, |
353 | // but only in this subclass do we define handleWebSocket |
354 | void handleWebSocket(virtual WebSocket ws) { |
355 | set(ws, onClose := r { |
356 | var info = webSockets.get(ws); |
357 | if (info != null) pcall { onWebSocketClose(info); } |
358 | webSockets.remove(ws); |
359 | }); |
360 | |
361 | set(ws, onOpen := rEnter { |
362 | S uri = cast rcall getUri(ws); |
363 | SS params = cast rcall getParms(ws); |
364 | print("WebSocket opened! uri: " + uri + ", params: " + params); |
365 | WebSocketInfo info = new(ws); |
366 | IWebRequest webRequest = proxy IWebRequest(call(ws, "webRequest")); |
367 | info.req = webRequestToReq(webRequest); |
368 | S cookie = cookieFromWebRequest(webRequest); |
369 | AuthedDialogID auth = authObject(cookie); |
370 | fillReqAuthFromCookie(info.req, cookie, auth); |
371 | temp tempSetTL(currentReq, info.req); // why not |
372 | |
373 | info.uri = uri; |
374 | info.params = params; |
375 | webSockets.put(ws, info); |
376 | pcall { onNewWebSocket(info); } |
377 | long objectID = toLong(params.get("objectID")); |
378 | long modified = toLong(params.get("modified")); |
379 | if (objectID != 0) { |
380 | UserPost c = getConceptOpt UserPost(objectID); |
381 | print("Modification: " + c.xmodified + " / " + c._modified + " / " + modified); |
382 | if (c != null && c.xmodified > modified) |
383 | reloadBody(ws); |
384 | } |
385 | }); |
386 | |
387 | setFieldToIVF1Proxy(ws, onMessage := msg -> { temp enter(); pcall { |
388 | WebSocketInfo info = webSockets.get(ws); |
389 | if (info == null) ret; |
390 | S data = rcall_string getTextPayload(msg); |
391 | Map map = jsonDecodeMap(data); |
392 | O div = map.get("liveDiv"); |
393 | if (div cast S) { |
394 | S contentDesc = div; |
395 | syncAdd(info.liveDivs, pair(div, (O) contentDesc)); |
396 | reloadDiv(ws, div, calcDivContent(contentDesc)); |
397 | } |
398 | }}); |
399 | } |
400 | |
401 | S serveRegisterForm(SS params) { |
402 | S user = trim(params.get("user")); |
403 | S pw = trim(params.get("f_pw")); |
404 | S pw2 = trim(params.get("pw2")); |
405 | S redirect = params.get("redirect"); |
406 | S contact = trim(params.get("contact")); |
407 | |
408 | redirect = dropParamFromURL(redirect, "logout"); // don't log us out right again |
409 | if (empty(redirect)) redirect = baseLink + "/"; |
410 | |
411 | new LS msgs; |
412 | if (nempty(user) || nempty(pw)) { |
413 | lock dbLock(); |
414 | if (empty(user)) msgs.add("Please enter a user name"); |
415 | else if (l(user) < 4) msgs.add("Minimum user name length: 4 characters"); |
416 | else if (l(user) > 30) msgs.add("Maximum user name length: 30 characters"); |
417 | else if (hasConceptIC User(name := user)) |
418 | msgs.add("This user exists, please choose a different name"); |
419 | if (regexpContainsIC("[^a-z0-9\\-\\.]", user)) |
420 | msgs.add("Bad characters in user name (please use only a-z, 0-9, - and .)"); |
421 | 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."); |
422 | else if (l(pw) < 6) msgs.add("Minimum password length: 6 characters"); |
423 | if (neq(pw, pw2)) msgs.add("Passwords don't match"); |
424 | |
425 | if (empty(msgs)) { |
426 | User userObj = cnew User(name := user, passwordMD5 := SecretValue(hashPW(pw)), +contact); |
427 | vmBus_send userCreated(userObj); |
428 | ret hrefresh(5.0, redirect) + "User " + user + " created! Redirecting..."; |
429 | } |
430 | } |
431 | |
432 | ret hhtml(hhead(htitle("Register new user") |
433 | + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter( |
434 | h3_htmlEncode(adminName + " | Register new user") |
435 | + hpostform( |
436 | hhidden(+redirect) |
437 | + tag table( |
438 | (empty(msgs) ? "" : tr(td() + td() + td(htmlEncode2_nlToBr(lines_rtrim(msgs))))) |
439 | + tr(td_valignTop("Choose a user name:") |
440 | + td_valignTop(hinputfield(+user) + "<br>" + "(Minimum length 4. Characters allowed: a-z, 0-9, - and .)")) |
441 | + tr(td_valignTop("Choose a password:") |
442 | + td_valignTop(hpassword(f_pw := pw) + "<br>" + "(Minimum length 6 characters)")) |
443 | + tr(td_valignTop("Repeat the password please:") + td(hpassword(+pw2))) |
444 | + tr(td_valignTop("Way to contact you (e.g. e-mail) - optional:") + td(hinputfield(+contact))) |
445 | + tr(td() + td(hsubmit("Register"))), class := "responstable"), |
446 | action := baseLink + "/register") |
447 | ))); |
448 | } |
449 | |
450 | bool calcMasterAuthed(Req req) { |
451 | ret super.calcMasterAuthed(req) |
452 | || req.auth != null && req.auth.user.has() && req.auth.user->isMaster; |
453 | } |
454 | |
455 | O serveOtherPage(Req req) { |
456 | S uri = req.uri; |
457 | new Matches m; |
458 | |
459 | if (eq(uri, "/register")) |
460 | ret serveRegisterForm(req.params); |
461 | |
462 | if (eq(uri, "/becomeMaster") && req.auth != null && req.auth.user != null) { |
463 | S pw = trim(req.params.get("masterPW")); |
464 | if (eq(pw, realPW())) { |
465 | cset(req.auth.user, isMaster := true); |
466 | ret "You are master, " + req.auth.user + "!"; |
467 | } |
468 | if (nempty(pw)) |
469 | ret "Bad master PW"; |
470 | ret hhtml(hhead(htitle("Become master") |
471 | + hsansserif() + hmobilefix() + hresponstable()) + hbody(hfullcenter( |
472 | h3_htmlEncode(adminName + " | Become master") |
473 | + hpostform( |
474 | tag table( |
475 | tr(td_valignTop("You are:") |
476 | + td_valignTop(htmlEncode2(req.auth.user->name))) |
477 | + tr(td_valignTop("Enter the master password:") |
478 | + td_valignTop(hpassword(masterPW := pw))) |
479 | + tr(td() + td(hsubmit("Become master user"))), class := "responstable"), |
480 | action := baseLink + "/becomeMaster") |
481 | ))); |
482 | } |
483 | |
484 | if (swic(uri, "/text/", m)) { |
485 | long id = parseLong(m.rest()); |
486 | UserPost post = getConceptOpt UserPost(id); |
487 | if (post == null) ret serve404("Post with ID " + id + " not found"); |
488 | if (!post.isPublic) ret serve404("Post is not public"); |
489 | S ct = req.params.get("ct"); |
490 | O response = nempty(ct) |
491 | ? serveWithContentType(post.text(), ct) |
492 | : serveText(post.text()); |
493 | if (eq(ct, "text/javascript")) |
494 | addHeader("Service-Worker-Allowed", "/", response); |
495 | ret response; |
496 | } |
497 | |
498 | if (swic(uri, "/postRefs/", m)) { |
499 | long id = parseLong(m.rest()); |
500 | UserPost post = getConceptOpt UserPost(id); |
501 | if (post == null) ret serve404("Post with ID " + id + " not found"); |
502 | if (!post.isPublic) ret serve404("Post is not public"); |
503 | ret serveJSON(lmap conceptID(post.postRefs)); |
504 | } |
505 | |
506 | if (swic(uri, "/postRefsWithTags/", m)) { |
507 | long id = parseLong(m.rest()); |
508 | UserPost post = getConceptOpt UserPost(id); |
509 | if (post == null) ret serve404("Post with ID " + id + " not found"); |
510 | if (!post.isPublic) ret serve404("Post is not public"); |
511 | ret serveJSON(map(post.postRefsWithTags(), p -> litorderedmap(id := conceptID(p.a), as := p.b))); |
512 | } |
513 | |
514 | // Serve a post |
515 | |
516 | S uri2 = dropSlashPrefix(uri); |
517 | if (isInteger(uri2)) { |
518 | long id = parseLong(uri2); |
519 | try object serveIntegerLink(req, id); |
520 | } |
521 | |
522 | try object serveOtherPage2(req); |
523 | |
524 | ret super.serveOtherPage(req); |
525 | } |
526 | |
527 | !include #1029962 // serveOtherPage2 |
528 | |
529 | O servePost(UserPost post, Req req) { |
530 | if (!post.isPublic) ret serve404("Post is not public"); |
531 | HTMLFramer1 framer = framer(); |
532 | |
533 | framer.add(hjs("webSocketQuery = " + jsQuote("?objectID=" + post.id + "&modified=" + post.xmodified) + ";\n" |
534 | + [[if (ws != null) ws.url = ws.url.split("?")[0] + webSocketQuery;]])); |
535 | framer.title = "[" + post.id + "] " + or2(post.title, shorten(post.text)); |
536 | framer.add(p("By " + htmlEncode2(post.author()) + ". " + renderConceptDate(post))); |
537 | if (nempty(post.type)) |
538 | framer.add(p("Post type: "+ htmlEncode2(post.type))); |
539 | |
540 | new LS actions; |
541 | |
542 | // show render link |
543 | if (eqic(post.type, "HTML")) |
544 | actions.add(targetBlank("/html/" + post.id, "Show as HTML page")); |
545 | |
546 | if (eqic(post.type, "HTML (embedded)")) |
547 | actions.add(targetBlank("/htmlEmbedded/" + post.id, "Show as HTML page")); |
548 | |
549 | if (eqicOneOf(post.type, "JavaX Code (HTML Bot)", "JavaX Code (Live Home Page)")) |
550 | actions.add(targetBlank(htmlBotURL(post.id), "Show as HTML page")); |
551 | |
552 | if (eqic(post.type, "Conversation HTML")) |
553 | actions.add(targetBlank("/demo?cookie=htmlDemo&_autoOpenBot=1&chatContentsPost=" + post.id, "Show in chat box")); |
554 | |
555 | // show edit link |
556 | if (canEditPost(post)) { |
557 | actions.add(ahref(conceptEditLink(post), "Edit post")); |
558 | actions.add(ahref(touchLink(post), "Touch post")); |
559 | actions.add(post.hidden ? ahref(unhideLink(post), "Unhide post") : ahref(hideLink(post), "Hide post")); |
560 | } |
561 | |
562 | // show reply link etc. |
563 | actions.add(ahref(replyLink(post), "Reply")); |
564 | actions.add(ahref(conceptDuplicateLink(post), "Duplicate")); |
565 | actions.add(ahref(conceptEditLink(post, onlyFields := "title"), "Rename")); |
566 | if (postHasHistory(post)) |
567 | actions.add(ahref("/history/" + post.id, "History")); |
568 | actions.add(ahref(baseLink + "/text/" + post.id, "Raw Text")); |
569 | |
570 | // show "Talk to this bot" |
571 | if (post.isJavaXCode()) { |
572 | actions.add(targetBlank(simulateBotLink(post), "Talk to this bot")); |
573 | actions.add(ahref(gazelleBotURL + "/transpilation/" + post.id, "Show Java transpilation")); |
574 | } |
575 | |
576 | framer.add(p_vbar(actions)); |
577 | |
578 | // show post refs |
579 | L<PostReferenceable> postRefs = cloneList(post.postRefs); |
580 | if (nempty(postRefs)) { |
581 | framer.add(p("In reference to:")); |
582 | LPair<PostReferenceable, S> l = post.postRefsWithTags(); |
583 | framer.add(ul(mapPairsToList(l, (ref, as) -> |
584 | htmlEncode2(as) + " " + objectToHTML(ref)))); |
585 | } |
586 | |
587 | S text = post.text; |
588 | S html = null; |
589 | if (post.isJavaXCode()) pcall { |
590 | new JavaXHyperlinker hl; |
591 | hl.targetBlank = true; |
592 | html = hl.codeToHTML(text); |
593 | } |
594 | if (html == null) |
595 | html = htmlEncodeWithLinking(post.text); |
596 | |
597 | if (eq(req.params.get("showLineFeeds"), "1")) |
598 | html = html_showLineFeedsForPRE(html); |
599 | |
600 | // render the actual text |
601 | framer.add(div(sourceCodeToHTML_noEncode(html), style := hstyle_sourceCodeLikeInRepo())); |
602 | |
603 | // show referencing posts |
604 | Cl<UserPost> refs = referencingPosts(post); |
605 | refs = reversed(refs); // latest on top |
606 | Pair<L<UserPost>> p = filterAntiFilter(refs, post2 -> !post2.hidden); |
607 | if (nempty(p.a)) { |
608 | framer.add(p("Referenced by posts (latest first):")); |
609 | UserPost latestCodeSafetyPost = highestBy(p2 -> p2.modifiedOrBumped(), filter(p.a, p2 -> eqic(p2.botInfo, "Code Safety Checker"))); |
610 | framer.add(ul(map(p.a, p2 -> { |
611 | S html2 = htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2); |
612 | if (p2 == latestCodeSafetyPost && cic(p2.text, "Unknown identifiers:")) |
613 | html2 += " " + hbuttonLink("/markSafe/" + p2.id, htmlEncode2("Mark safe")); |
614 | ret html2; |
615 | }))); |
616 | } |
617 | if (nempty(p.b)) { |
618 | framer.add(p("Referenced by hidden posts:")); |
619 | framer.add(ul(map(p.b, p2 -> htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2)))); |
620 | } |
621 | |
622 | if (showPostStats) { |
623 | S stats = nLines(countLines(post.text)) + ", distance to user post: " + post.distanceToUserPost(); |
624 | framer.add(p_alignRight(small(span_title(stats, "Post stats")))); |
625 | } |
626 | ret framer.render(); |
627 | } |
628 | |
629 | S getPostRefTag(UserPost a, UserPost b) { |
630 | ret get(a.postRefTags, indexOf(a.postRefs, b)); |
631 | } |
632 | |
633 | // turn "post 123" into a link to post 123 |
634 | S htmlEncodeWithLinking(S text) { |
635 | S html = htmlEncode2(text); |
636 | ret regexpReplace(html, gazelle_postMentionRegexp(), |
637 | matcher -> { |
638 | UserPost post = getConceptOpt UserPost(parseLong(matcher.group(1))); |
639 | ret post == null ? matcher.group() |
640 | : ahref(postLink(post), matcher.group()); |
641 | }); |
642 | } |
643 | |
644 | L<UserPost> referencingPosts(UserPost post) { |
645 | ret sortedByCalculatedField(p -> p.modifiedOrBumped(), instancesOf UserPost(allBackRefs(post))); |
646 | } |
647 | |
648 | L<UserPost> referencingPostsWithTag(UserPost post, S tag) { |
649 | ret filter(referencingPosts(post), p -> eqic_unnull(getPostRefTag(p, post), tag)); |
650 | } |
651 | |
652 | |
653 | S serveAuthForm(S redirect) { |
654 | ret hAddToHead(super.serveAuthForm(redirect), hInitWebSocket()); |
655 | } |
656 | |
657 | S authFormMoreContent() { |
658 | ret navDiv(); |
659 | } |
660 | |
661 | S navDiv() { |
662 | ret div_vbar(navLinks(), style := "margin-bottom: 0.5em"); |
663 | } |
664 | |
665 | // handle user log-in (create/update AuthedDialogID concept) |
666 | O handleAuth(Req req, S cookie) null { |
667 | S name = trim(req.params.get('user)), pw = trim(req.params.get("pw")); |
668 | if (nempty(name) && nempty(pw) && nempty(cookie)) { |
669 | Domain authDomain; |
670 | |
671 | User user = findOrCreateUserForLogin(name, pw); |
672 | if (user == null) |
673 | ret errorMsg("User " + htmlEncode2(name) + " not found"); |
674 | if (!eq(getVar(user.passwordMD5), hashPW(pw))) |
675 | ret errorMsg("Bad password, please try again"); |
676 | |
677 | // auth |
678 | cset(uniq AuthedDialogID(+cookie), restrictedToDomain := null, master := user.isMaster, +user); |
679 | |
680 | S redirect = req.params.get('redirect); |
681 | if (nempty(redirect)) |
682 | ret hrefresh(redirect); |
683 | } |
684 | } |
685 | |
686 | // e.g. grab from central database |
687 | // does not have to check the PW |
688 | User findOrCreateUserForLogin(S name, S pw) { |
689 | ret conceptWhereCI User(+name); |
690 | } |
691 | |
692 | |
693 | O serve2(Req req) { |
694 | /*S userMode = req.params.get("userMode"); |
695 | if (nempty(userMode) && req.auth != null) |
696 | req.auth.userMode = eq(userMode, "1");*/ |
697 | |
698 | if (req.auth != null) { |
699 | cset(req.auth, userMode := false); |
700 | |
701 | if (req.auth.user! != null) |
702 | cset(req.auth.user!, lastSeen := now()); |
703 | } |
704 | |
705 | ret super.serve2(req); |
706 | } |
707 | |
708 | S hashPW(S pw) { |
709 | if (nempty(salterService)) |
710 | ret assertMD5(postPage(salterService, +pw)); |
711 | else |
712 | ret md5(pw + passwordSalt()); |
713 | } |
714 | |
715 | Req currentReq() { |
716 | ret currentReq!; |
717 | } |
718 | |
719 | bool inMasterMode aka isMasterAuthed aka masterAuthed() { |
720 | ret inMasterMode(currentReq()); |
721 | } |
722 | |
723 | bool inMasterMode aka isMasterAuthed aka masterAuthed(Req req) { |
724 | ret req.masterAuthed && !(req.auth != null && req.auth.userMode); |
725 | } |
726 | |
727 | MapSO filtersForClass(Class c, Req req) { |
728 | if (c == UserPost) { |
729 | if (req.auth != null && !inMasterMode(req)) |
730 | ret litmap(creator := req.auth.user!); |
731 | } |
732 | ret super.filtersForClass(c, req); |
733 | } |
734 | |
735 | S loggedInUserDesc_html(Req req) { |
736 | // no auth |
737 | if (req.auth == null) |
738 | ret ahref(loginLink(), "not logged in"); |
739 | |
740 | // old school nameless master auth |
741 | if (req.auth.user! == null) |
742 | ret req.masterAuthed ? "master user" : "anonymous user " + req.auth.id; |
743 | |
744 | // actual Gazelle user |
745 | ret "user " + req.auth.user->name; |
746 | } |
747 | |
748 | S loginLink() { ret baseLink + "/"; } |
749 | |
750 | <A extends Concept> HCRUD_Concepts<A> crudData(Class<A> c, Req req) { |
751 | HCRUD_Concepts<A> cc = super.crudData(c, req); |
752 | |
753 | if (c == UserPost) { |
754 | cc.useDynamicComboBoxes = true; |
755 | cc.lsMagic = true; |
756 | cc.itemName = () -> "Post"; |
757 | |
758 | cc.dropEmptyListValues = false; |
759 | //cc.verbose = true; |
760 | |
761 | if (eq(req.params.get("noace"), "1")) |
762 | cc.addRenderer("text", new HCRUD_Data.TextArea(80, 20)); |
763 | else |
764 | cc.addRenderer("text", new HCRUD_Data.AceEditor(80, 20)); |
765 | |
766 | cc.addRenderer("postRefTags", new HCRUD_Data.FlexibleLengthList(new HCRUD_Data.TextField(20))); |
767 | cc.addRenderer("type", new HCRUD_Data.ComboBox(true, itemPlus("", userPostTypesByPopularity()))); |
768 | |
769 | cc.fieldHelp( |
770 | type := "Type of post (any format, optional)", |
771 | title := "Title for this post (any format, optional)", |
772 | text := "Text contents of this post (any format)", |
773 | nameInRepo := "Name in repository (not really used yet)", |
774 | botInfo := "Info on which bot made this post (if any)"); |
775 | |
776 | cc.massageItemMapForList = (item, map) -> { |
777 | applyFunctionToValue shorten(map, "text", "title", "type", "nameInRepo"); |
778 | L<PostReferenceable> refs = item/UserPost.postRefs; |
779 | map.put("postRefs", HTML(joinWithBR(lmap objectToHTML(refs)))); |
780 | }; |
781 | |
782 | cc.getObject = id -> { |
783 | MapSO map = cc.getObject_base(id); |
784 | L refs = cast map.get("postRefs"); |
785 | L tags = cast map.get("postRefTags"); |
786 | print("counts:" + l(refs) + "/" + l(tags)); |
787 | map.put("postRefTags", padList(tags, l(refs), "")); |
788 | ret map; |
789 | }; |
790 | |
791 | cc.emptyObject = () -> { |
792 | MapSO item = cc.emptyObject_base(); |
793 | item.put(creator := req.auth.user!); // set default creator to current user |
794 | ret item; |
795 | }; |
796 | |
797 | cc.getObjectForDuplication = id -> { |
798 | MapSO item = cc.getObjectForDuplication_base(id); |
799 | item.put(creator := req.auth.user!); // set default creator to current user |
800 | |
801 | // get post and refs |
802 | |
803 | UserPost post = getConcept UserPost(toLong(id)); |
804 | L postRefs = cloneList((L) item.get("postRefs")); |
805 | LS tags = cloneList((L) item.get("postRefTags")); |
806 | |
807 | // drop old "created from" references |
808 | for i over tags: |
809 | if (eqic(tags.get(i), "created from")) { |
810 | remove(postRefs, i); |
811 | tags.remove(i--); |
812 | } |
813 | |
814 | // add "created from" reference |
815 | int idx = l(postRefs); |
816 | postRefs.add(post); |
817 | listSet(tags, idx, "created from"); |
818 | |
819 | // store refs |
820 | item.put("postRefs", postRefs); |
821 | item.put("postRefTags", tags); |
822 | print(+tags); |
823 | |
824 | ret item; |
825 | }; |
826 | |
827 | cc.fieldsToHideInCreationForm = litset("hidden", "creating"); |
828 | |
829 | cc.lockDB = true; |
830 | cc.afterUpdate = (post, oldValues) -> { |
831 | MapSO newValues = cgetAll_cloneLists(post, keys(oldValues)); |
832 | for (S field, O newVal : newValues) { |
833 | O oldVal = oldValues.get(field); |
834 | if (eq(oldVal, newVal)) { |
835 | printVars_str(+field, +oldVal); |
836 | continue; |
837 | } |
838 | IF1 f = o -> o instanceof Concept ? o/Concept.id : null; |
839 | O oldValForLog = defaultMetaTransformer().transform(f, oldVal); |
840 | O newValForLog = defaultMetaTransformer().transform(f, newVal); |
841 | Map logEntry = litorderedmap(date := now(), +field, oldVal := oldValForLog, newVal := newValForLog); |
842 | printStruct(+logEntry); |
843 | logStructure(postHistoryFile(post/UserPost), logEntry); |
844 | } |
845 | }; |
846 | |
847 | cc.actuallyDeleteConcept = post -> deletePost(post/UserPost); |
848 | } |
849 | ret cc; |
850 | } |
851 | |
852 | <A extends Concept> HCRUD makeCRUD(Class<A> c, Req req) { |
853 | HCRUD crud = super.makeCRUD(c, req); |
854 | if (c == UserPost) { |
855 | crud.refreshAfterCommand = (params, msgs) -> { |
856 | UserPost post = getConcept UserPost(toLong(crud.objectIDToHighlight)); |
857 | ret post != null ? hrefresh(postLink(post)) : crud.refreshAfterCommand_base(params, msgs); |
858 | }; |
859 | |
860 | crud.renderCmds = map -> { |
861 | UserPost post = getConcept UserPost(toLong(crud.itemID(map))); |
862 | ret joinNemptiesWithVBar(crud.renderCmds_base(map), |
863 | ahref(postLink(post), "Show post"), |
864 | ahref(touchLink(post), "Touch", title := "Mark post as modified so it is looked at by bots again")); |
865 | }; |
866 | crud.uneditableFields = litset("xmodified", "creating", "bumped"); |
867 | |
868 | if (showSuggestorBot) |
869 | crud.moreSelectizeOptions = name -> [[, onChange: function() { console.log("selectize onChange"); sugTrigger(); }]]; |
870 | |
871 | crud.showQuickSaveButton = true; |
872 | crud.massageFormMatrix = (map, matrix) -> { |
873 | /*if (map.containsKey(crud.idField())) // editing, not creating |
874 | matrix.add(0, ll("", hbuttonOnClick_noSubmit("Save & keep editing", [[ |
875 | $.ajax({ |
876 | type: 'POST', |
877 | url: $('#crudForm').attr('action'), |
878 | data: $('#crudForm').serialize(), |
879 | success: function(response) { successNotification("Saved"); }, |
880 | }).error(function() { errorNotification("Couldn't save"); }); |
881 | ]])));*/ |
882 | |
883 | // mergeTables businesss broken somehow |
884 | /*int idx1 = indexOfPred(matrix, row -> cic(first(row), "Post Refs")); |
885 | int idx2 = indexOfPred(matrix, row -> cic(first(row), "Post Ref Tags")); |
886 | if (idx1 < 0 || idx2 < 0) ret; // reduced form (e.g. rename) |
887 | LS row1 = matrix.get(idx1), row2 = matrix.get(idx2); |
888 | row1.set(1, hcrud_mergeTables(row1.get(1), row2.get(1), "as")); |
889 | matrix.remove(idx2);*/ |
890 | |
891 | // TODO: handle changes in AceEditor |
892 | if (showSuggestorBot) { |
893 | LS entries = ((HCRUD_Concepts) crud.data).comboBoxItems( |
894 | conceptsWhereIC(UserPost, type := "JavaX Code (Post Edit Suggestor)")); |
895 | S js = hscript(replaceVars([[ |
896 | const suggestorSelector = $("[name=suggestorID]"); |
897 | var sugLoading = false, sugTriggerAgain = false; |
898 | |
899 | function sugTrigger() { |
900 | //console.log("sugTrigger"); |
901 | if (sugLoading) { sugTriggerAgain = true; return; } |
902 | |
903 | const sugText = suggestorSelector.text(); |
904 | //console.log("sugText=" + sugText); |
905 | const sugMatch = sugText.match(/\d+/); |
906 | if (!sugMatch) { |
907 | //$("#suggestorResult").html(""); |
908 | $("#suggestorResult").hide(); |
909 | return; |
910 | } |
911 | const suggestorID = sugMatch[0]; |
912 | |
913 | // get form data as JSON |
914 | |
915 | var data = {}; |
916 | $(suggestorSelector).closest("form").serializeArray().map(function(x){data[x.name] = x.value;}); |
917 | const json = JSON.stringify(data); |
918 | console.log("JSON: " + json); |
919 | |
920 | const url = "]] + gazelleBotURL + [[/chatBotReply/" + suggestorID; |
921 | console.log("Loading " + url); |
922 | sugLoading = true; |
923 | $.post(url, {q : json}, |
924 | function(result) { |
925 | console.log("Suggestor result: " + result); |
926 | const answer = !result ? "" : JSON.parse(result).answer; |
927 | if (answer) { |
928 | $("#suggestorResult .sectionContents").html(answer); |
929 | $("#suggestorResult").show(); |
930 | } else |
931 | $("#suggestorResult").hide(); |
932 | //$("#suggestorResult").html(answer ? "Suggestor says: " + answer : ""); |
933 | } |
934 | ).always(function() { |
935 | console.log("sug loading done"); |
936 | setTimeout(function() { |
937 | sugLoading = false; |
938 | if (sugTriggerAgain) { sugTriggerAgain = false; sugTrigger(); } |
939 | }, delayAfterSuggestion); |
940 | }); |
941 | } |
942 | |
943 | $(document).ready(function() { |
944 | $("input[type=text], input[type=hidden], textarea, select").on('input propertychange change', sugTrigger); |
945 | sugTrigger(); |
946 | }); |
947 | suggestorSelector.change(sugTrigger); |
948 | ]], +delayAfterSuggestion)); |
949 | |
950 | long defaultSuggestor = parseFirstLong(userDefaults(req.auth.user!).get("Default Suggestor Bot")); |
951 | S selectedSuggestor = firstWhereFirstLongIs(entries, defaultSuggestor); |
952 | |
953 | matrix.add(0, ll("Suggestor Bot", crud.addHelpText( |
954 | "Choose a bot to assist you in editing this post [feature in development]", |
955 | p_alignRight(crud.renderComboBox("suggestorID", selectedSuggestor, entries, false))) |
956 | //+ hdiv("", id := "suggestorResult") |
957 | + htitledSectionWithDiv("Suggestion", "", |
958 | id := "suggestorResult", style := "display: none", |
959 | innerDivStyle := "max-height: 150px; overflow: auto") |
960 | + js)); |
961 | } |
962 | }; |
963 | |
964 | crud.flexibleLengthListLeeway = 3; |
965 | } |
966 | |
967 | if (c == User) { |
968 | crud.allowCreate = false; |
969 | //crud.uneditableFields = litset("passwordMD5"); |
970 | } |
971 | |
972 | ret crud; |
973 | } |
974 | |
975 | S authFormHeading() { |
976 | ret h3_htmlEncode("Welcome to the Gazelle AI System") |
977 | + p(hsnippetimg_scaleToWidth(200, #1101482, 425, 257, title := "Gazelle")); |
978 | } |
979 | |
980 | void makeFramer(Req req) { |
981 | super.makeFramer(req); |
982 | /*if (req.masterAuthed) |
983 | req.framer.add(p_vbar( |
984 | ahrefIf(req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 0), "Master mode", title := "View pages as master"), |
985 | ahrefIf(!req.auth.userMode, appendQueryToURL(baseLink + req.uri, userMode := 1), "User mode", title := "View pages as user")));*/ |
986 | req.framer.renderTitle = () -> h1(ahref(baseLink + "/", himgsnippet(#1102967, style := "height: 1em; vertical-align: bottom", title := "Gazelle.rocks Logo")) |
987 | + " " + req.framer.encodedTitle()); |
988 | |
989 | req.framer.add(() -> navDiv()); // calculate on completeFrame |
990 | |
991 | if (showMetaBotOnEveryPage && !eq(req.params.get("_reloading"), "1")) |
992 | req.framer.willRender.add(r { |
993 | req.framer.add(hscript([[var botConfig = "codePost=]] + defaultBotPost + [["; ]]) + hjssrc(baseLink + "/script")); |
994 | }); |
995 | } |
996 | |
997 | HTMLFramer1 framer(Req req default currentReq!) { |
998 | if (req.framer == null) makeFramer(req); |
999 | ret req.framer; |
1000 | } |
1001 | |
1002 | // link to show post |
1003 | S postLink(UserPost post) { |
1004 | ret objectLink(post); |
1005 | } |
1006 | |
1007 | S postLink(long postID) { |
1008 | ret objectLink(getConcept UserPost(postID)); |
1009 | } |
1010 | |
1011 | S touchLink(UserPost post) { ret baseLink + "/touchPost/" + post.id; } |
1012 | S hideLink(UserPost post) { ret baseLink + "/hidePost/" + post.id; } |
1013 | S unhideLink(UserPost post) { ret baseLink + "/unhidePost/" + post.id; } |
1014 | |
1015 | S objectLink(Concept c) { |
1016 | ret baseLink + "/" + c.id; |
1017 | } |
1018 | |
1019 | S objectToHTML(Concept c) { |
1020 | ret c == null ? "-" : ahref(objectLink(c), htmlEncode2(str(c))); |
1021 | } |
1022 | |
1023 | S postToHTMLWithDate(UserPost post) { |
1024 | ret post == null ? "" : objectToHTML(post) |
1025 | + " " + span(htmlEncode2( |
1026 | renderHowLongAgoPlusModified(post.created, post._modified)), style := "color: #888"); |
1027 | } |
1028 | |
1029 | bool canEditPost(UserPost post) { |
1030 | User user = currentUser(); |
1031 | ret user != null && (user.isMaster || user == post.creator!); |
1032 | } |
1033 | |
1034 | User currentUser() { |
1035 | Req req = currentReq!; |
1036 | ret req != null && req.auth != null ? req.auth.user! : null; |
1037 | } |
1038 | |
1039 | S replyLink(UserPost post) { |
1040 | ret appendQueryToURL(crudLink(UserPost), cmd := "new", fList_postRefs_0 := post?.id); |
1041 | } |
1042 | |
1043 | S newPostLink() { |
1044 | ret replyLink(null); |
1045 | } |
1046 | |
1047 | LS navLinks(O... _) { |
1048 | ret gazelle_navLinks(baseLink, inlineSearch && !eq(currentReq->uri, "/search") ? "" : null, |
1049 | paramsPlus(_, withTeam := teamPostID != 0)); |
1050 | } |
1051 | |
1052 | L<Class> botCmdClasses() { |
1053 | ret ll(); |
1054 | } |
1055 | |
1056 | L<Class> crudClasses(Req req) { |
1057 | L<Class> l = super.crudClasses(req); |
1058 | l = listMinusSet(l, hiddenCrudClasses()); |
1059 | l.add(UserPost); |
1060 | l.add(UploadedImage); |
1061 | if (req.masterAuthed) { |
1062 | l.add(User); |
1063 | |
1064 | // currently not putting in CRUD list |
1065 | // (but can be viewed with /crud/WebPushSubscription) |
1066 | //if (webPushEnabled) l.add(WebPushSubscription); |
1067 | } |
1068 | ret l; |
1069 | } |
1070 | |
1071 | @Override Cl<Class> cruddableClasses(Req req) { |
1072 | ret addAllAndReturnCollection(super.cruddableClasses(req), WebPushSubscription); |
1073 | } |
1074 | |
1075 | Set<Class> hiddenCrudClasses() { |
1076 | ret litset(Lead, ConversationFeedback, Domain, UserKeyword, UploadedSound, Settings); |
1077 | } |
1078 | |
1079 | O getPostFieldForBot(UserPost post, S field) { |
1080 | if (eq(field, "creatorID")) ret post.creator->id; |
1081 | if (eq(field, "creatorName")) ret post.creator->name; |
1082 | if (eq(field, "creatorIsMaster")) ret post.creator->isMaster; |
1083 | if (eq(field, "postRefs")) ret lmap conceptID(post.postRefs); |
1084 | ret cget(post, field); |
1085 | } |
1086 | |
1087 | void deletePost(UserPost post) { |
1088 | if (post == null) ret; |
1089 | print("Deleting post " + post); |
1090 | lock dbLock(); |
1091 | long filePos = l(deletedPostsFile()); |
1092 | logStructure(deletedPostsFile(), |
1093 | mapToValues(UserPost.fieldsToSaveOnDelete, |
1094 | field -> getPostFieldForBot(post, field))); |
1095 | for (Pair<PostReferenceable, S> p : post.postRefsWithTags()) |
1096 | if (p.a instanceof UserPost) |
1097 | logStructure(deletedRepliesFile((UserPost) p.a), litorderedmap(tag := p.b, id := post.id, +filePos, type := post.type)); |
1098 | cdelete(post); |
1099 | } |
1100 | |
1101 | File deletedPostsFile() { |
1102 | ret programFile("deleted-posts.log"); |
1103 | } |
1104 | |
1105 | O serveHTMLPost(long id) { |
1106 | UserPost post = getConcept UserPost(id); |
1107 | if (post == null) ret serve404("Post " + id + " not found"); |
1108 | ret post.text; |
1109 | } |
1110 | |
1111 | transient ReliableSingleThread_Multi<UserPost> rstDistributePostChanges = new(1000, lambda1 distributePostChanges_impl); |
1112 | |
1113 | void distributePostChanges(UserPost post) { |
1114 | rstDistributePostChanges.add(post); |
1115 | } |
1116 | |
1117 | // This is also called when replies change |
1118 | void distributePostChanges_impl(UserPost post) enter { |
1119 | //print("distributePostChanges_impl " + post); |
1120 | |
1121 | S uri = "/" + post.id; |
1122 | for (Pair<virtual WebSocket, WebSocketInfo> p : syncMapToPairs(webSockets)) { |
1123 | if (eq(p.b.uri, uri)) |
1124 | reloadBody(p.a); |
1125 | } |
1126 | |
1127 | if (eqic(post.type, "Line Labels")) { |
1128 | UserPost parent = post.primaryRefPost(); |
1129 | print("Line labels post " + post.id + ", parent=" + parent); |
1130 | updateLineLabels(parent); |
1131 | } |
1132 | } |
1133 | |
1134 | void updateLineLabels(UserPost post) pcall { |
1135 | if (post == null || !eqic(post.type, "Detailed Conversation Mirror")) ret; |
1136 | print("Looking at conversation mirror " + post.id + " - " + post.title); |
1137 | long convID = toLong(regexpFirstGroupIC("Conversation (\\d+) with details", post.title)); |
1138 | Conversation conv = getConcept Conversation(convID); |
1139 | if (conv == null) ret; |
1140 | print("updateLineLabels " + convID + " / " + post.id); |
1141 | |
1142 | new MultiMap<Pair<Long, S>, S> labels; |
1143 | for (UserPost p : referencingPostsWithTag(post, "")) |
1144 | if (eqic(p.type, "Line Labels")) pcall { |
1145 | Map map = safeUnstructMapAllowingClasses(p.text(), Pair); |
1146 | print("Got labels map: " + map); |
1147 | putAll(labels, map); |
1148 | } |
1149 | |
1150 | bool change; |
1151 | for (Msg msg : cloneList(conv.msgs)) { |
1152 | LS lbls = uniquifyAndSortAlphaNum(allToUpper(labels.get(pair(msg.time, msg.text)))); |
1153 | if (nempty(lbls)) |
1154 | print("Got labels: " + lbls); |
1155 | if (!eq(msg.labels, lbls)) { |
1156 | msg.labels = lbls; set change; |
1157 | } |
1158 | } |
1159 | |
1160 | if (change) |
1161 | conv.incReloadCounter(); |
1162 | } |
1163 | |
1164 | void reloadBody(virtual WebSocket ws) { |
1165 | print("Reloading body through WebSocket"); |
1166 | S jsCode = [[ { |
1167 | const loc = new URL(document.location); |
1168 | const params = new URLSearchParams(loc.search); |
1169 | params.set('_reloading', '1'); |
1170 | loc.search = params; |
1171 | console.log("Getting: " + loc); |
1172 | $.get(loc, function(html) { |
1173 | var bodyHtml = /<body.*?>([\s\S]*)<\/body>/.exec(html)[1]; |
1174 | if (bodyHtml) { |
1175 | //$("body").html(bodyHtml); |
1176 | $('body > *:not(.chatbot)').remove(); |
1177 | $("body").prepend(bodyHtml); |
1178 | } |
1179 | }); |
1180 | } ]]; |
1181 | dm_call(ws, "send", jsonEncode(litmap(eval := jsCode))); |
1182 | } |
1183 | |
1184 | transient ReliableSingleThread_Multi<S> rstDistributeDivChanges = new(1000, lambda1 distributeDivChanges_impl, func -> AutoCloseable { enter() }); |
1185 | |
1186 | void distributeDivChanges(S contentDesc) { |
1187 | rstDistributeDivChanges.add(contentDesc); |
1188 | } |
1189 | |
1190 | void distributeDivChanges_impl(S contentDesc) enter { |
1191 | //print("distributeDivChanges_impl " + contentDesc); |
1192 | S content = null; |
1193 | for (Pair<virtual WebSocket, WebSocketInfo> p : syncMapToPairs(webSockets)) { |
1194 | for (S div : asForPairsWithB(p.b.liveDivs, contentDesc)) { |
1195 | if (content == null) content = calcDivContent(contentDesc); |
1196 | if (content == null) { print("No content for " + contentDesc); ret; } |
1197 | reloadDiv(p.a, div, content); |
1198 | } |
1199 | } |
1200 | } |
1201 | |
1202 | void reloadDiv(virtual WebSocket ws, S div, S content) { |
1203 | //print("Reloading div " + div + " through WebSocket"); |
1204 | S jsCode = replaceDollarVars( |
1205 | [[ $("#" + $div).html($content);]], |
1206 | div := jsQuote(div), content := jsQuote(content)); |
1207 | dm_call(ws, "send", jsonEncode(litmap(eval := jsCode))); |
1208 | } |
1209 | |
1210 | S calcDivContent(S contentDesc) { |
1211 | if (eq(contentDesc, "postCount")) |
1212 | ret nPosts(countConcepts(UserPost)); |
1213 | |
1214 | if (eq(contentDesc, "webRequestsRightHemi")) |
1215 | ret n2(requestsServed); |
1216 | |
1217 | if (eq(contentDesc, "serverLoadRightHemi")) |
1218 | ret formatDoubleX(systemLoad, 1); |
1219 | |
1220 | if (eq(contentDesc, "memRightHemi")) |
1221 | ret str_toM(processSize); |
1222 | |
1223 | null; |
1224 | } |
1225 | |
1226 | int countUserPosts() { ret countConcepts(UserPost, botInfo := null) + countConcepts(UserPost, botInfo := ""); } |
1227 | int countBotPosts() { ret countConcepts(UserPost)-countUserPosts(); } |
1228 | |
1229 | Cl<UserPost> allUserPosts() { ret filter(list(UserPost), p -> !p.isBotPost()); } |
1230 | |
1231 | LS userPostTypesByPopularity() { |
1232 | ret listToTopTenCI(map(allUserPosts(), p -> p.type)); |
1233 | } |
1234 | |
1235 | void startMainScript(Conversation conv) { |
1236 | UserPost post = getConceptOpt UserPost(parseLong(mapGet(conv.botConfig, "codePost"))); |
1237 | if (post != null) { |
1238 | CustomBotStep step = uniq CustomBotStep(codePostID := post.id); |
1239 | if (executeStep(step, conv)) |
1240 | nextStep(conv); |
1241 | } else |
1242 | super.startMainScript(conv); |
1243 | } |
1244 | |
1245 | S simulateBotLink(UserPost post) { |
1246 | ret appendQueryToURL(baseLink + "/demo", _botConfig := makePostData(codePost := post.id), |
1247 | _newConvCookie := 1, |
1248 | _autoOpenBot := 1); |
1249 | } |
1250 | |
1251 | @Override |
1252 | bool isRequestFromBot(Req req) { |
1253 | //print("isRequestFromBot: " + req.uri); |
1254 | ret startsWith(req.uri, "/bot/"); |
1255 | } |
1256 | |
1257 | User internalUser() { |
1258 | ret uniqCI(User, name := "internal"); |
1259 | } |
1260 | |
1261 | transient ReliableSingleThread_Multi<Conversation> rstUpdateMirrorPosts = new(100, c -> c.updateMirrorPost()); |
1262 | |
1263 | File postHistoryFile(UserPost post) { |
1264 | ret programFile("Post Histories/" + post.id + ".log"); |
1265 | } |
1266 | |
1267 | bool postHasHistory(UserPost post) { |
1268 | ret fileNempty(postHistoryFile(post)); |
1269 | } |
1270 | |
1271 | File deletedRepliesFile(UserPost post) { |
1272 | ret programFile("Deleted Replies/" + post.id + ".log"); |
1273 | } |
1274 | |
1275 | <A extends Concept> S makeClassNavItem(Class<A> c, Req req) { |
1276 | S html = super.makeClassNavItem(c, req); |
1277 | if (c == UserPost) { |
1278 | int count1 = countUserPosts(), count2 = countBotPosts(); |
1279 | if (count1 != 0 || count2 != 0) |
1280 | html += " " + roundBracket(n2(count1) + " from users, " |
1281 | + n2(count2) + " from bots"); |
1282 | } |
1283 | ret html; |
1284 | } |
1285 | |
1286 | @Override |
1287 | S modifyTemplateBeforeDelivery(S html, Req req) { |
1288 | // for design testing |
1289 | S contentsPostID = req.params.get("chatContentsPost"); |
1290 | if (nempty(contentsPostID)) { |
1291 | html = html.replace("#N#", "99999"); |
1292 | html = html.replace("chatBot_interval = 1000", "chatBot_interval = 3600*1000"); |
1293 | html = html.replace("#INCREMENTALURL#", baseLink + "/text/" + contentsPostID + "?"); |
1294 | } |
1295 | ret html; |
1296 | } |
1297 | |
1298 | S headingForReq(Req req) { |
1299 | S codePost = decodeHQuery(req.params.get("_botConfig")).get("codePost"); |
1300 | if (nempty(codePost)) { |
1301 | UserPost post = getConcept UserPost(parseLong(codePost)); |
1302 | ret post?.title; |
1303 | } |
1304 | null; |
1305 | } |
1306 | |
1307 | UserPost homePagePost() { |
1308 | ret firstThat(p -> p.isMasterMade(), conceptsWhereIC UserPost(type := "JavaX Code (Live Home Page)")); |
1309 | } |
1310 | |
1311 | O serveHomePage() { |
1312 | pcall { |
1313 | UserPost post = homePagePost(); |
1314 | if (post != null) |
1315 | ret loadPage(htmlBotURLOnBotServer(post.id)); |
1316 | } |
1317 | null; |
1318 | } |
1319 | |
1320 | S callHtmlBot(long id, SS params) { |
1321 | ret id == 0 ? "" : doPost(htmlBotURLOnBotServer(id), params); |
1322 | } |
1323 | |
1324 | S callHtmlBot_dropMadeByComment(long id, SS params) { |
1325 | ret regexpReplace_direct(callHtmlBot(id, params), "^<!-- Made by (.*?) -->\n", ""); |
1326 | } |
1327 | |
1328 | S htmlBotURL(long postID) { |
1329 | ret baseLink + "/htmlBot/" + postID; |
1330 | } |
1331 | |
1332 | S htmlBotURLOnBotServer(long postID) { |
1333 | ret gazelleBotURL + "/htmlBot/" + postID; |
1334 | } |
1335 | |
1336 | UserPost userDefaultsPost(User user) { |
1337 | ret conceptWhereIC UserPost(creator := user, type := "My Defaults"); |
1338 | } |
1339 | |
1340 | SS userDefaults(User user) { |
1341 | UserPost post = userDefaultsPost(user); |
1342 | ret post == null ? emptyMap() : parseColonPropertyCIMap(post.text); |
1343 | } |
1344 | |
1345 | double favIconCacheHours() { |
1346 | ret 24; |
1347 | } |
1348 | |
1349 | O favIconHeaders(O response) { |
1350 | call(response, "addHeader", "Cache-Control", "public, max-age=" + iround(hoursToSeconds(favIconCacheHours()))); |
1351 | ret response; |
1352 | } |
1353 | |
1354 | O serveFavIcon() { |
1355 | if (favIconID != 0) { |
1356 | UploadedFile f = getConcept UploadedFile(favIconID); |
1357 | if (f != null) |
1358 | ret favIconHeaders(subBot_serveFile(f.theFile(), faviconMimeType())); |
1359 | } |
1360 | |
1361 | ret favIconHeaders(subBot_serveFile(loadLibrary(defaultFavIconSnippet), faviconMimeType())); |
1362 | } |
1363 | |
1364 | S cssURL() { |
1365 | ret gazelle_textURL(parseLong(cssID)); |
1366 | } |
1367 | |
1368 | S getText(long postID) { |
1369 | UserPost post = getConcept UserPost(postID); |
1370 | ret post?.text(); |
1371 | } |
1372 | |
1373 | @Override |
1374 | void addThingsAboveCRUDTable(Req req, Class c, LS aboveTable) { |
1375 | if (c == User) |
1376 | aboveTable.add(p(ahref(baseLink + "/register", "Register new user"))); |
1377 | } |
1378 | |
1379 | User user(Req req) { |
1380 | if (req == null) null; |
1381 | AuthedDialogID auth = req.auth; |
1382 | ret auth?.user(); |
1383 | } |
1384 | |
1385 | O serveIntegerLink(Req req, long id) { |
1386 | UserPost post = getConceptOpt UserPost(id); |
1387 | if (post == null) null; //ret serve404("Post with ID " + id + " not found"); |
1388 | ret servePost(post, req); |
1389 | } |
1390 | |
1391 | void onNewWebSocket(WebSocketInfo ws) {} |
1392 | void onWebSocketClose(WebSocketInfo ws) { |
1393 | ws.callCloseHandlers(); |
1394 | } |
1395 | |
1396 | S cookieFromWebRequest(IWebRequest req) { |
1397 | if (req == null) null; |
1398 | try answer req.cookie(); |
1399 | ret afterLastEquals(req.headers().get("cookie")); |
1400 | } |
1401 | |
1402 | void makeMaster { |
1403 | var users = listMinus(list(User), internalUser()); |
1404 | if (empty(users)) |
1405 | ret with infoMessage("Please first register a user in the web interface!"); |
1406 | |
1407 | new ShowComboBoxForm cb; |
1408 | cb.desc = "Select user to upgrade"; |
1409 | cb.itemDesc = "User to make master"; |
1410 | cb.items = map(users, user -> user.id + " " + user.name); |
1411 | cb.action = user -> thread { |
1412 | enter(); |
1413 | long userID = parseFirstLong(user); |
1414 | User userObj = getConcept User(userID); |
1415 | printVars(+user, +userID, +userObj); |
1416 | makeMaster(userObj); |
1417 | }; |
1418 | cb.show(); |
1419 | } |
1420 | |
1421 | void makeMaster(User user) { |
1422 | if (user == null) ret; |
1423 | if (!swingConfirm("Turn " + user + " into a master user?")) ret; |
1424 | cset(user, isMaster := true); |
1425 | infoMessage(user + " is now my master!!"); |
1426 | } |
1427 | |
1428 | O[] popDownButtonEntries() { |
1429 | ret objectArrayPlus(super.popDownButtonEntries(), |
1430 | "Make master user..." := rThreadEnter makeMaster |
1431 | ); |
1432 | } |
1433 | } // end of module |
1434 | |
1435 | // Talk to a bot that is implemented in a Gazelle post |
1436 | concept CustomBotStep > BotStep implements IInputHandler { |
1437 | long codePostID; |
1438 | |
1439 | toString { ret "CustomBotStep " + codePostID; } |
1440 | |
1441 | bool run(Conversation conv) { |
1442 | S answer; |
1443 | try { |
1444 | Map result = cast postJSONPage(replyURL(), initial := 1, cookie := conv.cookie); |
1445 | answer = (S) result.get("answer"); |
1446 | } catch print e { |
1447 | answer = "Error: " + e.getMessage(); |
1448 | } |
1449 | conv.add(new Msg(false, answer)); |
1450 | cset(conv, inputHandler := this); |
1451 | false; |
1452 | } |
1453 | |
1454 | S replyURL() { |
1455 | ret ((DynGazelleRocks) botMod()).gazelleBotURL + "/chatBotReply/" + codePostID; |
1456 | } |
1457 | |
1458 | public bool handleInput(S s, Conversation conv) { |
1459 | S answer; |
1460 | try { |
1461 | Map result = cast postJSONPage(replyURL(), q := s, cookie := conv.cookie); |
1462 | answer = (S) result.get("answer"); |
1463 | } catch print e { |
1464 | answer = "Error: " + e.getMessage(); |
1465 | } |
1466 | conv.add(new Msg(false, answer)); |
1467 | true; // acknowledge that we handled the input |
1468 | } |
1469 | } // end of CustomBotStep |
1470 | |
1471 | sS userName(User user) { |
1472 | ret user != null ? user.name : "?"; |
1473 | } |
1474 | |
1475 | concept WebPushSubscription { |
1476 | S clientIP; |
1477 | MapSO data; |
1478 | } |
Began life as a copy of #1029913
download show line numbers debug dex old transpilations
Travelled to 4 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, vouqrxazstgt
No comments. add comment
Snippet ID: | #1030607 |
Snippet name: | DynGazelleRocks |
Eternal ID of this version: | #1030607/103 |
Text MD5: | a7c07e154b92c64bc47693a87c3a5d3a |
Transpilation MD5: | 6a806ec87525860fc18369d73478f31c |
Author: | stefan |
Category: | javax |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2021-10-11 04:35:05 |
Source code size: | 50750 bytes / 1478 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 607 / 1179 |
Version history: | 102 change(s) |
Referenced in: | [show references] |