Warning: session_start(): open(/var/lib/php/sessions/sess_72t0pbmfc4prapdiupntdhb1rr, O_RDWR) failed: No space left on device (28) in /var/www/tb-usercake/models/config.php on line 51
Warning: session_start(): Failed to read session data: files (path: /var/lib/php/sessions) in /var/www/tb-usercake/models/config.php on line 51
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 {}
concept User > PostReferenceable {
S name;
SecretValue passwordMD5;
S contact; // e.g. mail address
bool isMaster;
SecretValue botToken = aSecretGlobalID/*UnlessLoading*/();
toString { ret nempty(name) ? "User " + name : super.toString(); }
}
extend AuthedDialogID {
new Ref user; // who is logged in
bool userMode; // show things as if user was not master
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;
switchable S gazelleBotURL = "https://gazellebot.botcompany.de";
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;
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 {
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");
}
class WebSocketInfo {
S uri;
SS params;
WeakReference webSocket;
O dbRepresentation;
new Set> liveDivs; // id/content info
}
transient Map webSockets = syncWeakHashMap();
void cleanMeUp_webSockets {
closeAllKeysAndClear((Map) webSockets);
}
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);
new WebSocketInfo info;
info.webSocket = weakRef(ws);
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");
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 = conceptWhereCI User(+name);
if (user == null)
ret errorMsg("User " + htmlEncode2(name) + " not found");
if (!eq(getVar(user.passwordMD5), hashPW(pw)))
ret errorMsg("Bad password, please try again");
// auth
cset(uniq AuthedDialogID(+cookie), restrictedToDomain := null, master := user.isMaster, +user);
S redirect = req.params.get('redirect);
if (nempty(redirect))
ret hrefresh(redirect);
}
}
O serve2(Req req) {
/*S userMode = req.params.get("userMode");
if (nempty(userMode) && req.auth != null)
req.auth.userMode = eq(userMode, "1");*/
if (req.auth != null) cset(req.auth, userMode := false);
ret super.serve2(req);
}
S hashPW(S pw) { ret md5(pw + passwordSalt()); }
bool inMasterMode aka isMasterAuthed() {
ret inMasterMode(currentReq!);
}
bool inMasterMode aka isMasterAuthed(Req req) {
ret req.masterAuthed && !req.auth.userMode;
}
MapSO filtersForClass(Class c, Req req) {
if (c == UserPost) {
if (req.auth != null && !inMasterMode(req))
ret litmap(creator := req.auth.user!);
}
ret super.filtersForClass(c, req);
}
S loggedInUserDesc_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" : "weird user";
// 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"); });
]])));*/
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());
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 = 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;
}
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)
html += " " + roundBracket(n2(countUserPosts()) + " from users, "
+ n2(countBotPosts()) + " 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)"));
}
S 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);
}
O serveFavIcon() {
if (favIconID != 0) {
UploadedFile f = getConcept UploadedFile(favIconID);
if (f != null)
ret subBot_serveFile(f.theFile(), faviconMimeType());
ret subBot_serve404();
}
ret subBot_serveFile(loadLibrary(#1400439), 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) ret serve404("Post with ID " + id + " not found");
ret servePost(post, req);
}
void onNewWebSocket(WebSocketInfo ws) {}
void onWebSocketClose(WebSocketInfo ws) {}
} // 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;
}