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 file_redirection
 */

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

    public $__table = 'file_redirection';                // table name
    public $urlhash;                         // varchar(64) primary_key not_null
    public $url;                             // text
    public $file_id;                         // int(4)
    public $redirections;                    // int(4)
    public $httpcode;                        // int(4)
    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(
            'fields' => array(
                'urlhash' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'sha256 hash of the URL'),
                'url' => array('type' => 'text', 'description' => 'short URL (or any other kind of redirect) for file (id)'),
                'file_id' => array('type' => 'int', 'description' => 'short URL for what URL/file'),
                'redirections' => array('type' => 'int', 'description' => 'redirect count'),
                'httpcode' => array('type' => 'int', 'description' => 'HTTP status code (20x, 30x, etc.)'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('urlhash'),
            'foreign keys' => array(
                'file_redirection_file_id_fkey' => array('file' => array('file_id' => 'id')),
            ),
        );
    }

    static public function getByUrl($url)
    {
        $file = new File_redirection();
        $file->urlhash = File::hashurl($url);
        if (!$file->find(true)) {
            throw new NoResultException($file);
        }
        return $file;
    }

    static function _commonHttp($url, $redirs) {
        $request = new HTTPClient($url);
        $request->setConfig(array(
            'connect_timeout' => 10, // # seconds to wait
            'max_redirs' => $redirs, // # max number of http redirections to follow
            'follow_redirects' => true, // Follow redirects
            'store_body' => false, // We won't need body content here.
        ));
        return $request;
    }

    /**
     * Check if this URL is a redirect and return redir info.
     *
     * Most code should call File_redirection::where instead, to check if we
     * already know that redirection and avoid extra hits to the web.
     *
     * The URL is hit and any redirects are followed, up to 10 levels or until
     * a protected URL is reached.
     *
     * @param string $in_url
     * @return mixed one of:
     *         string - target URL, if this is a direct link or can't be followed
     *         array - redirect info if this is an *unknown* redirect:
     *              associative array with the following elements:
     *                code: HTTP status code
     *                redirects: count of redirects followed
     *                url: URL string of final target
     *                type (optional): MIME type from Content-Type header
     *                size (optional): byte size from Content-Length header
     *                time (optional): timestamp from Last-Modified header
     */
    public function lookupWhere($short_url, $redirs = 10, $protected = false) {
        if ($redirs < 0) return false;

        if(strpos($short_url,'://') === false){
            return $short_url;
        }
        try {
            $request = self::_commonHttp($short_url, $redirs);
            // Don't include body in output
            $request->setMethod(HTTP_Request2::METHOD_HEAD);
            $response = $request->send();

            if (405 == $response->getStatus() || 204 == $response->getStatus()) {
                // HTTP 405 Unsupported Method
                // Server doesn't support HEAD method? Can this really happen?
                // We'll try again as a GET and ignore the response data.
                //
                // HTTP 204 No Content
                // YFrog sends 204 responses back for our HEAD checks, which
                // seems like it may be a logic error in their servers. If
                // we get a 204 back, re-run it as a GET... if there's really
                // no content it'll be cheap. :)
                $request = self::_commonHttp($short_url, $redirs);
                $response = $request->send();
            }
        } catch (Exception $e) {
            // Invalid URL or failure to reach server
            common_log(LOG_ERR, "Error while following redirects for $short_url: " . $e->getMessage());
            return $short_url;
        }

        if ($response->getRedirectCount() && File::isProtected($response->getUrl())) {
            // Bump back up the redirect chain until we find a non-protected URL
            return self::lookupWhere($short_url, $response->getRedirectCount() - 1, true);
        }

        $ret = array('code' => $response->getStatus()
                , 'redirects' => $response->getRedirectCount()
                , 'url' => $response->getUrl());

        $type = $response->getHeader('Content-Type');
        if ($type) $ret['type'] = $type;
        if ($protected) $ret['protected'] = true;
        $size = $response->getHeader('Content-Length'); // @fixme bytes?
        if ($size) $ret['size'] = $size;
        $time = $response->getHeader('Last-Modified');
        if ($time) $ret['time'] = strtotime($time);
        return $ret;
    }

    /**
     * Check if this URL is a redirect and return redir info.
     * If a File record is present for this URL, it is not considered a redirect.
     * If a File_redirection record is present for this URL, the recorded target is returned.
     *
     * If no File or File_redirect record is present, the URL is hit and any
     * redirects are followed, up to 10 levels or until a protected URL is
     * reached.
     *
     * @param string $in_url
     * @param boolean $discover true to attempt dereferencing the redirect if we don't know it already
     * @return mixed one of:
     *         string - target URL, if this is a direct link or a known redirect
     *         array - redirect info if this is an *unknown* redirect:
     *              associative array with the following elements:
     *                code: HTTP status code
     *                redirects: count of redirects followed
     *                url: URL string of final target
     *                type (optional): MIME type from Content-Type header
     *                size (optional): byte size from Content-Length header
     *                time (optional): timestamp from Last-Modified header
     */
    public function where($in_url, $discover=true) {
        // let's see if we know this...
        try {
            $a = File::getByUrl($in_url);
            // this is a direct link to $a->url
            return $a->url;
        } catch (NoResultException $e) {
            try {
                $b = File_redirection::getByUrl($in_url);
                // this is a redirect to $b->file_id
                $a = File::getKV('id', $b->file_id);
                return $a->url;
            } catch (NoResultException $e) {
                // Oh well, let's keep going
            }
        }

        if ($discover) {
            $ret = File_redirection::lookupWhere($in_url);
            return $ret;
        } else {
            // No manual dereferencing; leave the unknown URL as is.
            return $in_url;
        }
    }

    /**
     * Shorten a URL with the current user's configured shortening
     * options, if applicable.
     *
     * If it cannot be shortened or the "short" URL is longer than the
     * original, the original is returned.
     *
     * If the referenced item has not been seen before, embedding data
     * may be saved.
     *
     * @param string $long_url
     * @param User $user whose shortening options to use; defaults to the current web session user
     * @return string
     */
    function makeShort($long_url, $user=null)
    {
        $canon = File_redirection::_canonUrl($long_url);

        $short_url = File_redirection::_userMakeShort($canon, $user);

        // Did we get one? Is it shorter?

        if (!empty($short_url)) {
            return $short_url;
        } else {
            return $long_url;
        }
    }

    /**
     * Shorten a URL with the current user's configured shortening
     * options, if applicable.
     *
     * If it cannot be shortened or the "short" URL is longer than the
     * original, the original is returned.
     *
     * If the referenced item has not been seen before, embedding data
     * may be saved.
     *
     * @param string $long_url
     * @return string
     */

    function forceShort($long_url, $user)
    {
        $canon = File_redirection::_canonUrl($long_url);

        $short_url = File_redirection::_userMakeShort($canon, $user, true);

        // Did we get one? Is it shorter?
        if (!empty($short_url)) {
            return $short_url;
        } else {
            return $long_url;
        }
    }

    function _userMakeShort($long_url, User $user=null, $force = false) {
        $short_url = common_shorten_url($long_url, $user, $force);
        if (!empty($short_url) && $short_url != $long_url) {
            $short_url = (string)$short_url;
            // store it
            $file = File::getKV('url', $long_url);
            if ($file instanceof File) {
                $file_id = $file->id;
            } else {
                // Check if the target URL is itself a redirect...
                $redir_data = File_redirection::where($long_url);
                if (is_array($redir_data)) {
                    // We haven't seen the target URL before.
                    // Save file and embedding data about it!
                    $file = File::saveNew($redir_data, $long_url);
                    $file_id = $file->id;
                } else if (is_string($redir_data)) {
                    // The file is a known redirect target.
                    $file = File::getKV('url', $redir_data);
                    if (empty($file)) {
                        // @fixme should we save a new one?
                        // this case was triggering sometimes for redirects
                        // with unresolvable targets; found while fixing
                        // "can't linkify" bugs with shortened links to
                        // SSL sites with cert issues.
                        return null;
                    }
                    $file_id = $file->id;
                }
            }
            $file_redir = File_redirection::getKV('url', $short_url);
            if (!$file_redir instanceof File_redirection) {
                $file_redir = new File_redirection;
                $file_redir->urlhash = File::hashurl($short_url);
                $file_redir->url = $short_url;
                $file_redir->file_id = $file_id;
                $file_redir->insert();
            }
            return $short_url;
        }
        return null;
    }

    /**
     * Basic attempt to canonicalize a URL, cleaning up some standard variants
     * such as funny syntax or a missing path. Used internally when cleaning
     * up URLs for storage and following redirect chains.
     *
     * Note that despite being on File_redirect, this function DOES NOT perform
     * any dereferencing of redirects.
     *
     * @param string $in_url input URL
     * @param string $default_scheme if given a bare link; defaults to 'http://'
     * @return string
     */
    function _canonUrl($in_url, $default_scheme = 'http://') {
        if (empty($in_url)) return false;
        $out_url = $in_url;
        $p = parse_url($out_url);
        if (empty($p['host']) || empty($p['scheme'])) {
            list($scheme) = explode(':', $in_url, 2);
            switch (strtolower($scheme)) {
            case 'fax':
            case 'tel':
                $out_url = str_replace('.-()', '', $out_url);
                break;

            case 'mailto':
            case 'magnet':
            case 'aim':
            case 'jabber':
            case 'xmpp':
                // don't touch anything
                break;

            default:
                $out_url = $default_scheme . ltrim($out_url, '/');
                $p = parse_url($out_url);
                if (empty($p['scheme'])) return false;
                break;
            }
        }

        if (('ftp' == $p['scheme']) || ('ftps' == $p['scheme']) || ('http' == $p['scheme']) || ('https' == $p['scheme'])) {
            if (empty($p['host'])) return false;
            if (empty($p['path'])) {
                $out_url .= '/';
            }
        }

        return $out_url;
    }

    function saveNew($data, $file_id, $url) {
        $file_redir = new File_redirection;
        $file_redir->urlhash = File::hashurl($short_url);
        $file_redir->url = $url;
        $file_redir->file_id = $file_id;
        $file_redir->redirections = intval($data['redirects']);
        $file_redir->httpcode = intval($data['code']);
        $file_redir->insert();
    }

    static public function beforeSchemaUpdate()
    {
        $table = strtolower(get_called_class());
        $schema = Schema::get();
        $schemadef = $schema->getTableDef($table);

        // 2015-02-19 We have to upgrade our table definitions to have the urlhash field populated
        if (isset($schemadef['fields']['urlhash']) && in_array('urlhash', $schemadef['primary key'])) {
            // We already have the urlhash field, so no need to migrate it.
            return;
        }
        echo "\nFound old $table table, upgrading it to contain 'urlhash' field...";
        // We have to create a urlhash that is _not_ the primary key,
        // transfer data and THEN run checkSchema
        $schemadef['fields']['urlhash'] = array (
                                              'type' => 'varchar',
                                              'length' => 64,
                                              'not null' => true,
                                              'description' => 'sha256 hash of the URL',
                                            );
        $schemadef['fields']['url'] = array (
                                              'type' => 'text',
                                              'description' => 'short URL (or any other kind of redirect) for file (id)',
                                            );
        unset($schemadef['primary key']);
        $schema->ensureTable($table, $schemadef);
        echo "DONE.\n";

        $classname = ucfirst($table);
        $tablefix = new $classname;
        // urlhash is hash('sha256', $url) in the File table
        echo "Updating urlhash fields in $table table...";
        // Maybe very MySQL specific :(
        $tablefix->query(sprintf('UPDATE %1$s SET %2$s=%3$s;',
                            $schema->quoteIdentifier($table),
                            'urlhash',
                            // The line below is "result of sha256 on column `url`"
                            'SHA2(url, 256)'));
        echo "DONE.\n";
        echo "Resuming core schema upgrade...";
    }
}


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 file_thumbnail
 */

