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)
$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;
// @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();
$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();
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;
}
* 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
$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);
- throw new ClientException(_('You are banned from posting notices on this site.'));
+
+ // 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);
}
$notice = new Notice();
$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
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.'));
}
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.'));
}
}
$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;
}
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?
$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
* @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);
+ }
}
/**
*/
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);
}
$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);
if ($f2p->find()) {
while ($f2p->fetch()) {
$f = File::staticGet($f2p->file_id);
- $att[] = clone($f);
+ if ($f) {
+ $att[] = clone($f);
+ }
}
}
return $att;
function getStreamByIds($ids)
{
- $cache = common_memcache();
+ $cache = Cache::instance();
if (!empty($cache)) {
$notices = array();
$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);
$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();
$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();
1,
1
);
+
if ($conversation->N > 0) {
return true;
}
}
/**
- * @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;
}
$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;
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");
$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) {
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.'));
}
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;
$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);
return $groups;
}
- // 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($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();
- if (Event::handle('StartActivityStart', array(&$this, &$xs, &$attrs))) {
- $xs->elementStart('entry', $attrs);
- Event::handle('EndActivityStart', array(&$this, &$xs, &$attrs));
- }
+ if (Event::handle('StartNoticeAsActivity', array($this, &$act))) {
- if (Event::handle('StartActivitySource', array(&$this, &$xs))) {
+ $profile = $this->getProfile();
- if ($source) {
+ $act->actor = ActivityObject::fromProfile($profile);
+ $act->actor->extra[] = $profile->profileInfo($cur);
+ $act->verb = ActivityVerb::POST;
+ $act->objects[] = ActivityObject::fromNotice($this);
- $atom_feed = $profile->getAtomFeed();
+ // XXX: should this be handled by default processing for object entry?
- if (!empty($atom_feed)) {
+ $act->time = strtotime($this->created);
+ $act->link = $this->bestUrl();
- $xs->elementStart('source');
+ $act->content = common_xml_safe_str($this->rendered);
+ $act->id = $this->uri;
+ $act->title = common_xml_safe_str($this->content);
- // XXX: we should store the actual feed ID
+ // Categories
- $xs->element('id', null, $atom_feed);
+ $tags = $this->getTags();
- // XXX: we should store the actual feed title
+ foreach ($tags as $tag) {
+ $cat = new AtomCategory();
+ $cat->term = $tag;
- $xs->element('title', null, $profile->getBestName());
+ $act->categories[] = $cat;
+ }
- $xs->element('link', array('rel' => 'alternate',
- 'type' => 'text/html',
- 'href' => $profile->profileurl));
+ // Enclosures
+ // XXX: use Atom Media and/or File activity objects instead
- $xs->element('link', array('rel' => 'self',
- 'type' => 'application/atom+xml',
- 'href' => $atom_feed));
+ $attachments = $this->attachments();
- $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
+ foreach ($attachments as $attachment) {
+ $enclosure = $attachment->getEnclosure();
+ if ($enclosure) {
+ $act->enclosures[] = $enclosure;
+ }
+ }
- $notice = $profile->getCurrentNotice();
+ $ctx = new ActivityContext();
- if (!empty($notice)) {
- $xs->element('updated', null, self::utcDate($notice->created));
- }
+ if (!empty($this->reply_to)) {
+ $reply = Notice::staticGet('id', $this->reply_to);
+ if (!empty($reply)) {
+ $ctx->replyToID = $reply->uri;
+ $ctx->replyToUrl = $reply->bestUrl();
+ }
+ }
- $user = User::staticGet('id', $profile->id);
+ $ctx->location = $this->getLocation();
- if (!empty($user)) {
- $xs->element('link', array('rel' => 'license',
- 'href' => common_config('license', 'url')));
- }
+ $conv = null;
- $xs->elementEnd('source');
+ if (!empty($this->conversation)) {
+ $conv = Conversation::staticGet('id', $this->conversation);
+ if (!empty($conv)) {
+ $ctx->conversation = $conv->uri;
}
}
- 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));
- }
+ $reply_ids = $this->getReplies();
- $atomAuthor = '';
+ foreach ($reply_ids as $id) {
+ $profile = Profile::staticGet('id', $id);
+ if (!empty($profile)) {
+ $ctx->attention[] = $profile->getUri();
+ }
+ }
- if ($author) {
- $atomAuthor = $profile->asAtomAuthor($cur);
- }
+ $groups = $this->getGroups();
- if (Event::handle('StartActivityAuthor', array(&$this, &$xs, &$atomAuthor))) {
- if (!empty($atomAuthor)) {
- $xs->raw($atomAuthor);
- Event::handle('EndActivityAuthor', array(&$this, &$xs, &$atomAuthor));
+ foreach ($groups as $group) {
+ $ctx->attention[] = $group->getUri();
}
- }
- $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();
}
- }
- $url = $this->bestUrl();
+ $act->context = $ctx;
- 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));
- }
+ // Source
- $id = $this->uri;
+ $atom_feed = $profile->getAtomFeed();
- if (Event::handle('StartActivityId', array(&$this, &$xs, &$id))) {
- $xs->element('id', null, $id);
- Event::handle('EndActivityId', array(&$this, &$xs, $id));
- }
+ if (!empty($atom_feed)) {
- $published = self::utcDate($this->created);
+ $act->source = new ActivitySource();
- 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 ID
- $updated = $published; // XXX: notices are usually immutable
+ $act->source->id = $atom_feed;
- if (Event::handle('StartActivityUpdated', array(&$this, &$xs, &$updated))) {
- $xs->element('updated', null, $updated);
- Event::handle('EndActivityUpdated', array(&$this, &$xs, $updated));
- }
+ // XXX: we should store the actual feed title
- $content = common_xml_safe_str($this->rendered);
+ $act->source->title = $profile->getBestName();
- if (Event::handle('StartActivityContent', array(&$this, &$xs, &$content))) {
- $xs->element('content', array('type' => 'html'), $content);
- Event::handle('EndActivityContent', array(&$this, &$xs, $content));
- }
+ $act->source->links['alternate'] = $profile->profileurl;
+ $act->source->links['self'] = $atom_feed;
- // Most of our notices represent POSTing a NOTE. This is the default verb
- // for activity streams, so we normally just leave it out.
+ $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
- $verb = ActivityVerb::POST;
+ $notice = $profile->getCurrentNotice();
- if (Event::handle('StartActivityVerb', array(&$this, &$xs, &$verb))) {
- $xs->element('activity:verb', null, $verb);
- Event::handle('EndActivityVerb', array(&$this, &$xs, $verb));
- }
+ if (!empty($notice)) {
+ $act->source->updated = self::utcDate($notice->created);
+ }
- // 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.
+ $user = User::staticGet('id', $profile->id);
- $type = ActivityObject::NOTE;
+ 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;
+ }
- if (Event::handle('StartActivityDefaultObjectType', array(&$this, &$xs, &$type))) {
- $xs->element('activity:object-type', null, $type);
- Event::handle('EndActivityDefaultObjectType', array(&$this, &$xs, $type));
+ Event::handle('EndNoticeAsActivity', array($this, &$act));
}
- // 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.).
+ self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act);
- $objects = array();
+ return $act;
+ }
- if (Event::handle('StartActivityObjects', array(&$this, &$xs, &$objects))) {
- foreach ($objects as $object) {
- $xs->raw($object->asString());
- }
- Event::handle('EndActivityObjects', array(&$this, &$xs, $objects));
- }
+ // This has gotten way too long. Needs to be sliced up into functional bits
+ // or ideally exported to a utility class.
- $noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering)
+ 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 <statusnet:notice_info> element
+ */
+
+ function noticeInfo($cur)
+ {
+ // local notice ID (useful to clients for ordering)
+
+ $noticeInfoAttr = array('local_id' => $this->id);
+
+ // notice source
$ns = $this->getSource();
$noticeInfoAttr['source_link'] = $ns->url;
if (!empty($ns->name)) {
$noticeInfoAttr['source'] = '<a href="'
- . htmlspecialchars($ns->url)
+ . htmlspecialchars($ns->url)
. '" rel="nofollow">'
- . htmlspecialchars($ns->name)
+ . htmlspecialchars($ns->name)
. '</a>';
}
}
}
+ // 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()));
- }
- 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()));
- }
- 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);
}
/**
* @param string $element one of 'subject', 'object', 'target'
* @return string
*/
+
function asActivityNoun($element)
{
$noun = ActivityObject::fromNotice($this);
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 ||
$max_id)));
}
- $idkey = common_cache_key($cachekey);
+ $idkey = Cache::key($cachekey);
$idstr = $cache->get($idkey);
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
$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();
$options = array();
if (!empty($location_id) && !empty($location_ns)) {
-
$options['location_id'] = $location_id;
$options['location_ns'] = $location_ns;
}
} else if (!empty($lat) && !empty($lon)) {
-
$options['lat'] = $lat;
$options['lon'] = $lon;
$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;
$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();
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();
}
}
{
// 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)) {
$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));
+ }
+ }
}