X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=d520f4728f60d98758041aae7fce74a923a497a0;hb=39f8d2c72830d7bc3c08f60d1df38d19c846a74b;hp=36943be84b044f7a57c2e0626cf42dabc2af9035;hpb=77a96e3d7cecd5fd61ad72b698a15a8a3c7b0940;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 36943be84b..d520f4728f 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -72,6 +72,7 @@ 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) /* Static get */ function staticGet($k,$v=NULL) @@ -93,7 +94,9 @@ class Notice extends Memcached_DataObject $profile = Profile::staticGet('id', $this->profile_id); if (empty($profile)) { - throw new ServerException(sprintf(_('No such profile (%d) for notice (%d)'), $this->profile_id, $this->id)); + // 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; @@ -107,6 +110,11 @@ class Notice extends Memcached_DataObject // @fixme we have some cases where things get re-run and so the // insert fails. $deleted = Deleted_notice::staticGet('id', $this->id); + + if (!$deleted) { + $deleted = Deleted_notice::staticGet('uri', $this->uri); + } + if (!$deleted) { $deleted = new Deleted_notice(); @@ -119,16 +127,20 @@ class Notice extends Memcached_DataObject $deleted->insert(); } - // Clear related records + if (Event::handle('NoticeDeleteRelated', array($this))) { - $this->clearReplies(); - $this->clearRepeats(); - $this->clearFaves(); - $this->clearTags(); - $this->clearGroupInboxes(); + // Clear related records - // NOTE: we don't clear inboxes - // NOTE: we don't clear queue items + $this->clearReplies(); + $this->clearRepeats(); + $this->clearFaves(); + $this->clearTags(); + $this->clearGroupInboxes(); + $this->clearFiles(); + + // NOTE: we don't clear inboxes + // NOTE: we don't clear queue items + } $result = parent::delete(); @@ -142,7 +154,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; } @@ -229,6 +241,9 @@ class Notice extends Memcached_DataObject * in place of extracting # tags from content * 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) + * * @fixme tag override * * @return Notice @@ -238,43 +253,57 @@ class Notice extends Memcached_DataObject $defaults = array('uri' => null, 'url' => null, 'reply_to' => null, - 'repeat_of' => null); + 'repeat_of' => null, + 'distribute' => true); if (!empty($options)) { $options = $options + $defaults; extract($options); + } else { + extract($defaults); } if (!isset($is_local)) { $is_local = Notice::LOCAL_PUBLIC; } - $profile = Profile::staticGet($profile_id); - - $final = common_shorten_links($content); + $profile = Profile::staticGet('id', $profile_id); + $user = User::staticGet('id', $profile_id); + if ($user) { + // Use the local user's shortening preferences, if applicable. + $final = $user->shortenLinks($content); + } else { + $final = common_shorten_links($content); + } if (Notice::contentTooLong($final)) { + // TRANS: Client exception thrown if a notice contains too many characters. throw new ClientException(_('Problem saving notice. Too long.')); } if (empty($profile)) { + // TRANS: Client exception thrown when trying to save a notice for an unknown user. throw new ClientException(_('Problem saving notice. Unknown user.')); } if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); + // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame. throw new ClientException(_('Too many notices too fast; take a breather '. 'and post again in a few minutes.')); } if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); + // TRANS: Client exception thrown when a user tries to post too many duplicate notices in a given time frame. throw new ClientException(_('Too many duplicate messages too quickly;'. ' take a breather and post again in a few minutes.')); } if (!$profile->hasRight(Right::NEWNOTICE)) { common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname); + + // TRANS: Client exception thrown when a user tries to post while being banned. throw new ClientException(_('You are banned from posting notices on this site.'), 403); } @@ -333,6 +362,12 @@ 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 (Event::handle('StartNoticeSave', array(&$notice))) { // XXX: some of these functions write to the DB @@ -341,6 +376,7 @@ class Notice extends Memcached_DataObject if (!$id) { common_log_db_error($notice, 'INSERT', __FILE__); + // TRANS: Server exception thrown when a notice cannot be saved. throw new ServerException(_('Problem saving notice.')); } @@ -367,6 +403,7 @@ class Notice extends Memcached_DataObject if ($changed) { if (!$notice->update($orig)) { common_log_db_error($notice, 'UPDATE', __FILE__); + // TRANS: Server exception thrown when a notice cannot be updated. throw new ServerException(_('Problem saving notice.')); } } @@ -406,8 +443,10 @@ class Notice extends Memcached_DataObject $notice->saveUrls(); } - // Prepare inbox delivery, may be queued to background. - $notice->distribute(); + if ($distribute) { + // Prepare inbox delivery, may be queued to background. + $notice->distribute(); + } return $notice; } @@ -415,7 +454,10 @@ class Notice extends Memcached_DataObject function blowOnInsert($conversation = false) { self::blow('profile:notice_ids:%d', $this->profile_id); - self::blow('public'); + + if ($this->isPublic()) { + self::blow('public'); + } // XXX: Before we were blowing the casche only if the notice id // was not the root of the conversation. What to do now? @@ -450,7 +492,10 @@ class Notice extends Memcached_DataObject $this->blowOnInsert(); self::blow('profile:notice_ids:%d;last', $this->profile_id); - self::blow('public;last'); + + if ($this->isPublic()) { + self::blow('public;last'); + } } /** save all urls in the notice to the db @@ -461,7 +506,9 @@ class Notice extends Memcached_DataObject * @return void */ function saveUrls() { - common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + if (common_config('attachments', 'process_links')) { + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + } } /** @@ -474,17 +521,18 @@ class Notice extends Memcached_DataObject */ function saveKnownUrls($urls) { - // @fixme validation? - foreach ($urls as $url) { - File::processNew($url, $this->id); + if (common_config('attachments', 'process_links')) { + // @fixme validation? + foreach (array_unique($urls) as $url) { + File::processNew($url, $this->id); + } } } /** * @private callback */ - function saveUrl($data) { - list($url, $notice_id) = $data; + function saveUrl($url, $notice_id) { File::processNew($url, $notice_id); } @@ -509,10 +557,8 @@ class Notice extends Memcached_DataObject $notice = new Notice(); $notice->profile_id = $profile_id; $notice->content = $content; - if (common_config('db','type') == 'pgsql') - $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); - else - $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); + $threshold = common_sql_date(time() - common_config('site', 'dupelimit')); + $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold))); $cnt = $notice->count(); return ($cnt == 0); @@ -568,7 +614,9 @@ class Notice extends Memcached_DataObject if ($f2p->find()) { while ($f2p->fetch()) { $f = File::staticGet($f2p->file_id); - $att[] = clone($f); + if ($f) { + $att[] = clone($f); + } } } return $att; @@ -576,7 +624,7 @@ class Notice extends Memcached_DataObject function getStreamByIds($ids) { - $cache = common_memcache(); + $cache = Cache::instance(); if (!empty($cache)) { $notices = array(); @@ -631,7 +679,7 @@ class Notice extends Memcached_DataObject $notice->selectAdd(); // clears it $notice->selectAdd('id'); - $notice->orderBy('id DESC'); + $notice->orderBy('created DESC, id DESC'); if (!is_null($offset)) { $notice->limit($offset, $limit); @@ -645,13 +693,8 @@ class Notice extends Memcached_DataObject $notice->whereAdd('is_local !='. Notice::GATEWAY); } - if ($since_id != 0) { - $notice->whereAdd('id > ' . $since_id); - } - - if ($max_id != 0) { - $notice->whereAdd('id <= ' . $max_id); - } + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); $ids = array(); @@ -686,19 +729,14 @@ class Notice extends Memcached_DataObject $notice->conversation = $id; - $notice->orderBy('id DESC'); + $notice->orderBy('created DESC, id DESC'); if (!is_null($offset)) { $notice->limit($offset, $limit); } - if ($since_id != 0) { - $notice->whereAdd('id > ' . $since_id); - } - - if ($max_id != 0) { - $notice->whereAdd('id <= ' . $max_id); - } + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); $ids = array(); @@ -728,6 +766,7 @@ class Notice extends Memcached_DataObject 1, 1 ); + if ($conversation->N > 0) { return true; } @@ -736,15 +775,22 @@ class Notice extends Memcached_DataObject } /** - * @param $groups array of Group *objects* - * @param $recipients array of profile *ids* + * Pull up a full list of local recipients who will be getting + * this notice in their inbox. Results will be cached, so don't + * change the input data wily-nilly! + * + * @param array $groups optional list of Group objects; + * if left empty, will be loaded from group_inbox records + * @param array $recipient optional list of reply profile ids + * if left empty, will be loaded from reply records + * @return array associating recipient user IDs with an inbox source constant */ function whoGets($groups=null, $recipients=null) { $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; } @@ -770,33 +816,42 @@ class Notice extends Memcached_DataObject $ni[$id] = NOTICE_INBOX_SOURCE_SUB; } - $profile = $this->getProfile(); - foreach ($groups as $group) { $users = $group->getUserMembers(); foreach ($users as $id) { if (!array_key_exists($id, $ni)) { - $user = User::staticGet('id', $id); - if (!$user->hasBlocked($profile)) { - $ni[$id] = NOTICE_INBOX_SOURCE_GROUP; - } + $ni[$id] = NOTICE_INBOX_SOURCE_GROUP; } } } foreach ($recipients as $recipient) { - if (!array_key_exists($recipient, $ni)) { - $recipientUser = User::staticGet('id', $recipient); - if (!empty($recipientUser)) { - $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY; - } + $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]); } } 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; @@ -878,11 +933,12 @@ class Notice extends Memcached_DataObject function saveKnownGroups($group_ids) { if (!is_array($group_ids)) { - throw new ServerException("Bad type provided to saveKnownGroups"); + // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups(). + throw new ServerException(_('Bad type provided to saveKnownGroups.')); } $groups = array(); - foreach ($group_ids as $id) { + foreach (array_unique($group_ids) as $id) { $group = User_group::staticGet('id', $id); if ($group) { common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); @@ -916,7 +972,7 @@ class Notice extends Memcached_DataObject $groups = array(); /* extract all !group */ - $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/', + $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', strtolower($this->content), $match); if (!$count) { @@ -976,6 +1032,7 @@ class Notice extends Memcached_DataObject if (!$result) { common_log_db_error($gi, 'INSERT', __FILE__); + // TRANS: Server exception thrown when an update for a group inbox fails. throw new ServerException(_('Problem saving group inbox.')); } @@ -1002,24 +1059,32 @@ class Notice extends Memcached_DataObject if (empty($uris)) { return; } + $sender = Profile::staticGet($this->profile_id); - foreach ($uris as $uri) { + foreach (array_unique($uris) as $uri) { - $user = User::staticGet('uri', $uri); + $profile = Profile::fromURI($uri); - if (!empty($user)) { - if ($user->hasBlocked($sender)) { - continue; - } + if (empty($profile)) { + common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'"); + continue; + } - $reply = new Reply(); + if ($profile->hasBlocked($sender)) { + common_log(LOG_INFO, "Not saving reply to profile {$profile->id} ($uri) from sender {$sender->id} because of a block."); + continue; + } - $reply->notice_id = $this->id; - $reply->profile_id = $user->id; + $reply = new Reply(); - $id = $reply->insert(); - } + $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"); + + $id = $reply->insert(); } return; @@ -1076,12 +1141,15 @@ class Notice extends Memcached_DataObject $reply->notice_id = $this->id; $reply->profile_id = $mentioned->id; + $reply->modified = $this->created; $id = $reply->insert(); if (!$id) { common_log_db_error($reply, 'INSERT', __FILE__); - throw new ServerException("Couldn't save reply for {$this->id}, {$mentioned->id}"); + // TRANS: Server exception thrown when a reply cannot be saved. + // TRANS: %1$d is a notice ID, %2$d is the ID of the mentioned user. + throw new ServerException(sprintf(_('Could not save reply for %1$d, %2$d.'), $this->id, $mentioned->id)); } else { $replied[$mentioned->id] = 1; self::blow('reply:stream:%d', $mentioned->id); @@ -1184,205 +1252,221 @@ class Notice extends Memcached_DataObject return $groups; } - function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null) + /** + * Convert a notice into an activity for export. + * + * @param User $cur Current user + * + * @return Activity activity object representing this Notice. + */ + + function asActivity($cur) { - $profile = $this->getProfile(); + $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); - $xs = new XMLStringer(true); - - if ($namespace) { - $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', - 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', - 'xmlns:georss' => 'http://www.georss.org/georss', - 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', - 'xmlns:media' => 'http://purl.org/syndication/atommedia', - 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', - 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', - 'xmlns:statusnet' => 'http://status.net/schema/api/1/'); - } else { - $attrs = array(); + if (!empty($act)) { + return $act; } + $act = new Activity(); - $xs->elementStart('entry', $attrs); + if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { - if ($source) { - $xs->elementStart('source'); - $xs->element('id', null, $profile->profileurl); - $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name')); - $xs->element('link', array('href' => $profile->profileurl)); - $user = User::staticGet('id', $profile->id); - if (!empty($user)) { - $atom_feed = common_local_url('ApiTimelineUser', - array('format' => 'atom', - 'id' => $profile->nickname)); - $xs->element('link', array('rel' => 'self', - 'type' => 'application/atom+xml', - 'href' => $profile->profileurl)); - $xs->element('link', array('rel' => 'license', - 'href' => common_config('license', 'url'))); - } + $profile = $this->getProfile(); - $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); - $xs->element('updated', null, common_date_w3dtf($this->created)); - } + $act->actor = ActivityObject::fromProfile($profile); + $act->actor->extra[] = $profile->profileInfo($cur); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); - if ($source) { - $xs->elementEnd('source'); - } + // XXX: should this be handled by default processing for object entry? - $xs->element('title', null, common_xml_safe_str($this->content)); + $act->time = strtotime($this->created); + $act->link = $this->bestUrl(); - if ($author) { - $xs->raw($profile->asAtomAuthor($cur)); - $xs->raw($profile->asActivityActor()); - } + $act->content = common_xml_safe_str($this->rendered); + $act->id = $this->uri; + $act->title = common_xml_safe_str($this->content); - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $this->bestUrl())); + // Categories - $xs->element('id', null, $this->uri); + $tags = $this->getTags(); - $xs->element('published', null, common_date_w3dtf($this->created)); - $xs->element('updated', null, common_date_w3dtf($this->created)); + foreach ($tags as $tag) { + $cat = new AtomCategory(); + $cat->term = $tag; - $source = null; + $act->categories[] = $cat; + } - $ns = $this->getSource(); + // Enclosures + // XXX: use Atom Media and/or File activity objects instead - if ($ns) { - if (!empty($ns->name) && !empty($ns->url)) { - $source = '' - . htmlspecialchars($ns->name) - . ''; - } else { - $source = $ns->code; + $attachments = $this->attachments(); + + foreach ($attachments as $attachment) { + $enclosure = $attachment->getEnclosure(); + if ($enclosure) { + $act->enclosures[] = $enclosure; + } } - } - $noticeInfoAttr = array( - 'local_id' => $this->id, // local notice ID (useful to clients for ordering) - 'source' => $source, // the client name (source attribution) - ); + $ctx = new ActivityContext(); - $ns = $this->getSource(); - if ($ns) { - if (!empty($ns->url)) { - $noticeInfoAttr['source_link'] = $ns->url; + if (!empty($this->reply_to)) { + $reply = Notice::staticGet('id', $this->reply_to); + if (!empty($reply)) { + $ctx->replyToID = $reply->uri; + $ctx->replyToUrl = $reply->bestUrl(); + } } - } - if (!empty($cur)) { - $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; - $profile = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($profile->hasRepeated($this->id)) ? "true" : "false"; - } - - if (!empty($this->repeat_of)) { - $noticeInfoAttr['repeat_of'] = $this->repeat_of; - } + $ctx->location = $this->getLocation(); - $xs->element('statusnet:notice_info', $noticeInfoAttr, null); + $conv = null; - if ($this->reply_to) { - $reply_notice = Notice::staticGet('id', $this->reply_to); - if (!empty($reply_notice)) { - $xs->element('link', array('rel' => 'related', - 'href' => $reply_notice->bestUrl())); - $xs->element('thr:in-reply-to', - array('ref' => $reply_notice->uri, - 'href' => $reply_notice->bestUrl())); + if (!empty($this->conversation)) { + $conv = Conversation::staticGet('id', $this->conversation); + if (!empty($conv)) { + $ctx->conversation = $conv->uri; + } } - } - - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); + $reply_ids = $this->getReplies(); - if (!empty($conv)) { - $xs->element( - 'link', array( - 'rel' => 'ostatus:conversation', - 'href' => $conv->uri - ) - ); + foreach ($reply_ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $ctx->attention[] = $profile->getUri(); + } } - } - $reply_ids = $this->getReplies(); + $groups = $this->getGroups(); - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $xs->element( - 'link', array( - 'rel' => 'ostatus:attention', - 'href' => $profile->getUri() - ) - ); + foreach ($groups as $group) { + $ctx->attention[] = $group->getUri(); } - } - $groups = $this->getGroups(); + // XXX: deprecated; use ActivityVerb::SHARE instead - foreach ($groups as $group) { - $xs->element( - 'link', array( - 'rel' => 'ostatus:attention', - 'href' => $group->permalink() - ) - ); - } + $repeat = null; - if (!empty($this->repeat_of)) { - $repeat = Notice::staticGet('id', $this->repeat_of); - if (!empty($repeat)) { - $xs->element( - 'ostatus:forward', - array('ref' => $repeat->uri, 'href' => $repeat->bestUrl()) - ); + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + $ctx->forwardID = $repeat->uri; + $ctx->forwardUrl = $repeat->bestUrl(); } - } - $xs->element( - 'content', - array('type' => 'html'), - common_xml_safe_str($this->rendered) - ); + $act->context = $ctx; - $tag = new Notice_tag(); - $tag->notice_id = $this->id; - if ($tag->find()) { - while ($tag->fetch()) { - $xs->element('category', array('term' => $tag->tag)); + // Source + + $atom_feed = $profile->getAtomFeed(); + + if (!empty($atom_feed)) { + + $act->source = new ActivitySource(); + + // XXX: we should store the actual feed ID + + $act->source->id = $atom_feed; + + // XXX: we should store the actual feed title + + $act->source->title = $profile->getBestName(); + + $act->source->links['alternate'] = $profile->profileurl; + $act->source->links['self'] = $atom_feed; + + $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE); + + $notice = $profile->getCurrentNotice(); + + if (!empty($notice)) { + $act->source->updated = self::utcDate($notice->created); + } + + $user = User::staticGet('id', $profile->id); + + if (!empty($user)) { + $act->source->links['license'] = common_config('license', 'url'); + } + } + + if ($this->isLocal()) { + $act->selfLink = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + $act->editLink = $act->selfLink; } + + Event::handle('EndNoticeAsActivity', array($this, &$act)); } - $tag->free(); - # Enclosures - $attachments = $this->attachments(); - if($attachments){ - foreach($attachments as $attachment){ - $enclosure=$attachment->getEnclosure(); - if ($enclosure) { - $attributes = array('rel'=>'enclosure','href'=>$enclosure->url,'type'=>$enclosure->mimetype,'length'=>$enclosure->size); - if($enclosure->title){ - $attributes['title']=$enclosure->title; - } - $xs->element('link', $attributes, null); + self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); + + return $act; + } + + // This has gotten way too long. Needs to be sliced up into functional bits + // or ideally exported to a utility class. + + function asAtomEntry($namespace=false, + $source=false, + $author=true, + $cur=null) + { + $act = $this->asActivity($cur); + $act->extra[] = $this->noticeInfo($cur); + return $act->asString($namespace, $author, $source); + } + + /** + * Extra notice info for atom entries + * + * Clients use some extra notice info in the atom stream. + * This gives it to them. + * + * @param User $cur Current user + * + * @return array representation of element + */ + + function noticeInfo($cur) + { + // local notice ID (useful to clients for ordering) + + $noticeInfoAttr = array('local_id' => $this->id); + + // notice source + + $ns = $this->getSource(); + + if (!empty($ns)) { + $noticeInfoAttr['source'] = $ns->code; + if (!empty($ns->url)) { + $noticeInfoAttr['source_link'] = $ns->url; + if (!empty($ns->name)) { + $noticeInfoAttr['source'] = '' + . htmlspecialchars($ns->name) + . ''; } } } - if (!empty($this->lat) && !empty($this->lon)) { - $xs->element('georss:point', null, $this->lat . ' ' . $this->lon); + // favorite and repeated + + if (!empty($cur)) { + $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; + $cp = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; } - $xs->elementEnd('entry'); + if (!empty($this->repeat_of)) { + $noticeInfoAttr['repeat_of'] = $this->repeat_of; + } - return $xs->getString(); + return array('statusnet:notice_info', $noticeInfoAttr, null); } /** @@ -1394,6 +1478,7 @@ class Notice extends Memcached_DataObject * @param string $element one of 'subject', 'object', 'target' * @return string */ + function asActivityNoun($element) { $noun = ActivityObject::fromNotice($this); @@ -1414,7 +1499,7 @@ class Notice extends Memcached_DataObject function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) { - $cache = common_memcache(); + $cache = Cache::instance(); if (empty($cache) || $since_id != 0 || $max_id != 0 || @@ -1424,7 +1509,7 @@ class Notice extends Memcached_DataObject $max_id))); } - $idkey = common_cache_key($cachekey); + $idkey = Cache::key($cachekey); $idstr = $cache->get($idkey); @@ -1606,17 +1691,17 @@ class Notice extends Memcached_DataObject 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 @@ -1636,10 +1721,10 @@ class Notice extends Memcached_DataObject $notice->repeat_of = $this->id; - $notice->orderBy('created'); // NB: asc! + $notice->orderBy('created, id'); // NB: asc! - if (!is_null($offset)) { - $notice->limit($offset, $limit); + if (!is_null($limit)) { + $notice->limit(0, $limit); } $ids = array(); @@ -1661,7 +1746,6 @@ class Notice extends Memcached_DataObject $options = array(); if (!empty($location_id) && !empty($location_ns)) { - $options['location_id'] = $location_id; $options['location_ns'] = $location_ns; @@ -1673,7 +1757,6 @@ class Notice extends Memcached_DataObject } } else if (!empty($lat) && !empty($lon)) { - $options['lat'] = $lat; $options['lon'] = $lon; @@ -1684,7 +1767,6 @@ class Notice extends Memcached_DataObject $options['location_ns'] = $location->location_ns; } } else if (!empty($profile)) { - if (isset($profile->lat) && isset($profile->lon)) { $options['lat'] = $profile->lat; $options['lon'] = $profile->lon; @@ -1729,6 +1811,21 @@ class Notice extends Memcached_DataObject $reply->free(); } + function clearFiles() + { + $f2p = new File_to_post(); + + $f2p->post_id = $this->id; + + if ($f2p->find()) { + while ($f2p->fetch()) { + $f2p->delete(); + } + } + // FIXME: decide whether to delete File objects + // ...and related (actual) files + } + function clearRepeats() { $repeatNotice = new Notice(); @@ -1770,10 +1867,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(); } } @@ -1801,6 +1898,7 @@ class Notice extends Memcached_DataObject { // We always insert for the author so they don't // have to wait + Event::handle('StartNoticeDistribute', array($this)); $user = User::staticGet('id', $this->profile_id); if (!empty($user)) { @@ -1901,4 +1999,138 @@ class Notice extends Memcached_DataObject $this->is_local == Notice::LOCAL_NONPUBLIC); } + public function getTags() + { + $tags = array(); + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + if ($tag->find()) { + while ($tag->fetch()) { + $tags[] = $tag->tag; + } + } + $tag->free(); + return $tags; + } + + static private function utcDate($dt) + { + $dateStr = date('d F Y H:i:s', strtotime($dt)); + $d = new DateTime($dateStr, new DateTimeZone('UTC')); + return $d->format(DATE_W3C); + } + + /** + * Look up the creation timestamp for a given notice ID, even + * if it's been deleted. + * + * @param int $id + * @return mixed string recorded creation timestamp, or false if can't be found + */ + public static function getAsTimestamp($id) + { + if (!$id) { + return false; + } + + $notice = Notice::staticGet('id', $id); + if ($notice) { + return $notice->created; + } + + $deleted = Deleted_notice::staticGet('id', $id); + if ($deleted) { + return $deleted->created; + } + + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a since_id + * parameter, matching notices posted after the given one (exclusive). + * + * If the referenced notice can't be found, will return false. + * + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function whereSinceId($id, $idField='id', $createdField='created') + { + $since = Notice::getAsTimestamp($id); + if ($since) { + return sprintf("($createdField = '%s' and $idField > %d) or ($createdField > '%s')", $since, $id, $since); + } + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a since_id + * parameter, matching notices posted after the given one (exclusive), and + * if necessary add it to the data object's query. + * + * @param DB_DataObject $obj + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function addWhereSinceId(DB_DataObject $obj, $id, $idField='id', $createdField='created') + { + $since = self::whereSinceId($id, $idField, $createdField); + if ($since) { + $obj->whereAdd($since); + } + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a max_id + * parameter, matching notices posted before the given one (inclusive). + * + * If the referenced notice can't be found, will return false. + * + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function whereMaxId($id, $idField='id', $createdField='created') + { + $max = Notice::getAsTimestamp($id); + if ($max) { + return sprintf("($createdField < '%s') or ($createdField = '%s' and $idField <= %d)", $max, $max, $id); + } + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a max_id + * parameter, matching notices posted before the given one (inclusive), and + * if necessary add it to the data object's query. + * + * @param DB_DataObject $obj + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function addWhereMaxId(DB_DataObject $obj, $id, $idField='id', $createdField='created') + { + $max = self::whereMaxId($id, $idField, $createdField); + if ($max) { + $obj->whereAdd($max); + } + } + + function isPublic() + { + if (common_config('public', 'localonly')) { + return ($this->is_local == Notice::LOCAL_PUBLIC); + } else { + return (($this->is_local != Notice::LOCAL_NONPUBLIC) && + ($this->is_local != Notice::GATEWAY)); + } + } }