/* webboard [version 2.0] - by Claus Brabrand */ require require service { #include "webboard.docs" #include "config.wig" schema Message { int id, reply_to; string name, email, subject, content; time time_stamp; } schema User { string login, password, name, email; // identity int no_login, indent, gmtoffset, filter_days; bool ampm, filter, grayout, custom_filter, navbar, new_first, thread; char notify; // 'n'o, 'y'es, ' 'my postings. time logout_time; } schema MsgPrefs { int msg_id; string user_login; } protected shared int no_login; protected shared relation Message board; protected shared int next_id; protected shared relation User users; protected shared relation MsgPrefs collapse; protected shared relation MsgPrefs has_read; format Re = concat("[Re: ", star(anychar),"]"); format Digit = range('0', '9'); format Number = plus(Digit); format Alpha = union(range('a', 'z'), range('A', 'Z')); format Word = plus(union(Alpha, Digit,"-","_")); format Email = concat(Word, "@", Word, star(concat(".", Word))); format GmtOffset = relax(-12,12); format Line = concat([line = star(charcomplement('\n'))],"\n",[rest = star(anychar)]); tuple User user; int no_read, no_write; session Notify(int new_msg_id) { relation User U; tuple Message m; relation Message B; string content; if (new_msg_id!=0) { flash Notifying users of new WebBoard message...; reader(board) B = board; m = getMessage(B, new_msg_id); content = "This is an autogenerated message.\nA new message has arrived to the " + WEBBOARD_NAME + " WebBoard:\n\n==================================================\nSUBJECT: " + m.subject + "\nAUTHOR: " + m.name + " (" + m.email + ")\nTIME: " + prettyTime(m.time_stamp, false) + "\n--------------------------------------------------\n" + m.content + "\n==================================================\n\nThe " + WEBBOARD_NAME + " WebBoard: \n" + url Use(); reader (users) U = users; factor(U; notify, email) { if (#.notify=='y' || (#.notify==' ' && getFather(B, new_msg_id).email==#.email)) { sendemail(#.email, SENDER_EMAIL, "New " + WEBBOARD_NAME + " WebBoard Message", content); } }; } } time client2serverTime(time t) { if (t!=notime) return setHour(t,getHour(t)-(user.gmtoffset-SERVER_GMTOFFSET)); return t; } time server2clientTime(time t) { if (t!=notime) return setHour(t, getHour(t)+(user.gmtoffset-SERVER_GMTOFFSET)); return t; } string prettyMonth(time t) { return vector { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }[getMonth(t)-1]; } string twoDigits(int n) { if (n<10) return "0" + n; else return (string) n; } string prettyTime(time t, bool ampm) { string date; if (t==notime) return "notime"; t = server2clientTime(t); date = prettyMonth(t) + " " + getDay(t) + ", " + getYear(t) + " @ "; if (ampm) { date += ((getHour(t)+11)%12)+1 + ":" + twoDigits(getMinute(t)) + " "; if (getHour(t)<12) date += "am"; else date += "pm"; } else { date += twoDigits(getHour(t)) + ":" + twoDigits(getMinute(t)); } return date; } tuple Message getMessage(relation Message B, int id) { tuple Message m; factor (B) { if (#.id==id) m = #; }; return m; } tuple Message getFather(relation Message B, int id) { tuple Message m, father; m = getMessage(B,id); factor (B) { if (#.id==m.reply_to) father = #; }; return father; } relation Message getChildren(relation Message B, int id) { return Select * from B where (#.reply_to==id); } tuple User changePreferences(tuple User user) { html P = PreferenceDoc; P = P <[login = user.login, password = user.password, name = user.name, email = user.email, filter = (user.filter ? "checked" : ""), grayout = (user.grayout ? "checked" : ""), thread = (user.thread ? "checked" : ""), custom_filter_true = (user.custom_filter ? "checked" : ""), custom_filter_false = (!user.custom_filter ? "checked" : ""), filter_days = user.filter_days, logout_time = prettyTime(server2clientTime(user.logout_time),user.ampm), indent = user.indent, gmtoffset = user.gmtoffset, notify_no = (user.notify=='n' ? "checked" : ""), notify_my = (user.notify==' ' ? "checked" : ""), notify_yes = (user.notify=='y' ? "checked" : ""), ampm = (user.ampm ? "checked" : ""), new_first_true = (user.new_first ? "checked" : ""), new_first_false = (!user.new_first ? "checked" : "")]; show P receive [user.login = login, user.password = password, user.new_first = new_first, user.ampm = ampm, user.notify = notify, user.filter = filter, user.grayout = grayout, user.thread = thread, user.custom_filter = custom_filter, user.filter_days = filter_days, user.indent = indent, user.gmtoffset = gmtoffset]; return user; } void quit() { int n; html H; string name; if (user.login!="") { name = user.name; H = GoodbyeInfoDoc <[no_read = no_read, no_read_s = (no_read!=1 ? "s" : ""), no_write = no_write, no_user_login = user.no_login]; } else { name = "guest"; } reader(no_login) n = no_login; exit GoodbyeDoc <[name = name, no_login = n, info = H]; } /* ---------------------- VIEW BOARD -------------------------- */ bool filter(time msg_time) { time filter_time; if (user.custom_filter) { filter_time = server2clientTime(now()); filter_time = setHour(filter_time,00); filter_time = setMinute(filter_time,00); filter_time = setSecond(filter_time,00); filter_time = client2serverTime(filter_time); filter_time = setDay(filter_time,getDay(filter_time)-user.filter_days); } else { filter_time = user.logout_time; } return (user.filter && msg_time <= filter_time); } html renderMessage(int indent, tuple Message message, relation MsgPrefs Read, html CollapseImgDoc) { int i; html H = MainDoc; html I = NoIndentDoc; html SubjectDoc = UnReadSubjectDoc; factor(Read) { if (message.id==#.msg_id) SubjectDoc = ReadSubjectDoc; // message already read. }; for (i=0; i=0; i--) { H =<[replies = renderMessage(indent, V[i], Read, getCollapseDoc(V[i].id, Collapse))]; } } else { for(i=0; i<|V|; i++) { H =<[replies = renderMessage(indent, V[i], Read, getCollapseDoc(V[i].id, Collapse))]; } } return H; } relation Message collapseFilter(relation Message B, relation MsgPrefs Collapse) { relation MsgPrefs C; if (|Collapse|==0) return B; B = factor(B, rename in Collapse from msg_id to reply_to) { if (|@2|==0) { return cart(@1, relation{#}); } else { C = Union(C, factor(@1) { return tuple{ msg_id = #.id , user_login = ""}; }); } }; return collapseFilter(B, C); } html makeBoard() { html x; int total; relation Message B; relation MsgPrefs Collapse, Read; reader (board) B = board; total = |B|; if (user.login!="") { reader (collapse) { Collapse = Select * from collapse where #.user_login==user.login; } reader (has_read) { Read = Select * from has_read where #.user_login==user.login; } } B = collapseFilter(B, Collapse); if (user.thread) { x = makeBoardRec(0,0,B,Collapse,Read); } else { x = makeBoardList(DEFAULT_INDENT,B,Collapse,Read); } return ShowBoardDoc <[no_msgs = |B|, total_no_msgs = total, msgs = x]; } /* =========================== USE ============================ */ session Use() { string action; /* ------------------------ LOGIN ----------------------------- */ void updateUser() { writer (users) { users = Union(Select * from users where #.login!=user.login, relation{user}); } } void register(string login, string password, string name, string email) { int ran; int key = -1; relation User U; vector User V; string action; show RegisterDoc <[login = login, password = password, name = name, email = email] receive [login = login, password = password, name = name, email = email, action = continue]; switch(action) { case "REGISTER": reader (users) U = users; V = (vector User) Select * from U where #.login==login; if (|V|>0) { show MsgDoc <[title = "Login Already Taken", msg = "Please choose another..."]; register("", password, name, email); // tail call. } else { /* create new (default) `user' profile */ user = DEFAULT_USER << tuple {login=login,password=password,name=name,email=email}; ran = random(1000000); sendemail(email, SENDER_EMAIL, "WebBoard Security Key", "This is an autogenerated message.\nYour WebBoard security key is: " + ran + "\n\nClick here to continue: \n" + url + "\n\n/" + WEBBOARD_NAME + " WebBoard"); while (key!=ran) { show KeyCheckDoc receive [key = key]; } user = changePreferences(user); updateUser(); } break; case "CANCEL": doLogin(login,password); break; case "QUIT": quit(); break; } } void doLogin(string login, string password) { relation User U; vector User V; string action; show LoginDoc <[login = login, password = password] receive [action = continue, login = login, password = password]; switch (action) { case "GUEST": user = DEFAULT_USER; show GuestLoginDoc; break; case "REGISTER": reader (users) U = users; V = (vector User) Select * from U where #.login==login; if (|V|>0) { show MsgDoc <[title = "Login Already Taken", msg = "Please choose another..."]; doLogin(login, password); } else { register(login, password, "", ""); } break; case "LOGIN": reader (users) U = users; V = (vector User) Select * from U where #.login==login; if (|V|>0 && V[0].password==password) { /* Successful login */ user = V[0]; // get and set `user' profile. user.no_login++; updateUser(); } else { show MsgDoc <[title = "Login Incorrect", msg = "Login or password incorrect. Please try again..."]; doLogin(login, password); // tail call. } break; case "FORGOT": sendPassword(); doLogin("",""); break; case "QUIT": quit(); break; } } /* -------------------- REPLY MESSAGE ------------------------- */ string addReply(string s) { if (match(s, Re)[]) return s; else return "[Re: "+s+"]"; } int lineCut(string s) { int i; for (i = WRAP-1; i>0; i--) if (s[i]==' ') return i+1; return WRAP; } string copyQuotes(string s) { if (|s|>=|QUOTE| && s[0..|QUOTE|]==QUOTE) return QUOTE + copyQuotes(s[|QUOTE|..|s|]); return ""; } string lineWrap(string rest) { string x, line; rest += "\n"; while (match(rest, Line)[line = line, rest = rest]) { // INV: `x' is "" or ends in '\n' while (|line|>WRAP) { string new; int cut; cut = lineCut(line); new = line[0..cut]; x += new + "\n"; // add new line + newline. line = copyQuotes(new) + line[cut..|line|]; } x += line + "\n"; } /* handle very last line */ return x + rest; } string quote(string rest) { string x, line; while (match(rest, Line)[line = line, rest = rest]) { // INV: `x' is "" or ends in '\n' x += QUOTE + line + "\n"; } /* handle very last line */ return x + QUOTE + rest; } void replyMessage(int reply_to) { int new_id; bool submit, linewrap; relation Message B; tuple Message New, Old; string subject, content; if (user.login=="") { doLogin("",""); if (user.login=="") return; } if (reply_to!=0) { // This was a reply to some message and not a new message. reader (board) B = board; Old = getMessage(B,reply_to); subject = addReply(Old.subject); content = quote(Old.content); } show ReplyDoc <[cols = WRAP, subject = subject, content = content] receive [subject = subject, content = content, linewrap = linewrap, submit = continue]; if (submit) { if (subject=="") subject = "no subject"; if (linewrap) content = lineWrap(content); writer (next_id) new_id = ++next_id; New = tuple { id = new_id, reply_to = reply_to, name = user.name, email = user.email, subject = subject, content = content, time_stamp = now() }; no_write++; writer (board) board = Union(board, relation { New } ); writer (has_read) has_read = Union(has_read, relation{tuple{ user_login = user.login, msg_id = new_id}}); get (url Notify(New.id)); // Will eventually `SPAWN'!!! } } /* --------------------- READ MESSAGE ------------------------- */ void readMessage(tuple Message message) { string action; relation Message B; html N; no_read++; if (user.login!="") { writer(has_read) { has_read = Union(has_read, relation {tuple {msg_id = message.id, user_login = user.login}}); } } if (user.navbar) { int father_id; tuple Message father; relation Message B, children; relation MsgPrefs C,R; reader (board) B = board; father_id = message.reply_to; father = getMessage(B,father_id); children = getChildren(B,message.id); B = Union(relation{message},children); if (father.id!=0) B = Union(B,relation{father}); // not `root' node. if (user.login!="") { reader (collapse) { C = Select * from collapse where #.user_login==user.login; } reader (has_read) { R = Select * from has_read where #.user_login==user.login; } } N = NavBarDoc <[navbar = makeBoardRec(user.indent,father.id,B,C,R)]; } show ReadMessageDoc <[name = message.name, subject = message.subject, content = message.content, pretty_time = prettyTime(message.time_stamp, user.ampm), navbar = N] receive [action = continue]; if (match(action,Number)[]) { reader (board) B = board; readMessage(getMessage(B,(int) action)); // tail call. } else { if (action=="REPLY") replyMessage(message.id); } } /* ---------------------- SEND PASSWORD ----------------------- */ void sendPassword() { int i; string email; relation User U; vector User V; show EnterEmailDoc receive [email = email]; reader (users) U = users; V = (vector User) Select * from U where #.email==email; if (|V|>0) { for (i=0; i<|V|; i++) { sendemail(email, SENDER_EMAIL, WEBBOARD_NAME + " WebBoard password", "This is an autogenerated message.\nHere is your password for the " + WEBBOARD_NAME + " WebBoard:\n\nLOGIN: " + V[i].login + "\nPASSWORD: " + V[i].password + "\n\nThe " + WEBBOARD_NAME + " WebBoard: \n" + url Use()); } show MsgDoc <[title = "Password Sent", msg = "Your login and password have been sent to you by email."]; } else { show MsgDoc <[title = "No Such User", msg = "Sorry, you are not listed as a user."]; } } /* ------------------------- SEARCH --------------------------- */ void search(string title) { string query, action; relation Message B; relation MsgPrefs Read; show SearchDoc <[title = title] receive [query = query, action = continue]; if (action=="SEARCH" && query!="") { reader(board) B = board; B = factor(B) { if (match(#.content, query)[]) return #; }; if (|B|!=0) { html H; reader(has_read) Read = has_read; H = makeBoardList(DEFAULT_INDENT,B,relation{},Read); H = BrowseSearchDoc <[no_msgs = |B|, msgs = H]; repeat { show H receive [action = continue]; if (action!="BACK") handleRead((int) action); } until (action=="BACK"); } else { search("No Results Found"); // tail call. } } } /* ---------------------- SESSION CODE ------------------------ */ void invertMsgPrefs(int msg_id, string user_login) { bool was_collapsed; relation MsgPrefs C; tuple MsgPrefs t = tuple{msg_id = msg_id, user_login=user_login}; writer(collapse) { C = factor(collapse) { if (#==t) was_collapsed = true; else return #; }; if (was_collapsed) collapse = C; else collapse = Union(C,relation{t}); } } void handleRead(int msg_id) { if (msg_id>0) { relation Message B; reader (board) B = board; readMessage(getMessage(B,msg_id)); } else if (msg_id<0) { if (user.login!="") { invertMsgPrefs(-msg_id, user.login); } else { doLogin("",""); } } } doLogin("", ""); writer(no_login) no_login++; while (action!="QUIT") { show makeBoard() receive [action = continue]; switch (action) { case "NEW": replyMessage(0); // New messages are replies to `message' no. zero break; case "PREFS": if (user.login=="") { doLogin("",""); } else { user = changePreferences(user); updateUser(); } break; case "SEARCH": search("Search"); break; default: handleRead((int) action); break; } } if (user.login!="") { user.logout_time = now(); updateUser(); } quit(); } #include "admin.wig" }