Esempio n. 1
0
 public static function addNew(Notice $notice, Profile $actor = null)
 {
     if (is_null($actor)) {
         $actor = $notice->getProfile();
     }
     if ($notice->getProfile()->hasRole(Profile_role::DELETED)) {
         // Don't emit notices if the notice author is (being) deleted
         return false;
     }
     $act = new Activity();
     $act->verb = ActivityVerb::DELETE;
     $act->time = time();
     $act->id = $notice->getUri();
     $act->content = sprintf(_m('<a href="%1$s">%2$s</a> deleted notice <a href="%3$s">{{%4$s}}</a>.'), htmlspecialchars($actor->getUrl()), htmlspecialchars($actor->getBestName()), htmlspecialchars($notice->getUrl()), htmlspecialchars($notice->getUri()));
     $act->actor = $actor->asActivityObject();
     $act->target = new ActivityObject();
     // We don't save the notice object, as it's supposed to be removed!
     $act->target->id = $notice->getUri();
     $act->target->type = $notice->getObjectType();
     $act->objects = array(clone $act->target);
     $url = $notice->getUrl();
     $act->selfLink = $url;
     $act->editLink = $url;
     // This will make ActivityModeration run saveObjectFromActivity which adds
     // a new Deleted_notice entry in the database as well as deletes the notice
     // if the actor has permission to do so.
     $stored = Notice::saveActivity($act, $actor);
     return $stored;
 }
Esempio n. 2
0
 /**
  * Save a favorite record.
  * @fixme post-author notification should be moved here
  *
  * @param Profile $actor  the local or remote Profile who favorites
  * @param Notice  $target the notice that is favorited
  * @return Fave record on success
  * @throws Exception on failure
  */
 static function addNew(Profile $actor, Notice $target)
 {
     if (self::existsForProfile($target, $actor)) {
         // TRANS: Client error displayed when trying to mark a notice as favorite that already is a favorite.
         throw new AlreadyFulfilledException(_('You have already favorited this!'));
     }
     $act = new Activity();
     $act->type = ActivityObject::ACTIVITY;
     $act->verb = ActivityVerb::FAVORITE;
     $act->time = time();
     $act->id = self::newUri($actor, $target, common_sql_date($act->time));
     $act->title = _("Favor");
     // TRANS: Message that is the "content" of a favorite (%1$s is the actor's nickname, %2$ is the favorited
     //        notice's nickname and %3$s is the content of the favorited notice.)
     $act->content = sprintf(_('%1$s favorited something by %2$s: %3$s'), $actor->getNickname(), $target->getProfile()->getNickname(), $target->rendered ?: $target->content);
     $act->actor = $actor->asActivityObject();
     $act->target = $target->asActivityObject();
     $act->objects = array(clone $act->target);
     $url = common_local_url('AtomPubShowFavorite', array('profile' => $actor->id, 'notice' => $target->id));
     $act->selfLink = $url;
     $act->editLink = $url;
     // saveActivity will in turn also call Fave::saveActivityObject which does
     // what this function used to do before this commit.
     $stored = Notice::saveActivity($act, $actor);
     return $stored;
 }
Esempio n. 3
0
 /**
  * Process an incoming post activity from this remote feed.
  * @param Activity $activity
  * @param string $method 'push' or 'salmon'
  * @return mixed saved Notice or false
  */
 public function processPost($activity, $method)
 {
     $actor = ActivityUtils::checkAuthorship($activity, $this->localProfile());
     $options = array('is_local' => Notice::REMOTE);
     try {
         $stored = Notice::saveActivity($activity, $actor, $options);
         Ostatus_source::saveNew($stored, $this, $method);
     } catch (Exception $e) {
         common_log(LOG_ERR, "OStatus save of remote message {$sourceUri} failed: " . $e->getMessage());
         throw $e;
     }
     return $stored;
 }
