]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
considerable coding
authorEvan Prodromou <evan@prodromou.name>
Wed, 14 May 2008 14:54:36 +0000 (10:54 -0400)
committerEvan Prodromou <evan@prodromou.name>
Wed, 14 May 2008 14:54:36 +0000 (10:54 -0400)
darcs-hash:20080514145436-84dde-d0994cb35d3fe8545d3f08abeec3cdfe7559c67d.gz

15 files changed:
TODO [new file with mode: 0644]
URLS.txt
actions/login.php
actions/logout.php [new file with mode: 0644]
actions/newnotice.php [new file with mode: 0644]
actions/register.php [new file with mode: 0644]
actions/settings.php [new file with mode: 0644]
actions/shownotice.php
actions/showstream.php
actions/subscribe.php [new file with mode: 0644]
actions/unsubscribe.php [new file with mode: 0644]
classes/User.php
common.php
index.php
stoica.sql

diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..08ac0a5
--- /dev/null
+++ b/TODO
@@ -0,0 +1,62 @@
++ login
++ register
+- settings
++ disallow login if user is logged in
++ disallow register if user is logged in
++ common_current_user()
++ common_logged_in()
++ session variable for login
++ post notice
++ logout
++ subscribe
++ unsubscribe
++ subscribe links on profile
+- licenses
+- header menu
+- footer menu
+- disallow direct to PHP files
+- common_local_url()
+- configuration system ($config)
+- RSS 1.0 feeds of a user's notices
+- RSS 1.0 dump of a user's notices
+- RSS 1.0 feed of all public notices
+- RDF dump of entire site
+- FOAF dump for user
+- delete a notice
+- make sure canonical usernames are unique
+- upload avatar
+- design from Open Source Web Designs
+- release 0.1
+- gettext
+- subscribe remote
+- add subscriber remote
+- send remote notice
+- receive remote notice
+- confirmation email
+- tinyurl-ification of URLs
+- jQuery for as much as possible
+- themes
+- release 0.2
+- @ messages
+- # tags
+- L: location
+- stay logged in between sessions
+- use RSS as a subscription
+- URL notices
+- image notices
+- video notices
+- audio notices
+- release 0.3
+- forward notices to Jabber
+- forward notices to other IM
+- forward notices to mobile phone
+- machine tags
+- release 0.4
+- include twitter subscriptions
+- include Pownce subscriptions
+- privacy
+- Wrap DB_DataObject with memcached caching layer
+- login throttle to prevent brute-force attacks
+- form token in login to prevent XSS
+- release 1.0
+- Atom Publishing Protocol
index b544bd03694c41bc00699fddb887a635cd4891b7..bcbad40da22cb416302be55a6c7b95054797162a 100644 (file)
--- a/URLS.txt
+++ b/URLS.txt
@@ -11,6 +11,7 @@
 
 /main/login                    login to site
 /main/register                 register to site
+/main/settings                 change account settings
 /main/recover                  recover password
 /doc/                          documentation
      about                     about this site
index a95dc9e3a3d4511e5f3e3ac81570a242c01e5c4b..b939362973cc36e5247112a47453d505687b117c 100644 (file)
@@ -1,25 +1,59 @@
 <?php
 
