A (random) key has been sent to you by email. Please enter it below:
Key:
;
html GuestLoginDoc =
Webboard: guest login
Guest Login
You are logged in as a `guest' user. Restrictions apply.
Continue
;
html RegisterDoc =
Webboard: register
<bigwig> WebBoard Register
Login:
Password:
Name:
Email*:
*) As a security you will receive a (random) key via email.
Register CancelQuit
;
html LoginDoc =
Webboard Login
<bigwig> WebBoard Login
Guest
Login:
Password:
LoginRegister Forgot PasswordQuit
;
html PreferenceDoc =
Webboard: preferences
Preferences
Identity:
Login:
Name:
<[name]>
Password:
Email:
<[email]>
Notification:
Send me new postings by email:
Never.
Only replies to my postings.
Always.
Filter:
Enable filter:
Filter messages older than
days.
Filter messages before (last logout): <[logout_time]>
Enable grayout:
Appearance:
Thread view:
Newest postings at the:
top
/
bottom
Display time using am/pm:
Local timezone (GMT offset): GMT +
Reply message indent level:
Continue
;
html ShowBoardDoc =
Webboard: show board
<bigwig> WebBoard
Post New MessageRefreshPreferencesSearchQuit
<[msgs]>
Number of messages: <[no_msgs]> (<[total_no_msgs]>).
Post New MessageRefreshPreferencesSearchQuit
;
html MainDoc =
<[replies]>
;
html MinusDoc = ;
html PlusDoc = ;
html CollapseDoc =
<[collapse_symbol]>
;
html NoIndentDoc = <[indent]>;
html IndentDoc = <[indent]>;
html ReadSubjectDoc = <[subject]>;
html UnReadSubjectDoc = <[subject]>;
html GrayoutMessageDoc =
<[indent]><[collapse]>"<[subject]>" - <[name]> (<[pretty_time]>)
<[replies]>
;
html MessageDoc =
<[indent]><[collapse]>"<[subject]>" - <[name]> (<[pretty_time]>)
<[replies]>
;
html NewFirstDoc =
<[reply]>
<[replies]>
;
html NewLastDoc =
<[replies]>
<[reply]>
;
html ReplyDoc =
Webboard: reply
Enter Reply
SubmitCancel
Subject:
Message:
Wrap lines: (
yes /
no ) ?
SubmitCancel
;
html NavBarDoc =
Local Navigation:
<[navbar]>
;
html ReadMessageDoc =
Webboard: read message
<[subject]>
ReplyBack
<[content]>
/<[name]>
<[pretty_time]>
ReplyBack
<[navbar]>
;
html SearchDoc =
Webboard: search
<[title]>
Please enter your substring regexp query:
Query:
Regexp Syntax:
c
the character 'c'
.
any character
[a-z]
any character n the range from 'a' to 'z'
RS
concatenation of R and S
R*
zero-or-more repetitions of R
R+
one-or-more repetitions of R
R\{m,n\}
m to n repetitions of R
See man regexp for more details.
SearchBack
;
html BrowseSearchDoc =
Webboard: browse search
Search Results
Exit Search Mode
<[msgs]>
Number of search results: <[no_msgs]>.
Exit Search Mode
;
html GoodbyeInfoDoc =
You read <[no_read]> message<[no_read_s]> and wrote <[no_write]>.
Number of times you have logged in: <[no_user_login]>.
;
html GoodbyeDoc =
Webboard: goodbye
Goodbye <[name]>.
Thank you for using the WebBoard.
<[info]>
Total number of logins to the WebBoard: <[no_login]>.
;
string WEBBOARD_NAME = "";
string SENDER_EMAIL = "bigwig@brics.dk";
int SERVER_GMTOFFSET = +1;
int DEFAULT_INDENT = 3;
int WRAP = 72;
string QUOTE = "> ";
tuple User DEFAULT_USER = tuple {
login="", password="", name="", email="", no_login=0,
indent=DEFAULT_INDENT, ampm=false, notify='n', gmtoffset=SERVER_GMTOFFSET,
filter=true, grayout=true, custom_filter=false, logout_time=notime,
filter_days=7, navbar=true, new_first=false, thread=true };
schema Message {
int id, reply_to;
string name, email, subject, content;
time time_stamp;
}
schema User {
string login, password, name, email;
int no_login, indent, gmtoffset, filter_days;
bool ampm, filter, grayout, custom_filter, navbar, new_first, thread;
char notify;
time logout_time;
}
schema MsgPrefs {
int msg_id;
string user_login;
}
protected shared int no_login ;
protected shared relation tuple Message board ;
protected shared int next_id ;
protected shared relation tuple User users ;
protected shared relation tuple MsgPrefs collapse ;
protected shared relation tuple 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 tuple User U;
tuple Message m;
relation tuple 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 )
/* empty */;
}
};
}
}
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 tuple Message B, int id) {
tuple Message m;
factor(B) {
if (#.id == id) m = #;
};
return m;
}
tuple Message getFather(relation tuple Message B, int id) {
tuple Message m, father;
m = getMessage(B, id);
factor(B) {
if (#.id == m.reply_to) father = #;
};
return father;
}
relation tuple Message getChildren(relation tuple 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];
}
bool filter(time msg_time) {
time filter_time;
if (user.custom_filter) {
filter_time = server2clientTime(now());
filter_time = (setHour ( filter_time , 0 ));
filter_time = (setMinute ( filter_time , 0 ));
filter_time = (setSecond ( filter_time , 0 ));
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 tuple MsgPrefs Read, html CollapseImgDoc) {
int i;
html H = MainDoc;
html I = NoIndentDoc;
html SubjectDoc = UnReadSubjectDoc;
factor(Read) {
if (message.id == #.msg_id) SubjectDoc = ReadSubjectDoc;
};
for (i = 0 ; i < indent ; i++) I =<[indent=IndentDoc];
if (user.grayout || !filter(message.time_stamp)) {
if (user.grayout && filter(message.time_stamp)) {
H = GrayoutMessageDoc;
}
else {
H = MessageDoc;
}
H =<[indent=I,
id=message.id,
subject=SubjectDoc <[subject=message.subject],
name=message.name,
pretty_time=prettyTime(message.time_stamp, user.ampm),
collapse=CollapseDoc <[collapse_symbol=CollapseImgDoc,
collapse_id=-message.id]];
}
return H;
}
html getCollapseDoc(int id, relation tuple MsgPrefs Collapse) {
html H = MinusDoc;
factor(Collapse) {
if (id == #.msg_id) H = PlusDoc;
};
return H;
}
html makeBoardRec(int indent, int id, relation tuple Message B, relation tuple MsgPrefs Collapse, relation tuple MsgPrefs Read) {
int i;
html H;
tuple Message message;
vector tuple Message children;
if (id != 0) {
message = getMessage(B, id);
H = renderMessage(indent, message, Read, getCollapseDoc(message.id, Collapse));
}
else {
H = MainDoc;
}
children = sort((vector tuple Message)getChildren(B, id); id);
for (i = 0 ; i < |children| ; i++) {
html M;
if (user.new_first) M = NewLastDoc;
else M = NewFirstDoc;
H =<[replies=M <[reply=makeBoardRec(indent + user.indent, children[i].id, B, Collapse, Read)]];
}
return H <[replies=""];
}
html makeBoardList(int indent, relation tuple Message B, relation tuple MsgPrefs Collapse, relation tuple MsgPrefs Read) {
html H = MainDoc;
vector tuple Message V = sort((vector tuple Message)B; id);
int i;
if (user.new_first) {
for (i = |V| - 1 ; 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 tuple Message collapseFilter(relation tuple Message B, relation tuple MsgPrefs Collapse) {
relation tuple 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 tuple Message B;
relation tuple 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];
}
session Use() {
string action;
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 tuple User U;
vector tuple 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 tuple User)(Select * from U where #.login == login);
if (|V| > 0) {
show MsgDoc <[title="Login Already Taken", msg="Please choose another..."];
register("", password, name, email);
}
else {
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" )
/* empty */;
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 tuple User U;
vector tuple 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 tuple 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 tuple User)(Select * from U where #.login == login);
if (|V| > 0 && V[0].password == password) {
user = V[0];
user.no_login++;
updateUser();
}
else {
show MsgDoc <[title="Login Incorrect", msg="Login or password incorrect. Please try again..."];
doLogin(login, password);
}
break;
case "FORGOT":
sendPassword();
doLogin("", "");
break;
case "QUIT":
quit();
break;
}
}
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]) {
while (|line| > WRAP) {
string new;
int cut;
cut = lineCut(line);
new = line[0..cut];
x += new + "\n";
line = copyQuotes(new) + line[cut..|line|];
}
x += line + "\n";
}
return x + rest;
}
string quote(string rest) {
string x, line;
while (match (rest, Line) [line=line, rest=rest]) {
x += QUOTE + line + "\n";
}
return x + QUOTE + rest;
}
void replyMessage(int reply_to) {
int new_id;
bool submit, linewrap;
relation tuple Message B;
tuple Message New, Old;
string subject, content;
if (user.login == "") {
doLogin("", "");
if (user.login == "") return;
}
if (reply_to != 0) {
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)) [];
}
}
void readMessage(tuple Message message) {
string action;
relation tuple 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 tuple Message B, children;
relation tuple 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 }));
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));
}
else {
if (action == "REPLY") replyMessage(message.id);
}
}
void sendPassword() {
int i;
string email;
relation tuple User U;
vector tuple User V;
show EnterEmailDoc receive [email=email];
reader (users) U = users;
V = (vector tuple 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() )
/* empty */;
}
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."];
}
}
void search(string title) {
string query, action;
relation tuple Message B;
relation tuple 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");
}
}
}
void invertMsgPrefs(int msg_id, string user_login) {
bool was_collapsed;
relation tuple 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 tuple 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);
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();
}
html AdminLoginDoc =
Webboard: admin login
<bigwig> WebBoard Administration Login
Please enter the administration password:
Password:
LoginChange Password
;
html UpdatePasswordDoc =
Webboard: admin update password
<[users]>
;
html DeleteUserDoc =
Webboard: admin delete user
<bigwig> WebBoard Users
DeleteBack
Identity
Filter
Misc.
Delete?
Login
Name
Email
notify
filter
grayout
days
custom
last
navbar
indent
ampm
gmt
#Logins
<[users]>
Total number of users: <[no_users]>
DeleteBack
;
html AdminShowBoardDoc =
Webboard: admin show board
<bigwig> WebBoard Administration
Show UsersQuit
<[]>MESSAGES:
<[msgs]>
Total number of messages: <[no_msgs]>
Cut'n'Paste Buffer
Cut:
Click on a message to move it (along with all its replies)
to the cut'n'paste buffer. Paste:
Click on a message to move the cut'n'paste buffer onto the
board as a reply to it.
<[cutpaste]>
Total number of messages: <[no_cutpaste_msgs]>
Empty Buffer
Show UsersQuit
;
html AdminByeByeDoc =
Webboard: admin goodbye
Goodbye Administrator.
Thank you for using the WebBoard.
You made <[no_cut]> cut<[cut_s]> and <[no_paste]> paste<[paste_s]>.
;
session Administrate() {
protected shared string password ;
int id, no_cut, no_paste;
string action;
bool cut;
void updateAdminPassword() {
bool done;
string pass1, pass2;
while (!done) {
show UpdatePasswordDoc receive [pass1=pass1, pass2=pass2, action=continue];
if (action == "CANCEL") {
done = true;
show MsgDoc <[title="Password Update Cancelled", msg="Password not updated"];
}
else {
if (pass1 != pass2) {
show MsgDoc <[title="Passwords Not Equal", msg="The two passwords are not equal as they should be"];
}
else {
done = true;
writer (password) password = pass1;
show MsgDoc <[title="Password Updated", msg="Your password has been updated"];
}
}
}
}
void adminLogin() {
bool done;
string pass, passwd;
reader (password) passwd = password;
if (passwd == "") updateAdminPassword();
while (pass != passwd) {
show AdminLoginDoc receive [pass=pass, action=continue];
if (pass != passwd) show MsgDoc <[title="Incorrect Password", msg="Please try again..."];
}
if (action == "CHANGE") {
updateAdminPassword();
}
}
void showUsers() {
int i;
html H;
string action;
vector string del;
relation tuple User U;
vector tuple User V;
relation tuple MsgPrefs M;
while (action != "BACK") {
H = DeleteUserDoc;
reader (users) V = sort((vector tuple User)users; login);
for (i = 0 ; i < |V| ; i++) H = H <[users=UserDoc <[login_id=V[i].login,
login=V[i].login,
name=V[i].name,
mailto="mailto:" + V[i].email,
email=V[i].email,
notify=V[i].notify,
filter=V[i].filter,
grayout=V[i].grayout,
filter_days=V[i].filter_days,
custom_filter=V[i].custom_filter,
logout_time=V[i].logout_time,
navbar=V[i].navbar,
indent=V[i].indent,
ampm=V[i].ampm,
gmtoffset=V[i].gmtoffset,
no_login=V[i].no_login]];
show H <[no_users=|V|] receive [del=del, action=continue];
if (action == "DELETE") {
writer (users) {
U = users;
for (i = 0 ; i < |del| ; i++) {
U = (Select * from U where #.login != del[i]);
}
users = U;
}
writer (collapse) {
M = collapse;
for (i = 0 ; i < |del| ; i++) {
M = (Select * from M where #.user_login != del[i]);
}
collapse = M;
}
writer (has_read) {
M = has_read;
for (i = 0 ; i < |del| ; i++) {
M = (Select * from M where #.user_login != del[i]);
}
has_read = M;
}
}
}
}
shared relation tuple Message cutpaste, source, target;
shared int cutpaste_id;
void moveMessage(int id) {
int i;
tuple Message message;
vector tuple Message children;
target = (Union (target, Select * from source where #.id == id));
source = (Select * from source where #.id != id);
children = (vector tuple Message)getChildren(source, id);
for (i = 0 ; i < |children| ; i++) {
moveMessage(children[i].id);
}
}
void cutMessage(int id) {
source = board;
target = relation { };
moveMessage(id);
board = source;
cutpaste = factor(target) {
if (#.id == id) return # << tuple { reply_to=0 };
else return #;
};
cutpaste_id = id;
}
void pasteMessage(int id) {
source = factor(cutpaste) {
if (#.id == cutpaste_id) return # << tuple { reply_to=id };
else return #;
};
target = board;
moveMessage(cutpaste_id);
board = target;
cutpaste = relation { };
cutpaste_id = 0;
}
html makeAdminBoard() {
relation tuple Message B, C;
relation tuple MsgPrefs empty;
reader (board) {
B = board;
C = cutpaste;
}
return AdminShowBoardDoc <[no_msgs=|B|,
msgs=makeBoardRec(0, 0, B, empty, empty),
no_cutpaste_msgs=|C|,
cutpaste=makeBoardRec(0, 0, C, empty, empty)];
}
adminLogin();
user.indent = DEFAULT_INDENT;
while (action != "QUIT") {
show makeAdminBoard() receive [action=continue, cut=cut];
switch (action) {
case "EMPTY":
writer (board) {
cutpaste = relation { };
cutpaste_id = 0;
}
break;
case "USERS":
showUsers();
break;
default:
if (match (action, Number) []) {
tuple Message m;
id = (int)action;
reader (board) m = getMessage(board, id);
if (id == m.id) {
if (cut) {
writer (board) cutMessage(id);
no_cut++;
}
else {
writer (board) pasteMessage(id);
no_paste++;
}
}
}
break;
}
}
exit AdminByeByeDoc <[no_cut=no_cut,
no_paste=no_paste];
}
}