X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=d18ac2e536eb860a4c1f765ca63b5d32c67b3ed8;hb=f0d762f1968fa0ac33b39d73cc9c0225c9e8a989;hp=157fdf2dc466c698233118994b75a67718e34f22;hpb=e109b2592f86dec977922bc4474dde36aed109cb;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 157fdf2dc4..d18ac2e536 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -45,7 +45,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; /* We keep 200 notices, the max number of notices available per API request, * in the memcached cache. */ -define('NOTICE_CACHE_WINDOW', 200); +define('NOTICE_CACHE_WINDOW', CachingNoticeStream::CACHE_WINDOW); define('MAX_BOXCARS', 128); @@ -72,6 +72,8 @@ class Notice extends Memcached_DataObject public $location_id; // int(4) public $location_ns; // int(4) public $repeat_of; // int(4) + public $object_type; // varchar(255) + public $scope; // int(4) /* Static get */ function staticGet($k,$v=NULL) @@ -88,17 +90,27 @@ class Notice extends Memcached_DataObject const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; + const PUBLIC_SCOPE = 0; // Useful fake constant + const SITE_SCOPE = 1; + const ADDRESSEE_SCOPE = 2; + const GROUP_SCOPE = 4; + const FOLLOWER_SCOPE = 8; + + protected $_profile = -1; + function getProfile() { - $profile = Profile::staticGet('id', $this->profile_id); + if (is_int($this->_profile) && $this->_profile == -1) { + $this->_profile = Profile::staticGet('id', $this->profile_id); - if (empty($profile)) { - // TRANS: Server exception thrown when a user profile for a notice cannot be found. - // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number). - throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id)); + if (empty($this->_profile)) { + // TRANS: Server exception thrown when a user profile for a notice cannot be found. + // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number). + throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id)); + } } - return $profile; + return $this->_profile; } function delete() @@ -196,7 +208,7 @@ class Notice extends Memcached_DataObject if (!$id) { // TRANS: Server exception. %s are the error details. - throw new ServerException(sprintf(_('Database error inserting hashtag: %s'), + throw new ServerException(sprintf(_('Database error inserting hashtag: %s.'), $last_error->message)); return; } @@ -241,6 +253,8 @@ class Notice extends Memcached_DataObject * array 'urls' list of attached/referred URLs to save with the * notice in place of extracting links from content * boolean 'distribute' whether to distribute the notice, default true + * string 'object_type' URL of the associated object type (default ActivityObject::NOTE) + * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise * * @fixme tag override * @@ -252,6 +266,7 @@ class Notice extends Memcached_DataObject 'url' => null, 'reply_to' => null, 'repeat_of' => null, + 'scope' => null, 'distribute' => true); if (!empty($options)) { @@ -310,7 +325,7 @@ class Notice extends Memcached_DataObject $autosource = common_config('public', 'autosource'); - # Sandboxed are non-false, but not 1, either + // Sandboxed are non-false, but not 1, either if (!$profile->hasRight(Right::PUBLICNOTICE) || ($source && $autosource && in_array($source, $autosource))) { @@ -331,17 +346,80 @@ class Notice extends Memcached_DataObject $notice->uri = $uri; $notice->url = $url; + // Get the groups here so we can figure out replies and such + + if (!isset($groups)) { + $groups = self::groupsFromText($notice->content, $profile); + } + + $reply = null; + // Handle repeat case if (isset($repeat_of)) { + + // Check for a private one + + $repeat = Notice::staticGet('id', $repeat_of); + + if (empty($repeat)) { + // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice. + throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); + } + + if ($profile->id == $repeat->profile_id) { + // TRANS: Client error displayed when trying to repeat an own notice. + throw new ClientException(_('You cannot repeat your own notice.')); + } + + if ($repeat->scope != Notice::SITE_SCOPE && + $repeat->scope != Notice::PUBLIC_SCOPE) { + // TRANS: Client error displayed when trying to repeat a non-public notice. + throw new ClientException(_('Cannot repeat a private notice.'), 403); + } + + if (!$repeat->inScope($profile)) { + // The generic checks above should cover this, but let's be sure! + // TRANS: Client error displayed when trying to repeat a notice you cannot access. + throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403); + } + + if ($profile->hasRepeated($repeat->id)) { + // TRANS: Client error displayed when trying to repeat an already repeated notice. + throw new ClientException(_('You already repeated that notice.')); + } + $notice->repeat_of = $repeat_of; } else { - $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); - } + $reply = self::getReplyTo($reply_to, $profile_id, $source, $final); + + if (!empty($reply)) { + + if (!$reply->inScope($profile)) { + // TRANS: Client error displayed when trying to reply to a notice a the target has no access to. + // TRANS: %1$s is a user nickname, %2$d is a notice ID (number). + throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'), + $profile->nickname, $reply->id), 403); + } + + $notice->reply_to = $reply->id; + $notice->conversation = $reply->conversation; - if (!empty($notice->reply_to)) { - $reply = Notice::staticGet('id', $notice->reply_to); - $notice->conversation = $reply->conversation; + // If the original is private to a group, and notice has no group specified, + // make it to the same group(s) + + if (empty($groups) && ($reply->scope | Notice::GROUP_SCOPE)) { + $groups = array(); + $replyGroups = $reply->getGroups(); + foreach ($replyGroups as $group) { + if ($profile->isMember($group)) { + $groups[] = $group->id; + } + } + } + + // Scope set below + } } if (!empty($lat) && !empty($lon)) { @@ -360,6 +438,46 @@ class Notice extends Memcached_DataObject $notice->rendered = common_render_content($final, $notice); } + if (empty($object_type)) { + $notice->object_type = (empty($notice->reply_to)) ? ActivityObject::NOTE : ActivityObject::COMMENT; + } else { + $notice->object_type = $object_type; + } + + if (is_null($scope)) { // 0 is a valid value + if (!empty($reply)) { + $notice->scope = $reply->scope; + } else { + $notice->scope = common_config('notice', 'defaultscope'); + } + } else { + $notice->scope = $scope; + } + + // For private streams + + $user = $profile->getUser(); + + if (!empty($user)) { + if ($user->private_stream && + ($notice->scope == Notice::PUBLIC_SCOPE || + $notice->scope == Notice::SITE_SCOPE)) { + $notice->scope |= Notice::FOLLOWER_SCOPE; + } + } + + // Force the scope for private groups + + foreach ($groups as $groupId) { + $group = User_group::staticGet('id', $groupId); + if (!empty($group)) { + if ($group->force_scope) { + $notice->scope |= Notice::GROUP_SCOPE; + break; + } + } + } + if (Event::handle('StartNoticeSave', array(&$notice))) { // XXX: some of these functions write to the DB @@ -402,8 +520,8 @@ class Notice extends Memcached_DataObject } - # Clear the cache for subscribed users, so they'll update at next request - # XXX: someone clever could prepend instead of clearing the cache + // Clear the cache for subscribed users, so they'll update at next request + // XXX: someone clever could prepend instead of clearing the cache $notice->blowOnInsert(); @@ -423,10 +541,14 @@ class Notice extends Memcached_DataObject // Note: groups may save tags, so must be run after tags are saved // to avoid errors on duplicates. - if (isset($groups)) { - $notice->saveKnownGroups($groups); + // Note: groups should always be set. + + $notice->saveKnownGroups($groups); + + if (isset($peopletags)) { + $notice->saveProfileTags($peopletags); } else { - $notice->saveGroups(); + $notice->saveProfileTags(); } if (isset($urls)) { @@ -455,6 +577,7 @@ class Notice extends Memcached_DataObject // was not the root of the conversation. What to do now? self::blow('notice:conversation_ids:%d', $this->conversation); + self::blow('conversation::notice_count:%d', $this->conversation); if (!empty($this->repeat_of)) { self::blow('notice:repeats:%d', $this->repeat_of); @@ -488,6 +611,13 @@ class Notice extends Memcached_DataObject if ($this->isPublic()) { self::blow('public;last'); } + + self::blow('fave:by_notice', $this->id); + + if ($this->conversation) { + // In case we're the first, will need to calc a new root. + self::blow('notice:conversation_root:%d', $this->conversation); + } } /** save all urls in the notice to the db @@ -533,7 +663,7 @@ class Notice extends Memcached_DataObject if (empty($profile)) { return false; } - $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW); + $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW); if (!empty($notice)) { $last = 0; while ($notice->fetch()) { @@ -544,8 +674,8 @@ class Notice extends Memcached_DataObject } } } - # If we get here, oldest item in cache window is not - # old enough for dupe limit; do direct check against DB + // If we get here, oldest item in cache window is not + // old enough for dupe limit; do direct check against DB $notice = new Notice(); $notice->profile_id = $profile_id; $notice->content = $content; @@ -561,16 +691,16 @@ class Notice extends Memcached_DataObject if (empty($profile)) { return false; } - # Get the Nth notice + // Get the Nth notice $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1); if ($notice && $notice->fetch()) { - # If the Nth notice was posted less than timespan seconds ago + // If the Nth notice was posted less than timespan seconds ago if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) { - # Then we throttle + // Then we throttle return false; } } - # Either not N notices in the stream, OR the Nth was not posted within timespan seconds + // Either not N notices in the stream, OR the Nth was not posted within timespan seconds return true; } @@ -599,149 +729,50 @@ class Notice extends Memcached_DataObject } function attachments() { - // XXX: cache this - $att = array(); - $f2p = new File_to_post; - $f2p->post_id = $this->id; - if ($f2p->find()) { - while ($f2p->fetch()) { - $f = File::staticGet($f2p->file_id); - if ($f) { - $att[] = clone($f); - } - } - } - return $att; - } - - function getStreamByIds($ids) - { - $cache = Cache::instance(); - - if (!empty($cache)) { - $notices = array(); - foreach ($ids as $id) { - $n = Notice::staticGet('id', $id); - if (!empty($n)) { - $notices[] = $n; - } - } - return new ArrayWrapper($notices); - } else { - $notice = new Notice(); - if (empty($ids)) { - //if no IDs requested, just return the notice object - return $notice; - } - $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); - - $notice->find(); - - $temp = array(); - while ($notice->fetch()) { - $temp[$notice->id] = clone($notice); - } + $keypart = sprintf('notice:file_ids:%d', $this->id); - $wrapped = array(); + $idstr = self::cacheGet($keypart); - foreach ($ids as $id) { - if (array_key_exists($id, $temp)) { - $wrapped[] = $temp[$id]; + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $ids = array(); + $f2p = new File_to_post; + $f2p->post_id = $this->id; + if ($f2p->find()) { + while ($f2p->fetch()) { + $ids[] = $f2p->file_id; } } - - return new ArrayWrapper($wrapped); + self::cacheSet($keypart, implode(',', $ids)); } - } - - function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $ids = Notice::stream(array('Notice', '_publicStreamDirect'), - array(), - 'public', - $offset, $limit, $since_id, $max_id); - return Notice::getStreamByIds($ids); - } - - function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $notice = new Notice(); - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - if (common_config('public', 'localonly')) { - $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC); - } else { - # -1 == blacklisted, -2 == gateway (i.e. Twitter) - $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC); - $notice->whereAdd('is_local !='. Notice::GATEWAY); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $ids = array(); + $att = array(); - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; + foreach ($ids as $id) { + $f = File::staticGet('id', $id); + if (!empty($f)) { + $att[] = clone($f); } } - $notice->free(); - $notice = NULL; - - return $ids; + return $att; } - function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) - { - $ids = Notice::stream(array('Notice', '_conversationStreamDirect'), - array($id), - 'notice:conversation_ids:'.$id, - $offset, $limit, $since_id, $max_id); - - return Notice::getStreamByIds($ids); - } - function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0) + function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) { - $notice = new Notice(); - - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->conversation = $id; - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); + $stream = new PublicNoticeStream(); + return $stream->getNotices($offset, $limit, $since_id, $max_id); + } - $ids = array(); - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; + function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) + { + $stream = new ConversationNoticeStream($id); - return $ids; + return $stream->getNotices($offset, $limit, $since_id, $max_id); } /** @@ -766,6 +797,35 @@ class Notice extends Memcached_DataObject return false; } + /** + * Grab the earliest notice from this conversation. + * + * @return Notice or null + */ + function conversationRoot() + { + if (!empty($this->conversation)) { + $c = self::memcache(); + + $key = Cache::key('notice:conversation_root:' . $this->conversation); + $notice = $c->get($key); + if ($notice) { + return $notice; + } + + $notice = new Notice(); + $notice->conversation = $this->conversation; + $notice->orderBy('CREATED'); + $notice->limit(1); + $notice->find(true); + + if ($notice->N) { + $c->set($key, $notice); + return $notice; + } + } + return null; + } /** * Pull up a full list of local recipients who will be getting * this notice in their inbox. Results will be cached, so don't @@ -797,6 +857,7 @@ class Notice extends Memcached_DataObject } $users = $this->getSubscribedUsers(); + $ptags = $this->getProfileTags(); // FIXME: kind of ignoring 'transitional'... // we'll probably stop supporting inboxless mode @@ -804,41 +865,60 @@ class Notice extends Memcached_DataObject $ni = array(); - foreach ($users as $id) { - $ni[$id] = NOTICE_INBOX_SOURCE_SUB; - } + // Give plugins a chance to add folks in at start... + if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) { - foreach ($groups as $group) { - $users = $group->getUserMembers(); foreach ($users as $id) { - if (!array_key_exists($id, $ni)) { - $ni[$id] = NOTICE_INBOX_SOURCE_GROUP; - } + $ni[$id] = NOTICE_INBOX_SOURCE_SUB; } - } - foreach ($recipients as $recipient) { - if (!array_key_exists($recipient, $ni)) { - $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY; + foreach ($groups as $group) { + $users = $group->getUserMembers(); + foreach ($users as $id) { + if (!array_key_exists($id, $ni)) { + $ni[$id] = NOTICE_INBOX_SOURCE_GROUP; + } + } } - } - // Exclude any deleted, non-local, or blocking recipients. - $profile = $this->getProfile(); - $originalProfile = null; - if ($this->repeat_of) { - // Check blocks against the original notice's poster as well. - $original = Notice::staticGet('id', $this->repeat_of); - if ($original) { - $originalProfile = $original->getProfile(); + foreach ($ptags as $ptag) { + $users = $ptag->getUserSubscribers(); + foreach ($users as $id) { + if (!array_key_exists($id, $ni)) { + $user = User::staticGet('id', $id); + if (!$user->hasBlocked($profile)) { + $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG; + } + } + } } - } - foreach ($ni as $id => $source) { - $user = User::staticGet('id', $id); - if (empty($user) || $user->hasBlocked($profile) || - ($originalProfile && $user->hasBlocked($originalProfile))) { - unset($ni[$id]); + + foreach ($recipients as $recipient) { + if (!array_key_exists($recipient, $ni)) { + $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY; + } + + // Exclude any deleted, non-local, or blocking recipients. + $profile = $this->getProfile(); + $originalProfile = null; + if ($this->repeat_of) { + // Check blocks against the original notice's poster as well. + $original = Notice::staticGet('id', $this->repeat_of); + if ($original) { + $originalProfile = $original->getProfile(); + } + } + foreach ($ni as $id => $source) { + $user = User::staticGet('id', $id); + if (empty($user) || $user->hasBlocked($profile) || + ($originalProfile && $user->hasBlocked($originalProfile))) { + unset($ni[$id]); + } + } } + + // Give plugins a chance to filter out... + Event::handle('EndNoticeWhoGets', array($this, &$ni)); } if (!empty($c)) { @@ -913,6 +993,39 @@ class Notice extends Memcached_DataObject return $ids; } + function getProfileTags() + { + // Don't save ptags for repeats, for now. + + if (!empty($this->repeat_of)) { + return array(); + } + + // XXX: cache me + + $ptags = array(); + + $ptagi = new Profile_tag_inbox(); + + $ptagi->selectAdd(); + $ptagi->selectAdd('profile_tag_id'); + + $ptagi->notice_id = $this->id; + + if ($ptagi->find()) { + while ($ptagi->fetch()) { + $profile_list = Profile_list::staticGet('id', $ptagi->profile_tag_id); + if ($profile_list) { + $ptags[] = $profile_list; + } + } + } + + $ptagi->free(); + + return $ptags; + } + /** * Record this notice to the given group inboxes for delivery. * Overrides the regular parsing of !group markup. @@ -939,7 +1052,15 @@ class Notice extends Memcached_DataObject common_log_db_error($gi, 'INSERT', __FILE__); } - // @fixme should we save the tags here or not? + // we automatically add a tag for every group name, too + + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->nickname), + 'notice_id' => $this->id)); + + if (is_null($tag)) { + $this->saveTag($group->nickname); + } + $groups[] = clone($group); } else { common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist"); @@ -961,36 +1082,19 @@ class Notice extends Memcached_DataObject return array(); } - $groups = array(); - - /* extract all !group */ - $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', - strtolower($this->content), - $match); - if (!$count) { - return $groups; - } - $profile = $this->getProfile(); + $groups = self::groupsFromText($this->content, $profile); + /* Add them to the database */ - foreach (array_unique($match[1]) as $nickname) { + foreach ($groups as $group) { /* XXX: remote groups. */ - $group = User_group::getForNickname($nickname, $profile); if (empty($group)) { continue; } - // we automatically add a tag for every group name, too - - $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname), - 'notice_id' => $this->id)); - - if (is_null($tag)) { - $this->saveTag($nickname); - } if ($profile->isMember($group)) { @@ -1034,6 +1138,70 @@ class Notice extends Memcached_DataObject return true; } + /** + * record targets into profile_tag_inbox. + * @return array of Profile_list objects + */ + function saveProfileTags($known=array()) + { + // Don't save ptags for repeats, for now + + if (!empty($this->repeat_of)) { + return array(); + } + + if (is_array($known)) { + $ptags = $known; + } else { + $ptags = array(); + } + + $ptag = new Profile_tag(); + $ptag->tagged = $this->profile_id; + + if($ptag->find()) { + while($ptag->fetch()) { + $plist = Profile_list::getByTaggerAndTag($ptag->tagger, $ptag->tag); + $ptags[] = clone($plist); + } + } + + foreach ($ptags as $target) { + $this->addToProfileTagInbox($target); + } + + return $ptags; + } + + function addToProfileTagInbox($plist) + { + $ptagi = Profile_tag_inbox::pkeyGet(array('profile_tag_id' => $plist->id, + 'notice_id' => $this->id)); + + if (empty($ptagi)) { + + $ptagi = new Profile_tag_inbox(); + + $ptagi->query('BEGIN'); + $ptagi->profile_tag_id = $plist->id; + $ptagi->notice_id = $this->id; + $ptagi->created = $this->created; + + $result = $ptagi->insert(); + if (!$result) { + common_log_db_error($ptagi, 'INSERT', __FILE__); + // TRANS: Server exception thrown when saving profile_tag inbox fails. + throw new ServerException(_('Problem saving profile_tag inbox.')); + } + + $ptagi->query('COMMIT'); + + self::blow('profile_tag:notice_ids:%d', $ptagi->profile_tag_id); + } + + return true; + } + /** * Save reply records indicating that this notice needs to be * delivered to the local users with the given URIs. @@ -1161,23 +1329,28 @@ class Notice extends Memcached_DataObject */ function getReplies() { - // XXX: cache me + $keypart = sprintf('notice:reply_ids:%d', $this->id); - $ids = array(); + $idstr = self::cacheGet($keypart); - $reply = new Reply(); - $reply->selectAdd(); - $reply->selectAdd('profile_id'); - $reply->notice_id = $this->id; + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $ids = array(); - if ($reply->find()) { - while($reply->fetch()) { - $ids[] = $reply->profile_id; + $reply = new Reply(); + $reply->selectAdd(); + $reply->selectAdd('profile_id'); + $reply->notice_id = $this->id; + + if ($reply->find()) { + while($reply->fetch()) { + $ids[] = $reply->profile_id; + } } + self::cacheSet($keypart, implode(',', $ids)); } - $reply->free(); - return $ids; } @@ -1219,27 +1392,39 @@ class Notice extends Memcached_DataObject return array(); } - // XXX: cache me + $ids = array(); - $groups = array(); + $keypart = sprintf('notice:groups:%d', $this->id); - $gi = new Group_inbox(); + $idstr = self::cacheGet($keypart); - $gi->selectAdd(); - $gi->selectAdd('group_id'); + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $gi = new Group_inbox(); - $gi->notice_id = $this->id; + $gi->selectAdd(); + $gi->selectAdd('group_id'); - if ($gi->find()) { - while ($gi->fetch()) { - $group = User_group::staticGet('id', $gi->group_id); - if ($group) { - $groups[] = $group; + $gi->notice_id = $this->id; + + if ($gi->find()) { + while ($gi->fetch()) { + $ids[] = $gi->group_id; } } + + self::cacheSet($keypart, implode(',', $ids)); } - $gi->free(); + $groups = array(); + + foreach ($ids as $id) { + $group = User_group::staticGet('id', $id); + if ($group) { + $groups[] = $group; + } + } return $groups; } @@ -1326,9 +1511,9 @@ class Notice extends Memcached_DataObject $reply_ids = $this->getReplies(); foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $ctx->attention[] = $profile->getUri(); + $rprofile = Profile::staticGet('id', $id); + if (!empty($rprofile)) { + $ctx->attention[] = $rprofile->getUri(); } } @@ -1489,61 +1674,6 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) - { - $cache = Cache::instance(); - - if (empty($cache) || - $since_id != 0 || $max_id != 0 || - is_null($limit) || - ($offset + $limit) > NOTICE_CACHE_WINDOW) { - return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, - $max_id))); - } - - $idkey = Cache::key($cachekey); - - $idstr = $cache->get($idkey); - - if ($idstr !== false) { - // Cache hit! Woohoo! - $window = explode(',', $idstr); - $ids = array_slice($window, $offset, $limit); - return $ids; - } - - $laststr = $cache->get($idkey.';last'); - - if ($laststr !== false) { - $window = explode(',', $laststr); - $last_id = $window[0]; - $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - $last_id, 0, null))); - - $new_window = array_merge($new_ids, $window); - - $new_windowstr = implode(',', $new_window); - - $result = $cache->set($idkey, $new_windowstr); - $result = $cache->set($idkey . ';last', $new_windowstr); - - $ids = array_slice($new_window, $offset, $limit); - - return $ids; - } - - $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - 0, 0, null))); - - $windowstr = implode(',', $window); - - $result = $cache->set($idkey, $windowstr); - $result = $cache->set($idkey . ';last', $windowstr); - - $ids = array_slice($window, $offset, $limit); - - return $ids; - } /** * Determine which notice, if any, a new notice is in reply to. @@ -1578,7 +1708,7 @@ class Notice extends Memcached_DataObject if (!empty($reply_to)) { $reply_notice = Notice::staticGet('id', $reply_to); if (!empty($reply_notice)) { - return $reply_to; + return $reply_notice; } } @@ -1617,8 +1747,10 @@ class Notice extends Memcached_DataObject $last = $recipient->getCurrentNotice(); if (!empty($last)) { - return $last->id; + return $last; } + + return null; } static function maxContent() @@ -1654,6 +1786,15 @@ class Notice extends Memcached_DataObject return $location; } + /** + * Convenience function for posting a repeat of an existing message. + * + * @param int $repeater_id: profile ID of user doing the repeat + * @param string $source: posting source key, eg 'web', 'api', etc + * @return Notice + * + * @throws Exception on failure or permission problems + */ function repeat($repeater_id, $source) { $author = Profile::staticGet('id', $this->profile_id); @@ -1675,8 +1816,13 @@ class Notice extends Memcached_DataObject $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; } - return self::saveNew($repeater_id, $content, $source, - array('repeat_of' => $this->id)); + // Scope is same as this one's + + return self::saveNew($repeater_id, + $content, + $source, + array('repeat_of' => $this->id, + 'scope' => $this->scope)); } // These are supposed to be in chron order! @@ -1701,7 +1847,7 @@ class Notice extends Memcached_DataObject } } - return Notice::getStreamByIds($ids); + return NoticeStream::getStreamByIds($ids); } function _repeatStreamDirect($limit) @@ -1991,17 +2137,32 @@ class Notice extends Memcached_DataObject $this->is_local == Notice::LOCAL_NONPUBLIC); } + /** + * Get the list of hash tags saved with this notice. + * + * @return array of strings + */ public function getTags() { $tags = array(); - $tag = new Notice_tag(); - $tag->notice_id = $this->id; - if ($tag->find()) { - while ($tag->fetch()) { - $tags[] = $tag->tag; + + $keypart = sprintf('notice:tags:%d', $this->id); + + $tagstr = self::cacheGet($keypart); + + if ($tagstr !== false) { + $tags = explode(',', $tagstr); + } else { + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + if ($tag->find()) { + while ($tag->fetch()) { + $tags[] = $tag->tag; + } } + self::cacheSet($keypart, implode(',', $tags)); } - $tag->free(); + return $tags; } @@ -2125,4 +2286,132 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } + + /** + * Check that the given profile is allowed to read, respond to, or otherwise + * act on this notice. + * + * The $scope member is a bitmask of scopes, representing a logical AND of the + * scope requirement. So, 0x03 (Notice::ADDRESSEE_SCOPE | Notice::SITE_SCOPE) means + * "only visible to people who are mentioned in the notice AND are users on this site." + * Users on the site who are not mentioned in the notice will not be able to see the + * notice. + * + * @param Profile $profile The profile to check; pass null to check for public/unauthenticated users. + * + * @return boolean whether the profile is in the notice's scope + */ + function inScope($profile) + { + $keypart = sprintf('notice:in-scope-for:%d:%d', $this->id, $profile->id); + + $result = self::cacheGet($keypart); + + if ($result === false) { + $bResult = $this->_inScope($profile); + $result = ($bResult) ? 1 : 0; + self::cacheSet($keypart, $result, 0, 300); + } + + return ($result == 1) ? true : false; + } + + protected function _inScope($profile) + { + // If there's no scope, anyone (even anon) is in scope. + + if ($this->scope == 0) { + return true; + } + + // If there's scope, anon cannot be in scope + + if (empty($profile)) { + return false; + } + + // Author is always in scope + + if ($this->profile_id == $profile->id) { + return true; + } + + // Only for users on this site + + if ($this->scope & Notice::SITE_SCOPE) { + $user = $profile->getUser(); + if (empty($user)) { + return false; + } + } + + // Only for users mentioned in the notice + + if ($this->scope & Notice::ADDRESSEE_SCOPE) { + + // XXX: just query for the single reply + + $replies = $this->getReplies(); + + if (!in_array($profile->id, $replies)) { + return false; + } + } + + // Only for members of the given group + + if ($this->scope & Notice::GROUP_SCOPE) { + + // XXX: just query for the single membership + + $groups = $this->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + + // Only for followers of the author + + if ($this->scope & Notice::FOLLOWER_SCOPE) { + $author = $this->getProfile(); + if (!Subscription::exists($profile, $author)) { + return false; + } + } + + return true; + } + + static function groupsFromText($text, $profile) + { + $groups = array(); + + /* extract all !group */ + $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', + strtolower($text), + $match); + + if (!$count) { + return $groups; + } + + foreach (array_unique($match[1]) as $nickname) { + $group = User_group::getForNickname($nickname, $profile); + if (!empty($group) && $profile->isMember($group)) { + $groups[] = $group->id; + } + } + + return $groups; + } }