Warning: session_start(): open(/var/lib/php/sessions/sess_51mlh017drls3b2ebmlrhs953e, 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
!7
sS passwordSalt() {
ret loadTextFileOrCreateWithRandomID(javaxSecretDir("password-salt"));
}
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
}
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)
// 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 = "isPublic text type title postRefs";
toString {
S content = nempty(title) ? shorten(title) : shorten(text);
S _type = isReply() ? "reply" : "post";
if (hidden) _type += " (hidden)";
ret firstToUpper(_type) + " by " + author() + ": " + content;
}
bool isReply() { ret nempty(postRefs); }
bool isBotPost() { ret nempty(botInfo); }
S author() {
S author = userName(creator!);
if (isBotPost()) author += "'s bot " + quote(botInfo);
ret author;
}
void change {
super.change();
((GazelleExamples) botMod()).distributePostChanges(this);
}
LPair postRefsWithTags() {
ret zipTwoListsToPairs_lengthOfFirst(postRefs, postRefTags);
}
void _setModified(long modified) {
super._setModified(modified);
setXModified(modified);
}
void bump {
bumped = now();
_change_withoutUpdatingModifiedField();
for (UserPost p : syncInstancesOf UserPost(postRefs)) p.setXModified();
}
void setXModified(long modified default now()) {
bool changed = modified > xmodified;
xmodified = modified;
_change_withoutUpdatingModifiedField();
if (changed)
((GazelleExamples) botMod()).distributePostChanges(this);
}
void _backRefsModified {
super._backRefsModified();
setXModified(now());
}
} // end of UserPost
/*concept BotHandle > UserCreatedObject {
S globalID = aGlobalIDUnlessLoading();
S comment;
SecretValue token;
}*/
set flag NoNanoHTTPD.
cmodule2 GazelleExamples > DynNewBot2 {
void start {
botName = heading = adminName = "gazelle.rocks";
set enableUsers;
set useWebSockets;
set showRegisterLink;
unset showTalkToBotLink;
set alwaysRedirectToHttps;
set redirectOnLogout;
passwordSalt(); // make early
super.start();
db_mainConcepts().modifyOnCreate = true;
indexConceptFieldCI(User, "name");
indexConceptFieldDesc(UserPost, "xmodified");
indexConceptFieldCI(UserPost, "botInfo");
// legacy clean-up
/*for (UserPost post) {
for i to 5:
cset(post, "postRefTags_" + i, null);
}*/
}
// web socket stuff
class WebSocketInfo {
S uri;
SS params;
}
transient Map webSockets = syncWeakHashMap();
void cleanMeUp_webSockets {
closeAllKeysAndClear((Map) webSockets);
}
void handleWebSocket(virtual WebSocket ws) {
set(ws, onClose := r { 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.uri = uri;
info.params = params;
webSockets.put(ws, 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);
}
});
}
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)) {
cnew User(name := user, passwordMD5 := SecretValue(hashPW(pw)), +contact);
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(hpassword(+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");
ret serveText(post.text());
}
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);
UserPost post = getConceptOpt UserPost(id);
if (post == null) ret serve404("Post with ID " + id + " not found");
ret servePost(post);
}
try object serveOtherPage2(req);
ret super.serveOtherPage(req);
}
!include #1029962 // serveOtherPage2
O servePost(UserPost post) {
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(ahref("/html/" + post.id, "Show as HTML page"));
// 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
actions.add(ahref(replyLink(post), "Reply"));
actions.add(ahref(conceptDuplicateLink(post), "Duplicate"));
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))));
}
framer.add(div(sourceCodeToHTML_noEncode(htmlEncodeWithLinking(post.text)), 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:"));
framer.add(ul(map(p.a, p2 -> htmlEncode2(squareBracketIfNempty(getPostRefTag(p2, post))) + " " + objectToHTML(p2))));
}
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))));
}
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 -> max(p._modified, p.bumped), instancesOf UserPost(allBackRefs(post)));
}
S serveAuthForm(S redirect) {
ret hAddToHead(super.serveAuthForm(redirect), hInitWebSocket());
}
S authFormMoreContent() {
ret p_vbar(navLinks());
}
// 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(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(Req req) {
ret req.auth == null ? "not logged in" : req.auth.user! == null ? (req.masterAuthed ? "master user" : "weird user") : "user " + req.auth.user->name;
}
HCRUD_Concepts crudData(Class c, Req req) {
HCRUD_Concepts cc = super.crudData(c, req);
if (c == UserPost) {
cc.itemName = () -> "Post";
cc.dropEmptyListValues = false;
//cc.verbose = true;
cc.addRenderer("text", new HCRUD_Data.TextArea(80, 10));
cc.addRenderer("postRefTags", new HCRUD_Data.FlexibleLengthList(new HCRUD_Data.TextField(20)));
cc.addRenderer("type", new HCRUD_Data.ComboBox(true, 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
ret item;
};
cc.fieldsToHideInCreationForm = litset("hidden", "creating");
}
ret cc;
}
HCRUD makeCRUD(Class c, Req req) {
HCRUD crud = super.makeCRUD(c, req);
crud.buttonsOnTop = true;
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");
crud.massageFormMatrix = (map, matrix) -> {
// TODO
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) print("Can't massage matrix");
LS row1 = matrix.get(idx1), row2 = matrix.get(idx2);
row1.set(1, hcrud_mergeTables(row1.get(1), row2.get(1), "as"));
matrix.remove(idx2);
};
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.add(p_vbar(navLinks()));
}
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 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) {
Req req = currentReq!;
ret req != null && req.auth != null && req.auth.user! != null
&& (req.auth.user->isMaster || req.auth.user! == post.creator!);
}
S replyLink(UserPost post) {
ret appendQueryToURL(crudLink(UserPost), cmd := "new", fList_postRefs_0 := post?.id);
}
S newPostLink() {
ret replyLink(null);
}
User currentUser() {
if (currentReq! == null) null;
AuthedDialogID auth = currentReq->auth;
if (auth == null) null;
ret auth.user!;
}
LS navLinks() {
ret llNonNulls(
//currentUser() != null ? ahref(crudLink(UserPost), "Admin") : null,
ahref(newPostLink(), "New Post"),
ahref("/search", "Search"),
ahref("/rootPosts", "Root Posts"),
ahref("/allPosts", "All Posts"),
ahref("/latestPosts", "Latest Posts"),
ahref("/team", "Team"));
}
L botCmdClasses() {
ret ll();
}
L crudClasses(Req req) {
L l = super.crudClasses(req);
l = listMinusSet(l, litset(Conversation, Lead, ConversationFeedback, Domain, UserKeyword, UploadedSound, Settings));
l.add(UserPost);
l.add(UploadedImage);
if (req.masterAuthed)
l.add(User);
ret l;
}
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);
logStructure(programFile("deleted-posts.log"),
mapToValues(ll("id", "text", "title", "type", "creatorID", "postRefs", "postRefTags", "hidden", "isPublic", "created", "_modified", "xmodified", "botInfo", "creating"),
field -> getPostFieldForBot(post, field)));
cdelete(post);
}
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(100, lambda1 distributePostChanges_impl);
void distributePostChanges(UserPost post) {
rstDistributePostChanges.add(post);
}
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);
}
}
void reloadBody(virtual WebSocket ws) {
print("Reloading body through WebSocket");
S jsCode = [[
$.get(document.location, function(html) {
var bodyHtml = /([\s\S]*)<\/body>/.exec(html)[1];
if (bodyHtml)
$("body").html(bodyHtml);
});
]];
dm_call(ws, "send", jsonEncode(litmap(eval := jsCode)));
}
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(seq, conv))
nextStep(conv);
} else
super.startMainScript(conv);
}
} // end of module
concept CustomBotStep > BotStep implements IInputHandler {
long codePostID;
toString { ret "CustomBotStep " + codePostID; }
bool run(Conversation conv) {
conv.add(new Msg(false, "Bot " + codePostID + " ready"));
cset(conv, inputHandler := this);
false;
}
public bool handleInput(S s, Conversation conv) {
Map result = cast postJSONPage("https://gazellebot.botcompany.de/chatBotReply/" + codePostID, q := s);
S answer = result.get("answer");
conv.add(new Msg(answer));
true; // acknowledge that we handled the input
}
}
sS userName(User user) {
ret user != null ? user.name : "?";
}