class File_thumbnail extends Managed_DataObject
{
    public $__table = 'file_thumbnail';                  // table name
    public $file_id;                         // int(4)  primary_key not_null
    public $url;                             // text
    public $filename;                        // text
    public $width;                           // int(4)  primary_key
    public $height;                          // int(4)  primary_key
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'thumbnail for what URL/file'),
                'url' => array('type' => 'text', 'not null' => false, 'description' => 'URL of thumbnail'),
                'filename' => array('type' => 'text', 'description' => 'if stored locally, filename is put here'),
                'width' => array('type' => 'int', 'description' => 'width of thumbnail'),
                'height' => array('type' => 'int', 'description' => 'height of thumbnail'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('file_id', 'width', 'height'),
            'indexes' => array(
                'file_thumbnail_file_id_idx' => array('file_id'),
            ),
            'foreign keys' => array(
                'file_thumbnail_file_id_fkey' => array('file', array('file_id' => 'id')),
            )
        );
    }

    /**
     * Save oEmbed-provided thumbnail data
     *
     * @param object $data
     * @param int $file_id
     */
    public static function saveNew($data, $file_id) {
        if (!empty($data->thumbnail_url)) {
            // Non-photo types such as video will usually
            // show us a thumbnail, though it's not required.
            self::saveThumbnail($file_id,
                                $data->thumbnail_url,
                                $data->thumbnail_width,
                                $data->thumbnail_height);
        } else if ($data->type == 'photo') {
            // The inline photo URL given should also fit within
            // our requested thumbnail size, per oEmbed spec.
            self::saveThumbnail($file_id,
                                $data->url,
                                $data->width,
                                $data->height);
        }
    }

    /**
     * Fetch an entry by using a File's id
     */
    static function byFile(File $file) {
        $file_thumbnail = self::getKV('file_id', $file->id);
        if (!$file_thumbnail instanceof File_thumbnail) {
            throw new ServerException(sprintf('No File_thumbnail entry for File id==%u', $file->id));
        }
        return $file_thumbnail;
    }

    /**
     * Save a thumbnail record for the referenced file record.
     *
     * FIXME: Add error handling
     *
     * @param int $file_id
     * @param string $url
     * @param int $width
     * @param int $height
     */
    static function saveThumbnail($file_id, $url, $width, $height, $filename=null)
    {
        $tn = new File_thumbnail;
        $tn->file_id = $file_id;
        $tn->url = $url;
        $tn->filename = $filename;
        $tn->width = intval($width);
        $tn->height = intval($height);
        $tn->insert();
        return $tn;
    }

    static function path($filename)
    {
        // TODO: Store thumbnails in their own directory and don't use File::path here
        return File::path($filename);
    }

    static function url($filename)
    {
        // TODO: Store thumbnails in their own directory and don't use File::url here
        return File::url($filename);
    }

    public function getPath()
    {
        $filepath = self::path($this->filename);
        if (!file_exists($filepath)) {
            throw new FileNotFoundException($filepath);
        }
        return $filepath;
    }

    public function getUrl()
    {
        if (!empty($this->getFile()->filename)) {
            // A locally stored File, so we can dynamically generate a URL.
            if (!empty($this->url)) {
                // Let's just clear this field as there is no point in having it for local files.
                $orig = clone($this);
                $this->url = null;
                $this->update($orig);
            }
            $url = common_local_url('attachment_thumbnail', array('attachment'=>$this->file_id));
            if (strpos($url, '?') === false) {
                $url .= '?';
            }
            return $url . http_build_query(array('w'=>$this->width, 'h'=>$this->height));
        }

        // No local filename available, return the URL we have stored
        return $this->url;
    }

    public function delete($useWhere=false)
    {
        if (!empty($this->filename) && file_exists(File_thumbnail::path($this->filename))) {
            $deleted = @unlink(self::path($this->filename));
            if (!$deleted) {
                common_log(LOG_ERR, sprintf('Could not unlink existing file: "%s"', self::path($this->filename)));
            }
        }

        return parent::delete($useWhere);
    }

    public function getFile()
    {
        $file = new File();
        $file->id = $this->file_id;
        if (!$file->find(true)) {
            throw new NoResultException($file);
        }
        return $file;
    }
}

