code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008, 2009, 2010 StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) { exit(1); }

class Status_network_tag extends Safe_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'status_network_tag';                      // table name
    public $site_id;                  // int(4)  primary_key not_null
    public $tag;                      // varchar(64)  primary_key not_null
    public $created;                 // datetime()   not_null

    function __construct()
    {
        global $config;
        global $_DB_DATAOBJECT;

        $sn = new Status_network();
        $sn->_connect();

        $config['db']['table_'. $this->tableName()] = $sn->_database;

        $this->_connect();
    }

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    /* Static get */
    static function getKV($k,$v=null)
    {
        // TODO: This probably has to be converted to a non-static call
        $i = DB_DataObject::staticGet('Status_network_tag',$k,$v);

        // Don't use local process cache; if we're fetching multiple
        // times it's because we're reloading it in a long-running
        // process; we need a fresh copy!
        global $_DB_DATAOBJECT;
        unset($_DB_DATAOBJECT['CACHE']['status_network_tag']);
        return $i;
    }

    static function pkeyGet($kv)
    {
        return Memcached_DataObject::pkeyGetClass('Status_network_tag', $kv);
    }

    /**
     * Fetch the (possibly cached) tag entries for the given site id.
     * Uses status_network's cache settings.
     *
     * @param string $site_id
     * @return array of strings
     */
    static function getTags($site_id)
    {
        $key = 'status_network_tags:' . $site_id;
        if (Status_network::$cache) {
            $packed = Status_network::$cache->get($key);
            if (is_string($packed)) {
                if ($packed == '') {
                    return array();
                } else {
                    return explode('|', $packed);
                }
            }
        }

        $result = array();

        $tags = new Status_network_tag();
        $tags->site_id = $site_id;
        if ($tags->find()) {
            while ($tags->fetch()) {
                $result[] = $tags->tag;
            }
        }

        if (Status_network::$cache) {
            $packed = implode('|', $result);
            Status_network::$cache->set($key, $packed, 0, 3600);
        }

        return $result;
    }

    /**
     * Drop the cached tag entries for this site.
     * Needed after inserting/deleting a tag entry.
     */
    function decache()
    {
        $key = 'status_network_tags:' . $this->site_id;
        if (Status_network::$cache || Status_network::$cacheInitialized) {
            // FIXME: this was causing errors, so I'm hiding them.
            // I'm a big chicken and lazy.
            @Status_network::$cache->delete($key);
        }
    }

    function insert()
    {
        $ret = parent::insert();
        $this->decache();
        return $ret;
    }

    function delete($useWhere=false)
    {
        $this->decache();
        return parent::delete($useWhere);
    }

    static function withTag($tag)
    {
        $snt = new Status_network_tag();

        $snt->tag = $tag;

        $snt->find();

        return $snt;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008, 2009, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Table Definition for subscription
 */
class Subscription extends Managed_DataObject
{
    const CACHE_WINDOW = 201;
    const FORCE = true;

    public $__table = 'subscription';                    // table name
    public $subscriber;                      // int(4)  primary_key not_null
    public $subscribed;                      // int(4)  primary_key not_null
    public $jabber;                          // tinyint(1)   default_1
    public $sms;                             // tinyint(1)   default_1
    public $token;                           // varchar(191)   not 255 because utf8mb4 takes more space
    public $secret;                          // varchar(191)   not 255 because utf8mb4 takes more space
    public $uri;                             // varchar(191)   not 255 because utf8mb4 takes more space
    public $created;                         // datetime()   not_null
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'profile listening'),
                'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'profile being listened to'),
                'jabber' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'deliver jabber messages'),
                'sms' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'deliver sms messages'),
                'token' => array('type' => 'varchar', 'length' => 191, 'description' => 'authorization token'),
                'secret' => array('type' => 'varchar', 'length' => 191, 'description' => 'token secret'),
                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('subscriber', 'subscribed'),
            'unique keys' => array(
                'subscription_uri_key' => array('uri'),
            ),
            'indexes' => array(
                'subscription_subscriber_idx' => array('subscriber', 'created'),
                'subscription_subscribed_idx' => array('subscribed', 'created'),
                'subscription_token_idx' => array('token'),
            ),
        );
    }

    /**
     * Make a new subscription
     *
     * @param Profile $subscriber party to receive new notices
     * @param Profile $other      party sending notices; publisher
     * @param bool    $force      pass Subscription::FORCE to override local subscription approval
     *
     * @return mixed Subscription or Subscription_queue: new subscription info
     */

    static function start(Profile $subscriber, Profile $other, $force=false)
    {
        if (!$subscriber->hasRight(Right::SUBSCRIBE)) {
            // TRANS: Exception thrown when trying to subscribe while being banned from subscribing.
            throw new Exception(_('You have been banned from subscribing.'));
        }

        if (self::exists($subscriber, $other)) {
            // TRANS: Exception thrown when trying to subscribe while already subscribed.
            throw new AlreadyFulfilledException(_('Already subscribed!'));
        }

        if ($other->hasBlocked($subscriber)) {
            // TRANS: Exception thrown when trying to subscribe to a user who has blocked the subscribing user.
            throw new Exception(_('User has blocked you.'));
        }

        if (Event::handle('StartSubscribe', array($subscriber, $other))) {
            // unless subscription is forced, the user policy for subscription approvals is tested
            if (!$force && $other->requiresSubscriptionApproval($subscriber)) {
                try {
                    $sub = Subscription_queue::saveNew($subscriber, $other);
                    $sub->notify();
                } catch (AlreadyFulfilledException $e) {
                    $sub = Subscription_queue::getSubQueue($subscriber, $other);
                }
            } else {
                $otherUser = User::getKV('id', $other->id);
                $sub = self::saveNew($subscriber, $other);
                $sub->notify();

                self::blow('user:notices_with_friends:%d', $subscriber->id);

                self::blow('subscription:by-subscriber:'.$subscriber->id);
                self::blow('subscription:by-subscribed:'.$other->id);

                $subscriber->blowSubscriptionCount();
                $other->blowSubscriberCount();

                if ($otherUser instanceof User &&
                    $otherUser->autosubscribe &&
                    !self::exists($other, $subscriber) &&
                    !$subscriber->hasBlocked($other)) {

                    try {
                        self::start($other, $subscriber);
                    } catch (AlreadyFulfilledException $e) {
                        // This shouldn't happen due to !self::exists above
                        common_debug('Tried to autosubscribe a user to its new subscriber.');
                    } catch (Exception $e) {
                        common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}");
                    }
                }
            }

            if ($sub instanceof Subscription) { // i.e. not Subscription_queue
                Event::handle('EndSubscribe', array($subscriber, $other));
            }
        }

        return $sub;
    }

    static function ensureStart(Profile $subscriber, Profile $other, $force=false)
    {
        try {
            $sub = self::start($subscriber, $other, $force);
        } catch (AlreadyFulfilledException $e) {
            return self::getSubscription($subscriber, $other);
        }
        return $sub;
    }

    /**
     * Low-level subscription save.
     * Outside callers should use Subscription::start()
     */
    protected static function saveNew(Profile $subscriber, Profile $other)
    {
        $sub = new Subscription();

        $sub->subscriber = $subscriber->getID();
        $sub->subscribed = $other->getID();
        $sub->jabber     = 1;
        $sub->sms        = 1;
        $sub->created    = common_sql_now();
        $sub->uri        = self::newUri($subscriber,
                                        $other,
                                        $sub->created);

        $result = $sub->insert();

        if ($result===false) {
            common_log_db_error($sub, 'INSERT', __FILE__);
            // TRANS: Exception thrown when a subscription could not be stored on the server.
            throw new Exception(_('Could not save subscription.'));
        }

        return $sub;
    }

    function notify()
    {
        // XXX: add other notifications (Jabber, SMS) here
        // XXX: queue this and handle it offline
        // XXX: Whatever happens, do it in Twitter-like API, too

        $this->notifyEmail();
    }

    function notifyEmail()
    {
        $subscribedUser = User::getKV('id', $this->subscribed);

        if ($subscribedUser instanceof User) {

            $subscriber = Profile::getKV('id', $this->subscriber);

            mail_subscribe_notify_profile($subscribedUser, $subscriber);
        }
    }

    /**
     * Cancel a subscription
     *
     */
    static function cancel(Profile $subscriber, Profile $other)
    {
        if (!self::exists($subscriber, $other)) {
            // TRANS: Exception thrown when trying to unsibscribe without a subscription.
            throw new AlreadyFulfilledException(_('Not subscribed!'));
        }

        // Don't allow deleting self subs

        if ($subscriber->id == $other->id) {
            // TRANS: Exception thrown when trying to unsubscribe a user from themselves.
            throw new Exception(_('Could not delete self-subscription.'));
        }

        if (Event::handle('StartUnsubscribe', array($subscriber, $other))) {

            $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
                                               'subscribed' => $other->id));

            // note we checked for existence above

            assert(!empty($sub));

            $result = $sub->delete();

            if (!$result) {
                common_log_db_error($sub, 'DELETE', __FILE__);
                // TRANS: Exception thrown when a subscription could not be deleted on the server.
                throw new Exception(_('Could not delete subscription.'));
            }

            self::blow('user:notices_with_friends:%d', $subscriber->id);

            self::blow('subscription:by-subscriber:'.$subscriber->id);
            self::blow('subscription:by-subscribed:'.$other->id);

            $subscriber->blowSubscriptionCount();
            $other->blowSubscriberCount();

            Event::handle('EndUnsubscribe', array($subscriber, $other));
        }

        return;
    }

    static function exists(Profile $subscriber, Profile $other)
    {
        try {
            $sub = self::getSubscription($subscriber, $other);
        } catch (NoResultException $e) {
            return false;
        }

        return true;
    }

    static function getSubscription(Profile $subscriber, Profile $other)
    {
        // This is essentially a pkeyGet but we have an object to return in NoResultException
        $sub = new Subscription();
        $sub->subscriber = $subscriber->id;
        $sub->subscribed = $other->id;
        if (!$sub->find(true)) {
            throw new NoResultException($sub);
        }
        return $sub;
    }

    public function getSubscriber()
    {
        return Profile::getByID($this->subscriber);
    }

    public function getSubscribed()
    {
        return Profile::getByID($this->subscribed);
    }

    function asActivity()
    {
        $subscriber = $this->getSubscriber();
        $subscribed = $this->getSubscribed();

        $act = new Activity();

        $act->verb = ActivityVerb::FOLLOW;

        // XXX: rationalize this with the URL

        $act->id   = $this->getUri();

        $act->time    = strtotime($this->created);
        // TRANS: Activity title when subscribing to another person.
        $act->title = _m('TITLE','Follow');
        // TRANS: Notification given when one person starts following another.
        // TRANS: %1$s is the subscriber, %2$s is the subscribed.
        $act->content = sprintf(_('%1$s is now following %2$s.'),
                               $subscriber->getBestName(),
                               $subscribed->getBestName());

        $act->actor     = $subscriber->asActivityObject();
        $act->objects[] = $subscribed->asActivityObject();

        $url = common_local_url('AtomPubShowSubscription',
                                array('subscriber' => $subscriber->id,
                                      'subscribed' => $subscribed->id));

        $act->selfLink = $url;
        $act->editLink = $url;

        return $act;
    }

    /**
     * Stream of subscriptions with the same subscriber
     *
     * Useful for showing pages that list subscriptions in reverse
     * chronological order. Has offset & limit to make paging
     * easy.
     *
     * @param integer $profile_id   ID of the subscriber profile
     * @param integer $offset       Offset from latest
     * @param integer $limit        Maximum number to fetch
     *
     * @return Subscription stream of subscriptions; use fetch() to iterate
     */
    public static function bySubscriber($profile_id, $offset = 0, $limit = PROFILES_PER_PAGE)
    {
        // "by subscriber" means it is the list of subscribed users we want
        $ids = self::getSubscribedIDs($profile_id, $offset, $limit);
        return Subscription::listFind('subscribed', $ids);
    }

    /**
     * Stream of subscriptions with the same subscriber
     *
     * Useful for showing pages that list subscriptions in reverse
     * chronological order. Has offset & limit to make paging
     * easy.
     *
     * @param integer $profile_id   ID of the subscribed profile
     * @param integer $offset       Offset from latest
     * @param integer $limit        Maximum number to fetch
     *
     * @return Subscription stream of subscriptions; use fetch() to iterate
     */
    public static function bySubscribed($profile_id, $offset = 0, $limit = PROFILES_PER_PAGE)
    {
        // "by subscribed" means it is the list of subscribers we want
        $ids = self::getSubscriberIDs($profile_id, $offset, $limit);
        return Subscription::listFind('subscriber', $ids);
    }

    // The following are helper functions to the subscription lists,
    // notably the public ones get used in places such as Profile
    public static function getSubscribedIDs($profile_id, $offset, $limit) {
        return self::getSubscriptionIDs('subscribed', $profile_id, $offset, $limit);
    }

    public static function getSubscriberIDs($profile_id, $offset, $limit) {
        return self::getSubscriptionIDs('subscriber', $profile_id, $offset, $limit);
    }

    private static function getSubscriptionIDs($get_type, $profile_id, $offset, $limit)
    {
        switch ($get_type) {
        case 'subscribed':
            $by_type  = 'subscriber';
            break;
        case 'subscriber':
            $by_type  = 'subscribed';
            break;
        default:
            throw new Exception('Bad type argument to getSubscriptionIDs');
        }

        $cacheKey = 'subscription:by-'.$by_type.':'.$profile_id;

        $queryoffset = $offset;
        $querylimit = $limit;

        if ($offset + $limit <= self::CACHE_WINDOW) {
            // Oh, it seems it should be cached
            $ids = self::cacheGet($cacheKey);
            if (is_array($ids)) {
                return array_slice($ids, $offset, $limit);
            }
            // Being here indicates we didn't find anything cached
            // so we'll have to fill it up simultaneously
            $queryoffset = 0;
            $querylimit  = self::CACHE_WINDOW;
        }

        $sub = new Subscription();
        $sub->$by_type = $profile_id;
        $sub->selectAdd($get_type);
        $sub->whereAdd("{$get_type} != {$profile_id}");
        $sub->orderBy('created DESC');
        $sub->limit($queryoffset, $querylimit);

        if (!$sub->find()) {
            return array();
        }

        $ids = $sub->fetchAll($get_type);

        // If we're simultaneously filling up cache, remember to slice
        if ($queryoffset === 0 && $querylimit === self::CACHE_WINDOW) {
            self::cacheSet($cacheKey, $ids);
            return array_slice($ids, $offset, $limit);
        }

        return $ids;
    }

    /**
     * Flush cached subscriptions when subscription is updated
     *
     * Because we cache subscriptions, it's useful to flush them
     * here.
     *
     * @param mixed $dataObject Original version of object
     *
     * @return boolean success flag.
     */
    function update($dataObject=false)
    {
        self::blow('subscription:by-subscriber:'.$this->subscriber);
        self::blow('subscription:by-subscribed:'.$this->subscribed);

        return parent::update($dataObject);
    }

    public function getUri()
    {
        return $this->uri ?: self::newUri($this->getSubscriber(), $this->getSubscribed(), $this->created);
    }
}

