public static function saveNew(Notice $notice, Profile $target, $reason = null) { try { $att = Attention::getByKeys(['notice_id' => $notice->getID(), 'profile_id' => $target->getID()]); throw new AlreadyFulfilledException('Attention already exists with reason: ' . var_export($att->reason, true)); } catch (NoResultException $e) { $att = new Attention(); $att->notice_id = $notice->getID(); $att->profile_id = $target->getID(); $att->reason = $reason; $att->created = common_sql_now(); $result = $att->insert(); if ($result === false) { throw new Exception('Failed Attention::saveNew for notice id==' . $notice->getID() . ' target id==' . $target->getID() . ', reason=="' . $reason . '"'); } } return $att; }
static function locFromStored(Notice $stored) { $loc = new Notice_location(); $loc->notice_id = $stored->getID(); if (!$loc->find(true)) { throw new NoResultException($loc); } return $loc->asLocation(); }
public function removeEntry(Profile $actor, Notice $target) { $fave = new Fave(); $fave->user_id = $actor->getID(); $fave->notice_id = $target->getID(); if (!$fave->find(true)) { // TRANS: Client error displayed when trying to remove a 'favor' when there is none in the first place. throw new AlreadyFulfilledException(_('This is already not favorited.')); } $result = $fave->delete(); if ($result === false) { common_log_db_error($fave, 'DELETE', __FILE__); // TRANS: Server error displayed when removing a favorite from the database fails. throw new ServerException(_('Could not delete favorite.')); } Fave::blowCacheForProfileId($actor->getID()); Fave::blowCacheForNoticeId($target->getID()); }
public function onEndShowThreadedNoticeTail(NoticeListItem $nli, Notice $notice, array $notices) { if ($this->prerender_replyforms) { $nli->out->elementStart('li', array('class' => 'notice-reply', 'style' => 'display: none;')); $replyForm = new NoticeForm($nli->out, array('inreplyto' => $notice->getID())); $replyForm->show(); $nli->out->elementEnd('li'); } return true; }
public function activityObjectFromNotice(Notice $notice) { $object = new ActivityObject(); $object->type = $notice->object_type ?: ActivityObject::NOTE; $object->id = $notice->getUri(); $object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $notice->getProfile()->getNickname()); $object->content = $notice->getRendered(); $object->link = $notice->getUrl(); $object->extra[] = array('status_net', array('notice_id' => $notice->getID())); return $object; }
public static function saveNew(Notice $notice, Profile $profile, $reason = null) { $att = new Attention(); $att->notice_id = $notice->getID(); $att->profile_id = $profile->getID(); $att->reason = $reason; $att->created = common_sql_now(); $result = $att->insert(); if ($result === false) { throw new Exception('Could not saveNew in Attention'); } return $att; }
static function processNew(File $file, Notice $notice) { static $seen = array(); $file_id = $file->getID(); $notice_id = $notice->getID(); if (!array_key_exists($notice_id, $seen)) { $seen[$notice_id] = array(); } if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) { try { $f2p = File_to_post::getByPK(array('post_id' => $notice_id, 'file_id' => $file_id)); } catch (NoResultException $e) { $f2p = new File_to_post(); $f2p->file_id = $file_id; $f2p->post_id = $notice_id; $f2p->insert(); $file->blowCache(); } $seen[$notice_id][] = $file_id; } }
/** * Handle distribution of a notice after we've saved it: * @li add to local recipient inboxes * @li send email notifications to local @-reply targets * @li run final EndNoticeSave plugin events * @li put any remaining post-processing into the queues * * If this function indicates failure, a warning will be logged * and the item is placed back in the queue to be re-run. * * @param Notice $notice * @return boolean true on success, false on failure */ public function handle(Notice $notice) { // We have to manually add attentions to non-profile subs and non-mentions $ptAtts = $notice->getAttentionsFromProfileTags(); foreach (array_keys($ptAtts) as $profile_id) { $profile = Profile::getKV('id', $profile_id); if ($profile instanceof Profile) { try { common_debug('Adding Attention for ' . $notice->getID() . ' profile ' . $profile->getID()); Attention::saveNew($notice, $profile); } catch (Exception $e) { $this->logit($notice, $e); } } } try { $notice->sendReplyNotifications(); } catch (Exception $e) { $this->logit($notice, $e); } try { Event::handle('EndNoticeDistribute', array($notice)); } catch (Exception $e) { $this->logit($notice, $e); } try { Event::handle('EndNoticeSave', array($notice)); } catch (Exception $e) { $this->logit($notice, $e); } try { // Enqueue for other handlers common_enqueue_notice($notice); } catch (Exception $e) { $this->logit($notice, $e); } return true; }
public static function getUrlFromNotice(Notice $notice, $anchor = true) { $conv = Conversation::getByID($notice->conversation); return $conv->getUrl($anchor ? $notice->getID() : null); }
static function saveActivity(Activity $act, Profile $actor, array $options = array()) { // First check if we're going to let this Activity through from the specific actor if (!$actor->hasRight(Right::NEWNOTICE)) { common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $actor->getNickname()); // TRANS: Client exception thrown when a user tries to post while being banned. throw new ClientException(_m('You are banned from posting notices on this site.'), 403); } if (common_config('throttle', 'enabled') && !self::checkEditThrottle($actor->id)) { common_log(LOG_WARNING, 'Excessive posting by profile #' . $actor->id . '; throttled.'); // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame. throw new ClientException(_m('Too many notices too fast; take a breather ' . 'and post again in a few minutes.')); } // Get ActivityObject properties $actobj = null; if (!empty($act->id)) { // implied object $options['uri'] = $act->id; $options['url'] = $act->link; } else { $actobj = count($act->objects) == 1 ? $act->objects[0] : null; if (!is_null($actobj) && !empty($actobj->id)) { $options['uri'] = $actobj->id; if ($actobj->link) { $options['url'] = $actobj->link; } elseif (preg_match('!^https?://!', $actobj->id)) { $options['url'] = $actobj->id; } } } $defaults = array('groups' => array(), 'is_local' => $actor->isLocal() ? self::LOCAL_PUBLIC : self::REMOTE, 'mentions' => array(), 'reply_to' => null, 'repeat_of' => null, 'scope' => null, 'source' => 'unknown', 'tags' => array(), 'uri' => null, 'url' => null, 'urls' => array(), 'distribute' => true); // options will have default values when nothing has been supplied $options = array_merge($defaults, $options); foreach (array_keys($defaults) as $key) { // Only convert the keynames we specify ourselves from 'defaults' array into variables ${$key} = $options[$key]; } extract($options, EXTR_SKIP); // dupe check $stored = new Notice(); if (!empty($uri) && !ActivityUtils::compareVerbs($act->verb, array(ActivityVerb::DELETE))) { $stored->uri = $uri; if ($stored->find()) { common_debug('cannot create duplicate Notice URI: ' . $stored->uri); // I _assume_ saving a Notice with a colliding URI means we're really trying to // save the same notice again... throw new AlreadyFulfilledException('Notice URI already exists'); } } $autosource = common_config('public', 'autosource'); // Sandboxed are non-false, but not 1, either if (!$actor->hasRight(Right::PUBLICNOTICE) || $source && $autosource && in_array($source, $autosource)) { // FIXME: ...what about remote nonpublic? Hmmm. That is, if we sandbox remote profiles... $stored->is_local = Notice::LOCAL_NONPUBLIC; } else { $stored->is_local = intval($is_local); } if (!$stored->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.'); } } $stored->profile_id = $actor->id; $stored->source = $source; $stored->uri = $uri; $stored->url = $url; $stored->verb = $act->verb; // Notice content. We trust local users to provide HTML we like, but of course not remote users. // FIXME: What about local users importing feeds? Mirror functions must filter out bad HTML first... $content = $act->content ?: $act->summary; if (is_null($content) && !is_null($actobj)) { $content = $actobj->content ?: $actobj->summary; } $stored->rendered = $actor->isLocal() ? $content : common_purify($content); // yeah, just don't use getRendered() here since it's not inserted yet ;) $stored->content = common_strip_html($stored->rendered); // Maybe a missing act-time should be fatal if the actor is not local? if (!empty($act->time)) { $stored->created = common_sql_date($act->time); } else { $stored->created = common_sql_now(); } $reply = null; if ($act->context instanceof ActivityContext && !empty($act->context->replyToID)) { $reply = self::getKV('uri', $act->context->replyToID); } if (!$reply instanceof Notice && $act->target instanceof ActivityObject) { $reply = self::getKV('uri', $act->target->id); } if ($reply instanceof Notice) { if (!$reply->inScope($actor)) { // 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). throw new ClientException(sprintf(_m('%1$s has no right to reply to notice %2$d.'), $actor->getNickname(), $reply->id), 403); } $stored->reply_to = $reply->id; $stored->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 (empty($groups) && $reply->scope & Notice::GROUP_SCOPE) { $replyGroups = $reply->getGroups(); foreach ($replyGroups as $group) { if ($actor->isMember($group)) { $groups[] = $group->id; } } } if (is_null($scope)) { $scope = $reply->scope; } } else { // If we don't know the reply, we might know the conversation! // This will happen if a known remote user replies to an // unknown remote user - within a known conversation. if (empty($stored->conversation) and !empty($act->context->conversation)) { $conv = Conversation::getKV('uri', $act->context->conversation); if ($conv instanceof Conversation) { common_debug('Conversation stitched together from (probably) a reply activity to unknown remote user. Activity creation time (' . $stored->created . ') should maybe be compared to conversation creation time (' . $conv->created . ').'); } else { // Conversation entry with specified URI was not found, so we must create it. common_debug('Conversation URI not found, so we will create it with the URI given in the context of the activity: ' . $act->context->conversation); // The insert in Conversation::create throws exception on failure $conv = Conversation::create($act->context->conversation, $stored->created); } $stored->conversation = $conv->getID(); unset($conv); } } // If it's not part of a conversation, it's the beginning of a new conversation. if (empty($stored->conversation)) { $conv = Conversation::create(); $stored->conversation = $conv->getID(); unset($conv); } $notloc = null; if ($act->context instanceof ActivityContext) { if ($act->context->location instanceof Location) { $notloc = Notice_location::fromLocation($act->context->location); } } else { $act->context = new ActivityContext(); } $stored->scope = self::figureOutScope($actor, $groups, $scope); foreach ($act->categories as $cat) { if ($cat->term) { $term = common_canonical_tag($cat->term); if (!empty($term)) { $tags[] = $term; } } } foreach ($act->enclosures as $href) { // @todo FIXME: Save these locally or....? $urls[] = $href; } if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) { if (empty($act->objects[0]->type)) { // Default type for the post verb is 'note', but we know it's // a 'comment' if it is in reply to something. $stored->object_type = empty($stored->reply_to) ? ActivityObject::NOTE : ActivityObject::COMMENT; } else { //TODO: Is it safe to always return a relative URI? The // JSON version of ActivityStreams always use it, so we // should definitely be able to handle it... $stored->object_type = ActivityUtils::resolveUri($act->objects[0]->type, true); } } if (Event::handle('StartNoticeSave', array(&$stored))) { // XXX: some of these functions write to the DB try { $result = $stored->insert(); // throws exception on error if ($notloc instanceof Notice_location) { $notloc->notice_id = $stored->getID(); $notloc->insert(); } $orig = clone $stored; // for updating later in this try clause $object = null; Event::handle('StoreActivityObject', array($act, $stored, $options, &$object)); if (empty($object)) { throw new ServerException('Unsuccessful call to StoreActivityObject ' . $stored->getUri() . ': ' . $act->asString()); } // If something changed in the Notice during StoreActivityObject $stored->update($orig); } catch (Exception $e) { if (empty($stored->id)) { common_debug('Failed to save stored object entry in database (' . $e->getMessage() . ')'); } else { common_debug('Failed to store activity object in database (' . $e->getMessage() . '), deleting notice id ' . $stored->id); $stored->delete(); } throw $e; } } if (!$stored instanceof Notice) { throw new ServerException('StartNoticeSave did not give back a Notice'); } // Only save 'attention' and metadata stuff (URLs, tags...) stuff if // the activityverb is a POST (since stuff like repeat, favorite etc. // reasonably handle notifications themselves. if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) { if (!empty($tags)) { $stored->saveKnownTags($tags); } else { $stored->saveTags(); } // Note: groups may save tags, so must be run after tags are saved // to avoid errors on duplicates. $stored->saveAttentions($act->context->attention); if (!empty($urls)) { $stored->saveKnownUrls($urls); } else { $stored->saveUrls(); } } if ($distribute) { // Prepare inbox delivery, may be queued to background. $stored->distribute(); } return $stored; }
function fixupNoticeConversation() { printfnq("Ensuring all notices have a conversation ID..."); $notice = new Notice(); $notice->whereAdd('conversation is null'); $notice->whereAdd('conversation = 0', 'OR'); $notice->orderBy('id'); // try to get originals before replies $notice->find(); while ($notice->fetch()) { try { $cid = null; $orig = clone $notice; if (!empty($notice->reply_to)) { $reply = Notice::getKV('id', $notice->reply_to); if ($reply instanceof Notice && !empty($reply->conversation)) { $notice->conversation = $reply->conversation; } unset($reply); } // if still empty if (empty($notice->conversation)) { $child = new Notice(); $child->reply_to = $notice->getID(); $child->limit(1); if ($child->find(true) && !empty($child->conversation)) { $notice->conversation = $child->conversation; } unset($child); } // if _still_ empty we just create our own conversation if (empty($notice->conversation)) { $notice->conversation = $notice->getID(); } $result = $notice->update($orig); unset($orig); } catch (Exception $e) { print "Error setting conversation: " . $e->getMessage(); } } printfnq("DONE.\n"); }