Esempio n. 4
0
 function saveNew($profile, $event, $verb, $options = array())
 {
     $eventNotice = $event->getNotice();
     $options = array_merge(array('source' => 'web'), $options);
     $act = new Activity();
     $act->type = ActivityObject::ACTIVITY;
     $act->verb = $verb;
     $act->time = $options['created'] ? strtotime($options['created']) : time();
     $act->title = _m("RSVP");
     $act->actor = $profile->asActivityObject();
     $act->target = $eventNotice->asActivityObject();
     $act->objects = array(clone $act->target);
     $act->content = RSVP::toHTML($profile, $event, self::codeFor($verb));
     $act->id = common_local_url('showrsvp', array('id' => UUID::gen()));
     $act->link = $act->id;
     $saved = Notice::saveActivity($act, $profile, $options);
     return $saved;
 }
 /**
  * Handle a posted object from Salmon
  *
  * @param Activity $activity activity to handle
  * @param mixed    $target   user or group targeted
  *
  * @return boolean hook value
  */
 function onStartHandleSalmonTarget(Activity $activity, $target)
 {
     if (!$this->isMyActivity($activity)) {
         return true;
     }
     $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
     if ($target instanceof User_group || $target->isGroup()) {
         $uri = $target->getUri();
         if (!array_key_exists($uri, $activity->context->attention)) {
             // @todo FIXME: please document (i18n).
             // TRANS: Client exception thrown when ...
             throw new ClientException(_('Object not posted to this group.'));
         }
     } elseif ($target instanceof Profile && $target->isLocal()) {
         $original = null;
         // FIXME: Shouldn't favorites show up with a 'target' activityobject?
         if (!ActivityUtils::compareTypes($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) {
             // If this is not a post, it's a verb targeted at something (such as a Favorite attached to a note)
             if (!empty($activity->objects[0]->id)) {
                 $activity->context->replyToID = $activity->objects[0]->id;
             }
         }
         if (!empty($activity->context->replyToID)) {
             $original = Notice::getKV('uri', $activity->context->replyToID);
         }
         if ((!$original instanceof Notice || $original->profile_id != $target->id) && !array_key_exists($target->getUri(), $activity->context->attention)) {
             // @todo FIXME: Please document (i18n).
             // TRANS: Client exception when ...
             throw new ClientException(_('Object not posted to this user.'));
         }
     } else {
         // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled.
         throw new ServerException(_('Do not know how to handle this kind of target.'));
     }
     $oactor = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
     $actor = $oactor->localProfile();
     // FIXME: will this work in all cases? I made it work for Favorite...
     if (ActivityUtils::compareTypes($activity->verb, array(ActivityVerb::POST))) {
         $object = $activity->objects[0];
     } else {
         $object = $activity;
     }
     $options = array('uri' => $object->id, 'url' => $object->link, 'is_local' => Notice::REMOTE, 'source' => 'ostatus');
     if (!isset($this->oldSaveNew)) {
         $notice = Notice::saveActivity($activity, $actor, $options);
     } else {
         $notice = $this->saveNoticeFromActivity($activity, $actor, $options);
     }
     return false;
 }
Esempio n. 6
0
 /**
  * Given a parsed ActivityStreams activity, save it into a notice
  * and other data structures.
  *
  * @param Activity $activity
  * @param Profile $actor
  * @param array $options=array()
  *
  * @return Notice the resulting notice
  */
 function saveNoticeFromActivity(Activity $activity, Profile $actor, array $options = array())
 {
     if (count($activity->objects) != 1) {
         // TRANS: Exception thrown when there are too many activity objects.
         throw new Exception(_m('Too many activity objects.'));
     }
     $happeningObj = $activity->objects[0];
     if ($happeningObj->type != Happening::OBJECT_TYPE) {
         // TRANS: Exception thrown when event plugin comes across a non-event type object.
         throw new Exception(_m('Wrong type for object.'));
     }
     $dtstart = $happeningObj->element->getElementsByTagName('dtstart');
     if ($dtstart->length == 0) {
         // TRANS: Exception thrown when has no start date
         throw new Exception(_m('No start date for event.'));
     }
     $dtend = $happeningObj->element->getElementsByTagName('dtend');
     if ($dtend->length == 0) {
         // TRANS: Exception thrown when has no end date
         throw new Exception(_m('No end date for event.'));
     }
     // convert RFC3339 dates delivered in Activity Stream to MySQL DATETIME date format
     $start_time = new DateTime($dtstart->item(0)->nodeValue);
     $start_time->setTimezone(new DateTimeZone('UTC'));
     $start_time = $start_time->format('Y-m-d H:i:s');
     $end_time = new DateTime($dtend->item(0)->nodeValue);
     $end_time->setTimezone(new DateTimeZone('UTC'));
     $end_time = $end_time->format('Y-m-d H:i:s');
     // location is optional
     $location = null;
     $location_object = $happeningObj->element->getElementsByTagName('location');
     if ($location_object->length > 0) {
         $location = $location_object->item(0)->nodeValue;
     }
     // url is optional
     $url = null;
     $url_object = $happeningObj->element->getElementsByTagName('url');
     if ($url_object->length > 0) {
         $url = $url_object->item(0)->nodeValue;
     }
     switch ($activity->verb) {
         case ActivityVerb::POST:
             // FIXME: get startTime, endTime, location and URL
             $notice = Happening::saveNew($actor, $start_time, $end_time, $happeningObj->title, $location, $happeningObj->summary, $url, $options);
             break;
         case RSVP::POSITIVE:
         case RSVP::NEGATIVE:
         case RSVP::POSSIBLE:
             return Notice::saveActivity($activity, $actor, $options);
             break;
         default:
             // TRANS: Exception thrown when event plugin comes across a undefined verb.
             throw new Exception(_m('Unknown verb for events.'));
     }
 }
Esempio n. 7
0
 protected function saveObjectFromActivity(Activity $act, Notice $stored, array $options = array())
 {
     assert($this->isMyActivity($act));
     // The below algorithm is mainly copied from the previous Ostatus_profile->processShare()
     if (count($act->objects) !== 1) {
         // TRANS: Client exception thrown when trying to share multiple activities at once.
         throw new ClientException(_m('Can only handle share activities with exactly one object.'));
     }
     $shared = $act->objects[0];
     if (!$shared instanceof Activity) {
         // TRANS: Client exception thrown when trying to share a non-activity object.
         throw new ClientException(_m('Can only handle shared activities.'));
     }
     $sharedUri = $shared->id;
     if (!empty($shared->objects[0]->id)) {
         // Because StatusNet since commit 8cc4660 sets $shared->id to a TagURI which
         // f***s up federation, because the URI is no longer recognised by the origin.
         // So we set it to the object ID if it exists, otherwise we trust $shared->id
         $sharedUri = $shared->objects[0]->id;
     }
     if (empty($sharedUri)) {
         throw new ClientException(_m('Shared activity does not have an id'));
     }
     try {
         // First check if we have the shared activity. This has to be done first, because
         // we can't use these functions to "ensureActivityObjectProfile" of a local user,
         // who might be the creator of the shared activity in question.
         $sharedNotice = Notice::getByUri($sharedUri);
     } catch (NoResultException $e) {
         // If no locally stored notice is found, process it!
         // TODO: Remember to check Deleted_notice!
         // TODO: If a post is shared that we can't retrieve - what to do?
         $other = Ostatus_profile::ensureActivityObjectProfile($shared->actor);
         $sharedNotice = Notice::saveActivity($shared, $other->localProfile(), array('source' => 'share'));
     } catch (FeedSubException $e) {
         // Remote feed could not be found or verified, should we
         // transform this into an "RT @user Blah, blah, blah..."?
         common_log(LOG_INFO, __METHOD__ . ' got a ' . get_class($e) . ': ' . $e->getMessage());
         return false;
     }
     // Setting this here because when the algorithm gets back to
     // Notice::saveActivity it will update the Notice object.
     $stored->repeat_of = $sharedNotice->getID();
     $stored->conversation = $sharedNotice->conversation;
     // We don't have to save a repeat in a separate table, we can
     // find repeats by just looking at the notice.repeat_of field.
     // By returning true here instead of something that evaluates
     // to false, we show that we have processed everything properly.
     return true;
 }
Esempio n. 8
0
 /**
  * This doPost saves a new notice, based on arguments
  *
  * If successful, will show the notice, or return an Ajax-y result.
  * If not, it will show an error message -- possibly Ajax-y.
  *
  * Also, if the notice input looks like a command, it will run the
  * command and show the results -- again, possibly ajaxy.
  *
  * @return void
  */
 protected function doPost()
 {
     assert($this->scoped instanceof Profile);
     // XXX: maybe an error instead...
     $user = $this->scoped->getUser();
     $content = $this->trimmed('status_textarea');
     $options = array('source' => 'web');
     Event::handle('StartSaveNewNoticeWeb', array($this, $user, &$content, &$options));
     if (empty($content)) {
         // TRANS: Client error displayed trying to send a notice without content.
         $this->clientError(_('No content!'));
     }
     $inter = new CommandInterpreter();
     $cmd = $inter->handle_command($user, $content);
     if ($cmd) {
         if (GNUsocial::isAjax()) {
             $cmd->execute(new AjaxWebChannel($this));
         } else {
             $cmd->execute(new WebChannel($this));
         }
         return;
     }
     if ($this->int('inreplyto')) {
         // Throws exception if the inreplyto Notice is given but not found.
         $parent = Notice::getByID($this->int('inreplyto'));
     } else {
         $parent = null;
     }
     $act = new Activity();
     $act->verb = ActivityVerb::POST;
     $act->time = time();
     $act->actor = $this->scoped->asActivityObject();
     $upload = null;
     try {
         // throws exception on failure
         $upload = MediaFile::fromUpload('attach', $this->scoped);
         if (Event::handle('StartSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options))) {
             $content .= ' ' . $upload->shortUrl();
         }
         Event::handle('EndSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options));
         // We could check content length here if the URL was added, but I'll just let it slide for now...
         $act->enclosures[] = $upload->getEnclosure();
     } catch (NoUploadedMediaException $e) {
         // simply no attached media to the new notice
     }
     // Reject notice if it is too long (without the HTML)
     // This is done after MediaFile::fromUpload etc. just to act the same as the ApiStatusesUpdateAction
     if (Notice::contentTooLong($content)) {
         // TRANS: Client error displayed when the parameter "status" is missing.
         // TRANS: %d is the maximum number of character for a notice.
         throw new ClientException(sprintf(_m('That\'s too long. Maximum notice size is %d character.', 'That\'s too long. Maximum notice size is %d characters.', Notice::maxContent()), Notice::maxContent()));
     }
     $act->context = new ActivityContext();
     if ($parent instanceof Notice) {
         $act->context->replyToID = $parent->getUri();
         $act->context->replyToUrl = $parent->getUrl(true);
         // maybe we don't have to send true here to force a URL?
     }
     if ($this->scoped->shareLocation()) {
         // use browser data if checked; otherwise profile data
         if ($this->arg('notice_data-geo')) {
             $locOptions = Notice::locationOptions($this->trimmed('lat'), $this->trimmed('lon'), $this->trimmed('location_id'), $this->trimmed('location_ns'), $this->scoped);
         } else {
             $locOptions = Notice::locationOptions(null, null, null, null, $this->scoped);
         }
         $act->context->location = Location::fromOptions($locOptions);
     }
     $content = $this->scoped->shortenLinks($content);
     // FIXME: Make sure NoticeTitle plugin gets a change to add the title to our activityobject!
     if (Event::handle('StartNoticeSaveWeb', array($this, $this->scoped, &$content, &$options))) {
         // FIXME: We should be able to get the attentions from common_render_content!
         // and maybe even directly save whether they're local or not!
         $act->context->attention = common_get_attentions($content, $this->scoped, $parent);
         $actobj = new ActivityObject();
         $actobj->type = ActivityObject::NOTE;
         $actobj->content = common_render_content($content, $this->scoped, $parent);
         // Finally add the activity object to our activity
         $act->objects[] = $actobj;
         $this->stored = Notice::saveActivity($act, $this->scoped, $options);
         if ($upload instanceof MediaFile) {
             $upload->attachToNotice($this->stored);
         }
         Event::handle('EndNoticeSaveWeb', array($this, $this->stored));
     }
     Event::handle('EndSaveNewNoticeWeb', array($this, $user, &$content, &$options));
     if (!GNUsocial::isAjax()) {
         $url = common_local_url('shownotice', array('notice' => $this->stored->id));
         common_redirect($url, 303);
     }
     return _('Saved the notice!');
 }
Esempio n. 9
0
 /**
  * Save a new notice bookmark
  *
  * @param Profile $profile     To save the bookmark for
  * @param string  $title       Title of the bookmark
  * @param string  $url         URL of the bookmark
  * @param array   $rawtags     array of tags
  * @param string  $description Description of the bookmark
  * @param array   $options     Options for the Notice::saveNew()
  *
  * @return Notice saved notice
  */
 static function addNew(Profile $actor, $title, $url, array $rawtags, $description, array $options = array())
 {
     $act = new Activity();
     $act->verb = ActivityVerb::POST;
     $act->time = time();
     $act->actor = $actor->asActivityObject();
     $actobj = new ActivityObject();
     $actobj->type = ActivityObject::BOOKMARK;
     $actobj->title = $title;
     $actobj->summary = $description;
     $actobj->extra[] = array('link', array('rel' => 'related', 'href' => $url), null);
     $act->objects[] = $actobj;
     $act->enclosures[] = $url;
     $tags = array();
     $replies = array();
     // filter "for:nickname" tags
     foreach ($rawtags as $tag) {
         if (strtolower(mb_substr($tag, 0, 4)) == 'for:') {
             // skip if done by caller
             if (!array_key_exists('replies', $options)) {
                 $nickname = mb_substr($tag, 4);
                 $other = common_relative_profile($actor, $nickname);
                 if (!empty($other)) {
                     $replies[] = $other->getUri();
                 }
             }
         } else {
             $tags[] = common_canonical_tag($tag);
         }
     }
     $hashtags = array();
     $taglinks = array();
     foreach ($tags as $tag) {
         $hashtags[] = '#' . $tag;
         $attrs = array('href' => Notice_tag::url($tag), 'rel' => $tag, 'class' => 'tag');
         $taglinks[] = XMLStringer::estring('a', $attrs, $tag);
     }
     // Use user's preferences for short URLs, if possible
     // FIXME: Should be possible to with the Profile object...
     try {
         $user = $actor->getUser();
         $shortUrl = File_redirection::makeShort($url, empty($user) ? null : $user);
     } catch (Exception $e) {
         // Don't let this stop us.
         $shortUrl = $url;
     }
     // TRANS: Rendered bookmark content.
     // TRANS: %1$s is a URL, %2$s the bookmark title, %3$s is the bookmark description,
     // TRANS: %4$s is space separated list of hash tags.
     $actobj->content = sprintf(_m('<span class="xfolkentry">' . '<a class="taggedlink" href="%1$s">%2$s</a> ' . '<span class="description">%3$s</span> ' . '<span class="meta">%4$s</span>' . '</span>'), htmlspecialchars($url), htmlspecialchars($title), htmlspecialchars($description), implode(' ', $taglinks));
     foreach ($tags as $term) {
         $catEl = new AtomCategory();
         $catEl->term = $term;
         $activity->categories[] = $catEl;
     }
     $options = array_merge(array('urls' => array($url), 'rendered' => $rendered, 'tags' => $tags, 'replies' => $replies, 'object_type' => ActivityObject::BOOKMARK), $options);
     return Notice::saveActivity($act, $actor, $options);
 }
Esempio n. 10
0
function linkback_save($source, $target, $response, $notice_or_user)
{
    $dupe = linkback_is_dupe('uri', $response->getEffectiveUrl());
    if (!$dupe) {
        $dupe = linkback_is_dupe('url', $response->getEffectiveUrl());
    }
    if (!$dupe) {
        $dupe = linkback_is_dupe('uri', $source);
    }
    if (!$dupe) {
        $dupe = linkback_is_dupe('url', $source);
    }
    $mf2 = new Mf2\Parser($response->getBody(), $response->getEffectiveUrl());
    $mf2 = $mf2->parse();
    $entry = linkback_find_entry($mf2, $target);
    if (!$entry) {
        preg_match('/<title>([^<]+)', $response->getBody(), $match);
        $entry = array('content' => array('html' => $response->getBody()), 'name' => $match[1] ? htmlspecialchars_decode($match[1]) : $source);
    }
    if (!$entry['url']) {
        $entry['url'] = array($response->getEffectiveUrl());
    }
    if (!$dupe) {
        $dupe = linkback_is_dupe('uri', $entry['url'][0]);
    }
    if (!$dupe) {
        $dupe = linkback_is_dupe('url', $entry['url'][0]);
    }
    $entry['type'] = linkback_entry_type($entry, $mf2, $target);
    list($profile, $author) = linkback_profile($entry, $mf2, $response, $target);
    list($content, $options) = linkback_notice($source, $notice_or_user, $entry, $author, $mf2);
    if ($dupe) {
        $orig = clone $dupe;
        try {
            // Ignore duplicate save error
            try {
                $dupe->saveKnownReplies($options['replies']);
            } catch (ServerException $ex) {
            }
            try {
                $dupe->saveKnownTags($options['tags']);
            } catch (ServerException $ex) {
            }
            try {
                $dupe->saveKnownUrls($options['urls']);
            } catch (ServerException $ex) {
            }
            if ($options['reply_to']) {
                $dupe->reply_to = $options['reply_to'];
            }
            if ($options['repeat_of']) {
                $dupe->repeat_of = $options['repeat_of'];
            }
            if ($dupe->reply_to != $orig->reply_to || $dupe->repeat_of != $orig->repeat_of) {
                $parent = Notice::getKV('id', $dupe->repost_of ? $dupe->repost_of : $dupe->reply_to);
                if ($parent instanceof Notice) {
                    // If we changed the reply_to or repeat_of we might live in a new conversation now
                    $dupe->conversation = $parent->conversation;
                }
            }
            if ($dupe->update($orig)) {
                $saved = $dupe;
            }
            if ($dupe->conversation != $orig->conversation && Conversation::noticeCount($orig->conversation) < 1) {
                // Delete empty conversation
                $emptyConversation = Conversation::getKV('id', $orig->conversation);
                $emptyConversation->delete();
            }
        } catch (Exception $e) {
            common_log(LOG_ERR, "Linkback update of remote message {$source} failed: " . $e->getMessage());
            return false;
        }
        common_log(LOG_INFO, "Linkback updated remote message {$source} as notice id {$saved->id}");
    } else {
        if ($entry['type'] == 'like' || $entry['type'] == 'reply' && $entry['rsvp']) {
            $act = new Activity();
            $act->type = ActivityObject::ACTIVITY;
            $act->time = $options['created'] ? strtotime($options['created']) : time();
            $act->title = $entry["name"] ? $entry["name"][0] : _m("Favor");
            $act->actor = $profile->asActivityObject();
            $act->target = $notice_or_user->asActivityObject();
            $act->objects = array(clone $act->target);
            // TRANS: Message that is the "content" of a favorite (%1$s is the actor's nickname, %2$ is the favorited
            //        notice's nickname and %3$s is the content of the favorited notice.)
            $act->content = sprintf(_('%1$s favorited something by %2$s: %3$s'), $profile->getNickname(), $notice_or_user->getProfile()->getNickname(), $notice_or_user->getRendered());
            if ($entry['rsvp']) {
                $act->content = $options['rendered'];
            }
            $act->verb = ActivityVerb::FAVORITE;
            if (strtolower($entry['rsvp'][0]) == 'yes') {
                $act->verb = 'http://activitystrea.ms/schema/1.0/rsvp-yes';
            } else {
                if (strtolower($entry['rsvp'][0]) == 'no') {
                    $act->verb = 'http://activitystrea.ms/schema/1.0/rsvp-no';
                } else {
                    if (strtolower($entry['rsvp'][0]) == 'maybe') {
                        $act->verb = 'http://activitystrea.ms/schema/1.0/rsvp-maybe';
                    }
                }
            }
            $act->id = $source;
            $act->link = $entry['url'][0];
            $options['source'] = 'linkback';
            $options['mentions'] = $options['replies'];
            unset($options['reply_to']);
            unset($options['repeat_of']);
            try {
                $saved = Notice::saveActivity($act, $profile, $options);
            } catch (Exception $e) {
                common_log(LOG_ERR, "Linkback save of remote message {$source} failed: " . $e->getMessage());
                return false;
            }
            common_log(LOG_INFO, "Linkback saved remote message {$source} as notice id {$saved->id}");
        } else {
            // Fallback is to make a notice manually
            try {
                $saved = Notice::saveNew($profile->id, $content, 'linkback', $options);
            } catch (Exception $e) {
                common_log(LOG_ERR, "Linkback save of remote message {$source} failed: " . $e->getMessage());
                return false;
            }
            common_log(LOG_INFO, "Linkback saved remote message {$source} as notice id {$saved->id}");
        }
    }
    return $saved->getLocalUrl();
}