repo_name: piratas/tortuga

license: mit


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('STATUSNET') && !defined('LACONICA')) { exit(1); }

require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

/**
 * Table Definition for file_to_post
 */

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

    public $__table = 'file_to_post';                    // table name
    public $file_id;                         // int(4)  primary_key not_null
    public $post_id;                         // int(4)  primary_key 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(
            'fields' => array(
                'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'id of URL/file'),
                'post_id' => array('type' => 'int', 'not null' => true, 'description' => 'id of the notice it belongs to'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('file_id', 'post_id'),
            'foreign keys' => array(
                'file_to_post_file_id_fkey' => array('file', array('file_id' => 'id')),
                'file_to_post_post_id_fkey' => array('notice', array('post_id' => 'id')),
            ),
            'indexes' => array(
                'file_id_idx' => array('file_id'),
                'post_id_idx' => array('post_id'),
            ),
        );
    }

    function processNew($file_id, $notice_id) {
        static $seen = array();
        if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) {

            $f2p = File_to_post::pkeyGet(array('post_id' => $notice_id,
                                               'file_id' => $file_id));
            if (empty($f2p)) {
                $f2p = new File_to_post;
                $f2p->file_id = $file_id;
                $f2p->post_id = $notice_id;
                $f2p->insert();

                $f = File::getKV($file_id);

                if (!empty($f)) {
                    $f->blowCache();
                }
            }

            if (empty($seen[$notice_id])) {
                $seen[$notice_id] = array($file_id);
            } else {
                $seen[$notice_id][] = $file_id;
            }
        }
    }

    static function getNoticeIDsByFile(File $file)
    {
        $f2p = new File_to_post();

        $f2p->selectAdd();
        $f2p->selectAdd('post_id');

        $f2p->file_id = $file->id;

        $ids = array();

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

        return $f2p->fetchAll('post_id');
    }

    function delete($useWhere=false)
    {
        $f = File::getKV('id', $this->file_id);
        if ($f instanceof File) {
            $f->blowCache();
        }
        return parent::delete($useWhere);
    }
}


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

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

    public $__table = 'foreign_link';                    // table name
    public $user_id;                         // int(4)  primary_key not_null
    public $foreign_id;                      // bigint(8)  primary_key not_null unsigned
    public $service;                         // int(4)  primary_key not_null
    public $credentials;                     // varchar(191)   not 255 because utf8mb4 takes more space
    public $noticesync;                      // tinyint(1)   not_null default_1
    public $friendsync;                      // tinyint(1)   not_null default_2
    public $profilesync;                     // tinyint(1)   not_null default_1
    public $last_noticesync;                 // datetime()
    public $last_friendsync;                 // datetime()
    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(
            'fields' => array(
                'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'link to user on this system, if exists'),
                'foreign_id' => array('type' => 'int', 'size' => 'big', 'unsigned' => true, 'not null' => true, 'description' => 'link to user on foreign service, if exists'),
                'service' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to service'),
                'credentials' => array('type' => 'varchar', 'length' => 191, 'description' => 'authc credentials, typically a password'),
                'noticesync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies'),
                'friendsync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 2, 'description' => 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming'),
                'profilesync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming'),
                'last_noticesync' => array('type' => 'datetime', 'description' => 'last time notices were imported'),
                'last_friendsync' => array('type' => 'datetime', 'description' => 'last time friends were imported'),
                '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('user_id', 'foreign_id', 'service'),
            'foreign keys' => array(
                'foreign_link_user_id_fkey' => array('user', array('user_id' => 'id')),
                'foreign_link_foreign_id_fkey' => array('foreign_user', array('foreign_id' => 'id', 'service' => 'service')),
                'foreign_link_service_fkey' => array('foreign_service', array('service' => 'id')),
            ),
            'indexes' => array(
                'foreign_user_user_id_idx' => array('user_id'),
            ),
        );
    }

    static function getByUserID($user_id, $service)
    {
        if (empty($user_id) || empty($service)) {
            return null;
        }

        $flink = new Foreign_link();

        $flink->service = $service;
        $flink->user_id = $user_id;
        $flink->limit(1);

        $result = $flink->find(true);

        return empty($result) ? null : $flink;
    }

    static function getByForeignID($foreign_id, $service)
    {
        if (empty($foreign_id) || empty($service)) {
            return null;
        } else {
            $flink = new Foreign_link();
            $flink->service = $service;
            $flink->foreign_id = $foreign_id;
            $flink->limit(1);

            $result = $flink->find(true);

            return empty($result) ? null : $flink;
        }
    }

    function set_flags($noticesend, $noticerecv, $replysync, $friendsync)
    {
        if ($noticesend) {
            $this->noticesync |= FOREIGN_NOTICE_SEND;
        } else {
            $this->noticesync &= ~FOREIGN_NOTICE_SEND;
        }

        if ($noticerecv) {
            $this->noticesync |= FOREIGN_NOTICE_RECV;
        } else {
            $this->noticesync &= ~FOREIGN_NOTICE_RECV;
        }

        if ($replysync) {
            $this->noticesync |= FOREIGN_NOTICE_SEND_REPLY;
        } else {
            $this->noticesync &= ~FOREIGN_NOTICE_SEND_REPLY;
        }

        if ($friendsync) {
            $this->friendsync |= FOREIGN_FRIEND_RECV;
        } else {
            $this->friendsync &= ~FOREIGN_FRIEND_RECV;
        }

        $this->profilesync = 0;
    }

    // Convenience methods
    function getForeignUser()
    {
        $fuser = new Foreign_user();
        $fuser->service = $this->service;
        $fuser->id = $this->foreign_id;

        $fuser->limit(1);

        if ($fuser->find(true)) {
            return $fuser;
        }

        return null;
    }

    function getUser()
    {
        return User::getKV($this->user_id);
    }

    function getProfile()
    {
        return Profile::getKV('id', $this->user_id);
    }

    // Make sure we only ever delete one record at a time
    function safeDelete()
    {
        if (!empty($this->user_id)
            && !empty($this->foreign_id)
            && !empty($this->service))
        {
            return $this->delete();
        } else {
            common_debug(LOG_WARNING,
                'Foreign_link::safeDelete() tried to delete a '
                . 'Foreign_link without a fully specified compound key: '
                . var_export($this, true));
            return false;
        }
    }
}


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

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

    public $__table = 'foreign_service';                 // table name
    public $id;                              // int(4)  primary_key not_null
    public $name;                            // varchar(32)  unique_key not_null
    public $description;                     // 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(
            'fields' => array(
                'id' => array('type' => 'int', 'not null' => true, 'description' => 'numeric key for service'),
                'name' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'name of the service'),
                'description' => array('type' => 'varchar', 'length' => 191, 'description' => 'description'),
                '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(
                'foreign_service_name_key' => array('name'),
            ),
        );
    }
}


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

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

    public $__table = 'foreign_subscription';            // table name
    public $service;                         // int(4)  primary_key not_null
    public $subscriber;                      // int(4)  primary_key not_null
    public $subscribed;                      // int(4)  primary_key not_null
    public $created;                         // datetime()   not_null

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

    public static function schemaDef()
    {
        return array(

            'fields' => array(
                'service' => array('type' => 'int', 'not null' => true, 'description' => 'service where relationship happens'),
                'subscriber' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'subscriber on foreign service'),
                'subscribed' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'subscribed user'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
            ),
            'primary key' => array('service', 'subscriber', 'subscribed'),
            'foreign keys' => array(
                'foreign_subscription_service_fkey' => array('foreign_service', array('service' => 'id')),
                'foreign_subscription_subscriber_fkey' => array('foreign_user', array('subscriber' => 'id', 'service' => 'service')),
                'foreign_subscription_subscribed_fkey' => array('foreign_user', array('subscribed' => 'id', 'service' => 'service')),
            ),
            'indexes' => array(
                'foreign_subscription_subscriber_idx' => array('service', 'subscriber'),
                'foreign_subscription_subscribed_idx' => array('service', 'subscribed'),
            ),
        );
    }
}


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

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

    public $__table = 'foreign_user';                    // table name
    public $id;                              // bigint(8)  primary_key not_null
    public $service;                         // int(4)  primary_key not_null
    public $uri;                             // varchar(191)  unique_key not_null   not 255 because utf8mb4 takes more space
    public $nickname;                        // 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(
            'fields' => array(
                'id' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'unique numeric key on foreign service'),
                'service' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to service'),
                'uri' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'identifying URI'),
                'nickname' => array('type' => 'varchar', 'length' => 191, 'description' => 'nickname on foreign service'),
                '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', 'service'),
            'foreign keys' => array(
                'foreign_user_service_fkey' => array('foreign_service', array('service' => 'id')),
            ),
            'unique keys' => array(
                'foreign_user_uri_key' => array('uri'),
            ),
        );
    }

    static function getForeignUser($id, $service) {

        $fuser = new Foreign_user();

        $fuser->id      = $id;
        $fuser->service = $service;

        $fuser->limit(1);

        $result = $fuser->find(true);

        return empty($result) ? null : $fuser;
    }

    static function getByNickname($nickname, $service)
    {
        if (empty($nickname) || empty($service)) {
            return null;
        } else {
            $fuser = new Foreign_user();
	    $fuser->service = $service;
	    $fuser->nickname = $nickname;
            $fuser->limit(1);

            $result = $fuser->find(true);

            return empty($result) ? null : $fuser;
        }
    }
}


