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; }
/** * 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; }
/** * 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; }
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; }
/** * 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.')); } }
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; }
/** * 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!'); }
/** * 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); }
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(); }