repo_name: dansup/gnu

license: mit


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Table Definition for subscription_queue
 */

class Subscription_queue extends Managed_DataObject
{
    public $__table = 'subscription_queue';       // table name
    public $subscriber;
    public $subscribed;
    public $created;

    public static function schemaDef()
    {
        return array(
            'description' => 'Holder for subscription requests awaiting moderation.',
            'fields' => array(
                'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile making the request'),
                'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile being subscribed to'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
            ),
            'primary key' => array('subscriber', 'subscribed'),
            'indexes' => array(
                'subscription_queue_subscriber_created_idx' => array('subscriber', 'created'),
                'subscription_queue_subscribed_created_idx' => array('subscribed', 'created'),
            ),
            'foreign keys' => array(
                'subscription_queue_subscriber_fkey' => array('profile', array('subscriber' => 'id')),
                'subscription_queue_subscribed_fkey' => array('profile', array('subscribed' => 'id')),
            )
        );
    }

    public static function saveNew(Profile $subscriber, Profile $subscribed)
    {
        if (self::exists($subscriber, $subscribed)) {
            throw new AlreadyFulfilledException(_('This subscription request is already in progress.'));
        }
        $rq = new Subscription_queue();
        $rq->subscriber = $subscriber->id;
        $rq->subscribed = $subscribed->id;
        $rq->created = common_sql_now();
        $rq->insert();
        return $rq;
    }

    static function exists(Profile $subscriber, Profile $other)
    {
        $sub = Subscription_queue::pkeyGet(array('subscriber' => $subscriber->getID(),
                                                 'subscribed' => $other->getID()));
        return ($sub instanceof Subscription_queue);
    }

    static function getSubQueue(Profile $subscriber, Profile $other)
    {
        // This is essentially a pkeyGet but we have an object to return in NoResultException
        $sub = new Subscription_queue();
        $sub->subscriber = $subscriber->id;
        $sub->subscribed = $other->id;
        if (!$sub->find(true)) {
            throw new NoResultException($sub);
        }
        return $sub;
    }

    /**
     * Complete a pending subscription, as we've got approval of some sort.
     *
     * @return Subscription
     */
    public function complete()
    {
        $subscriber = Profile::getKV('id', $this->subscriber);
        $subscribed = Profile::getKV('id', $this->subscribed);
        try {
            $sub = Subscription::start($subscriber, $subscribed, Subscription::FORCE);
            $this->delete();
        } catch (AlreadyFulfilledException $e) {
            common_debug('Tried to start a subscription which already existed.');
        }
        return $sub;
    }

    /**
     * Cancel an outstanding subscription request to the other profile.
     */
    public function abort()
    {
        $subscriber = Profile::getKV('id', $this->subscriber);
        $subscribed = Profile::getKV('id', $this->subscribed);
        if (Event::handle('StartCancelSubscription', array($subscriber, $subscribed))) {
            $this->delete();
            Event::handle('EndCancelSubscription', array($subscriber, $subscribed));
        }
    }

    /**
     * Send notifications via email etc to group administrators about
     * this exciting new pending moderation queue item!
     */
    public function notify()
    {
        $other = Profile::getKV('id', $this->subscriber);
        $listenee = User::getKV('id', $this->subscribed);
        mail_subscribe_pending_notify_profile($listenee, $other);
    }
}


