const GROUP_SCOPE = 4;
const FOLLOWER_SCOPE = 8;
- protected $_profile = -1;
+ protected $_profile = array();
+ /**
+ * Will always return a profile, if anything fails it will
+ * (through _setProfile) throw a NoProfileException.
+ */
public function getProfile()
{
- if ($this->_profile === -1) {
+ if (!isset($this->_profile[$this->profile_id])) {
$this->_setProfile(Profile::getKV('id', $this->profile_id));
}
- return $this->_profile;
+ return $this->_profile[$this->profile_id];
}
public function _setProfile(Profile $profile=null)
if (!$profile instanceof Profile) {
throw new NoProfileException($this->profile_id);
}
- $this->_profile = $profile;
+ $this->_profile[$this->profile_id] = $profile;
}
function delete($useWhere=false)
$this->clearReplies();
$this->clearRepeats();
- $this->clearFaves();
$this->clearTags();
$this->clearGroupInboxes();
$this->clearFiles();
+ $this->clearAttentions();
- // NOTE: we don't clear inboxes
// NOTE: we don't clear queue items
}
return $this->uri;
}
+ /*
+ * @param $root boolean If true, link to just the conversation root.
+ *
+ * @return URL to conversation
+ */
+ public function getConversationUrl($anchor=true)
+ {
+ return Conversation::getUrlFromNotice($this, $anchor);
+ }
+
+ /*
+ * Get the local representation URL of this notice.
+ */
+ public function getLocalUrl()
+ {
+ return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
+ }
+
+ public function getTitle()
+ {
+ $title = null;
+ if (Event::handle('GetNoticeTitle', array($this, &$title))) {
+ // TRANS: Title of a notice posted without a title value.
+ // TRANS: %1$s is a user name, %2$s is the notice creation date/time.
+ $title = sprintf(_('%1$s\'s status on %2$s'),
+ $this->getProfile()->getFancyName(),
+ common_exact_date($this->created));
+ }
+ return $title;
+ }
+
+ /*
+ * Get the original representation URL of this notice.
+ */
public function getUrl()
{
// The risk is we start having empty urls and non-http uris...
- return $this->url ?: $this->uri;
+ // and we can't really handle any other protocol right now.
+ switch (true) {
+ case common_valid_http_url($this->url): // should we allow non-http/https URLs?
+ return $this->url;
+ case $this->isLocal():
+ // let's generate a valid link to our locally available notice on demand
+ return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
+ case common_valid_http_url($this->uri):
+ return $this->uri;
+ default:
+ common_debug('No URL available for notice: id='.$this->id);
+ throw new InvalidUrlException($this->url);
+ }
+ }
+
+ public function get_object_type($canonical=false) {
+ return $canonical
+ ? ActivityObject::canonicalType($this->object_type)
+ : $this->object_type;
}
public static function getByUri($uri)
* int 'location_ns' geoname namespace to interpret location_id
* int 'reply_to'; notice ID this is a reply to
* int 'repeat_of'; notice ID this is a repeat of
- * string 'uri' unique ID for notice; defaults to local notice URL
+ * string 'uri' unique ID for notice; a unique tag uri (can be url or anything too)
* string 'url' permalink to notice; defaults to local notice URL
* string 'rendered' rendered HTML version of content
* array 'replies' list of profile URIs for reply delivery in
$notice->created = common_sql_now();
}
+ if (!$notice->isLocal()) {
+ // Only do these checks for non-local notices. Local notices will generate these values later.
+ if (!common_valid_http_url($url)) {
+ common_debug('Bad notice URL: ['.$url.'], URI: ['.$uri.']. Cannot link back to original! This is normal for shared notices etc.');
+ }
+ if (empty($uri)) {
+ throw new ServerException('No URI for remote notice. Cannot accept that.');
+ }
+ }
+
$notice->content = $final;
$notice->source = $source;
throw new ClientException(_('You already repeated that notice.'));
}
- $notice->repeat_of = $repeat_of;
+ $notice->repeat_of = $repeat->id;
+ $notice->conversation = $repeat->conversation;
} else {
- $reply = self::getReplyTo($reply_to, $profile_id, $source, $final);
+ $reply = null;
- if (!empty($reply)) {
+ // If $reply_to is specified, we check that it exists, and then
+ // return it if it does
+ if (!empty($reply_to)) {
+ $reply = Notice::getKV('id', $reply_to);
+ } elseif (in_array($source, array('xmpp', 'mail', 'sms'))) {
+ // If the source lacks capability of sending the "reply_to"
+ // metadata, let's try to find an inline replyto-reference.
+ $reply = self::getInlineReplyTo($profile, $final);
+ }
+ if ($reply instanceof Notice) {
if (!$reply->inScope($profile)) {
// TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
// TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
$profile->nickname, $reply->id), 403);
}
- $notice->reply_to = $reply->id;
+ // If it's a repeat, the reply_to should be to the original
+ if (!empty($reply->repeat_of)) {
+ $notice->reply_to = $reply->repeat_of;
+ } else {
+ $notice->reply_to = $reply->id;
+ }
+ // But the conversation ought to be the same :)
$notice->conversation = $reply->conversation;
- // If the original is private to a group, and notice has no group specified,
- // make it to the same group(s)
+ // If the original is private to a group, and notice has
+ // no group specified, make it to the same group(s)
if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) {
$groups = array();
$changed = false;
- if (empty($uri)) {
- $notice->uri = common_notice_uri($notice);
+ // We can only get here if it's a local notice, since remote notices
+ // should've bailed out earlier due to lacking a URI.
+ if (empty($notice->uri)) {
+ $notice->uri = sprintf('%s%s=%d:%s=%s',
+ TagURI::mint(),
+ 'noticeId', $notice->id,
+ 'objectType', $notice->get_object_type(true));
$changed = true;
}
if (common_config('attachments', 'process_links')) {
// @fixme validation?
foreach (array_unique($urls) as $url) {
- File::processNew($url, $this->id);
+ try {
+ File::processNew($url, $this->id);
+ } catch (ServerException $e) {
+ // Could not save URL. Log it?
+ }
}
}
}
* @private callback
*/
function saveUrl($url, $notice_id) {
- File::processNew($url, $notice_id);
+ try {
+ File::processNew($url, $notice_id);
+ } catch (ServerException $e) {
+ // Could not save URL. Log it?
+ }
}
static function checkDupes($profile_id, $content) {
return true;
}
- protected $_attachments = -1;
+ protected $_attachments = array();
function attachments() {
-
- if ($this->_attachments != -1) {
- return $this->_attachments;
+ if (isset($this->_attachments[$this->id])) {
+ return $this->_attachments[$this->id];
}
$f2ps = File_to_post::listGet('post_id', array($this->id));
$files = File::multiGet('id', $ids);
- $this->_attachments = $files->fetchAll();
+ $this->_attachments[$this->id] = $files->fetchAll();
- return $this->_attachments;
+ return $this->_attachments[$this->id];
}
function _setAttachments($attachments)
{
- $this->_attachments = $attachments;
+ $this->_attachments[$this->id] = $attachments;
}
function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
}
}
- if (is_null($groups)) {
- $groups = $this->getGroups();
- }
-
if (is_null($recipients)) {
$recipients = $this->getReplies();
}
- $users = $this->getSubscribedUsers();
- $ptags = $this->getProfileTags();
-
- // FIXME: kind of ignoring 'transitional'...
- // we'll probably stop supporting inboxless mode
- // in 0.9.x
-
$ni = array();
// Give plugins a chance to add folks in at start...
if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) {
+ $users = $this->getSubscribedUsers();
foreach ($users as $id) {
$ni[$id] = NOTICE_INBOX_SOURCE_SUB;
}
+ if (is_null($groups)) {
+ $groups = $this->getGroups();
+ }
foreach ($groups as $group) {
$users = $group->getUserMembers();
foreach ($users as $id) {
}
}
- foreach ($ptags as $ptag) {
- $users = $ptag->getUserSubscribers();
- foreach ($users as $id) {
- if (!array_key_exists($id, $ni)) {
- $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG;
- }
+ $ptAtts = $this->getAttentionsFromProfileTags();
+ foreach ($ptAtts as $key=>$val) {
+ if (!array_key_exists($key, $ni)) {
+ $ni[$key] = $val;
}
}
return $ni;
}
- /**
- * Adds this notice to the inboxes of each local user who should receive
- * it, based on author subscriptions, group memberships, and @-replies.
- *
- * Warning: running a second time currently will make items appear
- * multiple times in users' inboxes.
- *
- * @fixme make more robust against errors
- * @fixme break up massive deliveries to smaller background tasks
- *
- * @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
- */
- function addToInboxes(array $groups=null, array $recipients=null)
- {
- $ni = $this->whoGets($groups, $recipients);
-
- $ids = array_keys($ni);
-
- // Bulk insert
- Inbox::bulkInsert($this, $ids);
-
- return;
- }
-
function getSubscribedUsers()
{
$user = new User();
return $ptags;
}
+ public function getAttentionsFromProfileTags()
+ {
+ $ni = array();
+ $ptags = $this->getProfileTags();
+ foreach ($ptags as $ptag) {
+ $users = $ptag->getUserSubscribers();
+ foreach ($users as $id) {
+ $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG;
+ }
+ }
+ return $ni;
+ }
+
/**
* Record this notice to the given group inboxes for delivery.
* Overrides the regular parsing of !group markup.
$sender = Profile::getKV($this->profile_id);
foreach (array_unique($uris) as $uri) {
-
- $profile = Profile::fromURI($uri);
-
- if (!$profile instanceof Profile) {
+ try {
+ $profile = Profile::fromUri($uri);
+ } catch (UnknownUriException $e) {
common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'");
continue;
}
return array();
}
- $sender = Profile::getKV($this->profile_id);
+ $sender = $this->getProfile();
$replied = array();
// If it's a reply, save for the replied-to author
try {
$parent = $this->getParent();
- $author = $parent->getProfile();
- if ($author instanceof Profile) {
- $this->saveReply($author->id);
- $replied[$author->id] = 1;
- self::blow('reply:stream:%d', $author->id);
- }
+ $parentauthor = $parent->getProfile();
+ $this->saveReply($parentauthor->id);
+ $replied[$parentauthor->id] = 1;
+ self::blow('reply:stream:%d', $parentauthor->id);
} catch (Exception $e) {
// Not a reply, since it has no parent!
}
return $reply;
}
- protected $_replies = -1;
+ protected $_replies = array();
/**
* Pull the complete list of @-reply targets for this notice.
*/
function getReplies()
{
- if ($this->_replies != -1) {
- return $this->_replies;
+ if (isset($this->_replies[$this->id])) {
+ return $this->_replies[$this->id];
}
$replyMap = Reply::listGet('notice_id', array($this->id));
$ids[] = $reply->profile_id;
}
- $this->_replies = $ids;
+ $this->_replies[$this->id] = $ids;
return $ids;
}
function _setReplies($replies)
{
- $this->_replies = $replies;
+ $this->_replies[$this->id] = $replies;
}
/**
* @return array of Group objects
*/
- protected $_groups = -1;
+ protected $_groups = array();
function getGroups()
{
return array();
}
- if ($this->_groups != -1)
- {
- return $this->_groups;
+ if (isset($this->_groups[$this->id])) {
+ return $this->_groups[$this->id];
}
$gis = Group_inbox::listGet('notice_id', array($this->id));
$groups = User_group::multiGet('id', $ids);
- $this->_groups = $groups->fetchAll();
+ $this->_groups[$this->id] = $groups->fetchAll();
- return $this->_groups;
+ return $this->_groups[$this->id];
}
function _setGroups($groups)
{
- $this->_groups = $groups;
+ $this->_groups[$this->id] = $groups;
}
/**
$act->id = $this->uri;
$act->time = strtotime($this->created);
- $act->link = $this->bestUrl();
+ try {
+ $act->link = $this->getUrl();
+ } catch (InvalidUrlException $e) {
+ // The notice is probably a share or similar, which don't
+ // have a representational URL of their own.
+ }
$act->content = common_xml_safe_str($this->rendered);
$profile = $this->getProfile();
try {
$reply = $this->getParent();
- $ctx->replyToID = $reply->uri;
- $ctx->replyToUrl = $reply->bestUrl();
+ $ctx->replyToID = $reply->getUri();
+ $ctx->replyToUrl = $reply->getUrl();
} catch (Exception $e) {
// This is not a reply to something
}
// favorite and repeated
+ $scoped = null;
if (!empty($cur)) {
- $cp = $cur->getProfile();
- $noticeInfoAttr['favorite'] = ($cp->hasFave($this)) ? "true" : "false";
- $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this)) ? "true" : "false";
+ $scoped = $cur->getProfile();
+ $noticeInfoAttr['repeated'] = ($scoped->hasRepeated($this)) ? "true" : "false";
}
if (!empty($this->repeat_of)) {
$noticeInfoAttr['repeat_of'] = $this->repeat_of;
}
+ Event::handle('StatusNetApiNoticeInfo', array($this, &$noticeInfoAttr, $scoped));
+
return array('statusnet:notice_info', $noticeInfoAttr, null);
}
return $noun->asString('activity:' . $element);
}
- function bestUrl()
- {
- if (!empty($this->url)) {
- return $this->url;
- } else if (!empty($this->uri) && preg_match('/^https?:/', $this->uri)) {
- return $this->uri;
- } else {
- return common_local_url('shownotice',
- array('notice' => $this->id));
- }
- }
-
-
/**
* Determine which notice, if any, a new notice is in reply to.
*
* For conversation tracking, we try to see where this notice fits
- * in the tree. Rough algorithm is:
- *
- * if (reply_to is set and valid) {
- * return reply_to;
- * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) {
- * return ID of last notice by initial @name in content;
- * }
+ * in the tree. Beware that this may very well give false positives
+ * and add replies to wrong threads (if there have been newer posts
+ * by the same user as we're replying to).
*
- * Note that all @nickname instances will still be used to save "reply" records,
- * so the notice shows up in the mentioned users' "replies" tab.
- *
- * @param integer $reply_to ID passed in by Web or API
- * @param integer $profile_id ID of author
- * @param string $source Source tag, like 'web' or 'gwibber'
+ * @param Profile $sender Author profile
* @param string $content Final notice content
*
* @return integer ID of replied-to notice, or null for not a reply.
*/
- static function getReplyTo($reply_to, $profile_id, $source, $content)
+ static function getInlineReplyTo(Profile $sender, $content)
{
- static $lb = array('xmpp', 'mail', 'sms', 'omb');
-
- // If $reply_to is specified, we check that it exists, and then
- // return it if it does
-
- if (!empty($reply_to)) {
- $reply_notice = Notice::getKV('id', $reply_to);
- if ($reply_notice instanceof Notice) {
- return $reply_notice;
- }
- }
-
- // If it's not a "low bandwidth" source (one where you can't set
- // a reply_to argument), we return. This is mostly web and API
- // clients.
-
- if (!in_array($source, $lb)) {
- return null;
- }
-
// Is there an initial @ or T?
-
- if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) ||
- preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) {
+ if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match)
+ || preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) {
$nickname = common_canonical_nickname($match[1]);
} else {
return null;
}
// Figure out who that is.
-
- $sender = Profile::getKV('id', $profile_id);
- if (!$sender instanceof Profile) {
- return null;
- }
-
$recipient = common_relative_profile($sender, $nickname, common_sql_now());
- if (!$recipient instanceof Profile) {
- return null;
- }
-
- // Get their last notice
-
- $last = $recipient->getCurrentNotice();
-
- if ($last instanceof Notice) {
- return $last;
+ if ($recipient instanceof Profile) {
+ // Get their last notice
+ $last = $recipient->getCurrentNotice();
+ if ($last instanceof Notice) {
+ return $last;
+ }
+ // Maybe in the future we want to handle something else below
+ // so don't return getCurrentNotice() immediately.
}
return null;
/**
* Convenience function for posting a repeat of an existing message.
*
- * @param int $repeater_id: profile ID of user doing the repeat
+ * @param Profile $repeater Profile which is doing the repeat
* @param string $source: posting source key, eg 'web', 'api', etc
* @return Notice
*
* @throws Exception on failure or permission problems
*/
- function repeat($repeater_id, $source)
+ function repeat(Profile $repeater, $source)
{
- $author = Profile::getKV('id', $this->profile_id);
+ $author = $this->getProfile();
// TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'.
// TRANS: %1$s is the repeated user's name, %2$s is the repeated notice.
$content = sprintf(_('RT @%1$s %2$s'),
- $author->nickname,
+ $author->getNickname(),
$this->content);
- $maxlen = common_config('site', 'textlimit');
- if ($maxlen > 0 && mb_strlen($content) > $maxlen) {
- // Web interface and current Twitter API clients will
- // pull the original notice's text, but some older
- // clients and RSS/Atom feeds will see this trimmed text.
- //
- // Unfortunately this is likely to lose tags or URLs
- // at the end of long notices.
- $content = mb_substr($content, 0, $maxlen - 4) . ' ...';
- }
-
// Scope is same as this one's
-
- return self::saveNew($repeater_id,
+ return self::saveNew($repeater->id,
$content,
$source,
array('repeat_of' => $this->id,
return $options;
}
+ function clearAttentions()
+ {
+ $att = new Attention();
+ $att->notice_id = $this->getID();
+
+ if ($att->find()) {
+ while ($att->fetch()) {
+ // Can't do delete() on the object directly since it won't remove all of it
+ $other = clone($att);
+ $other->delete();
+ }
+ }
+ }
+
function clearReplies()
{
$replyNotice = new Notice();
}
}
- function clearFaves()
- {
- $fave = new Fave();
- $fave->notice_id = $this->id;
-
- if ($fave->find()) {
- while ($fave->fetch()) {
- self::blow('fave:ids_by_user_own:%d', $fave->user_id);
- self::blow('fave:ids_by_user_own:%d;last', $fave->user_id);
- self::blow('fave:ids_by_user:%d', $fave->user_id);
- self::blow('fave:ids_by_user:%d;last', $fave->user_id);
- $fave->delete();
- }
- }
-
- $fave->free();
- }
-
function clearTags()
{
$tag = new Notice_tag();
$notice->_setProfile($map[$notice->profile_id]);
}
} catch (NoProfileException $e) {
- common_log(LOG_WARNING, "Failed to fill profile in Notice with non-existing entry for profile_id: {$e->id}");
+ common_log(LOG_WARNING, "Failed to fill profile in Notice with non-existing entry for profile_id: {$e->profile_id}");
unset($notices[$entry]);
}
}
}
}
- static function _idsOf(&$notices)
+ static function _idsOf(array &$notices)
{
$ids = array();
foreach ($notices as $notice) {
- $ids[] = $notice->id;
+ $ids[$notice->id] = true;
}
- $ids = array_unique($ids);
- return $ids;
+ return array_keys($ids);
}
static function fillAttachments(&$notices)
}
}
- protected $_faves;
-
- /**
- * All faves of this notice
- *
- * @return array Array of Fave objects
- */
-
- function getFaves()
- {
- if (isset($this->_faves) && is_array($this->_faves)) {
- return $this->_faves;
- }
- $faveMap = Fave::listGet('notice_id', array($this->id));
- $this->_faves = $faveMap[$this->id];
- return $this->_faves;
- }
-
- function _setFaves($faves)
- {
- $this->_faves = $faves;
- }
-
- static function fillFaves(&$notices)
- {
- $ids = self::_idsOf($notices);
- $faveMap = Fave::listGet('notice_id', $ids);
- $cnt = 0;
- $faved = array();
- foreach ($faveMap as $id => $faves) {
- $cnt += count($faves);
- if (count($faves) > 0) {
- $faved[] = $id;
- }
- }
- foreach ($notices as $notice) {
- $faves = $faveMap[$notice->id];
- $notice->_setFaves($faves);
- }
- }
-
static function fillReplies(&$notices)
{
$ids = self::_idsOf($notices);
}
}
- protected $_repeats;
+ protected $_repeats = array();
function getRepeats()
{
- if (isset($this->_repeats) && is_array($this->_repeats)) {
- return $this->_repeats;
+ if (isset($this->_repeats[$this->id])) {
+ return $this->_repeats[$this->id];
}
$repeatMap = Notice::listGet('repeat_of', array($this->id));
- $this->_repeats = $repeatMap[$this->id];
- return $this->_repeats;
+ $this->_repeats[$this->id] = $repeatMap[$this->id];
+ return $this->_repeats[$this->id];
}
function _setRepeats($repeats)
{
- $this->_repeats = $repeats;
+ $this->_repeats[$this->id] = $repeats;
}
static function fillRepeats(&$notices)