code:
<?php
/**
 * Table Definition for group_alias
 *
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 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('STATUSNET') && !defined('LACONICA')) { exit(1); }

require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

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

    public $__table = 'group_alias';                     // table name
    public $alias;                           // varchar(64)  primary_key not_null
    public $group_id;                        // int(4)   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(
            'fields' => array(
                'alias' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'additional nickname for the group'),
                'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group profile is blocked from'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date alias was created'),
            ),
            'primary key' => array('alias'),
            'foreign keys' => array(
                'group_alias_group_id_fkey' => array('user_group', array('group_id' => 'id')),
            ),
            'indexes' => array(
                'group_alias_group_id_idx' => array('group_id'),
            ),
        );
    }

    public function getProfile()
    {
        $group = User_group::getKV('id', $this->group_id);
        if (!($group instanceof User_group)) {
            return null;    // TODO: Throw exception when other code is ready
        }
        return $group->getProfile();
    }
}


code:
<?php
/**
 * Table Definition for group_block
 *
 * 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('STATUSNET') && !defined('LACONICA')) { exit(1); }

require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

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

    public $__table = 'group_block';                     // table name
    public $group_id;                        // int(4)  primary_key not_null
    public $blocked;                         // int(4)  primary_key not_null
    public $blocker;                         // int(4)   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(
            'fields' => array(
                'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group profile is blocked from'),
                'blocked' => array('type' => 'int', 'not null' => true, 'description' => 'profile that is blocked'),
                'blocker' => array('type' => 'int', 'not null' => true, 'description' => 'user making the block'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date of blocking'),
            ),
            'primary key' => array('group_id', 'blocked'),
            'foreign keys' => array(
                'group_block_group_id_fkey' => array('user_group', array('group_id' => 'id')),
                'group_block_blocked_fkey' => array('profile', array('blocked' => 'id')),
                'group_block_blocker_fkey' => array('user', array('blocker' => 'id')),
            ),
        );
    }

    static function isBlocked($group, $profile)
    {
        $block = Group_block::pkeyGet(array('group_id' => $group->id,
                                            'blocked' => $profile->id));
        return !empty($block);
    }

    static function blockProfile($group, $profile, $blocker)
    {
        // Insert the block

        $block = new Group_block();

        $block->query('BEGIN');

        $block->group_id = $group->id;
        $block->blocked  = $profile->id;
        $block->blocker  = $blocker->id;

        $result = $block->insert();

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

        // Delete membership if any

        $member = new Group_member();

        $member->group_id   = $group->id;
        $member->profile_id = $profile->id;

        if ($member->find(true)) {
            $result = $member->delete();
            if ($result === false) {
                common_log_db_error($member, 'DELETE', __FILE__);
                return null;
            }
        }

        // Commit, since both have been done

        $block->query('COMMIT');

        return $block;
    }

    static function unblockProfile($group, $profile)
    {
        $block = Group_block::pkeyGet(array('group_id' => $group->id,
                                            'blocked' => $profile->id));

        if (empty($block)) {
            return null;
        }

        $result = $block->delete();

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

        return true;
    }
}


code:
<?php

/**
 * Table Definition for group_inbox
 */
