X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=classes%2FNotice.php;h=a4f530c44facfe768ed9aec178a5a749d8249448;hb=2d2c134a1f464021f2b3d3837eac95521ac02af5;hp=4522d1fc3889b4648afb6e8434afddfd0fbe8bd8;hpb=399977aebfa99362295b0a817fe08e2eeb07055d;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 4522d1fc38..a4f530c44f 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,6 +90,12 @@ 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; + function getProfile() { $profile = Profile::staticGet('id', $this->profile_id); @@ -153,7 +161,7 @@ class Notice extends Memcached_DataObject function saveTags() { /* extract all #hastags */ - $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/', strtolower($this->content), $match); + $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/u', strtolower($this->content), $match); if (!$count) { return true; } @@ -241,6 +249,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 +262,7 @@ class Notice extends Memcached_DataObject 'url' => null, 'reply_to' => null, 'repeat_of' => null, + 'scope' => null, 'distribute' => true); if (!empty($options)) { @@ -310,7 +321,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 +342,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 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; + } + } + } - if (!empty($notice->reply_to)) { - $reply = Notice::staticGet('id', $notice->reply_to); - $notice->conversation = $reply->conversation; + // Scope set below + } } if (!empty($lat) && !empty($lon)) { @@ -360,6 +434,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 +516,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,11 +537,9 @@ 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); - } else { - $notice->saveGroups(); - } + // Note: groups should always be set. + + $notice->saveKnownGroups($groups); if (isset($urls)) { $notice->saveKnownUrls($urls); @@ -488,6 +600,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 +652,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 +663,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 +680,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; } @@ -614,134 +733,19 @@ class Notice extends Memcached_DataObject return $att; } - function getStreamByIds($ids) - { - $cache = common_memcache(); - - 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); - } - - $wrapped = array(); - - foreach ($ids as $id) { - if (array_key_exists($id, $temp)) { - $wrapped[] = $temp[$id]; - } - } - - return new ArrayWrapper($wrapped); - } - } 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); + $stream = new PublicNoticeStream(); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } - 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(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; - } 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) - { - $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); - - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; + $stream = new ConversationNoticeStream($id); - return $ids; + return $stream->getNotices($offset, $limit, $since_id, $max_id); } /** @@ -766,6 +770,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 @@ -782,7 +815,7 @@ class Notice extends Memcached_DataObject $c = self::memcache(); if (!empty($c)) { - $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id)); + $ni = $c->get(Cache::key('notice:who_gets:'.$this->id)); if ($ni !== false) { return $ni; } @@ -804,46 +837,53 @@ 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 ($groups as $group) { + $users = $group->getUserMembers(); + foreach ($users as $id) { + if (!array_key_exists($id, $ni)) { + $ni[$id] = NOTICE_INBOX_SOURCE_GROUP; + } } } - } - foreach ($recipients as $recipient) { - if (!array_key_exists($recipient, $ni)) { - $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY; + 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(); + // 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]); + 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)) { // XXX: pack this data better - $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni); + $c->set(Cache::key('notice:who_gets:'.$this->id), $ni); } return $ni; @@ -939,7 +979,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 +1009,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)) { @@ -1072,6 +1103,7 @@ class Notice extends Memcached_DataObject $reply->notice_id = $this->id; $reply->profile_id = $profile->id; + $reply->modified = $this->created; common_log(LOG_INFO, __METHOD__ . ": saving reply: notice $this->id to profile $profile->id"); @@ -1132,6 +1164,7 @@ class Notice extends Memcached_DataObject $reply->notice_id = $this->id; $reply->profile_id = $mentioned->id; + $reply->modified = $this->created; $id = $reply->insert(); @@ -1250,23 +1283,23 @@ class Notice extends Memcached_DataObject * @return Activity activity object representing this Notice. */ - function asActivity() + function asActivity($cur) { $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); if (!empty($act)) { return $act; } - $act = new Activity(); if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { $profile = $this->getProfile(); - $act->actor = ActivityObject::fromProfile($profile); - $act->verb = ActivityVerb::POST; - $act->objects[] = ActivityObject::fromNotice($this); + $act->actor = ActivityObject::fromProfile($profile); + $act->actor->extra[] = $profile->profileInfo($cur); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); // XXX: should this be handled by default processing for object entry? @@ -1333,7 +1366,7 @@ class Notice extends Memcached_DataObject $groups = $this->getGroups(); foreach ($groups as $group) { - $ctx->attention[] = $group->uri; + $ctx->attention[] = $group->getUri(); } // XXX: deprecated; use ActivityVerb::SHARE instead @@ -1404,7 +1437,7 @@ class Notice extends Memcached_DataObject $author=true, $cur=null) { - $act = $this->asActivity(); + $act = $this->asActivity($cur); $act->extra[] = $this->noticeInfo($cur); return $act->asString($namespace, $author, $source); } @@ -1487,61 +1520,6 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) - { - $cache = common_memcache(); - - 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 = common_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. @@ -1576,7 +1554,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; } } @@ -1615,8 +1593,10 @@ class Notice extends Memcached_DataObject $last = $recipient->getCurrentNotice(); if (!empty($last)) { - return $last->id; + return $last; } + + return null; } static function maxContent() @@ -1652,6 +1632,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); @@ -1673,25 +1662,30 @@ 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! function repeatStream($limit=100) { - $cache = common_memcache(); + $cache = Cache::instance(); if (empty($cache)) { $ids = $this->_repeatStreamDirect($limit); } else { - $idstr = $cache->get(common_cache_key('notice:repeats:'.$this->id)); + $idstr = $cache->get(Cache::key('notice:repeats:'.$this->id)); if ($idstr !== false) { $ids = explode(',', $idstr); } else { $ids = $this->_repeatStreamDirect(100); - $cache->set(common_cache_key('notice:repeats:'.$this->id), implode(',', $ids)); + $cache->set(Cache::key('notice:repeats:'.$this->id), implode(',', $ids)); } if ($limit < 100) { // We do a max of 100, so slice down to limit @@ -1699,7 +1693,7 @@ class Notice extends Memcached_DataObject } } - return Notice::getStreamByIds($ids); + return NoticeStream::getStreamByIds($ids); } function _repeatStreamDirect($limit) @@ -1857,10 +1851,10 @@ class Notice extends Memcached_DataObject if ($tag->find()) { while ($tag->fetch()) { - self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, common_keyize($tag->tag)); - self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, common_keyize($tag->tag)); - self::blow('notice_tag:notice_ids:%s', common_keyize($tag->tag)); - self::blow('notice_tag:notice_ids:%s;last', common_keyize($tag->tag)); + self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, Cache::keyize($tag->tag)); + self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, Cache::keyize($tag->tag)); + self::blow('notice_tag:notice_ids:%s', Cache::keyize($tag->tag)); + self::blow('notice_tag:notice_ids:%s;last', Cache::keyize($tag->tag)); $tag->delete(); } } @@ -1989,6 +1983,11 @@ 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(); @@ -2123,4 +2122,117 @@ 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) + { + // 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; + } }