code:
<?php
/**
 * Table Definition for token
 */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Token extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'token';                           // table name
    public $consumer_key;                    // varchar(191)  primary_key not_null   not 255 because utf8mb4 takes more space
    public $tok;                             // char(32)  primary_key not_null
    public $secret;                          // char(32)   not_null
    public $type;                            // tinyint(1)   not_null
    public $state;                           // tinyint(1)
    public $verifier;                        // varchar(191)   not 255 because utf8mb4 takes more space
    public $verified_callback;               // varchar(191)   not 255 because utf8mb4 takes more space
    public $created;                         // datetime()   not_null
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE
    public static function schemaDef()
    {
        return array(
            'description' => 'OAuth token record',
            'fields' => array(
                'consumer_key' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'unique identifier, root URL'),
                'tok' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'identifying value'),
                'secret' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'secret value'),
                'type' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 0, 'description' => 'request or access'),
                'state' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'for requests, 0 = initial, 1 = authorized, 2 = used'),
                'verifier' => array('type' => 'varchar', 'length' => 191, 'description' => 'verifier string for OAuth 1.0a'),
                'verified_callback' => array('type' => 'varchar', 'length' => 191, 'description' => 'verified callback URL for OAuth 1.0a'),

                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('consumer_key', 'tok'),
            'foreign keys' => array(
                'token_consumer_key_fkey' => array('consumer', array('consumer_key'=> 'consumer_key')),
            ),
        );
    }
}


code:
<?php
/**
 * Data class for unavailable status networks
 *
 * PHP version 5
 *
 * @category Data
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
 * @link     http://status.net/
 *
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Keeps a list of unavailable status network names
 *
 * @category Data
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
 * @link     http://status.net/
 *
 * @see      Managed_DataObject
 */

class Unavailable_status_network extends Managed_DataObject
{
    public $__table = 'unavailable_status_network'; // table name

    public $nickname;              // varchar(64) UUID
    public $created;               // datetime

    /**
     * The One True Thingy that must be defined and declared.
     */
    public static function schemaDef()
    {
        return array(
            'description' => 'An unavailable status network nickname',
            'fields' => array(
                'nickname' => array('type' => 'varchar',
                                    'length' => 64,
                                    'not null' => true, 'description' => 'nickname not to use'),
                'created' => array('type' => 'datetime',
                                   'not null' => true),
            ),
            'primary key' => array('nickname'),
        );
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008, 2009, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Table Definition for user
 */

class User extends Managed_DataObject
{
    const SUBSCRIBE_POLICY_OPEN = 0;
    const SUBSCRIBE_POLICY_MODERATE = 1;

    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'user';                            // table name
    public $id;                              // int(4)  primary_key not_null
    public $nickname;                        // varchar(64)  unique_key
    public $password;                        // varchar(191)               not 255 because utf8mb4 takes more space
    public $email;                           // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $incomingemail;                   // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $emailnotifysub;                  // tinyint(1)   default_1
    public $emailnotifyfav;                  // tinyint(1)   default_1
    public $emailnotifynudge;                // tinyint(1)   default_1
    public $emailnotifymsg;                  // tinyint(1)   default_1
    public $emailnotifyattn;                 // tinyint(1)   default_1
    public $language;                        // varchar(50)
    public $timezone;                        // varchar(50)
    public $emailpost;                       // tinyint(1)   default_1
    public $sms;                             // varchar(64)  unique_key
    public $carrier;                         // int(4)
    public $smsnotify;                       // tinyint(1)
    public $smsreplies;                      // tinyint(1)
    public $smsemail;                        // varchar(191)               not 255 because utf8mb4 takes more space
    public $uri;                             // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $autosubscribe;                   // tinyint(1)
    public $subscribe_policy;                // tinyint(1)
    public $urlshorteningservice;            // varchar(50)   default_ur1.ca
    public $private_stream;                  // tinyint(1)   default_0
    public $created;                         // datetime()   not_null
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'description' => 'local users',
            'fields' => array(
                'id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),
                'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'nickname or username, duped in profile'),
                'password' => array('type' => 'varchar', 'length' => 191, 'description' => 'salted password, can be null for OpenID users'),
                'email' => array('type' => 'varchar', 'length' => 191, 'description' => 'email address for password recovery etc.'),
                'incomingemail' => array('type' => 'varchar', 'length' => 191, 'description' => 'email address for post-by-email'),
                'emailnotifysub' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of subscriptions'),
                'emailnotifyfav' => array('type' => 'int', 'size' => 'tiny', 'default' => null, 'description' => 'Notify by email of favorites'),
                'emailnotifynudge' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of nudges'),
                'emailnotifymsg' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of direct messages'),
                'emailnotifyattn' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of @-replies'),
                'language' => array('type' => 'varchar', 'length' => 50, 'description' => 'preferred language'),
                'timezone' => array('type' => 'varchar', 'length' => 50, 'description' => 'timezone'),
                'emailpost' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Post by email'),
                'sms' => array('type' => 'varchar', 'length' => 64, 'description' => 'sms phone number'),
                'carrier' => array('type' => 'int', 'description' => 'foreign key to sms_carrier'),
                'smsnotify' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to send notices to SMS'),
                'smsreplies' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to send notices to SMS on replies'),
                'smsemail' => array('type' => 'varchar', 'length' => 191, 'description' => 'built from sms and carrier'),
                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
                'autosubscribe' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'automatically subscribe to users who subscribe to us'),
                'subscribe_policy' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'),
                'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'internal', 'description' => 'service to use for auto-shortening URLs'),
                'private_stream' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to limit all notices to followers only'),

                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('id'),
            'unique keys' => array(
                'user_nickname_key' => array('nickname'),
                'user_email_key' => array('email'),
                'user_incomingemail_key' => array('incomingemail'),
                'user_sms_key' => array('sms'),
                'user_uri_key' => array('uri'),
            ),
            'foreign keys' => array(
                'user_id_fkey' => array('profile', array('id' => 'id')),
                'user_carrier_fkey' => array('sms_carrier', array('carrier' => 'id')),
            ),
            'indexes' => array(
                'user_smsemail_idx' => array('smsemail'),
            ),
        );
    }

    protected $_profile = array();

    /**
     * @return Profile
     *
     * @throws UserNoProfileException if user has no profile
     */
    public function getProfile()
    {
        if (!isset($this->_profile[$this->id])) {
            $profile = Profile::getKV('id', $this->id);
            if (!$profile instanceof Profile) {
                throw new UserNoProfileException($this);
            }
            $this->_profile[$this->id] = $profile;
        }
        return $this->_profile[$this->id];
    }

    public function sameAs(Profile $other)
    {
        return $this->getProfile()->sameAs($other);
    }

    public function getUri()
    {
        return $this->uri;
    }

    static function getByUri($uri)
    {
        $user = new User();
        $user->uri = $uri;
        if (!$user->find(true)) {
            throw new NoResultException($user);
        }
        return $user;
    }

    public function getNickname()
    {
        return $this->getProfile()->getNickname();
    }

    static function getByNickname($nickname)
    {
        $user = User::getKV('nickname', $nickname);
        if (!$user instanceof User) {
            throw new NoSuchUserException(array('nickname' => $nickname));
        }

        return $user;
    }

    function isSubscribed(Profile $other)
    {
        return $this->getProfile()->isSubscribed($other);
    }

    function hasPendingSubscription(Profile $other)
    {
        return $this->getProfile()->hasPendingSubscription($other);
    }

    /**
     * Get the most recent notice posted by this user, if any.
     *
     * @return mixed Notice or null
     */
    function getCurrentNotice()
    {
        return $this->getProfile()->getCurrentNotice();
    }

    function getCarrier()
    {
        return Sms_carrier::getKV('id', $this->carrier);
    }

    function hasBlocked($other)
    {
        return $this->getProfile()->hasBlocked($other);
    }