class Group_inbox extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'group_inbox';                     // table name
    public $group_id;                        // int(4)  primary_key not_null
    public $notice_id;                       // int(4)  primary_key not_null
    public $created;                         // datetime()   not_null

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

    public static function schemaDef()
    {
        return array(
            'description' => 'Many-many table listing notices posted to a given group, or which groups a given notice was posted to.',
            'fields' => array(
                'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group receiving the message'),
                'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice received'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice was created'),
            ),
            'primary key' => array('group_id', 'notice_id'),
            'foreign keys' => array(
                'group_inbox_group_id_fkey' => array('user_group', array('group_id' => 'id')),
                'group_inbox_notice_id_fkey' => array('notice', array('notice_id' => 'id')),
            ),
            'indexes' => array(
                'group_inbox_created_idx' => array('created'),
                'group_inbox_notice_id_idx' => array('notice_id'),
                'group_inbox_group_id_created_notice_id_idx' => array('group_id', 'created', 'notice_id'),
            ),
        );
    }
}


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

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

    public $__table = 'group_join_queue';       // table name
    public $profile_id;
    public $group_id;
    public $created;

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

    public static function schemaDef()
    {
        return array(
            'description' => 'Holder for group join requests awaiting moderation.',
            'fields' => array(
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile making the request'),
                'group_id' => array('type' => 'int', 'description' => 'remote or local group to join, if any'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
            ),
            'primary key' => array('profile_id', 'group_id'),
            'indexes' => array(
                'group_join_queue_profile_id_created_idx' => array('profile_id', 'created'),
                'group_join_queue_group_id_created_idx' => array('group_id', 'created'),
            ),
            'foreign keys' => array(
                'group_join_queue_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
                'group_join_queue_group_id_fkey' => array('user_group', array('group_id' => 'id')),
            )
        );
    }

    public static function saveNew(Profile $profile, User_group $group)
    {
        $rq = new Group_join_queue();
        $rq->profile_id = $profile->id;
        $rq->group_id = $group->id;
        $rq->created = common_sql_now();
        $rq->insert();
        return $rq;
    }

    function getMember()
    {
        $member = Profile::getKV('id', $this->profile_id);

        if (empty($member)) {
            // TRANS: Exception thrown providing an invalid profile ID.
            // TRANS: %s is the invalid profile ID.
            throw new Exception(sprintf(_('Profile ID %s is invalid.'),$this->profile_id));
        }

        return $member;
    }

    function getGroup()
    {
        $group  = User_group::getKV('id', $this->group_id);

        if (empty($group)) {
            // TRANS: Exception thrown providing an invalid group ID.
            // TRANS: %s is the invalid group ID.
            throw new Exception(sprintf(_('Group ID %s is invalid.'),$this->group_id));
        }

        return $group;
    }

    /**
     * Abort the pending group join...
     *
     * @param User_group $group
     */
    function abort()
    {
        $profile = $this->getMember();
        $group = $this->getGroup();
        if ($request) {
            if (Event::handle('StartCancelJoinGroup', array($profile, $group))) {
                $this->delete();
                Event::handle('EndCancelJoinGroup', array($profile, $group));
            }
        }
    }

    /**
     * Complete a pending group join...
     *
     * @return Group_member object on success
     */
    function complete()
    {
        $join = null;
        $profile = $this->getMember();
        $group = $this->getGroup();
        if (Event::handle('StartJoinGroup', array($profile, $group))) {
            $join = Group_member::join($group->id, $profile->id);
            $this->delete();
            Event::handle('EndJoinGroup', array($profile, $group));
        }
        if (!$join) {
            throw new Exception('Internal error: group join failed.');
        }
        $join->notify();
        return $join;
    }

    /**
     * Send notifications via email etc to group administrators about
     * this exciting new pending moderation queue item!
     */
    public function notify()
    {
        $joiner = Profile::getKV('id', $this->profile_id);
        $group = User_group::getKV('id', $this->group_id);
        mail_notify_group_join_pending($group, $joiner);
    }
}


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

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

    public $__table = 'group_member';                    // table name
    public $group_id;                        // int(4)  primary_key not_null
    public $profile_id;                      // int(4)  primary_key not_null
    public $is_admin;                        // tinyint(1)
    public $uri;                             // 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(
            'fields' => array(
                'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to user_group'),
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),
                'is_admin' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'is this user an admin?'),
                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universal 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('group_id', 'profile_id'),
            'unique keys' => array(
                'group_member_uri_key' => array('uri'),
            ),
            'foreign keys' => array(
                'group_member_group_id_fkey' => array('user_group', array('group_id' => 'id')),
                'group_member_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
            ),
            'indexes' => array(
                // @fixme probably we want a (profile_id, created) index here?
                'group_member_profile_id_idx' => array('profile_id'),
                'group_member_created_idx' => array('created'),
                'group_member_profile_id_created_idx' => array('profile_id', 'created'),
                'group_member_group_id_created_idx' => array('group_id', 'created'),
            ),
        );
    }

    /**
     * Method to add a user to a group.
     * In most cases, you should call Profile->joinGroup() instead.
     *
     * @param integer $group_id   Group to add to
     * @param integer $profile_id Profile being added
     *
     * @return Group_member new membership object
     */

    static function join($group_id, $profile_id)
    {
        $member = new Group_member();

        $member->group_id   = $group_id;
        $member->profile_id = $profile_id;
        $member->created    = common_sql_now();
        $member->uri        = self::newURI($profile_id, $group_id, $member->created);

        $result = $member->insert();

        if (!$result) {
            common_log_db_error($member, 'INSERT', __FILE__);
            // TRANS: Exception thrown when joining a group fails.
            throw new Exception(_("Group join failed."));
        }

        return $member;
    }

    static function leave($group_id, $profile_id)
    {
        $member = Group_member::pkeyGet(array('group_id' => $group_id,
                                              'profile_id' => $profile_id));

        if (empty($member)) {
            // TRANS: Exception thrown when trying to leave a group the user is not a member of.
            throw new Exception(_("Not part of group."));
        }

        $result = $member->delete();

        if (!$result) {
            common_log_db_error($member, 'INSERT', __FILE__);
            // TRANS: Exception thrown when trying to leave a group fails.
            throw new Exception(_("Group leave failed."));
        }

        return true;
    }

    function getMember()
    {
        $member = Profile::getKV('id', $this->profile_id);

        if (empty($member)) {
            // TRANS: Exception thrown providing an invalid profile ID.
            // TRANS: %s is the invalid profile ID.
            throw new Exception(sprintf(_("Profile ID %s is invalid."),$this->profile_id));
        }

        return $member;
    }

    function getGroup()
    {
        $group  = User_group::getKV('id', $this->group_id);

        if (empty($group)) {
            // TRANS: Exception thrown providing an invalid group ID.
            // TRANS: %s is the invalid group ID.
            throw new Exception(sprintf(_("Group ID %s is invalid."),$this->group_id));
        }

        return $group;
    }

    /**
     * Get stream of memberships by member
     *
     * @param integer $memberId profile ID of the member to fetch for
     * @param integer $offset   offset from start of stream to get
     * @param integer $limit    number of memberships to get
     *
     * @return Group_member stream of memberships, use fetch() to iterate
     */

    static function byMember($memberId, $offset=0, $limit=GROUPS_PER_PAGE)
    {
        $membership = new Group_member();

        $membership->profile_id = $memberId;

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

        $membership->limit($offset, $limit);

        $membership->find();

        return $membership;
    }

    function asActivity()
    {
        $member = $this->getMember();

        if (!$member) {
            throw new Exception("No such member: " . $this->profile_id);
        }

        $group  = $this->getGroup();

        if (!$group) {
            throw new Exception("No such group: " . $this->group_id);
        }

        $act = new Activity();

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

        $act->actor     = $member->asActivityObject();
        $act->verb      = ActivityVerb::JOIN;
        $act->objects[] = ActivityObject::fromGroup($group);

        $act->time  = strtotime($this->created);
        // TRANS: Activity title.
        $act->title = _("Join");

        // TRANS: Success message for subscribe to group attempt through OStatus.
        // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
        $act->content = sprintf(_('%1$s has joined group %2$s.'),
                                $member->getBestName(),
                                $group->getBestName());

        $url = common_local_url('AtomPubShowMembership',
                                array('profile' => $member->id,
                                      'group' => $group->id));

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

        return $act;
    }

    /**
     * Send notifications via email etc to group administrators about
     * this exciting new membership!
     */
    public function notify()
    {
        mail_notify_group_join($this->getGroup(), $this->getMember());
    }

    function getURI()
    {
        if (!empty($this->uri)) {
            return $this->uri;
        } else {
            return self::newURI($this->profile_id, $this->group_id, $this->created);
        }
    }

    static function newURI($profile_id, $group_id, $created)
    {
        return TagURI::mint('join:%d:%d:%s',
                            $profile_id,
                            $group_id,
                            common_date_iso8601($created));
    }
}


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

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

    public $__table = 'invitation';                      // table name
    public $code;                            // varchar(32)  primary_key not_null
    public $user_id;                         // int(4)   not_null
    public $address;                         // varchar(191)  multiple_key not_null   not 255 because utf8mb4 takes more space
    public $address_type;                    // varchar(8)  multiple_key not_null
    public $registered_user_id;              // int(4)   not_null
    public $created;                         // datetime()   not_null

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

    function convert($user)
    {
        $orig = clone($this);
        $this->registered_user_id = $user->id;
        return $this->update($orig);
    }

    public static function schemaDef()
    {
        return array(

            'fields' => array(
                'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'random code for an invitation'),
                'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'who sent the invitation'),
                'address' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'invitation sent to'),
                'address_type' => array('type' => 'varchar', 'length' => 8, 'not null' => true, 'description' => 'address type ("email", "xmpp", "sms")'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'registered_user_id' => array('type' => 'int', 'not null' => false, 'description' => 'if the invitation is converted, who the new user is'),
            ),
            'primary key' => array('code'),
            'foreign keys' => array(
                'invitation_user_id_fkey' => array('user', array('user_id' => 'id')),
                'invitation_registered_user_id_fkey' => array('user', array('registered_user_id' => 'id')),
            ),
            'indexes' => array(
                'invitation_address_idx' => array('address', 'address_type'),
                'invitation_user_id_idx' => array('user_id'),
                'invitation_registered_user_id_idx' => array('registered_user_id'),
            ),
        );
    }
}


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

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

    public $__table = 'local_group';                     // table name
    public $group_id;                        // int(4)  primary_key not_null
    public $nickname;                        // varchar(64)  unique_key
    public $created;                         // datetime   not_null default_0000-00-00%2000%3A00%3A00
    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' => 'Record for a user group on the local site, with some additional info not in user_group',
            'fields' => array(
                'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group represented'),
                'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'group represented'),

                '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('group_id'),
            'foreign keys' => array(
                'local_group_group_id_fkey' => array('user_group', array('group_id' => 'id')),
            ),
            'unique keys' => array(
                'local_group_nickname_key' => array('nickname'),
            ),
        );
    }

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

    public function getGroup()
    {
        $group = new User_group();
        $group->id = $this->group_id;
        $group->find(true);
        if (!$group instanceof User_group) {
            common_log(LOG_ERR, 'User_group does not exist for Local_group: '.$this->group_id);
            throw new NoResultException($group);
        }
        return $group;
    }

    function setNickname($nickname)
    {
        $this->decache();
        $qry = 'UPDATE local_group set nickname = "'.$this->escape($nickname).'" where group_id = ' . $this->group_id;

        $result = $this->query($qry);

        if ($result) {
            $this->nickname = $nickname;
            $this->fixupTimestamps();
            $this->encache();
        } else {
            common_log_db_error($local, 'UPDATE', __FILE__);
            // TRANS: Server exception thrown when updating a local group fails.
            throw new ServerException(_('Could not update local group.'));
        }

        return $result;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 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('STATUSNET') && !defined('LACONICA')) { exit(1); }

