<bigwig> service: webboard


service {
  html MsgDoc = <html>
    <head><title></title></head>
    <body bgcolor="#ffffff">
    <h1><[title]></h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <[msg]>
    </td></tr></table>
    </body>
  </html>;
  html EnterEmailDoc = <html>
    <head><title>Webboard: enter email</title></head>
    <body bgcolor="#ffffff">
    <h1>Enter Your Email</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    Please enter your email address:
    <br>
    Email: <input type="text" name="email">
    </td></tr></table>
    <format field="email"><bigwig-regexp idref="Email"/></format>
    <continue type=button>Continue</continue>
    </body>
  </html>;
  html KeyCheckDoc = <html>
    <head><title>Webboard: key check</title></head>
    <body bgcolor="#ffffff">
    <h1>WebBoard Key Verification</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    A (random) key has been sent to you by email. Please enter it below:
    <br>
    Key: <input type="text" name="key">
    </td></tr></table>
    <format field="key"><bigwig-regexp idref="Number"/></format>
    </body>
  </html>;
  html GuestLoginDoc = <html>
    <head><title>Webboard: guest login</title></head>
    <body bgcolor="#ffffff">
    <h1>Guest Login</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    You are logged in as a `guest' user. Restrictions apply.
    </td></tr></table>
    <continue type=button>Continue</continue>
    </body>
  </html>;
  html RegisterDoc = <html>
    <head><title>Webboard: register</title></head>
    <body bgcolor="#ffffff">
    <h1><tt><font color="#ff0000">&lt;</font>bigwig<font color="#ff0000">&gt;</font></tt> WebBoard Register</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <table>
      <tr>
        <td>Login:</td>
        <td><input type="text" name="login" value=[login]></td>
      </tr>
      <tr>
        <td>Password:</td>
        <td><input type="password" name="password" value=[password]></td>
      </tr>
      <tr>
        <td>Name:</td>
        <td><input type="text" name="name" value=[name]></td>
      </tr>
      <tr>
        <td>Email<sup>*</sup>:</td>
        <td><input type="text" name="email" value=[email]></td>
      </tr>
    </table>
    </td></tr></table>
    <sup>*</sup>) As a security you will receive a (random) key via email.
    <p>
    <continue type=button value="REGISTER">Register</continue>
    <br>
    <continue ignoreformats type=button value="CANCEL">Cancel</continue>
    <continue ignoreformats type=button value="QUIT">Quit</continue>
    <format field="login"><bigwig-regexp idref="Word"/></format>
    <format field="password"><bigwig-regexp idref="Word"/></format>
    <format field="email"><bigwig-regexp idref="Email"/></format>
    </body>
  </html>;
  html LoginDoc = <html>
    <head><title>Webboard Login</title></head>
    <body bgcolor="#ffffff">
    <h1><tt><font color="#ff0000">&lt;</font>bigwig<font color="#ff0000">&gt;</font></tt> WebBoard Login</h1>
    <continue ignoreformats type=button value="GUEST">Guest</continue>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <table>
      <tr>
        <td>Login:</td>
        <td><input type="text" name="login" value=[login]></td>
      </tr>
      <tr>
        <td>Password:</td>
        <td><input type="password" name="password" value=[password]></td>
      </tr>
    </table>
    </td></tr></table>
    <br>
    <p>
    <continue type=button value="LOGIN">Login</continue>
    <continue ignoreformats type=button value="REGISTER">Register</continue>
    <br>
    <continue ignoreformats type=button value="FORGOT">Forgot Password</continue>
    <continue ignoreformats type=button value="QUIT">Quit</continue>
    <format field="login"><bigwig-regexp idref="Word"/></format>
    <format field="password"><bigwig-regexp idref="Word"/></format>
    </p>
    </body>
  </html>;
  html PreferenceDoc = <html>
    <head><title>Webboard: preferences</title></head>
    <body bgcolor="#ffffff">
    <h1>Preferences</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <h3>Identity:</h3>
    <table>
      <tr>
        <td>Login:</td>
        <td><input type="text" size=10 name="login" value=[login]></td>
        <td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
        <td>Name:</td>
        <td><b><[name]></b></td>
      </tr>
      <tr>
        <td>Password:</td>
        <td><input type="password" size=10 name="password" value=[password]></td>
        <td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
        <td>Email:</td>
        <td><b><[email]></b></td>
      </tr>
    </table>
    <format field="login"><bigwig-regexp idref="Word"/></format>
    <format field="password"><bigwig-regexp idref="Word"/></format>
    <hr>
    <h3>Notification:</h3>
    Send me new postings by email:<br>
    &nbsp;&nbsp;&nbsp;<input type="radio" name="notify" value="n" [notify_no]> Never.<br>
    &nbsp;&nbsp;&nbsp;<input type="radio" name="notify" value=" " [notify_my]> Only replies to my postings. <br>
    &nbsp;&nbsp;&nbsp;<input type="radio" name="notify" value="y" [notify_yes]> Always.
    <hr>
    <h3>Filter:</h3>
    Enable filter:
    <input type="checkbox" name="filter" value="true" [filter]>
    <br>
    &nbsp;&nbsp;&nbsp;<input type="radio" name="custom_filter" value="true" [custom_filter_true]>
    Filter messages older than
    <input type="text" name="filter_days" size="4" value=[filter_days]> days.
    <format field="filter_days"><bigwig-regexp idref="Number"/></format>
    <br>
    &nbsp;&nbsp;&nbsp;<input type="radio" name="custom_filter" value="false" [custom_filter_false]>
    Filter messages before (last logout): <em><[logout_time]></em>
    <br>&nbsp;
    <br>
    Enable grayout:
    <input type="checkbox" name="grayout" value="true" [grayout]>
    <hr>
    <h3>Appearance:</h3>
    Thread view: 
    <input type="checkbox" name="thread" value="true" [thread]>
    <br>
    Newest postings at the: 
    <input type="radio" name="new_first" value="true" [new_first_true]>
    top
    &nbsp;&nbsp;/&nbsp;&nbsp; 
    <input type="radio" name="new_first" value="false" [new_first_false]>
    bottom
    <br>
    Display time using am/pm: <input type="checkbox" name="ampm" value="true" [ampm]><br>
    Local timezone (GMT offset): GMT +<input type="text" name="gmtoffset" size="3" maxlength="3" value=[gmtoffset]><br>
    Reply message indent level: <input type="text" size="1" maxlength="1" name="indent" value=[indent]><br>
    <format field="gmtoffset"><bigwig-regexp idref="GmtOffset"/></format>
    <format field="indent"><bigwig-regexp idref="Digit"/></format>
    </td></tr></table>
    <continue type=button>Continue</continue>
    </body>
  </html>;
  html ShowBoardDoc = <html>
    <head>
    <title>Webboard: show board</title>
    <style type="text/css">
    span.GrayOut {
      color : #999999;
    }
    span.GrayOut a { color: #9999ff; }
    </style>
    </head>
    <body bgcolor="#ffffff">
    <h1><tt><font color="#ff0000">&lt;</font>bigwig<font color="#ff0000">&gt;</font></tt> WebBoard</h1>
    <continue type=button value="NEW">Post New Message</continue>
    <continue type=button value="VIEW">Refresh</continue>
    <continue type=button value="PREFS">Preferences</continue>
    <continue type=button value="SEARCH">Search</continue>
    <continue type=button value="QUIT">Quit</continue>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <[msgs]>
    </td></tr></table>
    <br>
    Number of messages: <[no_msgs]> (<[total_no_msgs]>).
    <p>
    <continue type=button value="NEW">Post New Message</continue>
    <continue type=button value="VIEW">Refresh</continue>
    <continue type=button value="PREFS">Preferences</continue>
    <continue type=button value="SEARCH">Search</continue>
    <continue type=button value="QUIT">Quit</continue>
    </p>
    </body>
  </html>;
  html MainDoc = <html>
    <[replies]>
  </html>;
  html MinusDoc = <html><img alt="" border=0 src="../minus.gif"></html>;
  html PlusDoc = <html><img alt="" border=0 src="../plus.gif"></html>;
  html CollapseDoc = <html>
    <continue value=[collapse_id]><[collapse_symbol]></continue>
  </html>;
  html NoIndentDoc = <html><[indent]></html>;
  html IndentDoc = <html>&nbsp;&nbsp;&nbsp;<[indent]></html>;
  html ReadSubjectDoc = <html><[subject]></html>;
  html UnReadSubjectDoc = <html><b><[subject]></b></html>;
  html GrayoutMessageDoc = <html>
    <span class="GrayOut"><[indent]><[collapse]>&quot;<continue value=[id]><[subject]></continue>&quot; - <i><[name]></i> (<[pretty_time]>)</span><br>
    <[replies]>
  </html>;
  html MessageDoc = <html>
    <[indent]><[collapse]>&quot;<continue value=[id]><[subject]></continue>&quot; - <i><[name]></i> (<[pretty_time]>)<br>
    <[replies]>
  </html>;
  html NewFirstDoc = <html>
    <[reply]>
    <[replies]>
  </html>;
  html NewLastDoc = <html>
    <[replies]>
    <[reply]>
  </html>;
  html ReplyDoc = <html>
    <head><title>Webboard: reply</title></head>
    <body bgcolor="#ffffff">
    <h1>Enter Reply</h1>
    <continue type="button" value="true">Submit</continue>
    <continue type="button" value="false">Cancel</continue>
    <br>
    <table border="1" bgcolor="#ffffcc" width="100%"><tr><td>
    Subject:<br>
    <input type="text" name="subject" value=[subject]>
    <p>
    Message:<br>
    <textarea name="content" cols=[cols] rows="20"><[content]></textarea>
    </p>
    <p>
    Wrap lines: (
    yes <input type="radio" name="linewrap" value="true" checked> /
    no <input type="radio" name="linewrap" value="false"> ) ?
    </p>
    <br>
    </td></tr></table>
    <p>
    <continue type="button" value="true">Submit</continue>
    <continue type="button" value="false">Cancel</continue>
    </p>
    </body>
  </html>;
  html NavBarDoc = <html>
    <hr noshade size="3">
    <h3>Local Navigation:</h3>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <[navbar]>
    </td></tr></table>
  </html>;
  html ReadMessageDoc = <html>
    <head><title>Webboard: read message</title></head>
    <body bgcolor="#ffffff">
    <h1><[subject]></h1>
    <continue type="button" value="REPLY">Reply</continue>
    <continue type="button" value="BACK">Back</continue>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <pre><[content]></pre>
    <p align="right">
    <em>/<[name]></em>
    </p>
    </td></tr></table>
    <font size="-1"><[pretty_time]></font>
    <p>
    <continue type="button" value="REPLY">Reply</continue>
    <continue type="button" value="BACK">Back</continue>
    </p>
    <[navbar]>
    </body>
  </html>;
  html SearchDoc = <html>
    <head><title>Webboard: search</title></head>
    <body bgcolor="#ffffff">
    <h1><[title]></h1>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    Please enter your substring regexp query:<br>
    Query: <input type="text" name="query">
    <br>
    Regexp Syntax:<br>
    <table><tr>
      <td><code>c</code></td><td>the character 'c'</td>
    </tr><tr>
      <td><code>.</code></td><td>any character</td>
    </tr><tr>
      <td><code>[a-z]</code></td><td>any character n the range from 'a' to 'z'</td>
    </tr><tr>
      <td><code><em>RS</em></code></td><td>concatenation of <em>R</em> and <em>S</em></td>
    </tr><tr>
      <td><code><em>R</em>*</code></td><td>zero-or-more repetitions of <em>R</em></td>
    </tr><tr>
      <td><code><em>R</em>+</code></td><td>one-or-more repetitions of <em>R</em></td>
    </tr><tr>
      <td><code><em>R</em>\{m,n\}</code></td><td>m to n repetitions of <em>R</em></td>
    </tr></table>
    <p>
    See <code>man regexp</code> for more details.
    </p>
    </td></tr></table>
    <p>
    <continue type="button" value="SEARCH">Search</continue>
    <continue type="button" value="BACK">Back</continue>
    </p>
    </body>
  </html>;
  html BrowseSearchDoc = <html>
    <head><title>Webboard: browse search</title></head>
    <body bgcolor="#ffffff">
    <h1>Search Results</h1>
    <continue type=button value="BACK">Exit Search Mode</continue>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <[msgs]>
    </td></tr></table>
    <p>
    Number of search results: <[no_msgs]>.
    </p>
    <continue type=button value="BACK">Exit Search Mode</continue>
    </body>
  </html>;
  html GoodbyeInfoDoc = <html>
    You <i>read</i> <[no_read]> message<[no_read_s]> and <i>wrote</i> <[no_write]>.
    <br>
    Number of times you have logged in: <[no_user_login]>.
    <br>
  </html>;
  html GoodbyeDoc = <html>
    <head><title>Webboard: goodbye</title></head>
    <body bgcolor="#ffffff">
    <h1>Goodbye <[name]>.</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    Thank you for using the WebBoard.
    <p>
    <[info]>
    Total number of logins to the WebBoard: <[no_login]>.
    </p>
    </td></tr></table>
    </body>
  </html>;
  string WEBBOARD_NAME = "<bigwig>";
  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 <html>Notifying users of new WebBoard message...</html>;
      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 = <html>
    <head><title>Webboard: admin login</title></head>
    <body bgcolor="#ffffff">
    <h1><tt><font color="#ff0000">&lt;</font>bigwig<font color="#ff0000">&gt;</font></tt> WebBoard Administration Login</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    Please enter the administration password:
    <table>
      <tr>
        <td>Password:</td>
        <td><input type="password" name="pass"></td>
	<format field="pass"><bigwig-regexp idref="Word"/></format>
      </tr>
    </table>
    </td></tr></table>
    <continue type=button value="LOGIN">Login</continue>
    <continue type=button value="CHANGE">Change Password</continue>
    </body>
  </html>;
  html UpdatePasswordDoc = <html>
    <head><title>Webboard: admin update password</title></head>
    <body bgcolor="#ffffff">
    <h1>Update Password</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    Please enter your new password (twice):
    <table>
      <tr>
        <td>Password:</td>
        <td><input type="password" name="pass1"></td>
	<format field="pass1"><bigwig-regexp idref="Word"/></format>
      </tr>
      <tr>
        <td>(Again):</td>
        <td><input type="password" name="pass2"></td>
	<format field="pass2"><bigwig-regexp idref="Word"/></format>
      </tr>
    </table>
    </td></tr></table>
    <continue type=button value="UPDATE">Update</continue>
    <continue type=button value="CANCEL">Cancel</continue>
    </body>
  </html>;
  html UserDoc = <html>
    <tr>
      <td><input type="checkbox" name="del" value=[login_id]></td>
      <td><b><[login]></b></td>
      <td><[name]></td>
      <td><a href=[mailto]><[email]></a></td>

      <td><[notify]></td>

      <td><[filter]></td>
      <td><[grayout]></td>
      <td><[filter_days]></td>
      <td><[custom_filter]></td>
      <td><[logout_time]></td>

      <td><[navbar]></td>
      <td><[indent]></td>
      <td><[ampm]></td>
      <td><[gmtoffset]></td>

      <td><[no_login]></td>
    </tr>
    <[users]>
  </html>;
  html DeleteUserDoc = <html>
    <head><title>Webboard: admin delete user</title></head>
    <body bgcolor="#ffffff">
    <h1><tt><font color="#ff0000">&lt;</font>bigwig<font color="#ff0000">&gt;</font></tt> WebBoard Users</h1>
    <continue type="button" value="DELETE">Delete</continue>
    <continue type="button" value="BACK">Back</continue>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <table border=1>
    <tr>
      <th>&nbsp;</th>
      <th colspan="3">Identity</th>
      <th>&nbsp;</th>
      <th colspan="5">Filter</th>
      <th colspan="4">Misc.</th>
      <th>&nbsp;</th>
    </tr>
    <tr>
      <th>Delete?</th>

      <th>Login</th>
      <th>Name</th>
      <th>Email</th>

      <th>notify</th>

      <th>filter</th>
      <th>grayout</th>
      <th>days</th>
      <th>custom</th>
      <th>last</th>

      <th>navbar</th>
      <th>indent</th>
      <th>ampm</th>
      <th>gmt</th>

      <th>#Logins</th>
    </tr>
    <[users]>
    </table>
    <br>
    </td></tr></table>
    <p>
    Total number of users: <[no_users]>
    </p>
    <p>
    <continue type="button" value="DELETE">Delete</continue>
    <continue type="button" value="BACK">Back</continue>
    </p>
    </body>
  </html>;
  html AdminShowBoardDoc = <html>
    <head><title>Webboard: admin show board</title></head>
    <body bgcolor="#ffffff">
    <h1><tt><font color="#ff0000">&lt;</font>bigwig<font color="#ff0000">&gt;</font></tt> WebBoard Administration</h1>
    <continue type=button value="USERS">Show Users</continue>
    <continue type=button value="QUIT">Quit</continue>
    <br>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <[]><continue value="0"><b>MESSAGES:</b></continue>
    <br>
    <[msgs]>
    </td></tr></table>
    <br>
    Total number of messages: <[no_msgs]>
    <br>
    <hr noshade size=3>
    <br>
    <h3>Cut'n'Paste Buffer</h3>
    <input type="radio" name="cut" checked value="true"> <b>Cut</b>:
    Click on a message to move it (along with all its replies)
    to the cut'n'paste buffer.<br>
    <input type="radio" name="cut" value="false"> <b>Paste</b>:
    Click on a message to move the cut'n'paste buffer onto the 
    board as a reply to it.
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    <[cutpaste]>
    </td></tr></table>
    <br>
    Total number of messages: <[no_cutpaste_msgs]>
    <p>
    <continue type=button value="EMPTY">Empty Buffer</continue>
    </p>
    <br>
    <hr noshade size=3>
    <br>
    <continue type=button value="USERS">Show Users</continue>
    <continue type=button value="QUIT">Quit</continue>
    </body>
  </html>;
  html AdminByeByeDoc = <html>
    <head><title>Webboard: admin goodbye</title></head>
    <body bgcolor="#ffffff">
    <h1>Goodbye Administrator.</h1>
    <table border=1 bgcolor="#ffffcc" width="100%"><tr><td>
    Thank you for using the WebBoard.
    <p>
    You made <[no_cut]> <i>cut<[cut_s]></i> and <[no_paste]> <i>paste<[paste_s]></i>.
    </p>
    </td></tr></table>
    </body>
  </html>;
  
  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];
  }
}