    /**
     * Register a new user account and profile and set up default subscriptions.
     * If a new-user welcome message is configured, this will be sent.
     *
     * @param array $fields associative array of optional properties
     *              string 'bio'
     *              string 'email'
     *              bool 'email_confirmed' pass true to mark email as pre-confirmed
     *              string 'fullname'
     *              string 'homepage'
     *              string 'location' informal string description of geolocation
     *              float 'lat' decimal latitude for geolocation
     *              float 'lon' decimal longitude for geolocation
     *              int 'location_id' geoname identifier
     *              int 'location_ns' geoname namespace to interpret location_id
     *              string 'nickname' REQUIRED
     *              string 'password' (may be missing for eg OpenID registrations)
     *              string 'code' invite code
     *              ?string 'uri' permalink to notice; defaults to local notice URL
     * @return  User object
     * @throws  Exception on failure
     */
    static function register(array $fields, $accept_email_fail=false) {

        // MAGICALLY put fields into current scope

        extract($fields);

        $profile = new Profile();

        if (!empty($email)) {
            $email = common_canonical_email($email);
        }

        // Normalize _and_ check whether it is in use. Throw NicknameException on failure.
        $profile->nickname = Nickname::normalize($nickname, true);

        $profile->profileurl = common_profile_url($profile->nickname);

        if (!empty($fullname)) {
            $profile->fullname = $fullname;
        }
        if (!empty($homepage)) {
            $profile->homepage = $homepage;
        }
        if (!empty($bio)) {
            $profile->bio = $bio;
        }
        if (!empty($location)) {
            $profile->location = $location;

            $loc = Location::fromName($location);

            if (!empty($loc)) {
                $profile->lat         = $loc->lat;
                $profile->lon         = $loc->lon;
                $profile->location_id = $loc->location_id;
                $profile->location_ns = $loc->location_ns;
            }
        }

        $profile->created = common_sql_now();

        $user = new User();

        $user->nickname = $profile->nickname;

        $invite = null;

        // Users who respond to invite email have proven their ownership of that address

        if (!empty($code)) {
            $invite = Invitation::getKV($code);
            if ($invite instanceof Invitation && $invite->address && $invite->address_type == 'email' && $invite->address == $email) {
                $user->email = $invite->address;
            }
        }

        if(isset($email_confirmed) && $email_confirmed) {
            $user->email = $email;
        }

        // Set default-on options here, otherwise they'll be disabled
        // initially for sites using caching, since the initial encache
        // doesn't know about the defaults in the database.
        $user->emailnotifysub = 1;
        $user->emailnotifynudge = 1;
        $user->emailnotifymsg = 1;
        $user->emailnotifyattn = 1;
        $user->emailpost = 1;

        $user->created = common_sql_now();

        if (Event::handle('StartUserRegister', array($profile))) {

            $profile->query('BEGIN');

            $id = $profile->insert();
            if ($id === false) {
                common_log_db_error($profile, 'INSERT', __FILE__);
                $profile->query('ROLLBACK');
                // TRANS: Profile data could not be inserted for some reason.
                throw new ServerException(_m('Could not insert profile data for new user.'));
            }

            $user->id = $id;

            if (!empty($uri)) {
                $user->uri = $uri;
            } else {
                $user->uri = common_user_uri($user);
            }

            if (!empty($password)) { // may not have a password for OpenID users
                $user->password = common_munge_password($password);
            }

            $result = $user->insert();

            if ($result === false) {
                common_log_db_error($user, 'INSERT', __FILE__);
                $profile->query('ROLLBACK');
                // TRANS: User data could not be inserted for some reason.
                throw new ServerException(_m('Could not insert user data for new user.'));
            }

            // Everyone is subscribed to themself

            $subscription = new Subscription();
            $subscription->subscriber = $user->id;
            $subscription->subscribed = $user->id;
            $subscription->created = $user->created;

            $result = $subscription->insert();

            if (!$result) {
                common_log_db_error($subscription, 'INSERT', __FILE__);
                $profile->query('ROLLBACK');
                // TRANS: Subscription data could not be inserted for some reason.
                throw new ServerException(_m('Could not insert subscription data for new user.'));
            }

            // Mark that this invite was converted

            if (!empty($invite)) {
                $invite->convert($user);
            }

            if (!empty($email) && empty($user->email)) {
                // The actual email will be sent further down, after the database COMMIT

                $confirm = new Confirm_address();
                $confirm->code = common_confirmation_code(128);
                $confirm->user_id = $user->id;
                $confirm->address = $email;
                $confirm->address_type = 'email';

                $result = $confirm->insert();

                if ($result===false) {
                    common_log_db_error($confirm, 'INSERT', __FILE__);
                    $profile->query('ROLLBACK');
                    // TRANS: Email confirmation data could not be inserted for some reason.
                    throw new ServerException(_m('Could not insert email confirmation data for new user.'));
                }
            }

            if (!empty($code) && $user->email) {
                $user->emailChanged();
            }

            // Default system subscription

            $defnick = common_config('newuser', 'default');

            if (!empty($defnick)) {
                $defuser = User::getKV('nickname', $defnick);
                if (empty($defuser)) {
                    common_log(LOG_WARNING, sprintf("Default user %s does not exist.", $defnick),
                               __FILE__);
                } else {
                    Subscription::ensureStart($profile, $defuser->getProfile());
                }
            }

            $profile->query('COMMIT');

            if (!empty($email) && empty($user->email)) {
                try {
                    require_once(INSTALLDIR . '/lib/mail.php');
                    mail_confirm_address($user, $confirm->code, $profile->getNickname(), $email);
                } catch (EmailException $e) {
                    common_log(LOG_ERR, "Could not send user registration email for user id=={$profile->getID()}: {$e->getMessage()}");
                    if (!$accept_email_fail) {
                        throw $e;
                    }
                }
            }

            // Welcome message

            $welcome = common_config('newuser', 'welcome');

            if (!empty($welcome)) {
                $welcomeuser = User::getKV('nickname', $welcome);
                if (empty($welcomeuser)) {
                    common_log(LOG_WARNING, sprintf("Welcome user %s does not exist.", $defnick),
                               __FILE__);
                } else {
                    $notice = Notice::saveNew($welcomeuser->id,
                                              // TRANS: Notice given on user registration.
                                              // TRANS: %1$s is the sitename, $2$s is the registering user's nickname.
                                              sprintf(_('Welcome to %1$s, @%2$s!'),
                                                      common_config('site', 'name'),
                                                      $profile->getNickname()),
                                              'system');
                }
            }

            Event::handle('EndUserRegister', array($profile));
        }

        if (!$user instanceof User || empty($user->id)) {
            throw new ServerException('User could not be registered. Probably an event hook that failed.');
        }

        return $user;
    }

    // Things we do when the email changes
    function emailChanged()
    {

        $invites = new Invitation();
        $invites->address = $this->email;
        $invites->address_type = 'email';

        if ($invites->find()) {
            while ($invites->fetch()) {
                try {
                    $other = Profile::getByID($invites->user_id);
                    Subscription::start($other, $this->getProfile());
                } catch (NoResultException $e) {
                    // profile did not exist
                } catch (AlreadyFulfilledException $e) {
                    // already subscribed to this profile
                } catch (Exception $e) {
                    common_log(LOG_ERR, 'On-invitation-completion subscription failed when subscribing '._ve($invites->user_id).' to '.$this->getProfile()->getID().': '._ve($e->getMessage()));
                }
            }
        }
    }

    function mutuallySubscribed(Profile $other)
    {
        return $this->getProfile()->mutuallySubscribed($other);
    }

    function mutuallySubscribedUsers()
    {
        // 3-way join; probably should get cached
        $UT = common_config('db','type')=='pgsql'?'"user"':'user';
        $qry = "SELECT $UT.* " .
          "FROM subscription sub1 JOIN $UT ON sub1.subscribed = $UT.id " .
          "JOIN subscription sub2 ON $UT.id = sub2.subscriber " .
          'WHERE sub1.subscriber = %d and sub2.subscribed = %d ' .
          "ORDER BY $UT.nickname";
        $user = new User();
        $user->query(sprintf($qry, $this->id, $this->id));

        return $user;
    }

    function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
    {
        return $this->getProfile()->getReplies($offset, $limit, $since_id, $before_id);
    }