/**
 * Table Definition for location_namespace
 */

require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

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

    public $__table = 'location_namespace';              // table name
    public $id;                              // int(4)  primary_key not_null
    public $description;                     // varchar(191)
    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(
            'fields' => array(
                'id' => array('type' => 'int', 'not null' => true, 'description' => 'identity for this namespace'),
                'description' => array('type' => 'varchar', 'length' => 191, 'description' => 'description of the namespace'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('id'),
        );
    }
}


code:
<?php
/**
 * Table Definition for login_token
 *
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 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('STATUSNET') && !defined('LACONICA')) { exit(1); }

require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

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

    public $__table = 'login_token';         // table name
    public $user_id;                         // int(4)  primary_key not_null
    public $token;                           // char(32)  not_null
    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(
            'fields' => array(
                'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user owning this token'),
                'token' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'token useable for logging in'),
                '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('user_id'),
            'foreign keys' => array(
                'login_token_user_id_fkey' => array('user', array('user_id' => 'id')),
            ),
        );
    }

    const TIMEOUT = 120; // seconds after which to timeout the token

    function makeNew($user)
    {
        $login_token = Login_token::getKV('user_id', $user->id);

        if (!empty($login_token)) {
            $login_token->delete();
        }

        $login_token = new Login_token();

        $login_token->user_id = $user->id;
        $login_token->token   = common_random_hexstr(16);
        $login_token->created = common_sql_now();

        $result = $login_token->insert();

        if (!$result) {
            common_log_db_error($login_token, 'INSERT', __FILE__);
            // TRANS: Exception thrown when trying creating a login token failed.
            // TRANS: %s is the user nickname for which token creation failed.
            throw new Exception(sprintf(_('Could not create login token for %s'),
                                                 $user->nickname));
        }

        return $login_token;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 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/>.
 */