-function handle_login() {
-       if ($_REQUEST['METHOD'] == 'POST') {
-               if (login_check_user($_REQUEST['user'], $_REQUEST['password'])) {
-                       
+class LoginAction extends Action {
+       
+       function handle($args) {
+               parent::handle($args);
+               if (common_logged_in()) {
+                       common_user_error(_t('Already logged in.'));
+               } else if ($this->arg('METHOD') == 'POST') {
+                       $this->check_login();
                } else {
+                       $this->show_form();
                }
-       } else {
-               if (user_logged_in()) {
+       }
+
+       function check_login() {
+               # XXX: form token in $_SESSION to prevent XSS
+               # XXX: login throttle
+               $nickname = $this->arg('nickname');
+               $password = $this->arg('password');
+               if (common_check_user($nickname, $password)) {
+                       common_set_user($nickname);
+                       common_redirect(common_local_url('all',
+                                                                                        array('nickname' =>
+                                                                                                  $nickname)));
                } else {
-                       login_show_form();
+                       $this->show_form(_t('Incorrect username or password.'));
                }
        }
-}
        
-function login_show_form() {
-       html_start();
-       html_head("Login");
-       html_body();
+       function show_form($error=NULL) {
+               
+               common_show_header(_t('Login'));
+               if (!is_null($error)) {
+                       common_element('div', array('class' => 'error'), $msg);
+               }
+               common_start_element('form', array('method' => 'POST',
+                                                                                  'id' => 'login',
+                                                                                  'action' => common_local_url('login')));
+               common_element('label', array('for' => 'username'),
+                                          _t('Name'));
+               common_element('input', array('name' => 'username',
+                                                                         'type' => 'text',
+                                                                         'id' => 'username'));
+               common_element('label', array('for' => 'password'),
+                                          _t('Password'));
+               common_element('input', array('name' => 'password',
+                                                                         'type' => 'password',                                                                   
+                                                                         'id' => 'password'));
+               common_element('input', array('name' => 'submit',
+                                                                         'type' => 'submit',
+                                                                         'id' => 'submit'),
+                                          _t('Login'));
+               common_element('input', array('name' => 'cancel',
+                                                                         'type' => 'button',
+                                                                         'id' => 'cancel'),
+                                          _t('Cancel'));
+       }
 }
-       
-function login_check_user($username, $password) {
-       
-}
\ No newline at end of file
diff --git a/actions/logout.php b/actions/logout.php
new file mode 100644 (file)
index 0000000..a40400e
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+
+class LogoutAction extends Action {
+       function handle($args) {
+               parent::handle($args);
+               if (!common_logged_in()) {
+                       common_user_error(_t('Not logged in.'));
+               } else {
+                       common_set_user(NULL);
+                       common_redirect(common_local_url('main'));
+               }
+       }
+}
diff --git a/actions/newnotice.php b/actions/newnotice.php
new file mode 100644 (file)
index 0000000..bbfa328
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+class NewnoticeAction extends Action {
+       
+       function handle($args) {
+               parent::handle($args);
+               # XXX: Ajax!
+
+               if (!common_logged_in()) {
+                       common_user_error(_t('Not logged in.'));
+               } else if ($this->arg('METHOD') == 'POST') {
+                       if ($this->save_new_notice()) {
+                               # XXX: smarter redirects
+                               $user = common_current_user();
+                               assert(!is_null($user)); # see if... above
+                               # XXX: redirect to source
+                               # XXX: use Ajax instead of a redirect
+                               common_redirect(common_local_url('all',
+                                                                                                array('nickname' =>
+                                                                                                          $user->nickname)));
+                       } else {
+                               common_server_error(_t('Problem saving notice.'));
+                       }
+               } else {
+                       $this->show_form();
+               }
+       }
+       
+       function save_new_notice() {
+               $user = common_current_user();
+               assert($user); # XXX: maybe an error instead...
+               $notice = DB_DataObject::factory('notice');
+               assert($notice);
+               $notice->profile_id = $user->id; # user id *is* profile id
+               $notice->content = $this->arg('content');
+               $notice->created = time();
+               return $notice->insert();
+       }
+       
+       function show_form() {
+               common_start_element('form', array('id' => 'newnotice', 'method' => 'POST',
+                                                                                  'action' => common_local_url('newnotice')));
+               common_element('span', 'nickname', $profile->nickname);
+               common_element('textarea', array('rows' => 4, 'cols' => 80, 'id' => 'content'));
+               common_element('input', array('type' => 'submit'), 'Send');
+               common_end_element('form');
+       }
+}
\ No newline at end of file
diff --git a/actions/register.php b/actions/register.php
new file mode 100644 (file)
index 0000000..5972d58
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+
+class RegisterAction extends Action {
+       
+       function handle($args) {
+               parent::handle($args);
+               
+               if (common_logged_in()) {
+                       common_user_error(_t('Already logged in.'));
+               } else if ($this->arg('METHOD') == 'POST') {
+                       $this->try_register();
+               } else {
+                       $this->show_form();
+               }
+       }
+
+       function try_register() {
+               $nickname = $this->arg('nickname');
+               $password = $this->arg('password');
+               $confirm = $this->arg('confirm');
+               $email = $this->arg('email');
+               
+               # Input scrubbing
+               
+               $nickname = common_canonical_nickname($nickname);
+               $email = common_canonical_email($email);
+               
+               if ($this->nickname_exists($nickname)) {
+                       $this->show_form(_t('Username already exists.'));
+               } else if ($this->email_exists($email)) {
+                       $this->show_form(_t('Email address already exists.'));
+               } else if ($password != $confirm) {
+                       $this->show_form(_t('Passwords don\'t match.'));
+               } else if ($this->register_user($nickname, $password, $email)) {
+                       common_set_user($nickname);
+                       common_redirect(common_local_url('settings'));
+               } else {
+                       $this->show_form(_t('Invalid username or password.'));
+               }
+       }
+
+       # checks if *CANONICAL* nickname exists
+       
+       function nickname_exists($nickname) {
+               $user = User::staticGet('nickname', $nickname);
+               return ($user !== false);
+       }
+
+       # checks if *CANONICAL* email exists
+       
+       function email_exists($email) {
+               $email = common_canonicalize_email($email);
+               $user = User::staticGet('email', $email);
+               return ($user !== false);
+       }
+
+       function register_user($nickname, $password, $email) {
+               # TODO: wrap this in a transaction!
+               $profile = new Profile();
+               $profile->nickname = $nickname;
+               $profile->created = time();
+               $id = $profile->insert();
+               if (!$id) {
+                       return FALSE;
+               }
+               $user = new User();
+               $user->id = $id;
+               $user->nickname = $nickname;
+               $user->password = common_munge_password($password, $id);
+               $user->email = $email;
+               $user->created = time();
+               $result = $user->insert();
+               if (!$result) {
+                       # Try to clean up...
+                       $profile->delete();
+               }
+               return $result;
+       }
+       
+       function show_form($error=NULL) {
+               
+               common_show_header(_t('Login'));
+               common_start_element('form', array('method' => 'POST',
+                                                                                  'id' => 'login',
+                                                                                  'action' => common_local_url('login')));
+               common_element('label', array('for' => 'username'),
+                                          _t('Name'));
+               common_element('input', array('name' => 'username',
+                                                                         'type' => 'text',
+                                                                         'id' => 'username'));
+               common_element('label', array('for' => 'password'),
+                                          _t('Password'));
+               common_element('input', array('name' => 'password',
+                                                                         'type' => 'password',                                                                   
+                                                                         'id' => 'password'));
+               common_element('label', array('for' => 'confirm'),
+                                          _t('Confirm'));
+               common_element('input', array('name' => 'confirm',
+                                                                         'type' => 'password',                                                                   
+                                                                         'id' => 'confirm'));
+               common_element('label', array('for' => 'email'),
+                                          _t('Email'));
+               common_element('input', array('name' => 'email',
+                                                                         'type' => 'text',                                                                       
+                                                                         'id' => 'email'));
+               common_element('input', array('name' => 'submit',
+                                                                         'type' => 'submit',
+                                                                         'id' => 'submit'),
+                                          _t('Login'));
+               common_element('input', array('name' => 'cancel',
+                                                                         'type' => 'button',
+                                                                         'id' => 'cancel'),
+                                          _t('Cancel'));
+       }
+}
diff --git a/actions/settings.php b/actions/settings.php
new file mode 100644 (file)
index 0000000..826770a
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+class SettingsAction extends Action {
+       
+       function handle($args) {
+               parent::handle($args);
+               if ($this->arg('METHOD') == 'POST') {
+                       $nickname = $this->arg('nickname');
+                       $fullname = $this->arg('fullname');
+                       $email = $this->arg('email');
+                       $homepage = $this->arg('homepage');
+                       $bio = $this->arg('bio');
+                       $location = $this->arg('location');
+                       $oldpass = $this->arg('oldpass');
+                       $password = $this->arg('password');
+                       $confirm = $this->arg('confirm');
+                       
+                       if ($password) {
+                               if ($password != $confirm) {
+                                       $this->show_form(_t('Passwords don\'t match.'));
+                               }
+                       } else if (
+                       
+                       $error = $this->save_settings($nickname, $fullname, $email, $homepage,
+                                                                                 $bio, $location, $password);
+                       if (!$error) {
+                               $this->show_form(_t('Settings saved.'), TRUE);
+                       } else {
+                               $this->show_form($error);
+                       }
+               } else {
+                       $this->show_form();
+               }
+                               
\ No newline at end of file
index 4d4876122c0195c0093706f70c2f469f741c149b..b3204d0634fbbcb4634ab23cc9a189f7770f282d 100644 (file)
@@ -37,7 +37,8 @@ class ShownoticeAction extends Action {
                                                                                'class' => 'nickname'),
                                                         $profile->nickname);
                # FIXME: URL, image, video, audio
-               common_element('span', array('class' => 'content'), $notice->content);
+               common_element('span', array('class' => 'content'),
+                                          $notice->content);
                common_element('span', array('class' => 'date'),
                                           common_date_string($notice->created));
                common_end_element('div');
index 1eb060fdc943c2b653b541e6c53acb5e988f4a5a..5950a4ead032762c8a65d6c7c11a84b86612011f 100644 (file)
@@ -9,34 +9,43 @@ class ShowstreamAction extends StreamAction {
                
                parent::handle($args);
 
-               $nickname = $this->arg('profile');
-               $profile = Profile::staticGet('nickname', strtolower($nickname));
-       
-               if (!$profile) {
-                       $this->no_such_user();
-               } 
-               
-               $user = User::staticGet($profile->id);
-               
+               $nickname = common_canonicalize_nickname($this->arg('profile'));
+               $user = User::staticGet('nickname', $nickname);
+
                if (!$user) {
-                       // remote profile
                        $this->no_such_user();
+               } 
+
+               $profile = $user->getProfile();
+
+               if (!$profile) {
+                       common_server_error(_t('User record exists without profile.'));
                }
                
                # Looks like we're good; show the header
                
                common_show_header($profile->nickname);
+
+               $cur = common_current_user();
                
-               if ($profile->id == current_user()->id) {
+               if ($cur && $profile->id == $cur->id) {
                        $this->notice_form();
                }
        
                $this->show_profile($profile);
 
                $this->show_last_notice($profile);
+
+               if ($cur) {
+                       if ($cur->isSubscribed($profile)) {
+                               $this->show_unsubscribe_form($profile);
+                       } else {
+                               $this->show_subscribe_form($profile);
+                       }
+               }
                
                $this->show_statistics($profile);
-               
+
                $this->show_subscriptions($profile);
                
                $this->show_notices($profile);
@@ -75,13 +84,33 @@ class ShowstreamAction extends StreamAction {
                        common_element('div', 'bio', $profile->bio);
                }
        }
+
+       function show_subscribe_form($profile) {
+               common_start_element('form', array('id' => 'subscribe', 'method' => 'POST',
+                                                                                  'action' => common_local_url('subscribe')));
+               common_element('input', array('id' => 'subscribeto',
+                                                                         'name' => 'subscribeto',
+                                                                         'type' => 'hidden',
+                                                                         'value' => $profile->nickname));
+               common_element('input', array('type' => 'submit'), _t('subscribe'));
+               common_end_element('form');
+       }
+
+       function show_unsubscribe_form($profile) {
+               common_start_element('form', array('id' => 'unsubscribe', 'method' => 'POST',
+                                                                                  'action' => common_local_url('unsubscribe')));
+               common_element('input', array('id' => 'unsubscribeto',
+                                                                         'name' => 'unsubscribeto',
+                                                                         'type' => 'hidden',
+                                                                         'value' => $profile->nickname));
+               common_element('input', array('type' => 'submit'), _t('unsubscribe'));
+               common_end_element('form');
+       }
        
        function show_subscriptions($profile) {
-
-               # XXX: add a limit
                
+               # XXX: add a limit
                $subs = $profile->getLink('id', 'subscription', 'subscriber');
-
                common_start_element('div', 'subscriptions');
                
                $cnt = 0;
@@ -113,7 +142,7 @@ class ShowstreamAction extends StreamAction {
                                                                                                                         array('profile' => $profile->nickname))
                                                                  'class' => 'moresubscriptions'),
                                           _t('All subscriptions'));
-               
+
                common_end_element('div');
        }
 
@@ -174,7 +203,8 @@ class ShowstreamAction extends StreamAction {
                
                while ($notice->fetch()) {
                        # FIXME: URL, image, video, audio
-                       common_element('span', array('class' => 'content'), $notice->content);
+                       common_element('span', array('class' => 'content'), 
+                                                  $notice->content);
                        common_element('span', array('class' => 'date'),
                                                   common_date_string($notice->created));
                }
diff --git a/actions/subscribe.php b/actions/subscribe.php
new file mode 100644 (file)
index 0000000..35961d0
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+class SubscribeAction extends Action {
+       function handle($args) {
+               parent::handle($args);
+               
+               if (!common_logged_in()) {
+                       common_user_error(_t('Not logged in.'));
+                       return;
+               }
+               
+               $other_nickname = $this->arg('subscribeto');
+
+               $other = User::staticGet('nickname', $other_nickname);
+               
+               if (!$other) {
+                       common_user_error(_t('No such user.'));
+                       return;
+               }
+               
+               $user = common_current_user();
+
+               if ($user->isSubscribed($other)) {
+                       common_user_error(_t('Already subscribed!.'));
+                       return;
+               }
+               
+               $sub = new Subscription();
+               $sub->subscriber = $user->id;
+               $sub->subscribed = $other->id;
+               
+               $sub->created = time();
+               
+               if (!$sub->insert()) {
+                       common_server_error(_t('Couldn\'t create subscription.'));
+                       return;
+               }
+               
+               common_redirect(common_local_url('all', array('nickname' =>
+                                                                                                         $user->nickname)));
+       }
+}
\ No newline at end of file
diff --git a/actions/unsubscribe.php b/actions/unsubscribe.php
new file mode 100644 (file)
index 0000000..c4e6b98
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+class UnsubscribeAction extends Action {
+       function handle($args) {
+               parent::handle($args);
+               if (!common_logged_in()) {
+                       common_user_error(_t('Not logged in.'));
+                       return;
+               }
+               $other_nickname = $this->arg('unsubscribeto');
+               $other = User::staticGet('nickname', $other_nickname);
+               if (!$other) {
+                       common_user_error(_t('No such user.'));
+                       return;
+               }
+               
+               $user = common_current_user();
+
+               if (!$user->isSubscribed($other)) {
+                       common_server_error(_t('Not subscribed!.'));
+               }
+               
+               $sub = new Subscription();
+               $sub->subscriber = $user->id;
+               $sub->subscribed = $other->id;
+               
+               if (!$sub->delete()) {
+                       common_server_error(_t('Couldn\'t delete subscription.'));
+                       return;
+               }
+               
+               common_redirect(common_local_url('all', array('nickname' =>
+                                                                                                         $user->nickname)));
+       }
+}
index 4ed6003dcda78b84844fdd7005f665c78c27042e..8234e07848cf2e462b88bedc6dc8624665f74ff5 100644 (file)
@@ -21,4 +21,16 @@ class User extends DB_DataObject
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
+       
+       function getProfile() {
+               return Profile::staticGet($this->$id);
+       }
+       
+       function isSubscribed($other) {
+               assert(!is_null($other));
+               $sub = DB_DataObject::factory('subscription');
+               $sub->subscriber = $this->id;
+               $sub->subscribed = $other->id;
+               return $sub->find();
+       }
 }
index a6061920d4a3cec64bd89b25dd0a678a79b39b99..de0529a2e8bf0deffc918a7f62bc0a5ae50d7e57 100644 (file)
@@ -5,7 +5,7 @@
 // default configuration, overwritten in config.php
 
 $config =
-  array('site' => 
+  array('site' =>
                array('name' => 'Just another µB'),
                'dsn' =>
                array('phptype' => 'mysql',
@@ -20,20 +20,7 @@ $config =
 require_once(INSTALLDIR . '/config.php');
 require_once('DB.php');
 
-function common_database() {
-       global $config;
-       $db =& DB::connect($config['dsn'], $config['dboptions']);
-       if (PEAR::isError($db)) {
-               common_server_error($db->getMessage());
-       } else {
-               return $db;
-       }
-}
-
-function common_read_database() {
-       // XXX: read from slave server
-       return common_database();
-}
+# Show a server error
 
 function common_server_error($msg) {
        header('Status: 500 Server Error');
@@ -43,12 +30,14 @@ function common_server_error($msg) {
        exit();
 }
 
-function common_user_error($msg) {
+# Show a user error
+function common_user_error($msg, $code=200) {
        common_show_header('Error');
        common_element('div', array('class' => 'error'), $msg);
        common_show_footer();
 }
 
+# Start an HTML element
 function common_element_start($tag, $attrs=NULL) {
        print "<$tag";
        if (is_array($attrs)) {
@@ -67,7 +56,7 @@ function common_element_end($tag) {
 
 function common_element($tag, $attrs=NULL, $content=NULL) {
     common_element_start($tag, $attrs);
-       if ($content) print $content;
+       if ($content) print htmlspecialchars($content);
        common_element_end($tag);
 }
 
@@ -75,7 +64,8 @@ function common_show_header($pagetitle) {
        global $config;
        common_element_start('html');
        common_element_start('head');
-       common_element('title', NULL, $pagetitle . " - " . $config['site']['name']);
+       common_element('title', NULL, 
+                                  $pagetitle . " - " . $config['site']['name']);
        common_element_end('head');
        common_element_start('body');
 }
@@ -85,6 +75,82 @@ function common_show_footer() {
        common_element_end('html');
 }
 
-// TODO: set up gettext
+# salted, hashed passwords are stored in the DB
+
+function common_munge_password($id, $password) {
+       return md5($id . $password);
+}
+
+# check if a username exists and has matching password
+function common_check_user($nickname, $password) {
+       $user = User::staticGet('nickname', $nickname);
+       if (is_null($user)) {
+               return false;
+       } else {
+               return (0 == strcmp(common_munge_password($password, $user->id), 
+                                                       $user->password));
+       }
+}
+
+# is the current user logged in?
+function common_logged_in() {
+       return (!is_null(common_current_user()));
+}
+
+function common_have_session() {
+       return (0 != strcmp(session_id(), ''));
+}
+
+function common_ensure_session() {
+       if (!common_have_session()) {
+               @session_start();
+       }
+}
+
+function common_set_user($nickname) {
+       if (is_null($nickname) && common_have_session()) {
+               unset($_SESSION['userid']);
+               return true;
+       } else {
+               $user = User::staticGet('nickname', $nickname);
+               if ($user) {
+                       common_ensure_session();
+                       $_SESSION['userid'] = $user->id;
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+       return false;
+}
+
+# who is the current user?
+function common_current_user() {
+       static $user = NULL; # FIXME: global memcached
+       if (is_null($user)) {
+               if (common_have_session()) {
+                       $id = $_SESSION['userid'];
+                       if ($id) {
+                               $user = User::staticGet($id);
+                       }
+               }
+       }
+       return $user;
+}
+
+# get canonical version of nickname for comparison
+function common_canonical_nickname($nickname) {
+       # XXX: UTF-8 canonicalization (like combining chars)
+       return strtolower($nickname);
+}
+
+function common_render_content($text) {
+       # XXX: @ messages
+       # XXX: # tags
+       # XXX: machine tags
+       return htmlspecialchars($text);
+}
+
+// XXX: set up gettext
 
 function _t($str) { $str }
index d63d09edf537c2c5e0f8b2a498251fb0ec8d489c..7237c08bc00f0527baba544d45e188fd8b9e36a4 100644 (file)
--- a/index.php
+++ b/index.php
@@ -9,7 +9,7 @@ $actionfile = INSTALLDIR."/actions/$action.php";
 
 if (file_exists($actionfile)) {
        require_once($actionfile);
-       $action_function = 'handle_' . $action;
+       $action_class = ucfirst($action) . "Action";
        if (function_exists($action_function)) {
        call_user_func($action_function);
 } else {
index f7f32ad76fcb0ff29ab1ffdcb411e82e1882c35b..28e8f7662c32d455f43752b2aa22e1baa61ebdcb 100644 (file)
@@ -18,6 +18,7 @@ create table profile (
 
 create table user (
     id integer primary key comment 'foreign key to profile table' references profile (id),
+    nickname varchar(64) unique key comment 'nickname or username, duped in profile',
     password varchar(255) comment 'salted password, can be null for OpenID users',
     email varchar(255) unique key comment 'email address for password recovery etc.',
     created datetime not null comment 'date this record was created',
@@ -49,7 +50,7 @@ create table notice (
     id integer auto_increment primary key comment 'unique identifier',
     profile_id integer not null comment 'who made the update' references profile (id),
     content varchar(140) comment 'update content',
-    rendered varchar(140) comment 'pre-rendered content',
+    /* XXX: cache rendered content. */
     url varchar(255) comment 'URL of any attachment (image, video, bookmark, whatever)',
     created datetime not null comment 'date this record was created',
     modified timestamp comment 'date this record was modified',