X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=50909f97071e31f339b0b35d66921386c0a94014;hb=9a6ceb3303a98d1c5fba3587f32d0377e55062cc;hp=0539ca3b1d14215503e0ecf6036ed53cbeefe264;hpb=9f4891568f458a8135d59820b47b91e3f89ab93b;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 0539ca3b1d..50909f9707 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -234,6 +234,8 @@ 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 + * * @fixme tag override * * @return Notice @@ -243,7 +245,8 @@ 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; @@ -256,9 +259,14 @@ class Notice extends Memcached_DataObject $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. @@ -421,8 +429,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; } @@ -476,7 +486,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); + } } /** @@ -489,17 +501,18 @@ class Notice extends Memcached_DataObject */ function saveKnownUrls($urls) { - // @fixme validation? - foreach (array_unique($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); } @@ -524,10 +537,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); @@ -648,7 +659,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); @@ -662,13 +673,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(); @@ -703,19 +709,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(); @@ -745,6 +746,7 @@ class Notice extends Memcached_DataObject 1, 1 ); + if ($conversation->N > 0) { return true; } @@ -753,8 +755,15 @@ 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) { @@ -787,27 +796,27 @@ 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(); + foreach ($ni as $id => $source) { + $user = User::staticGet('id', $id); + if (empty($user) || $user->hasBlocked($profile)) { + unset($ni[$id]); } } @@ -896,7 +905,7 @@ class Notice extends Memcached_DataObject { if (!is_array($group_ids)) { // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups(). - throw new ServerException(_("Bad type provided to saveKnownGroups")); + throw new ServerException(_('Bad type provided to saveKnownGroups.')); } $groups = array(); @@ -1109,7 +1118,7 @@ class Notice extends Memcached_DataObject common_log_db_error($reply, 'INSERT', __FILE__); // 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)); + 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); @@ -1212,242 +1221,191 @@ class Notice extends Memcached_DataObject return $groups; } - function asActivity() - { - $profile = $this->getProfile(); - - $act = new Activity(); - - $act->actor = ActivityObject::fromProfile($profile); - $act->verb = ActivityVerb::POST; - $act->objects[] = ActivityObject::fromNotice($this); - - $act->time = strtotime($this->created); - $act->link = $this->bestUrl(); - - $act->content = common_xml_safe_str($this->rendered); - $act->id = $this->uri; - $act->title = common_xml_safe_str($this->content); - - $ctx = new ActivityContext(); - - if (!empty($this->reply_to)) { - $reply = Notice::staticGet('id', $this->reply_to); - if (!empty($reply)) { - $ctx->replyToID = $reply->uri; - $ctx->replyToUrl = $reply->bestUrl(); - } - } - - $ctx->location = $this->getLocation(); - - $conv = null; - - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); - if (!empty($conv)) { - $ctx->conversation = $conv->uri; - } - } - - $reply_ids = $this->getReplies(); - - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $ctx->attention[] = $profile->uri; - } - } - - $groups = $this->getGroups(); - - foreach ($groups as $group) { - $ctx->attention[] = $group->uri; - } - - $act->context = $ctx; - - return $act; - } - - // This has gotten way too long. Needs to be sliced up into functional bits - // or ideally exported to a utility class. + /** + * Convert a notice into an activity for export. + * + * @param User $cur Current user + * + * @return Activity activity object representing this Notice. + */ - function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null) + function asActivity() { - $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; } - if (Event::handle('StartActivityStart', array(&$this, &$xs, &$attrs))) { - $xs->elementStart('entry', $attrs); - Event::handle('EndActivityStart', array(&$this, &$xs, &$attrs)); - } - - if (Event::handle('StartActivitySource', array(&$this, &$xs))) { - - if ($source) { - - $atom_feed = $profile->getAtomFeed(); - - if (!empty($atom_feed)) { - - $xs->elementStart('source'); - - // XXX: we should store the actual feed ID - - $xs->element('id', null, $atom_feed); + $act = new Activity(); + + if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { - // XXX: we should store the actual feed title + $profile = $this->getProfile(); + + $act->actor = ActivityObject::fromProfile($profile); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); - $xs->element('title', null, $profile->getBestName()); + // XXX: should this be handled by default processing for object entry? - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $profile->profileurl)); + $act->time = strtotime($this->created); + $act->link = $this->bestUrl(); + + $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' => 'self', - 'type' => 'application/atom+xml', - 'href' => $atom_feed)); + // Categories - $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); + $tags = $this->getTags(); - $notice = $profile->getCurrentNotice(); + foreach ($tags as $tag) { + $cat = new AtomCategory(); + $cat->term = $tag; - if (!empty($notice)) { - $xs->element('updated', null, self::utcDate($notice->created)); - } + $act->categories[] = $cat; + } - $user = User::staticGet('id', $profile->id); + // Enclosures + // XXX: use Atom Media and/or File activity objects instead - if (!empty($user)) { - $xs->element('link', array('rel' => 'license', - 'href' => common_config('license', 'url'))); - } + $attachments = $this->attachments(); - $xs->elementEnd('source'); + foreach ($attachments as $attachment) { + $enclosure = $attachment->getEnclosure(); + if ($enclosure) { + $act->enclosures[] = $enclosure; } } - Event::handle('EndActivitySource', array(&$this, &$xs)); - } - - $title = common_xml_safe_str($this->content); - - if (Event::handle('StartActivityTitle', array(&$this, &$xs, &$title))) { - $xs->element('title', null, $title); - Event::handle('EndActivityTitle', array($this, &$xs, $title)); - } - - $atomAuthor = ''; - - if ($author) { - $atomAuthor = $profile->asAtomAuthor($cur); - } - - if (Event::handle('StartActivityAuthor', array(&$this, &$xs, &$atomAuthor))) { - if (!empty($atomAuthor)) { - $xs->raw($atomAuthor); - Event::handle('EndActivityAuthor', array(&$this, &$xs, &$atomAuthor)); + + $ctx = new ActivityContext(); + + if (!empty($this->reply_to)) { + $reply = Notice::staticGet('id', $this->reply_to); + if (!empty($reply)) { + $ctx->replyToID = $reply->uri; + $ctx->replyToUrl = $reply->bestUrl(); + } + } + + $ctx->location = $this->getLocation(); + + $conv = null; + + if (!empty($this->conversation)) { + $conv = Conversation::staticGet('id', $this->conversation); + if (!empty($conv)) { + $ctx->conversation = $conv->uri; + } + } + + $reply_ids = $this->getReplies(); + + foreach ($reply_ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $ctx->attention[] = $profile->getUri(); + } + } + + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $ctx->attention[] = $group->uri; } - } - $actor = ''; + // XXX: deprecated; use ActivityVerb::SHARE instead - if ($author) { - $actor = $profile->asActivityActor(); - } + $repeat = null; - if (Event::handle('StartActivityActor', array(&$this, &$xs, &$actor))) { - if (!empty($actor)) { - $xs->raw($actor); - Event::handle('EndActivityActor', array(&$this, &$xs, &$actor)); + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + $ctx->forwardID = $repeat->uri; + $ctx->forwardUrl = $repeat->bestUrl(); } - } + + $act->context = $ctx; - $url = $this->bestUrl(); + // Source - if (Event::handle('StartActivityLink', array(&$this, &$xs, &$url))) { - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $url)); - Event::handle('EndActivityLink', array(&$this, &$xs, $url)); - } + $atom_feed = $profile->getAtomFeed(); - $id = $this->uri; + if (!empty($atom_feed)) { - if (Event::handle('StartActivityId', array(&$this, &$xs, &$id))) { - $xs->element('id', null, $id); - Event::handle('EndActivityId', array(&$this, &$xs, $id)); - } + $act->source = new ActivitySource(); + + // XXX: we should store the actual feed ID - $published = self::utcDate($this->created); + $act->source->id = $atom_feed; - if (Event::handle('StartActivityPublished', array(&$this, &$xs, &$published))) { - $xs->element('published', null, $published); - Event::handle('EndActivityPublished', array(&$this, &$xs, $published)); - } + // XXX: we should store the actual feed title - $updated = $published; // XXX: notices are usually immutable + $act->source->title = $profile->getBestName(); - if (Event::handle('StartActivityUpdated', array(&$this, &$xs, &$updated))) { - $xs->element('updated', null, $updated); - Event::handle('EndActivityUpdated', array(&$this, &$xs, $updated)); - } + $act->source->links['alternate'] = $profile->profileurl; + $act->source->links['self'] = $atom_feed; - $content = common_xml_safe_str($this->rendered); + $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE); + + $notice = $profile->getCurrentNotice(); - if (Event::handle('StartActivityContent', array(&$this, &$xs, &$content))) { - $xs->element('content', array('type' => 'html'), $content); - Event::handle('EndActivityContent', array(&$this, &$xs, $content)); - } + if (!empty($notice)) { + $act->source->updated = self::utcDate($notice->created); + } - // Most of our notices represent POSTing a NOTE. This is the default verb - // for activity streams, so we normally just leave it out. + $user = User::staticGet('id', $profile->id); - $verb = ActivityVerb::POST; + if (!empty($user)) { + $act->source->links['license'] = common_config('license', 'url'); + } + } - if (Event::handle('StartActivityVerb', array(&$this, &$xs, &$verb))) { - $xs->element('activity:verb', null, $verb); - Event::handle('EndActivityVerb', array(&$this, &$xs, $verb)); + 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)); } + + self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); - // We use the default behavior for activity streams: if there's no activity:object, - // then treat the entry itself as the object. Here, you can set the type of that object, - // which is normally a NOTE. + return $act; + } - $type = ActivityObject::NOTE; + // This has gotten way too long. Needs to be sliced up into functional bits + // or ideally exported to a utility class. - if (Event::handle('StartActivityDefaultObjectType', array(&$this, &$xs, &$type))) { - $xs->element('activity:object-type', null, $type); - Event::handle('EndActivityDefaultObjectType', array(&$this, &$xs, $type)); - } + function asAtomEntry($namespace=false, + $source=false, + $author=true, + $cur=null) + { + $act = $this->asActivity(); + $act->extra[] = $this->noticeInfo($cur); + return $act->asString($namespace, $author, $source); + } - // Since we usually use the entry itself as an object, we don't have an explicit - // object. Some extensions may want to add them (for photo, event, music, etc.). + /** + * 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 + */ - $objects = array(); + function noticeInfo($cur) + { + // local notice ID (useful to clients for ordering) - if (Event::handle('StartActivityObjects', array(&$this, &$xs, &$objects))) { - foreach ($objects as $object) { - $xs->raw($object->asString()); - } - Event::handle('EndActivityObjects', array(&$this, &$xs, $objects)); - } + $noticeInfoAttr = array('local_id' => $this->id); - $noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering) + // notice source $ns = $this->getSource(); @@ -1457,163 +1415,27 @@ class Notice extends Memcached_DataObject $noticeInfoAttr['source_link'] = $ns->url; if (!empty($ns->name)) { $noticeInfoAttr['source'] = '' - . htmlspecialchars($ns->name) + . htmlspecialchars($ns->name) . ''; } } } + // favorite and repeated + if (!empty($cur)) { $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; - $profile = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($profile->hasRepeated($this->id)) ? "true" : "false"; + $cp = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; } if (!empty($this->repeat_of)) { $noticeInfoAttr['repeat_of'] = $this->repeat_of; } - if (Event::handle('StartActivityNoticeInfo', array(&$this, &$xs, &$noticeInfoAttr))) { - $xs->element('statusnet:notice_info', $noticeInfoAttr, null); - Event::handle('EndActivityNoticeInfo', array(&$this, &$xs, $noticeInfoAttr)); - } - - $replyNotice = null; - - if ($this->reply_to) { - $replyNotice = Notice::staticGet('id', $this->reply_to); - } - - if (Event::handle('StartActivityInReplyTo', array(&$this, &$xs, &$replyNotice))) { - if (!empty($replyNotice)) { - $xs->element('link', array('rel' => 'related', - 'href' => $replyNotice->bestUrl())); - $xs->element('thr:in-reply-to', - array('ref' => $replyNotice->uri, - 'href' => $replyNotice->bestUrl())); - Event::handle('EndActivityInReplyTo', array(&$this, &$xs, $replyNotice)); - } - } - - $conv = null; - - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); - } - - if (Event::handle('StartActivityConversation', array(&$this, &$xs, &$conv))) { - if (!empty($conv)) { - $xs->element('link', array('rel' => 'ostatus:conversation', - 'href' => $conv->uri)); - } - Event::handle('EndActivityConversation', array(&$this, &$xs, $conv)); - } - - $replyProfiles = array(); - - $reply_ids = $this->getReplies(); - - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $replyProfiles[] = $profile; - } - } - - if (Event::handle('StartActivityAttentionProfiles', array(&$this, &$xs, &$replyProfiles))) { - foreach ($replyProfiles as $profile) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $profile->getUri())); - $xs->element('link', array('rel' => 'mentioned', - 'href' => $profile->getUri())); - } - Event::handle('EndActivityAttentionProfiles', array(&$this, &$xs, $replyProfiles)); - } - - $groups = $this->getGroups(); - - if (Event::handle('StartActivityAttentionGroups', array(&$this, &$xs, &$groups))) { - foreach ($groups as $group) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $group->permalink())); - $xs->element('link', array('rel' => 'mentioned', - 'href' => $group->permalink())); - } - Event::handle('EndActivityAttentionGroups', array(&$this, &$xs, $groups)); - } - - $repeat = null; - - if (!empty($this->repeat_of)) { - $repeat = Notice::staticGet('id', $this->repeat_of); - } - - if (Event::handle('StartActivityForward', array(&$this, &$xs, &$repeat))) { - if (!empty($repeat)) { - $xs->element('ostatus:forward', - array('ref' => $repeat->uri, - 'href' => $repeat->bestUrl())); - } - - Event::handle('EndActivityForward', array(&$this, &$xs, $repeat)); - } - - $tags = $this->getTags(); - - if (Event::handle('StartActivityCategories', array(&$this, &$xs, &$tags))) { - foreach ($tags as $tag) { - $xs->element('category', array('term' => $tag)); - } - Event::handle('EndActivityCategories', array(&$this, &$xs, $tags)); - } - - // Enclosures - - $enclosures = array(); - - $attachments = $this->attachments(); - - foreach ($attachments as $attachment) { - $enclosure = $attachment->getEnclosure(); - if ($enclosure) { - $enclosures[] = $enclosure; - } - } - - if (Event::handle('StartActivityEnclosures', array(&$this, &$xs, &$enclosures))) { - foreach ($enclosures as $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); - } - Event::handle('EndActivityEnclosures', array(&$this, &$xs, $enclosures)); - } - - $lat = $this->lat; - $lon = $this->lon; - - if (Event::handle('StartActivityGeo', array(&$this, &$xs, &$lat, &$lon))) { - if (!empty($lat) && !empty($lon)) { - $xs->element('georss:point', null, $lat . ' ' . $lon); - } - Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon)); - } - - if (Event::handle('StartActivityEnd', array(&$this, &$xs))) { - $xs->elementEnd('entry'); - Event::handle('EndActivityEnd', array(&$this, &$xs)); - } - - return $xs->getString(); + return array('statusnet:notice_info', $noticeInfoAttr, null); } /** @@ -1625,6 +1447,7 @@ class Notice extends Memcached_DataObject * @param string $element one of 'subject', 'object', 'target' * @return string */ + function asActivityNoun($element) { $noun = ActivityObject::fromNotice($this); @@ -1867,10 +1690,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(); @@ -1892,7 +1715,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; @@ -1904,7 +1726,6 @@ class Notice extends Memcached_DataObject } } else if (!empty($lat) && !empty($lon)) { - $options['lat'] = $lat; $options['lon'] = $lon; @@ -1915,7 +1736,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; @@ -2032,6 +1852,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)) { @@ -2152,4 +1973,108 @@ class Notice extends Memcached_DataObject $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); + 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); + if ($max) { + $obj->whereAdd($max); + } + } }