/**
 * Wrapper for Memcached_DataObject which knows its own schema definition.
 * Builds its own damn settings from a schema definition.
 *
 * @author Brion Vibber <brion@status.net>
 */
abstract class Managed_DataObject extends Memcached_DataObject
{
    /**
     * The One True Thingy that must be defined and declared.
     */
    public static function schemaDef()
    {
        throw new MethodNotImplementedException(__METHOD__);
    }

    /**
     * Get an instance by key
     *
     * @param string $k Key to use to lookup (usually 'id' for this class)
     * @param mixed  $v Value to lookup
     *
     * @return get_called_class() object if found, or null for no hits
     *
     */
    static function getKV($k,$v=NULL)
    {
        return parent::getClassKV(get_called_class(), $k, $v);
    }

    /**
     * Get an instance by compound key
     *
     * This is a utility method to get a single instance with a given set of
     * key-value pairs. Usually used for the primary key for a compound key; thus
     * the name.
     *
     * @param array $kv array of key-value mappings
     *
     * @return get_called_class() object if found, or null for no hits
     *
     */
    static function pkeyGet(array $kv)
    {
        return parent::pkeyGetClass(get_called_class(), $kv);
    }

    /**
     * Get multiple items from the database by key
     *
     * @param string  $keyCol    name of column for key
     * @param array   $keyVals   key values to fetch
     * @param boolean $skipNulls return only non-null results?
     *
     * @return array Array of objects, in order
     */
	static function multiGet($keyCol, array $keyVals, $skipNulls=true)
	{
	    return parent::multiGetClass(get_called_class(), $keyCol, $keyVals, $skipNulls);
	}

    /**
     * Get multiple items from the database by key
     *
     * @param string  $keyCol    name of column for key
     * @param array   $keyVals   key values to fetch
     * @param array   $otherCols Other columns to hold fixed
     *
     * @return array Array mapping $keyVals to objects, or null if not found
     */
	static function pivotGet($keyCol, array $keyVals, array $otherCols=array())
	{
	    return parent::pivotGetClass(get_called_class(), $keyCol, $keyVals, $otherCols);
	}

    /**
     * Get a multi-instance object
     *
     * This is a utility method to get multiple instances with a given set of
     * values for a specific column.
     *
     * @param string $keyCol  key column name
     * @param array  $keyVals array of key values
     *
     * @return get_called_class() object with multiple instances if found,
     *         Exception is thrown when no entries are found.
     *
     */
    static function listFind($keyCol, array $keyVals)
    {
        return parent::listFindClass(get_called_class(), $keyCol, $keyVals);
    }

    /**
     * Get a multi-instance object separated into an array
     *
     * This is a utility method to get multiple instances with a given set of
     * values for a specific key column. Usually used for the primary key when
     * multiple values are desired. Result is an array.
     *
     * @param string $keyCol  key column name
     * @param array  $keyVals array of key values
     *
     * @return array with an get_called_class() object for each $keyVals entry
     *
     */
    static function listGet($keyCol, array $keyVals)
    {
        return parent::listGetClass(get_called_class(), $keyCol, $keyVals);
    }

    /**
     * get/set an associative array of table columns
     *
     * @access public
     * @return array (associative)
     */
    public function table()
    {
        $table = static::schemaDef();
        return array_map(array($this, 'columnBitmap'), $table['fields']);
    }

    /**
     * get/set an  array of table primary keys
     *
     * Key info is pulled from the table definition array.
     *
     * @access private
     * @return array
     */
    function keys()
    {
        return array_keys($this->keyTypes());
    }

    /**
     * Get a sequence key
     *
     * Returns the first serial column defined in the table, if any.
     *
     * @access private
     * @return array (column,use_native,sequence_name)
     */

    function sequenceKey()
    {
        $table = static::schemaDef();
        foreach ($table['fields'] as $name => $column) {
            if ($column['type'] == 'serial') {
                // We have a serial/autoincrement column.
                // Declare it to be a native sequence!
                return array($name, true, false);
            }
        }

        // No sequence key on this table.
        return array(false, false, false);
    }