    function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) {
        return $this->getProfile()->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id);
    }

    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
    {
        return $this->getProfile()->getNotices($offset, $limit, $since_id, $before_id);
    }

    function block(Profile $other)
    {
        // Add a new block record

        // no blocking (and thus unsubbing from) yourself

        if ($this->id == $other->id) {
            common_log(LOG_WARNING,
                sprintf(
                    "Profile ID %d (%s) tried to block themself.",
                    $this->id,
                    $this->nickname
                )
            );
            return false;
        }

        $block = new Profile_block();

        // Begin a transaction

        $block->query('BEGIN');

        $block->blocker = $this->id;
        $block->blocked = $other->id;

        $result = $block->insert();

        if (!$result) {
            common_log_db_error($block, 'INSERT', __FILE__);
            return false;
        }

        $self = $this->getProfile();
        if (Subscription::exists($other, $self)) {
            Subscription::cancel($other, $self);
        }
        if (Subscription::exists($self, $other)) {
            Subscription::cancel($self, $other);
        }

        $block->query('COMMIT');

        return true;
    }

    function unblock(Profile $other)
    {
        // Get the block record

        $block = Profile_block::exists($this->getProfile(), $other);

        if (!$block) {
            return false;
        }

        $result = $block->delete();

        if (!$result) {
            common_log_db_error($block, 'DELETE', __FILE__);
            return false;
        }

        return true;
    }

    function isMember(User_group $group)
    {
        return $this->getProfile()->isMember($group);
    }

    function isAdmin(User_group $group)
    {
        return $this->getProfile()->isAdmin($group);
    }

    function getGroups($offset=0, $limit=null)
    {
        return $this->getProfile()->getGroups($offset, $limit);
    }

    /**
     * Request to join the given group.
     * May throw exceptions on failure.
     *
     * @param User_group $group
     * @return Group_member
     */
    function joinGroup(User_group $group)
    {
        return $this->getProfile()->joinGroup($group);
    }

    /**
     * Leave a group that this user is a member of.
     *
     * @param User_group $group
     */
    function leaveGroup(User_group $group)
    {
        return $this->getProfile()->leaveGroup($group);
    }

    function getSubscribed($offset=0, $limit=null)
    {
        return $this->getProfile()->getSubscribed($offset, $limit);
    }

    function getSubscribers($offset=0, $limit=null)
    {
        return $this->getProfile()->getSubscribers($offset, $limit);
    }

    function getTaggedSubscribers($tag, $offset=0, $limit=null)
    {
        return $this->getProfile()->getTaggedSubscribers($tag, $offset, $limit);
    }

    function getTaggedSubscriptions($tag, $offset=0, $limit=null)
    {
        return $this->getProfile()->getTaggedSubscriptions($tag, $offset, $limit);
    }

    function hasRight($right)
    {
        return $this->getProfile()->hasRight($right);
    }

    function delete($useWhere=false)
    {
        if (empty($this->id)) {
            common_log(LOG_WARNING, "Ambiguous User->delete(); skipping related tables.");
            return parent::delete($useWhere);
        }

        try {
            if (!$this->hasRole(Profile_role::DELETED)) {
                $profile = $this->getProfile();
                $profile->delete();
            }
        } catch (UserNoProfileException $unp) {
            common_log(LOG_INFO, "User {$this->nickname} has no profile; continuing deletion.");
        }

        $related = array(
                         'Confirm_address',
                         'Remember_me',
                         'Foreign_link',
                         'Invitation',
                         );

        Event::handle('UserDeleteRelated', array($this, &$related));

        foreach ($related as $cls) {
            $inst = new $cls();
            $inst->user_id = $this->id;
            $inst->delete();
        }

        $this->_deleteTags();
        $this->_deleteBlocks();

        return parent::delete($useWhere);
    }

    function _deleteTags()
    {
        $tag = new Profile_tag();
        $tag->tagger = $this->id;
        $tag->delete();
    }

    function _deleteBlocks()
    {
        $block = new Profile_block();
        $block->blocker = $this->id;
        $block->delete();
        // XXX delete group block? Reset blocker?
    }

    function hasRole($name)
    {
        return $this->getProfile()->hasRole($name);
    }

    function grantRole($name)
    {
        return $this->getProfile()->grantRole($name);
    }

    function revokeRole($name)
    {
        return $this->getProfile()->revokeRole($name);
    }

    function isSandboxed()
    {
        return $this->getProfile()->isSandboxed();
    }

    function isSilenced()
    {
        return $this->getProfile()->isSilenced();
    }

    function receivesEmailNotifications()
    {
        // We could do this in one large if statement, but that's not as easy to read
        // Don't send notifications if we don't know the user's email address or it is
        // explicitly undesired by the user's own settings.
        if (empty($this->email) || !$this->emailnotifyattn) {
            return false;
        }
        // Don't send notifications to a user who is sandboxed or silenced
        if ($this->isSandboxed() || $this->isSilenced()) {
            return false;
        }
        return true;
    }

    function repeatedByMe($offset=0, $limit=20, $since_id=null, $max_id=null)
    {
        $stream = new RepeatedByMeNoticeStream($this);
        return $stream->getNotices($offset, $limit, $since_id, $max_id);
    }

    function repeatsOfMe($offset=0, $limit=20, $since_id=null, $max_id=null)
    {
        $stream = new RepeatsOfMeNoticeStream($this);

        return $stream->getNotices($offset, $limit, $since_id, $max_id);
    }

    public function repeatedToMe($offset=0, $limit=20, $since_id=null, $max_id=null)
    {
        return $this->getProfile()->repeatedToMe($offset, $limit, $since_id, $max_id);
    }

    public static function siteOwner()
    {
        $owner = self::cacheGet('user:site_owner');

        if ($owner === false) { // cache miss

            $pr = new Profile_role();
            $pr->role = Profile_role::OWNER;
            $pr->orderBy('created');
            $pr->limit(1);

            if (!$pr->find(true)) {
                throw new NoResultException($pr);
            }

            $owner = User::getKV('id', $pr->profile_id);

            self::cacheSet('user:site_owner', $owner);
        }

        if ($owner instanceof User) {
            return $owner;
        }

        throw new ServerException(_('No site owner configured.'));
    }

    /**
     * Pull the primary site account to use in single-user mode.
     * If a valid user nickname is listed in 'singleuser':'nickname'
     * in the config, this will be used; otherwise the site owner
     * account is taken by default.
     *
     * @return User
     * @throws ServerException if no valid single user account is present
     * @throws ServerException if called when not in single-user mode
     */
    public static function singleUser()
    {
        if (!common_config('singleuser', 'enabled')) {
            // TRANS: Server exception.
            throw new ServerException(_('Single-user mode code called when not enabled.'));
        }

        if ($nickname = common_config('singleuser', 'nickname')) {
            $user = User::getKV('nickname', $nickname);
            if ($user instanceof User) {
                return $user;
            }
        }

        // If there was no nickname or no user by that nickname,
        // try the site owner. Throws exception if not configured.
        return User::siteOwner();
    }

    /**
     * This is kind of a hack for using external setup code that's trying to
     * build single-user sites.
     *
     * Will still return a username if the config singleuser/nickname is set
     * even if the account doesn't exist, which normally indicates that the
     * site is horribly misconfigured.
     *
     * At the moment, we need to let it through so that router setup can
     * complete, otherwise we won't be able to create the account.
     *
     * This will be easier when we can more easily create the account and
     * *then* switch the site to 1user mode without jumping through hoops.
     *
     * @return string
     * @throws ServerException if no valid single user account is present
     * @throws ServerException if called when not in single-user mode
     */
    static function singleUserNickname()
    {
        try {
            $user = User::singleUser();
            return $user->nickname;
        } catch (Exception $e) {
            if (common_config('singleuser', 'enabled') && common_config('singleuser', 'nickname')) {
                common_log(LOG_WARNING, "Warning: code attempting to pull single-user nickname when the account does not exist. If this is not setup time, this is probably a bug.");
                return common_config('singleuser', 'nickname');
            }
            throw $e;
        }
    }

    /**
     * Find and shorten links in the given text using this user's URL shortening
     * settings.
     *
     * By default, links will be left untouched if the text is shorter than the
     * configured maximum notice length. Pass true for the $always parameter
     * to force all links to be shortened regardless.
     *
     * Side effects: may save file and file_redirection records for referenced URLs.
     *
     * @param string $text
     * @param boolean $always
     * @return string
     */
    public function shortenLinks($text, $always=false)
    {
        return common_shorten_links($text, $always, $this);
    }

    /*
     * Get a list of OAuth client applications that have access to this
     * user's account.
     */
    function getConnectedApps($offset = 0, $limit = null)
    {
        $qry =
          'SELECT u.* ' .
          'FROM oauth_application_user u, oauth_application a ' .
          'WHERE u.profile_id = %d ' .
          'AND a.id = u.application_id ' .
          'AND u.access_type > 0 ' .
          'ORDER BY u.created DESC ';

        if ($offset > 0) {
            if (common_config('db','type') == 'pgsql') {
                $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
            } else {
                $qry .= ' LIMIT ' . $offset . ', ' . $limit;
            }
        }

        $apps = new Oauth_application_user();

        $cnt = $apps->query(sprintf($qry, $this->id));

        return $apps;
    }

    /**
     * Magic function called at serialize() time.
     *
     * We use this to drop a couple process-specific references
     * from DB_DataObject which can cause trouble in future
     * processes.
     *
     * @return array of variable names to include in serialization.
     */

    function __sleep()
    {
        $vars = parent::__sleep();
        $skip = array('_profile');
        return array_diff($vars, $skip);
    }

    static function recoverPassword($nore)
    {
        require_once(INSTALLDIR . '/lib/mail.php');

        // $confirm_email will be used as a fallback if our user doesn't have a confirmed email
        $confirm_email = null;

        if (common_is_email($nore)) {
            $user = User::getKV('email', common_canonical_email($nore));

            // See if it's an unconfirmed email address
            if (!$user instanceof User) {
                // Warning: it may actually be legit to have multiple folks
                // who have claimed, but not yet confirmed, the same address.
                // We'll only send to the first one that comes up.
                $confirm_email = new Confirm_address();
                $confirm_email->address = common_canonical_email($nore);
                $confirm_email->address_type = 'email';
                if ($confirm_email->find(true)) {
                    $user = User::getKV('id', $confirm_email->user_id);
                }
            }

            // No luck finding anyone by that email address.
            if (!$user instanceof User) {
                if (common_config('site', 'fakeaddressrecovery')) {
                    // Return without actually doing anything! We fake address recovery
                    // to avoid revealing which email addresses are registered with the site.
                    return;
                }
                // TRANS: Information on password recovery form if no known e-mail address was specified.
                throw new ClientException(_('No user with that email address exists here.'));
            }
        } else {
            // This might throw a NicknameException on bad nicknames
            $user = User::getKV('nickname', common_canonical_nickname($nore));
            if (!$user instanceof User) {
                // TRANS: Information on password recovery form if no known username was specified.
                throw new ClientException(_('No user with that nickname exists here.'));
            }
        }

        // Try to get an unconfirmed email address if they used a user name
        if (empty($user->email) && $confirm_email === null) {
            $confirm_email = new Confirm_address();
            $confirm_email->user_id = $user->id;
            $confirm_email->address_type = 'email';
            $confirm_email->find();
            if (!$confirm_email->fetch()) {
                // Nothing found, so let's reset it to null
                $confirm_email = null;
            }
        }

        if (empty($user->email) && !$confirm_email instanceof Confirm_address) {
            // TRANS: Client error displayed on password recovery form if a user does not have a registered e-mail address.
            throw new ClientException(_('No registered email address for that user.'));
        }

        // Success! We have a valid user and a confirmed or unconfirmed email address

        $confirm = new Confirm_address();
        $confirm->code = common_confirmation_code(128);
        $confirm->address_type = 'recover';
        $confirm->user_id = $user->id;
        $confirm->address = $user->email ?: $confirm_email->address;

        if (!$confirm->insert()) {
            common_log_db_error($confirm, 'INSERT', __FILE__);
            // TRANS: Server error displayed if e-mail address confirmation fails in the database on the password recovery form.
            throw new ServerException(_('Error saving address confirmation.'));
        }

         // @todo FIXME: needs i18n.
        $body = "Hey, $user->nickname.";
        $body .= "\n\n";
        $body .= 'Someone just asked for a new password ' .
                 'for this account on ' . common_config('site', 'name') . '.';
        $body .= "\n\n";
        $body .= 'If it was you, and you want to confirm, use the URL below:';
        $body .= "\n\n";
        $body .= "\t".common_local_url('recoverpassword',
                                   array('code' => $confirm->code));
        $body .= "\n\n";
        $body .= 'If not, just ignore this message.';
        $body .= "\n\n";
        $body .= 'Thanks for your time, ';
        $body .= "\n";
        $body .= common_config('site', 'name');
        $body .= "\n";

        $headers = _mail_prepare_headers('recoverpassword', $user->nickname, $user->nickname);
        // TRANS: Subject for password recovery e-mail.
        mail_to_user($user, _('Password recovery requested'), $body, $headers, $confirm->address);
    }

    function streamModeOnly()
    {
        if (common_config('oldschool', 'enabled')) {
            $osp = Old_school_prefs::getKV('user_id', $this->id);
            if (!empty($osp)) {
                return $osp->stream_mode_only;
            }
        }

        return false;
    }

    function streamNicknames()
    {
        if (common_config('oldschool', 'enabled')) {
            $osp = Old_school_prefs::getKV('user_id', $this->id);
            if (!empty($osp)) {
                return $osp->stream_nicknames;
            }
        }
        return false;
    }

    function registrationActivity()
    {
        $profile = $this->getProfile();

        $service = new ActivityObject();

        $service->type  = ActivityObject::SERVICE;
        $service->title = common_config('site', 'name');
        $service->link  = common_root_url();
        $service->id    = $service->link;

        $act = new Activity();

        $act->actor = $profile->asActivityObject();
        $act->verb = ActivityVerb::JOIN;

        $act->objects[] = $service;

        $act->id = TagURI::mint('user:register:%d',
                                $this->id);

        $act->time = strtotime($this->created);

        $act->title = _("Register");

        $act->content = sprintf(_('%1$s joined %2$s.'),
                                $profile->getBestName(),
                                $service->title);
        return $act;
    }

    public function isPrivateStream()
    {
        return $this->getProfile()->isPrivateStream();
    }

    public function hasPassword()
    {
        return !empty($this->password);
    }

    public function setPassword($password)
    {
        $orig = clone($this);
        $this->password = common_munge_password($password, $this->getProfile());

        if ($this->validate() !== true) {
            // TRANS: Form validation error on page where to change password.
            throw new ServerException(_('Error saving user; invalid.'));
        }

        if (!$this->update($orig)) {
            common_log_db_error($this, 'UPDATE', __FILE__);
            // TRANS: Server error displayed on page where to change password when password change
            // TRANS: could not be made because of a server error.
            throw new ServerException(_('Cannot save new password.'));
        }
    }

    public function delPref($namespace, $topic)
    {
        return $this->getProfile()->delPref($namespace, $topic);
    }

    public function getPref($namespace, $topic, $default=null)
    {
        return $this->getProfile()->getPref($namespace, $topic, $default);
    }

    public function getConfigPref($namespace, $topic)
    {
        return $this->getProfile()->getConfigPref($namespace, $topic);
    }

    public function setPref($namespace, $topic, $data)
    {
        return $this->getProfile()->setPref($namespace, $topic, $data);
    }
}


