diff options
author | Zach Copley <zach@status.net> | 2010-01-13 19:17:49 +0000 |
---|---|---|
committer | Zach Copley <zach@status.net> | 2010-01-13 19:17:49 +0000 |
commit | c3188fd1fece2be7f7c4211d28f4a3d3a59c8fa1 (patch) | |
tree | aa8018e132936b00fc63224e75ca134d68999b4e /classes | |
parent | 43170b3d18153b3dfd8675bd77ae1133eed8148a (diff) | |
parent | 0e1f2d4b47e5e340679c4245b62e1d64c6b9c9b9 (diff) |
Merge branch '0.9.x' of git@gitorious.org:statusnet/mainline into 0.9.x
Diffstat (limited to 'classes')
-rw-r--r-- | classes/Group_member.php | 37 | ||||
-rw-r--r-- | classes/Inbox.php | 151 | ||||
-rw-r--r-- | classes/Memcached_DataObject.php | 29 | ||||
-rw-r--r-- | classes/Notice.php | 59 | ||||
-rw-r--r-- | classes/Notice_inbox.php | 132 | ||||
-rw-r--r-- | classes/Queue_item.php | 9 | ||||
-rw-r--r-- | classes/Status_network.php | 23 | ||||
-rw-r--r-- | classes/User.php | 133 | ||||
-rw-r--r-- | classes/statusnet.ini | 7 |
9 files changed, 311 insertions, 269 deletions
diff --git a/classes/Group_member.php b/classes/Group_member.php index 069b2c7a1..7b1760f76 100644 --- a/classes/Group_member.php +++ b/classes/Group_member.php @@ -25,4 +25,41 @@ class Group_member extends Memcached_DataObject { return Memcached_DataObject::pkeyGet('Group_member', $kv); } + + 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(); + + $result = $member->insert(); + + if (!$result) { + common_log_db_error($member, 'INSERT', __FILE__); + throw new Exception(_("Group join failed.")); + } + + return true; + } + + static function leave($group_id, $profile_id) + { + $member = Group_member::pkeyGet(array('group_id' => $group_id, + 'profile_id' => $profile_id)); + + if (empty($member)) { + throw new Exception(_("Not part of group.")); + } + + $result = $member->delete(); + + if (!$result) { + common_log_db_error($member, 'INSERT', __FILE__); + throw new Exception(_("Group leave failed.")); + } + + return true; + } } diff --git a/classes/Inbox.php b/classes/Inbox.php new file mode 100644 index 000000000..e14d4f4e7 --- /dev/null +++ b/classes/Inbox.php @@ -0,0 +1,151 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Data class for user location preferences + * + * PHP version 5 + * + * LICENCE: 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/>. + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +class Inbox extends Memcached_DataObject +{ + const BOXCAR = 128; + + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'inbox'; // table name + public $user_id; // int(4) primary_key not_null + public $notice_ids; // blob + + /* Static get */ + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Inbox',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Create a new inbox from existing Notice_inbox stuff + */ + + static function initialize($user_id) + { + $ids = array(); + + $ni = new Notice_inbox(); + + $ni->user_id = $user_id; + $ni->selectAdd(); + $ni->selectAdd('notice_id'); + $ni->orderBy('notice_id DESC'); + $ni->limit(0, 1024); + + if ($ni->find()) { + while($ni->fetch()) { + $ids[] = $ni->notice_id; + } + } + + $ni->free(); + unset($ni); + + $inbox = new Inbox(); + + $inbox->user_id = $user_id; + $inbox->notice_ids = call_user_func_array('pack', array_merge(array('N*'), $ids)); + + $result = $inbox->insert(); + + if (!$result) { + common_log_db_error($inbox, 'INSERT', __FILE__); + return null; + } + + return $inbox; + } + + static function insertNotice($user_id, $notice_id) + { + $inbox = Inbox::staticGet('user_id', $user_id); + + if (empty($inbox)) { + $inbox = Inbox::initialize($user_id); + } + + if (empty($inbox)) { + return false; + } + + $result = $inbox->query(sprintf('UPDATE inbox '. + 'set notice_ids = concat(cast(0x%08x as binary(4)), '. + 'substr(notice_ids, 1, 4092)) '. + 'WHERE user_id = %d', + $notice_id, $user_id)); + + if ($result) { + $c = self::memcache(); + + if (!empty($c)) { + $c->delete(self::cacheKey('inbox', 'user_id', $user_id)); + } + } + + return $result; + } + + static function bulkInsert($notice_id, $user_ids) + { + foreach ($user_ids as $user_id) + { + Inbox::insertNotice($user_id, $notice_id); + } + } + + function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false) + { + $inbox = Inbox::staticGet('user_id', $user_id); + + if (empty($inbox)) { + $inbox = Inbox::initialize($user_id); + if (empty($inbox)) { + return array(); + } + } + + $ids = unpack('N*', $inbox->notice_ids); + + // XXX: handle since_id + // XXX: handle max_id + + $ids = array_slice($ids, $offset, $limit); + + return $ids; + } +} diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index b68a4af8e..4ecab9db6 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -98,14 +98,16 @@ class Memcached_DataObject extends DB_DataObject } else { $i = DB_DataObject::factory($cls); if (empty($i)) { - return false; + $i = false; + return $i; } $result = $i->get($k, $v); if ($result) { $i->encache(); return $i; } else { - return false; + $i = false; + return $i; } } } @@ -329,6 +331,29 @@ class Memcached_DataObject extends DB_DataObject $exists = false; } + // @fixme horrible evil hack! + // + // In multisite configuration we don't want to keep around a separate + // connection for every database; we could end up with thousands of + // connections open per thread. In an ideal world we might keep + // a connection per server and select different databases, but that'd + // be reliant on having the same db username/pass as well. + // + // MySQL connections are cheap enough we're going to try just + // closing out the old connection and reopening when we encounter + // a new DSN. + // + // WARNING WARNING if we end up actually using multiple DBs at a time + // we'll need some fancier logic here. + if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS'])) { + foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) { + if (!empty($conn)) { + $conn->disconnect(); + } + unset($_DB_DATAOBJECT['CONNECTIONS'][$index]); + } + } + $result = parent::_connect(); if ($result && !$exists) { diff --git a/classes/Notice.php b/classes/Notice.php index 9bda47827..02cd20391 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -125,8 +125,7 @@ class Notice extends Memcached_DataObject 'Fave', 'Notice_tag', 'Group_inbox', - 'Queue_item', - 'Notice_inbox'); + 'Queue_item'); foreach ($related as $cls) { $inst = new $cls(); @@ -276,7 +275,6 @@ class Notice extends Memcached_DataObject if (isset($repeat_of)) { $notice->repeat_of = $repeat_of; - $notice->reply_to = $repeat_of; } else { $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); } @@ -300,8 +298,6 @@ class Notice extends Memcached_DataObject // XXX: some of these functions write to the DB - $notice->query('BEGIN'); - $id = $notice->insert(); if (!$id) { @@ -343,8 +339,6 @@ class Notice extends Memcached_DataObject $notice->saveUrls(); - $notice->query('COMMIT'); - Event::handle('EndNoticeSave', array($notice)); } @@ -503,20 +497,6 @@ class Notice extends Memcached_DataObject $original->free(); unset($original); } - - $ni = new Notice_inbox(); - - $ni->notice_id = $this->id; - - if ($ni->find()) { - while ($ni->fetch()) { - $tmk = common_cache_key('user:repeated_to_me:'.$ni->user_id); - $cache->delete($tmk); - } - } - - $ni->free(); - unset($ni); } } } @@ -842,11 +822,16 @@ class Notice extends Memcached_DataObject return $ids; } - function addToInboxes() + function whoGets() { - // XXX: loads constants + $c = self::memcache(); - $inbox = new Notice_inbox(); + if (!empty($c)) { + $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id)); + if ($ni !== false) { + return $ni; + } + } $users = $this->getSubscribedUsers(); @@ -887,7 +872,19 @@ class Notice extends Memcached_DataObject } } - Notice_inbox::bulkInsert($this->id, $this->created, $ni); + if (!empty($c)) { + // XXX: pack this data better + $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni); + } + + return $ni; + } + + function addToInboxes() + { + $ni = $this->whoGets(); + + Inbox::bulkInsert($this->id, array_keys($ni)); return; } @@ -921,6 +918,12 @@ class Notice extends Memcached_DataObject function saveGroups() { + // Don't save groups for repeats + + if (!empty($this->repeat_of)) { + return array(); + } + $groups = array(); /* extract all !group */ @@ -991,6 +994,12 @@ class Notice extends Memcached_DataObject */ function saveReplies() { + // Don't save reply data for repeats + + if (!empty($this->repeat_of)) { + return array(); + } + // Alternative reply format $tname = false; if (preg_match('/^T ([A-Z0-9]{1,64}) /', $this->content, $match)) { diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php index e350e6e2f..c27dcdfd6 100644 --- a/classes/Notice_inbox.php +++ b/classes/Notice_inbox.php @@ -1,7 +1,7 @@ <?php /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 2009, StatusNet, Inc. + * Copyright (C) 2008-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 @@ -17,7 +17,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!defined('STATUSNET')) { + exit(1); +} require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; @@ -29,12 +31,6 @@ define('NOTICE_INBOX_GC_MAX', 12800); define('NOTICE_INBOX_LIMIT', 1000); define('NOTICE_INBOX_SOFT_LIMIT', 1000); -define('NOTICE_INBOX_SOURCE_SUB', 1); -define('NOTICE_INBOX_SOURCE_GROUP', 2); -define('NOTICE_INBOX_SOURCE_REPLY', 3); -define('NOTICE_INBOX_SOURCE_FORWARD', 4); -define('NOTICE_INBOX_SOURCE_GATEWAY', -1); - class Notice_inbox extends Memcached_DataObject { ###START_AUTOCODE @@ -55,139 +51,31 @@ class Notice_inbox extends Memcached_DataObject function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false) { - return Notice::stream(array('Notice_inbox', '_streamDirect'), - array($user_id, $own), - ($own) ? 'notice_inbox:by_user:'.$user_id : - 'notice_inbox:by_user_own:'.$user_id, - $offset, $limit, $since_id, $max_id, $since); + throw new Exception('Notice_inbox no longer used; use Inbox'); } function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since) { - $inbox = new Notice_inbox(); - - $inbox->user_id = $user_id; - - if (!$own) { - $inbox->whereAdd('source != ' . NOTICE_INBOX_SOURCE_GATEWAY); - } - - if ($since_id != 0) { - $inbox->whereAdd('notice_id > ' . $since_id); - } - - if ($max_id != 0) { - $inbox->whereAdd('notice_id <= ' . $max_id); - } - - if (!is_null($since)) { - $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - - $inbox->orderBy('created DESC'); - - if (!is_null($offset)) { - $inbox->limit($offset, $limit); - } - - $ids = array(); - - if ($inbox->find()) { - while ($inbox->fetch()) { - $ids[] = $inbox->notice_id; - } - } - - return $ids; + throw new Exception('Notice_inbox no longer used; use Inbox'); } - function pkeyGet($kv) + function &pkeyGet($kv) { return Memcached_DataObject::pkeyGet('Notice_inbox', $kv); } - /** - * Trim inbox for a given user to latest NOTICE_INBOX_LIMIT items - * (up to NOTICE_INBOX_GC_MAX will be deleted). - * - * @param int $user_id - * @return int count of notices dropped from the inbox, if any - */ static function gc($user_id) { - $entry = new Notice_inbox(); - $entry->user_id = $user_id; - $entry->orderBy('created DESC'); - $entry->limit(NOTICE_INBOX_LIMIT - 1, NOTICE_INBOX_GC_MAX); - - $total = $entry->find(); - - if ($total > 0) { - $notices = array(); - $cnt = 0; - while ($entry->fetch()) { - $notices[] = $entry->notice_id; - $cnt++; - if ($cnt >= NOTICE_INBOX_GC_BOXCAR) { - self::deleteMatching($user_id, $notices); - $notices = array(); - $cnt = 0; - } - } - - if ($cnt > 0) { - self::deleteMatching($user_id, $notices); - $notices = array(); - } - } - - return $total; + throw new Exception('Notice_inbox no longer used; use Inbox'); } static function deleteMatching($user_id, $notices) { - $entry = new Notice_inbox(); - return $entry->query('DELETE FROM notice_inbox '. - 'WHERE user_id = ' . $user_id . ' ' . - 'AND notice_id in ('.implode(',', $notices).')'); + throw new Exception('Notice_inbox no longer used; use Inbox'); } static function bulkInsert($notice_id, $created, $ni) { - $cnt = 0; - - $qryhdr = 'INSERT INTO notice_inbox (user_id, notice_id, source, created) VALUES '; - $qry = $qryhdr; - - foreach ($ni as $id => $source) { - if ($cnt > 0) { - $qry .= ', '; - } - $qry .= '('.$id.', '.$notice_id.', '.$source.", '".$created. "') "; - $cnt++; - if (rand() % NOTICE_INBOX_SOFT_LIMIT == 0) { - // FIXME: Causes lag in replicated servers - // Notice_inbox::gc($id); - } - if ($cnt >= MAX_BOXCARS) { - $inbox = new Notice_inbox(); - $result = $inbox->query($qry); - if (PEAR::isError($result)) { - common_log_db_error($inbox, $qry); - } - $qry = $qryhdr; - $cnt = 0; - } - } - - if ($cnt > 0) { - $inbox = new Notice_inbox(); - $result = $inbox->query($qry); - if (PEAR::isError($result)) { - common_log_db_error($inbox, $qry); - } - } - - return; + throw new Exception('Notice_inbox no longer used; use Inbox'); } } diff --git a/classes/Queue_item.php b/classes/Queue_item.php index 9c673540d..cf805a606 100644 --- a/classes/Queue_item.php +++ b/classes/Queue_item.php @@ -25,10 +25,12 @@ class Queue_item extends Memcached_DataObject function sequenceKey() { return array(false, false); } - static function top($transport) { + static function top($transport=null) { $qi = new Queue_item(); - $qi->transport = $transport; + if ($transport) { + $qi->transport = $transport; + } $qi->orderBy('created'); $qi->whereAdd('claimed is null'); @@ -40,7 +42,8 @@ class Queue_item extends Memcached_DataObject # XXX: potential race condition # can we force it to only update if claimed is still null # (or old)? - common_log(LOG_INFO, 'claiming queue item = ' . $qi->notice_id . ' for transport ' . $transport); + common_log(LOG_INFO, 'claiming queue item = ' . $qi->notice_id . + ' for transport ' . $qi->transport); $orig = clone($qi); $qi->claimed = common_sql_now(); $result = $qi->update($orig); diff --git a/classes/Status_network.php b/classes/Status_network.php index 776f6abb0..ef8e1ed43 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -49,6 +49,13 @@ class Status_network extends DB_DataObject static $cache = null; static $base = null; + /** + * @param string $dbhost + * @param string $dbuser + * @param string $dbpass + * @param string $dbname + * @param array $servers memcached servers to use for caching config info + */ static function setupDB($dbhost, $dbuser, $dbpass, $dbname, $servers) { global $config; @@ -60,12 +67,17 @@ class Status_network extends DB_DataObject if (class_exists('Memcache')) { self::$cache = new Memcache(); + // Can't close persistent connections, making forking painful. + // + // @fixme only do this in *parent* CLI processes. + // single-process and child-processes *should* use persistent. + $persist = php_sapi_name() != 'cli'; if (is_array($servers)) { foreach($servers as $server) { - self::$cache->addServer($server); + self::$cache->addServer($server, 11211, $persist); } } else { - self::$cache->addServer($servers); + self::$cache->addServer($servers, 11211, $persist); } } @@ -89,7 +101,7 @@ class Status_network extends DB_DataObject if (empty($sn)) { $sn = self::staticGet($k, $v); if (!empty($sn)) { - self::$cache->set($ck, $sn); + self::$cache->set($ck, clone($sn)); } } @@ -121,6 +133,11 @@ class Status_network extends DB_DataObject return parent::delete(); } + /** + * @param string $servername hostname + * @param string $pathname URL base path + * @param string $wildcard hostname suffix to match wildcard config + */ static function setupSite($servername, $pathname, $wildcard) { global $config; diff --git a/classes/User.php b/classes/User.php index 34151778c..d6b52be01 100644 --- a/classes/User.php +++ b/classes/User.php @@ -291,6 +291,20 @@ class User extends Memcached_DataObject return false; } + // Everyone gets an inbox + + $inbox = new Inbox(); + + $inbox->user_id = $user->id; + $inbox->notice_ids = ''; + + $result = $inbox->insert(); + + if (!$result) { + common_log_db_error($inbox, 'INSERT', __FILE__); + return false; + } + // Everyone is subscribed to themself $subscription = new Subscription(); @@ -482,89 +496,30 @@ class User extends Memcached_DataObject function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { - $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, false); - + $ids = Inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, false); return Notice::getStreamByIds($ids); } function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { - $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, true); - + $ids = Inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, true); return Notice::getStreamByIds($ids); } function friendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { - $ids = Notice::stream(array($this, '_friendsTimelineDirect'), - array(false), - 'user:friends_timeline:'.$this->id, - $offset, $limit, $since_id, $before_id, $since); + $ids = Inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, false); return Notice::getStreamByIds($ids); } function ownFriendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { - $ids = Notice::stream(array($this, '_friendsTimelineDirect'), - array(true), - 'user:friends_timeline_own:'.$this->id, - $offset, $limit, $since_id, $before_id, $since); + $ids = Inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, true); return Notice::getStreamByIds($ids); } - function _friendsTimelineDirect($own, $offset, $limit, $since_id, $max_id, $since) - { - $qry = - 'SELECT notice.id AS id ' . - 'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' . - 'WHERE notice_inbox.user_id = ' . $this->id . ' ' . - 'AND notice.repeat_of IS NULL '; - - if (!$own) { - // XXX: autoload notice inbox for constant - $inbox = new Notice_inbox(); - - $qry .= 'AND notice_inbox.source != ' . NOTICE_INBOX_SOURCE_GATEWAY . ' '; - } - - if ($since_id != 0) { - $qry .= 'AND notice.id > ' . $since_id . ' '; - } - - if ($max_id != 0) { - $qry .= 'AND notice.id <= ' . $max_id . ' '; - } - - if (!is_null($since)) { - $qry .= 'AND notice.modified > \'' . date('Y-m-d H:i:s', $since) . '\' '; - } - - // NOTE: we sort by fave time, not by notice time! - - $qry .= 'ORDER BY notice_id DESC '; - - if (!is_null($offset)) { - $qry .= "LIMIT $limit OFFSET $offset"; - } - - $ids = array(); - - $notice = new Notice(); - - $notice->query($qry); - - while ($notice->fetch()) { - $ids[] = $notice->id; - } - - $notice->free(); - $notice = NULL; - - return $ids; - } - function blowFavesCache() { $cache = common_memcache(); @@ -777,7 +732,6 @@ class User extends Memcached_DataObject 'Remember_me', 'Foreign_link', 'Invitation', - 'Notice_inbox', ); Event::handle('UserDeleteRelated', array($this, &$related)); @@ -945,56 +899,7 @@ class User extends Memcached_DataObject function repeatedToMe($offset=0, $limit=20, $since_id=null, $max_id=null) { - $ids = Notice::stream(array($this, '_repeatedToMeDirect'), - array(), - 'user:repeated_to_me:'.$this->id, - $offset, $limit, $since_id, $max_id, null); - - return Notice::getStreamByIds($ids); - } - - function _repeatedToMeDirect($offset, $limit, $since_id, $max_id, $since) - { - $qry = - 'SELECT notice.id AS id ' . - 'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' . - 'WHERE notice_inbox.user_id = ' . $this->id . ' ' . - 'AND notice.repeat_of IS NOT NULL '; - - if ($since_id != 0) { - $qry .= 'AND notice.id > ' . $since_id . ' '; - } - - if ($max_id != 0) { - $qry .= 'AND notice.id <= ' . $max_id . ' '; - } - - if (!is_null($since)) { - $qry .= 'AND notice.modified > \'' . date('Y-m-d H:i:s', $since) . '\' '; - } - - // NOTE: we sort by fave time, not by notice time! - - $qry .= 'ORDER BY notice.id DESC '; - - if (!is_null($offset)) { - $qry .= "LIMIT $limit OFFSET $offset"; - } - - $ids = array(); - - $notice = new Notice(); - - $notice->query($qry); - - while ($notice->fetch()) { - $ids[] = $notice->id; - } - - $notice->free(); - $notice = NULL; - - return $ids; + throw new Exception("Not implemented since inbox change."); } function shareLocation() diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 0db2c5d6e..73727a6d6 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -241,6 +241,13 @@ address = 130 address_type = 130 created = 142 +[inbox] +user_id = 129 +notice_ids = 66 + +[inbox__keys] +user_id = K + [invitation__keys] code = K |