    /**
     * Return key definitions for DB_DataObject and Memcache_DataObject.
     *
     * DB_DataObject needs to know about keys that the table has; this function
     * defines them.
     *
     * @return array key definitions
     */

    function keyTypes()
    {
        $table = static::schemaDef();
        $keys = array();

        if (!empty($table['unique keys'])) {
            foreach ($table['unique keys'] as $idx => $fields) {
                foreach ($fields as $name) {
                    $keys[$name] = 'U';
                }
            }
        }

        if (!empty($table['primary key'])) {
            foreach ($table['primary key'] as $name) {
                $keys[$name] = 'K';
            }
        }
        return $keys;
    }

    /**
     * Build the appropriate DB_DataObject bitfield map for this field.
     *
     * @param array $column
     * @return int
     */
    function columnBitmap($column)
    {
        $type = $column['type'];

        // For quoting style...
        $intTypes = array('int',
                          'integer',
                          'float',
                          'serial',
                          'numeric');
        if (in_array($type, $intTypes)) {
            $style = DB_DATAOBJECT_INT;
        } else {
            $style = DB_DATAOBJECT_STR;
        }

        // Data type formatting style...
        $formatStyles = array('blob' => DB_DATAOBJECT_BLOB,
                              'text' => DB_DATAOBJECT_TXT,
                              'date' => DB_DATAOBJECT_DATE,
                              'time' => DB_DATAOBJECT_TIME,
                              'datetime' => DB_DATAOBJECT_DATE | DB_DATAOBJECT_TIME,
                              'timestamp' => DB_DATAOBJECT_MYSQLTIMESTAMP);

        if (isset($formatStyles[$type])) {
            $style |= $formatStyles[$type];
        }

        // Nullable?
        if (!empty($column['not null'])) {
            $style |= DB_DATAOBJECT_NOTNULL;
        }

        return $style;
    }

    function links()
    {
        $links = array();

        $table = static::schemaDef();

        foreach ($table['foreign keys'] as $keyname => $keydef) {
            if (count($keydef) == 2 && is_string($keydef[0]) && is_array($keydef[1]) && count($keydef[1]) == 1) {
                if (isset($keydef[1][0])) {
                    $links[$keydef[1][0]] = $keydef[0].':'.$keydef[1][1];
                }
            }
        }
        return $links;
    }

    /**
     * Return a list of all primary/unique keys / vals that will be used for
     * caching. This will understand compound unique keys, which
     * Memcached_DataObject doesn't have enough info to handle properly.
     *
     * @return array of strings
     */
    function _allCacheKeys()
    {
        $table = static::schemaDef();
        $ckeys = array();

        if (!empty($table['unique keys'])) {
            $keyNames = $table['unique keys'];
            foreach ($keyNames as $idx => $fields) {
                $val = array();
                foreach ($fields as $name) {
                    $val[$name] = self::valueString($this->$name);
                }
                $ckeys[] = self::multicacheKey($this->tableName(), $val);
            }
        }

        if (!empty($table['primary key'])) {
            $fields = $table['primary key'];
            $val = array();
            foreach ($fields as $name) {
                $val[$name] = self::valueString($this->$name);
            }
            $ckeys[] = self::multicacheKey($this->tableName(), $val);
        }
        return $ckeys;
    }

    public function escapedTableName()
    {
        return common_database_tablename($this->tableName());
    }

    /**
     * Returns an ID, checked that it is set and reasonably valid
     *
     * If this dataobject uses a special id field (not 'id'), just
     * implement your ID getting method in the child class.
     *
     * @return int ID of dataobject
     * @throws Exception (when ID is not available or not set yet)
     */
    public function getID()
    {
        // FIXME: Make these exceptions more specific (their own classes)
        if (!isset($this->id)) {
            throw new Exception('No ID set.');
        } elseif (empty($this->id)) {
            throw new Exception('Empty ID for object! (not inserted yet?).');
        }

        // FIXME: How about forcing to return an int? Or will that overflow eventually?
        return $this->id;
    }

    // 'update' won't write key columns, so we have to do it ourselves.
    // This also automatically calls "update" _before_ it sets the keys.
    // FIXME: This only works with single-column primary keys so far! Beware!
    /**
     * @param DB_DataObject &$orig  Must be "instanceof" $this
     * @param string         $pid   Primary ID column (no escaping is done on column name!)
     */
    public function updateWithKeys(&$orig, $pid='id')
    {
        if (!$orig instanceof $this) {
            throw new ServerException('Tried updating a DataObject with a different class than itself.');
        }

        // do it in a transaction
        $this->query('BEGIN');

        $parts = array();
        foreach ($this->keys() as $k) {
            if (strcmp($this->$k, $orig->$k) != 0) {
                $parts[] = $k . ' = ' . $this->_quote($this->$k);
            }
        }
        if (count($parts) == 0) {
            // No changes to keys, it's safe to run ->update(...)
            if ($this->update($orig) === false) {
                common_log_db_error($this, 'UPDATE', __FILE__);
                // rollback as something bad occurred
                $this->query('ROLLBACK');
                throw new ServerException("Could not UPDATE non-keys for {$this->__table}");
            }
            $orig->decache();
            $this->encache();

            // commit our db transaction since we won't reach the COMMIT below
            $this->query('COMMIT');
            // @FIXME return true only if something changed (otherwise 0)
            return true;
        }

        $qry = sprintf('UPDATE %1$s SET %2$s WHERE %3$s = %4$s',
                            common_database_tablename($this->tableName()),
                            implode(', ', $parts),
                            $pid,
                            $this->_quote($this->$pid));

        $result = $this->query($qry);
        if ($result === false) {
            common_log_db_error($this, 'UPDATE', __FILE__);
            // rollback as something bad occurred
            $this->query('ROLLBACK');
            throw new ServerException("Could not UPDATE key fields for {$this->__table}");
        }

        // Update non-keys too, if the previous endeavour worked.
        // The ->update call uses "$this" values for keys, that's why we can't do this until
        // the keys are updated (because they might differ from $orig and update the wrong entries).
        if ($this->update($orig) === false) {
            common_log_db_error($this, 'UPDATE', __FILE__);
            // rollback as something bad occurred
            $this->query('ROLLBACK');
            throw new ServerException("Could not UPDATE non-keys for {$this->__table}");
        }
        $orig->decache();
        $this->encache();

        // commit our db transaction
        $this->query('COMMIT');
        // @FIXME return true only if something changed (otherwise 0)
        return $result;
    }

    static public function beforeSchemaUpdate()
    {
        // NOOP
    }
}