code:
<?php
/**
 * Table Definition for user_group
 */

class User_group extends Managed_DataObject
{
    const JOIN_POLICY_OPEN = 0;
    const JOIN_POLICY_MODERATE = 1;
    const CACHE_WINDOW = 201;

    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'user_group';                      // table name
    public $id;                              // int(4)  primary_key not_null
    public $profile_id;                      // int(4)  primary_key not_null
    public $nickname;                        // varchar(64)
    public $fullname;                        // varchar(191)   not 255 because utf8mb4 takes more space
    public $homepage;                        // varchar(191)   not 255 because utf8mb4 takes more space
    public $description;                     // text
    public $location;                        // varchar(191)   not 255 because utf8mb4 takes more space
    public $original_logo;                   // varchar(191)   not 255 because utf8mb4 takes more space
    public $homepage_logo;                   // varchar(191)   not 255 because utf8mb4 takes more space
    public $stream_logo;                     // varchar(191)   not 255 because utf8mb4 takes more space
    public $mini_logo;                       // varchar(191)   not 255 because utf8mb4 takes more space
    public $created;                         // datetime   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
    public $uri;                             // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $mainpage;                        // varchar(191)   not 255 because utf8mb4 takes more space
    public $join_policy;                     // tinyint
    public $force_scope;                     // tinyint

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public function getObjectType()
    {
        return ActivityObject::GROUP;
    }

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),

                'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'nickname for addressing'),
                'fullname' => array('type' => 'varchar', 'length' => 191, 'description' => 'display name'),
                'homepage' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL, cached so we dont regenerate'),
                'description' => array('type' => 'text', 'description' => 'group description'),
                'location' => array('type' => 'varchar', 'length' => 191, 'description' => 'related physical location, if any'),

                'original_logo' => array('type' => 'varchar', 'length' => 191, 'description' => 'original size logo'),
                'homepage_logo' => array('type' => 'varchar', 'length' => 191, 'description' => 'homepage (profile) size logo'),
                'stream_logo' => array('type' => 'varchar', 'length' => 191, 'description' => 'stream-sized logo'),
                'mini_logo' => array('type' => 'varchar', 'length' => 191, 'description' => 'mini logo'),

                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),

                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universal identifier'),
                'mainpage' => array('type' => 'varchar', 'length' => 191, 'description' => 'page for group info to link to'),
                'join_policy' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=open; 1=requires admin approval'),
                'force_scope' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=never,1=sometimes,-1=always'),
            ),
            'primary key' => array('id'),
            'unique keys' => array(
                'user_group_uri_key' => array('uri'),
// when it's safe and everyone's run upgrade.php                'user_profile_id_key' => array('profile_id'),
            ),
            'foreign keys' => array(
                'user_group_id_fkey' => array('profile', array('profile_id' => 'id')),
            ),
            'indexes' => array(
                'user_group_nickname_idx' => array('nickname'),
                'user_group_profile_id_idx' => array('profile_id'), //make this unique in future
            ),
        );
    }

    protected $_profile = array();

    /**
     * @return Profile
     *
     * @throws GroupNoProfileException if user has no profile
     */
    public function getProfile()
    {
        if (!isset($this->_profile[$this->profile_id])) {
            $profile = Profile::getKV('id', $this->profile_id);
            if (!$profile instanceof Profile) {
                throw new GroupNoProfileException($this);
            }
            $this->_profile[$this->profile_id] = $profile;
        }
        return $this->_profile[$this->profile_id];
    }

    public function getNickname()
    {
        return $this->getProfile()->getNickname();
    }

    public static function defaultLogo($size)
    {
        static $sizenames = array(AVATAR_PROFILE_SIZE => 'profile',
                                  AVATAR_STREAM_SIZE => 'stream',
                                  AVATAR_MINI_SIZE => 'mini');
        return Theme::path('default-avatar-'.$sizenames[$size].'.png');
    }

    function homeUrl()
    {
        return $this->getProfile()->getUrl();
    }

    function getUri()
    {
        $uri = null;
        if (Event::handle('StartUserGroupGetUri', array($this, &$uri))) {
            if (!empty($this->uri)) {
                $uri = $this->uri;
            } elseif ($this->isLocal()) {
                $uri = common_local_url('groupbyid',
                                        array('id' => $this->id));
            }
        }
        Event::handle('EndUserGroupGetUri', array($this, &$uri));
        return $uri;
    }

    function permalink()
    {
        $url = null;
        if (Event::handle('StartUserGroupPermalink', array($this, &$url))) {
            if ($this->isLocal()) {
                $url = common_local_url('groupbyid',
                                        array('id' => $this->id));
            }
        }
        Event::handle('EndUserGroupPermalink', array($this, &$url));
        return $url;
    }

    function getNotices($offset, $limit, $since_id=null, $max_id=null)
    {
        $stream = new GroupNoticeStream($this);

        return $stream->getNotices($offset, $limit, $since_id, $max_id);
    }

    function getMembers($offset=0, $limit=null) {
        $ids = null;
        if (is_null($limit) || $offset + $limit > User_group::CACHE_WINDOW) {
            $ids = $this->getMemberIDs($offset,
                                       $limit);
        } else {
            $key = sprintf('group:member_ids:%d', $this->id);
            $window = self::cacheGet($key);
            if ($window === false) {
                $window = $this->getMemberIDs(0,
                                              User_group::CACHE_WINDOW);
                self::cacheSet($key, $window);
            }

            $ids = array_slice($window,
                               $offset,
                               $limit);
        }

        return Profile::multiGet('id', $ids);
    }

    function getMemberIDs($offset=0, $limit=null)
    {
        $gm = new Group_member();

        $gm->selectAdd();
        $gm->selectAdd('profile_id');

        $gm->group_id = $this->id;

        $gm->orderBy('created DESC');

        if (!is_null($limit)) {
            $gm->limit($offset, $limit);
        }

        $ids = array();

        if ($gm->find()) {
            while ($gm->fetch()) {
                $ids[] = $gm->profile_id;
            }
        }

        return $ids;
    }

    /**
     * Get pending members, who have not yet been approved.
     *
     * @param int $offset
     * @param int $limit
     * @return Profile
     */
    function getRequests($offset=0, $limit=null)
    {
        $rq = new Group_join_queue();
        $rq->group_id = $this->id;

        $members = new Profile();

        $members->joinAdd(['id', $rq, 'profile_id']);

        if ($limit != null) {
            $members->limit($offset, $limit);
        }

        $members->find();

        return $members;
    }

    public function getAdminCount()
    {
        $block = new Group_member();
        $block->group_id = $this->id;
        $block->is_admin = 1;

        return $block->count();
    }

    public function getMemberCount()
    {
        $key = sprintf("group:member_count:%d", $this->id);

        $cnt = self::cacheGet($key);

        if (is_integer($cnt)) {
            return (int) $cnt;
        }

        $mem = new Group_member();
        $mem->group_id = $this->id;

        // XXX: why 'distinct'?

        $cnt = (int) $mem->count('distinct profile_id');

        self::cacheSet($key, $cnt);

        return $cnt;
    }

    function getBlockedCount()
    {
        // XXX: WORM cache this

        $block = new Group_block();
        $block->group_id = $this->id;

        return $block->count();
    }

    function getQueueCount()
    {
        // XXX: WORM cache this

        $queue = new Group_join_queue();
        $queue->group_id = $this->id;

        return $queue->count();
    }

    function getAdmins($offset=null, $limit=null)   // offset is null because DataObject wants it, 0 would mean no results
    {
        $admins = new Profile();
        $admins->joinAdd(array('id', 'group_member:profile_id'));
        $admins->whereAdd(sprintf('group_member.group_id = %u AND group_member.is_admin = 1', $this->id));
        $admins->orderBy('group_member.modified ASC');
        $admins->limit($offset, $limit);
        $admins->find();

        return $admins;
    }

    function getBlocked($offset=null, $limit=null)   // offset is null because DataObject wants it, 0 would mean no results
    {
        $blocked = new Profile();
        $blocked->joinAdd(array('id', 'group_block:blocked'));
        $blocked->whereAdd(sprintf('group_block.group_id = %u', $this->id));
        $blocked->orderBy('group_block.modified DESC');
        $blocked->limit($offset, $limit);
        $blocked->find();

        return $blocked;
    }

    function setOriginal($filename)
    {
        // This should be handled by the Profile->setOriginal function so user and group avatars are handled the same
        $imagefile = new ImageFile(null, Avatar::path($filename));

        $sizes = array('homepage_logo' => AVATAR_PROFILE_SIZE,
                       'stream_logo' => AVATAR_STREAM_SIZE,
                       'mini_logo' => AVATAR_MINI_SIZE);

        $orig = clone($this);
        $this->original_logo = Avatar::url($filename);
        foreach ($sizes as $name=>$size) {
            $filename = Avatar::filename($this->profile_id, image_type_to_extension($imagefile->preferredType()),
                                         $size, common_timestamp());
            $imagefile->resizeTo(Avatar::path($filename), array('width'=>$size, 'height'=>$size));
            $this->$name = Avatar::url($filename);
        }
        common_debug(common_log_objstring($this));
        return $this->update($orig);
    }

    function getBestName()
    {
        return ($this->fullname) ? $this->fullname : $this->nickname;
    }

    /**
     * Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone
     * if no fullname is provided.
     *
     * @return string
     */
    function getFancyName()
    {
        if ($this->fullname) {
            // TRANS: Full name of a profile or group followed by nickname in parens
            return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
        } else {
            return $this->nickname;
        }
    }

    function getAliases()
    {
        $aliases = array();

        // XXX: cache this

        $alias = new Group_alias();

        $alias->group_id = $this->id;

        if ($alias->find()) {
            while ($alias->fetch()) {
                $aliases[] = $alias->alias;
            }
        }

        $alias->free();

        return $aliases;
    }

    function setAliases($newaliases) {

        $newaliases = array_unique($newaliases);

        $oldaliases = $this->getAliases();

        // Delete stuff that's old that not in new

        $to_delete = array_diff($oldaliases, $newaliases);

        // Insert stuff that's in new and not in old

        $to_insert = array_diff($newaliases, $oldaliases);

        $alias = new Group_alias();

        $alias->group_id = $this->id;

        foreach ($to_delete as $delalias) {
            $alias->alias = $delalias;
            $result = $alias->delete();
            if (!$result) {
                common_log_db_error($alias, 'DELETE', __FILE__);
                return false;
            }
        }

        foreach ($to_insert as $insalias) {
            if ($insalias === $this->nickname) {
                continue;
            }
            $alias->alias = Nickname::normalize($insalias, true);
            $result = $alias->insert();
            if (!$result) {
                common_log_db_error($alias, 'INSERT', __FILE__);
                return false;
            }
        }

        return true;
    }

    static function getForNickname($nickname, Profile $profile=null)
    {
        $nickname = Nickname::normalize($nickname);

        // Are there any matching remote groups this profile's in?
        if ($profile instanceof Profile) {
            $group = $profile->getGroups(0, null);
            while ($group instanceof User_group && $group->fetch()) {
                if ($group->nickname == $nickname) {
                    // @fixme is this the best way?
                    return clone($group);
                }
            }
        }

        // If not, check local groups.
        $group = Local_group::getKV('nickname', $nickname);
        if ($group instanceof Local_group) {
            return User_group::getKV('id', $group->group_id);
        }
        $alias = Group_alias::getKV('alias', $nickname);
        if ($alias instanceof Group_alias) {
            return User_group::getKV('id', $alias->group_id);
        }
        return null;
    }

    function getUserMembers()
    {
        // XXX: cache this

        $user = new User();
        if(common_config('db','quote_identifiers'))
            $user_table = '"user"';
        else $user_table = 'user';

        $qry =
          'SELECT id ' .
          'FROM '. $user_table .' JOIN group_member '.
          'ON '. $user_table .'.id = group_member.profile_id ' .
          'WHERE group_member.group_id = %d ';

        $user->query(sprintf($qry, $this->id));

        $ids = array();

        while ($user->fetch()) {
            $ids[] = $user->id;
        }

        $user->free();

        return $ids;
    }

    static function maxDescription()
    {
        $desclimit = common_config('group', 'desclimit');
        // null => use global limit (distinct from 0!)
        if (is_null($desclimit)) {
            $desclimit = common_config('site', 'textlimit');
        }
        return $desclimit;
    }

    static function descriptionTooLong($desc)
    {
        $desclimit = self::maxDescription();
        return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
    }

    function asAtomEntry($namespace=false, $source=false)
    {
        $xs = new XMLStringer(true);

        if ($namespace) {
            $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
        } else {
            $attrs = array();
        }

        $xs->elementStart('entry', $attrs);

        if ($source) {
            $xs->elementStart('source');
            $xs->element('id', null, $this->permalink());
            $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name'));
            $xs->element('link', array('href' => $this->permalink()));
            $xs->element('updated', null, $this->modified);
            $xs->elementEnd('source');
        }

        $xs->element('title', null, $this->nickname);
        $xs->element('summary', null, common_xml_safe_str($this->description));

        $xs->element('link', array('rel' => 'alternate',
                                   'href' => $this->permalink()));

        $xs->element('id', null, $this->permalink());

        $xs->element('published', null, common_date_w3dtf($this->created));
        $xs->element('updated', null, common_date_w3dtf($this->modified));

        $xs->element(
            'content',
            array('type' => 'html'),
            common_xml_safe_str($this->description)
        );

        $xs->elementEnd('entry');

        return $xs->getString();
    }

    function asAtomAuthor()
    {
        $xs = new XMLStringer(true);

        $xs->elementStart('author');
        $xs->element('name', null, $this->nickname);
        $xs->element('uri', null, $this->permalink());
        $xs->elementEnd('author');

        return $xs->getString();
    }

    /**
     * Returns an XML string fragment with group information as an
     * Activity Streams noun object with the given element type.
     *
     * Assumes that 'activity', 'georss', and 'poco' namespace has been
     * previously defined.
     *
     * @param string $element one of 'actor', 'subject', 'object', 'target'
     *
     * @return string
     */
    function asActivityNoun($element)
    {
        $noun = ActivityObject::fromGroup($this);
        return $noun->asString('activity:' . $element);
    }

    function getAvatar()
    {
        return empty($this->homepage_logo)
            ? User_group::defaultLogo(AVATAR_PROFILE_SIZE)
            : $this->homepage_logo;
    }

    static function register($fields) {
        if (!empty($fields['userid'])) {
            $profile = Profile::getKV('id', $fields['userid']);
            if ($profile && !$profile->hasRight(Right::CREATEGROUP)) {
                common_log(LOG_WARNING, "Attempted group creation from banned user: " . $profile->nickname);

                // TRANS: Client exception thrown when a user tries to create a group while banned.
                throw new ClientException(_('You are not allowed to create groups on this site.'), 403);
            }
        }

        $fields['nickname'] = Nickname::normalize($fields['nickname']);

        // MAGICALLY put fields into current scope
        // @fixme kill extract(); it makes debugging absurdly hard

		$defaults = array('nickname' => null,
						  'fullname' => null,
						  'homepage' => null,
						  'description' => null,
						  'location' => null,
						  'uri' => null,
						  'mainpage' => null,
						  'aliases' => array(),
						  'userid' => null);

		$fields = array_merge($defaults, $fields);

        extract($fields);

        $group = new User_group();

        if (empty($uri)) {
            // fill in later...
            $uri = null;
        }
        if (empty($mainpage)) {
            $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
        }

        // We must create a new, incrementally assigned profile_id
        $profile = new Profile();
        $profile->nickname   = $nickname;
        $profile->fullname   = $fullname;
        $profile->profileurl = $mainpage;
        $profile->homepage   = $homepage;
        $profile->bio        = $description;
        $profile->location   = $location;
        $profile->created    = common_sql_now();

        $group->nickname    = $profile->nickname;
        $group->fullname    = $profile->fullname;
        $group->homepage    = $profile->homepage;
        $group->description = $profile->bio;
        $group->location    = $profile->location;
        $group->mainpage    = $profile->profileurl;
        $group->created     = $profile->created;

        $profile->query('BEGIN');
        $id = $profile->insert();
        if ($id === false) {
            $profile->query('ROLLBACK');
            throw new ServerException(_('Profile insertion failed'));
        }

        $group->profile_id = $id;
        $group->uri        = $uri;

        if (isset($fields['join_policy'])) {
            $group->join_policy = intval($fields['join_policy']);
        } else {
            $group->join_policy = 0;
        }

        if (isset($fields['force_scope'])) {
            $group->force_scope = intval($fields['force_scope']);
        } else {
            $group->force_scope = 0;
        }

        if (Event::handle('StartGroupSave', array(&$group))) {

            $result = $group->insert();

            if ($result === false) {
                common_log_db_error($group, 'INSERT', __FILE__);
                // TRANS: Server exception thrown when creating a group failed.
                throw new ServerException(_('Could not create group.'));
            }

            if (!isset($uri) || empty($uri)) {
                $orig = clone($group);
                $group->uri = common_local_url('groupbyid', array('id' => $group->id));
                $result = $group->update($orig);
                if (!$result) {
                    common_log_db_error($group, 'UPDATE', __FILE__);
                    // TRANS: Server exception thrown when updating a group URI failed.
                    throw new ServerException(_('Could not set group URI.'));
                }
            }

            $result = $group->setAliases($aliases);

            if (!$result) {
                // TRANS: Server exception thrown when creating group aliases failed.
                throw new ServerException(_('Could not create aliases.'));
            }

            $member = new Group_member();

            $member->group_id   = $group->id;
            $member->profile_id = $userid;
            $member->is_admin   = 1;
            $member->created    = $group->created;

            $result = $member->insert();

            if (!$result) {
                common_log_db_error($member, 'INSERT', __FILE__);
                // TRANS: Server exception thrown when setting group membership failed.
                throw new ServerException(_('Could not set group membership.'));
            }

            self::blow('profile:groups:%d', $userid);

            if ($local) {
                $local_group = new Local_group();

                $local_group->group_id = $group->id;
                $local_group->nickname = $nickname;
                $local_group->created  = common_sql_now();

                $result = $local_group->insert();

                if (!$result) {
                    common_log_db_error($local_group, 'INSERT', __FILE__);
                    // TRANS: Server exception thrown when saving local group information failed.
                    throw new ServerException(_('Could not save local group info.'));
                }
            }

            Event::handle('EndGroupSave', array($group));
        }

        $profile->query('COMMIT');

        return $group;
    }

    /**
     * Handle cascading deletion, on the model of notice and profile.
     *
     * This should handle freeing up cached entries for the group's
     * id, nickname, URI, and aliases. There may be other areas that
     * are not de-cached in the UI, including the sidebar lists on
     * GroupsAction
     */
    function delete($useWhere=false)
    {
        if (empty($this->id)) {
            common_log(LOG_WARNING, "Ambiguous User_group->delete(); skipping related tables.");
            return parent::delete($useWhere);
        }

        try {
            $profile = $this->getProfile();
            $profile->delete();
        } catch (GroupNoProfileException $unp) {
            common_log(LOG_INFO, "Group {$this->nickname} has no profile; continuing deletion.");
        }

        // Safe to delete in bulk for now

        $related = array('Group_inbox',
                         'Group_block',
                         'Group_member',
                         'Related_group');

        Event::handle('UserGroupDeleteRelated', array($this, &$related));

        foreach ($related as $cls) {
            $inst = new $cls();
            $inst->group_id = $this->id;

            if ($inst->find()) {
                while ($inst->fetch()) {
                    $dup = clone($inst);
                    $dup->delete();
                }
            }
        }

        // And related groups in the other direction...
        $inst = new Related_group();
        $inst->related_group_id = $this->id;
        $inst->delete();

        // Aliases and the local_group entry need to be cleared explicitly
        // or we'll miss clearing some cache keys; that can make it hard
        // to create a new group with one of those names or aliases.
        $this->setAliases(array());

        // $this->isLocal() but we're using the resulting object
        $local = Local_group::getKV('group_id', $this->id);
        if ($local instanceof Local_group) {
            $local->delete();
        }

        // blow the cached ids
        self::blow('user_group:notice_ids:%d', $this->id);

        return parent::delete($useWhere);
    }

    public function update($dataObject=false)
    {
        // Whenever the User_group is updated, find the Local_group
        // and update its nickname too.
        if ($this->nickname != $dataObject->nickname) {
            $local = Local_group::getKV('group_id', $this->id);
            if ($local instanceof Local_group) {
                common_debug("Updating Local_group ({$this->id}) nickname from {$dataObject->nickname} to {$this->nickname}");
                $local->setNickname($this->nickname);
            }
        }

        // Also make sure the Profile table is up to date!
        $fields = array(/*group field => profile field*/
                    'nickname'      => 'nickname',
                    'fullname'      => 'fullname',
                    'mainpage'      => 'profileurl',
                    'homepage'      => 'homepage',
                    'description'   => 'bio',
                    'location'      => 'location',
                    'created'       => 'created',
                    'modified'      => 'modified',
                    );
        $profile = $this->getProfile();
        $origpro = clone($profile);
        foreach ($fields as $gf=>$pf) {
            $profile->$pf = $this->$gf;
        }
        if ($profile->update($origpro) === false) {
            throw new ServerException(_('Unable to update profile'));
        }

        return parent::update($dataObject);
    }

    function isPrivate()
    {
        return ($this->join_policy == self::JOIN_POLICY_MODERATE &&
                intval($this->force_scope) === 1);
    }

    public function isLocal()
    {
        $local = Local_group::getKV('group_id', $this->id);
        return ($local instanceof Local_group);
    }

    static function groupsFromText($text, Profile $profile)
    {
        $groups = array();

        /* extract all !group */
        $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/',
                                strtolower($text),
                                $match);

        if (!$count) {
            return $groups;
        }

        foreach (array_unique($match[1]) as $nickname) {
            $group = self::getForNickname($nickname, $profile);
            if ($group instanceof User_group && $profile->isMember($group)) {
                $groups[] = clone($group);
            }
        }

        return $groups;
    }

    static function idsFromText($text, Profile $profile)
    {
        $ids = array();
        $groups = self::groupsFromText($text, $profile);
        foreach ($groups as $group) {
            $ids[$group->id] = true;
        }
        return array_keys($ids);
    }
}