diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 1d0c4afdd0..0bb4860ea7 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -176,7 +176,8 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $atom->addEntryFromNotices($this->notices); - $this->raw($atom->getString()); + //$this->raw($atom->getString()); + print $atom->getString(); // temp hack until PuSH feeds are redone cleanly } catch (Atom10FeedException $e) { $this->serverError( diff --git a/classes/Notice.php b/classes/Notice.php index a12839d729..e8d5c45cb2 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -187,7 +187,14 @@ class Notice extends Memcached_DataObject * 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' permalink to notice; defaults to local notice URL + * string 'uri' unique ID for notice; defaults to local notice URL + * 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 + * place of extracting @-replies from content. + * array 'groups' list of group IDs to deliver to, in place of + * extracting ! tags from content + * @fixme tag override * * @return Notice * @throws ClientException @@ -342,6 +349,12 @@ class Notice extends Memcached_DataObject $notice->saveReplies(); } + if (isset($groups)) { + $notice->saveKnownGroups($groups); + } else { + $notice->saveGroups(); + } + $notice->distribute(); return $notice; @@ -692,7 +705,22 @@ class Notice extends Memcached_DataObject return $ni; } - function addToInboxes($groups, $recipients) + /** + * 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($groups=null, $recipients=null) { $ni = $this->whoGets($groups, $recipients); @@ -742,6 +770,42 @@ class Notice extends Memcached_DataObject } /** + * Record this notice to the given group inboxes for delivery. + * Overrides the regular parsing of !group markup. + * + * @param string $group_ids + * @fixme might prefer URIs as identifiers, as for replies? + * best with generalizations on user_group to support + * remote groups better. + */ + function saveKnownGroups($group_ids) + { + if (!is_array($group_ids)) { + throw new ServerException("Bad type provided to saveKnownGroups"); + } + + $groups = array(); + foreach ($group_ids as $id) { + $group = User_group::staticGet('id', $id); + if ($group) { + common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); + $result = $this->addToGroupInbox($group); + if (!$result) { + common_log_db_error($gi, 'INSERT', __FILE__); + } + + // @fixme should we save the tags here or not? + $groups[] = clone($group); + } else { + common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist"); + } + } + + return $groups; + } + + /** + * Parse !group delivery and record targets into group_inbox. * @return array of Group objects */ function saveGroups() @@ -824,6 +888,19 @@ class Notice extends Memcached_DataObject return true; } + /** + * Save reply records indicating that this notice needs to be + * delivered to the local users with the given URIs. + * + * Since this is expected to be used when saving foreign-sourced + * messages, we won't deliver to any remote targets as that's the + * source service's responsibility. + * + * @fixme Unlike saveReplies() there's no mail notification here. + * Move that to distrib queue handler? + * + * @param array of unique identifier URIs for recipients + */ function saveKnownReplies($uris) { foreach ($uris as $uri) { @@ -845,6 +922,13 @@ class Notice extends Memcached_DataObject } /** + * Pull @-replies from this message's content in StatusNet markup format + * and save reply records indicating that this message needs to be + * delivered to those users. + * + * Side effect: local recipients get e-mail notifications here. + * @fixme move mail notifications to distrib? + * * @return array of integer profile IDs */ @@ -934,9 +1018,10 @@ class Notice extends Memcached_DataObject } /** - * Same calculation as saveGroups but without the saving - * @fixme merge the functions - * @return array of Group_inbox objects + * Pull list of groups this notice needs to be delivered to, + * as previously recorded by saveGroups() or saveKnownGroups(). + * + * @return array of Group objects */ function getGroups() { @@ -959,7 +1044,10 @@ class Notice extends Memcached_DataObject if ($gi->find()) { while ($gi->fetch()) { - $groups[] = clone($gi); + $group = User_group::staticGet('id', $gi->group_id); + if ($group) { + $groups[] = $group; + } } } @@ -979,6 +1067,7 @@ class Notice extends Memcached_DataObject '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:poco' => 'http://portablecontacts.net/spec/1.0', 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); } else { $attrs = array(); @@ -1063,6 +1152,17 @@ class Notice extends Memcached_DataObject } } + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $xs->element( + 'link', array( + 'rel' => 'ostatus:attention', + 'href' => $group->permalink() + ) + ); + } + if (!empty($this->repeat_of)) { $repeat = Notice::staticGet('id', $this->repeat_of); if (!empty($repeat)) { @@ -1119,25 +1219,8 @@ class Notice extends Memcached_DataObject */ function asActivityNoun($element) { - $xs = new XMLStringer(true); - - $xs->elementStart('activity:' . $element); - $xs->element('activity:object-type', - null, - 'http://activitystrea.ms/schema/1.0/note'); - $xs->element('id', - null, - $this->uri); - $xs->element('content', - array('type' => 'text/html'), - $this->rendered); - $xs->element('link', - array('type' => 'text/html', - 'rel' => 'alternate', - 'href' => $this->bestUrl())); - $xs->elementEnd('activity:' . $element); - - return $xs->getString(); + $noun = ActivityObject::fromNotice($this); + return $noun->asString('activity:' . $element); } function bestUrl() diff --git a/classes/Profile.php b/classes/Profile.php index 7fb2b87bc3..78223b34a1 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -801,82 +801,8 @@ class Profile extends Memcached_DataObject */ function asActivityNoun($element) { - $xs = new XMLStringer(true); - - $xs->elementStart('activity:' . $element); - $xs->element( - 'activity:object-type', - null, - 'http://activitystrea.ms/schema/1.0/person' - ); - $xs->element( - 'id', - null, - $this->getUri() - ); - - // title should contain fullname - $xs->element('title', null, $this->getBestName()); - - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html'), - $this->profileurl); - - $xs->element('poco:preferredUsername', null, $this->nickname); - - // Portable Contacts stuff - - if (isset($this->bio)) { - - // XXX: Possible to use OpenSocial's aboutMe? - - $xs->element('poco:note', null, $this->bio); - } - - if (isset($this->homepage)) { - - $xs->elementStart('poco:urls'); - $xs->element('poco:value', null, $this->homepage); - $xs->element('poco:type', null, 'homepage'); - $xs->element('poco:primary', null, 'true'); - $xs->elementEnd('poco:urls'); - } - - if (isset($this->location)) { - $xs->elementStart('poco:address'); - $xs->element('poco:formatted', null, $this->location); - $xs->elementEnd('poco:address'); - } - - if (isset($this->lat) && isset($this->lon)) { - $this->element( - 'georss:point', - null, - (float)$this->lat . ' ' . (float)$this->lon - ); - } - - // XXX: Should we send all avatar sizes we have? I think - // cliqset does -Z - - $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE); - - $xs->element( - 'link', array( - 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, - 'rel' => 'avatar', - 'href' => empty($avatar) - ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) - : $avatar->displayUrl() - ), - '' - ); - - $xs->elementEnd('activity:' . $element); - - // XXX: Add people tags with plural? - - return $xs->getString(); + $noun = ActivityObject::fromProfile($this); + return $noun->asString('activity:' . $element); } /** diff --git a/classes/User_group.php b/classes/User_group.php index 379e6b7219..1382aa407c 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -39,14 +39,24 @@ class User_group extends Memcached_DataObject function homeUrl() { - return common_local_url('showgroup', - array('nickname' => $this->nickname)); + $url = null; + if (Event::handle('StartUserGroupHomeUrl', array($this, &$url))) { + $url = common_local_url('showgroup', + array('nickname' => $this->nickname)); + } + Event::handle('EndUserGroupHomeUrl', array($this, &$url)); + return $url; } function permalink() { - return common_local_url('groupbyid', - array('id' => $this->id)); + $url = null; + if (Event::handle('StartUserGroupPermalink', array($this, &$url))) { + $url = common_local_url('groupbyid', + array('id' => $this->id)); + } + Event::handle('EndUserGroupPermalink', array($this, &$url)); + return $url; } function getNotices($offset, $limit, $since_id=null, $max_id=null) diff --git a/plugins/OStatus/lib/activity.php b/lib/activity.php similarity index 85% rename from plugins/OStatus/lib/activity.php rename to lib/activity.php index 6cb9881bf6..3689dac385 100644 --- a/plugins/OStatus/lib/activity.php +++ b/lib/activity.php @@ -19,9 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - * @category OStatus + * @category Feed * @package StatusNet * @author Evan Prodromou + * @author Zach Copley * @copyright 2010 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 * @link http://status.net/ @@ -31,10 +32,117 @@ if (!defined('STATUSNET')) { exit(1); } +class PoCoURL +{ + const TYPE = 'type'; + const VALUE = 'value'; + const PRIMARY = 'primary'; + + public $type; + public $value; + public $primary; + + function __construct($type, $value, $primary = false) + { + $this->type = $type; + $this->value = $value; + $this->primary = $primary; + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->elementStart('poco:urls'); + $xs->element('poco:type', null, $this->type); + $xs->element('poco:value', null, $this->value); + if ($this->primary) { + $xs->element('poco:primary', null, 'true'); + } + $xs->elementEnd('poco:urls'); + return $xs->getString(); + } +} + +class PoCoAddress +{ + const ADDRESS = 'address'; + const FORMATTED = 'formatted'; + + public $formatted; + + function __construct($formatted) + { + if (empty($formatted)) { + return null; + } + $this->formatted = $formatted; + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->elementStart('poco:address'); + $xs->element('poco:formatted', null, $this->formatted); + $xs->elementEnd('poco:address'); + return $xs->getString(); + } +} + class PoCo { const NS = 'http://portablecontacts.net/spec/1.0'; - const USERNAME = 'preferredUsername'; + + const USERNAME = 'preferredUsername'; + const NOTE = 'note'; + const URLS = 'urls'; + + public $preferredUsername; + public $note; + public $address; + public $urls = array(); + + function __construct($profile) + { + $this->preferredUsername = $profile->nickname; + + $this->note = $profile->bio; + $this->address = new PoCoAddress($profile->location); + + if (!empty($profile->homepage)) { + array_push( + $this->urls, + new PoCoURL( + 'homepage', + $profile->homepage, + true + ) + ); + } + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->element( + 'poco:preferredUsername', + null, + $this->preferredUsername + ); + + if (!empty($this->note)) { + $xs->element('poco:note', null, $this->note); + } + + if (!empty($this->address)) { + $xs->raw($this->address->asString()); + } + + foreach ($this->urls as $url) { + $xs->raw($url->asString()); + } + + return $xs->getString(); + } } /** @@ -265,6 +373,7 @@ class ActivityObject public $link; public $source; public $avatar; + public $geopoint; /** * Constructor @@ -367,24 +476,25 @@ class ActivityObject return $object; } - /** - * @fixme missing avatar, bio info, etc - */ static function fromProfile($profile) { $object = new ActivityObject(); - $object->type = ActivityObject::PERSON; - $object->id = $profile->getUri(); - $object->title = $profile->getBestName(); - $object->link = $profile->profileurl; + $object->type = ActivityObject::PERSON; + $object->id = $profile->getUri(); + $object->title = $profile->getBestName(); + $object->link = $profile->profileurl; + $object->avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + + if (isset($profile->lat) && isset($profile->lon)) { + $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; + } + + $object->poco = new PoCo($profile); return $object; } - /** - * @fixme missing avatar, bio info, etc - */ function asString($tag='activity:object') { $xs = new XMLStringer(true); @@ -410,7 +520,32 @@ class ActivityObject if (!empty($this->link)) { $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'), - $this->content); + $this->link); + } + + if ($this->type == ActivityObject::PERSON) { + $xs->element( + 'link', array( + 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, + 'rel' => 'avatar', + 'href' => empty($this->avatar) + ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) + : $this->avatar->displayUrl() + ), + '' + ); + } + + if (!empty($this->geopoint)) { + $xs->element( + 'georss:point', + null, + $this->geopoint + ); + } + + if (!empty($this->poco)) { + $xs->raw($this->poco->asString()); } $xs->elementEnd($tag); diff --git a/lib/common.php b/lib/common.php index b95cd11752..68723955ec 100644 --- a/lib/common.php +++ b/lib/common.php @@ -123,6 +123,7 @@ require_once INSTALLDIR.'/lib/util.php'; require_once INSTALLDIR.'/lib/action.php'; require_once INSTALLDIR.'/lib/mail.php'; require_once INSTALLDIR.'/lib/subs.php'; +require_once INSTALLDIR.'/lib/activity.php'; require_once INSTALLDIR.'/lib/clientexception.php'; require_once INSTALLDIR.'/lib/serverexception.php'; diff --git a/lib/distribqueuehandler.php b/lib/distribqueuehandler.php index c31b675c1a..dc183fb36a 100644 --- a/lib/distribqueuehandler.php +++ b/lib/distribqueuehandler.php @@ -69,19 +69,7 @@ class DistribQueueHandler } try { - $groups = $notice->saveGroups(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $recipients = $notice->getReplies(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $notice->addToInboxes($groups, $recipients); + $notice->addToInboxes(); } catch (Exception $e) { $this->logit($notice, $e); } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 061ed4bd1b..472008419a 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -591,6 +591,22 @@ class OStatusPlugin extends Plugin return true; } + function onStartUserGroupHomeUrl($group, &$url) + { + return $this->onStartUserGroupPermalink($group, &$url); + } + + function onStartUserGroupPermalink($group, &$url) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + // @fixme this should probably be in the user_group table + // @fixme this uri not guaranteed to be a profile page + $url = $oprofile->uri; + return false; + } + } + function onStartShowSubscriptionsContent($action) { $user = common_current_user(); diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index c0e39add8f..e8cc13c6c1 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -501,6 +501,7 @@ class Ostatus_profile extends Memcached_DataObject /** * Process an incoming post activity from this remote feed. * @param Activity $activity + * @fixme break up this function, it's getting nasty long */ protected function processPost($activity) { @@ -518,7 +519,6 @@ class Ostatus_profile extends Memcached_DataObject } $oprofile = $this; } - $sourceUri = $activity->object->id; $dupe = Notice::staticGet('uri', $sourceUri); @@ -555,15 +555,76 @@ class Ostatus_profile extends Memcached_DataObject } } - // @fixme ensure that groups get handled correctly + $profile = $oprofile->localProfile(); + $params['groups'] = array(); + $params['replies'] = array(); + if ($activity->context) { + foreach ($activity->context->attention as $recipient) { + $roprofile = Ostatus_profile::staticGet('uri', $recipient); + if ($roprofile) { + if ($roprofile->isGroup()) { + // Deliver to local recipients of this remote group. + // @fixme sender verification? + $params['groups'][] = $roprofile->group_id; + continue; + } else { + // Delivery to remote users is the source service's job. + continue; + } + } + + $user = User::staticGet('uri', $recipient); + if ($user) { + // An @-reply directed to a local user. + // @fixme sender verification, spam etc? + $params['replies'][] = $recipient; + continue; + } + + // @fixme we need a uri on user_group + // $group = User_group::staticGet('uri', $recipient); + $template = common_local_url('groupbyid', array('id' => '31337')); + $template = preg_quote($template, '/'); + $template = str_replace('31337', '(\d+)', $template); + common_log(LOG_DEBUG, $template); + if (preg_match("/$template/", $recipient, $matches)) { + $id = $matches[1]; + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group. + // @fixme sender verification? + if ($profile->isMember($group)) { + common_log(LOG_DEBUG, "delivering to group $id $group->nickname"); + $params['groups'][] = $group->id; + } else { + common_log(LOG_DEBUG, "not delivering to group $id $group->nickname because sender $profile->nickname is not a member"); + } + continue; + } else { + common_log(LOG_DEBUG, "not delivering to missing group $id"); + } + } else { + common_log(LOG_DEBUG, "not delivering to groups for $recipient"); + } + } + } - $saved = Notice::saveNew($oprofile->localProfile()->id, - $content, - 'ostatus', - $params); + try { + $saved = Notice::saveNew($profile->id, + $content, + 'ostatus', + $params); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage()); + return; + } // Record which feed this came through... - Ostatus_source::saveNew($saved, $this, 'push'); + try { + Ostatus_source::saveNew($saved, $this, 'push'); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage()); + } } /** diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js index 148a05f6f6..1fc44b21b6 100644 --- a/plugins/OStatus/js/ostatus.js +++ b/plugins/OStatus/js/ostatus.js @@ -63,11 +63,10 @@ SN.U.DialogBox = { f.show(); } else { - a[0].href = (a[0].href.match(/[\\?]/) === null) ? a[0].href+'?' : a[0].href+'&'; $.ajax({ type: 'GET', dataType: 'xml', - url: a[0].href+'ajax=1', + url: a[0].href + ((a[0].href.match(/[\\?]/) === null)?'?':'&') + 'ajax=1', beforeSend: function(formData) { a.addClass('processing'); }, diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index 30a427e3fc..c2bd630f9c 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -36,7 +36,7 @@ class HubDistribQueueHandler extends QueueHandler $this->pushUser($notice); foreach ($notice->getGroups() as $group) { - $this->pushGroup($notice, $group->group_id); + $this->pushGroup($notice, $group->id); } return true; } diff --git a/plugins/OStatus/lib/magicsig.php b/plugins/OStatus/lib/magicsig.php index af65bad046..50eb301ab3 100644 --- a/plugins/OStatus/lib/magicsig.php +++ b/plugins/OStatus/lib/magicsig.php @@ -57,8 +57,10 @@ class MagicsigRsaSha256 $keypair = new Crypt_RSA_KeyPair($key_length); $params['public_key'] = $keypair->getPublicKey(); $params['private_key'] = $keypair->getPrivateKey(); - + + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); $this->keypair = new Crypt_RSA($params); + PEAR::popErrorHandling(); } @@ -79,6 +81,8 @@ class MagicsigRsaSha256 public function fromString($text) { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + // remove whitespace $text = preg_replace('/\s+/', '', $text); @@ -86,7 +90,6 @@ class MagicsigRsaSha256 if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(.([^\.]+))?/', $text, $matches)) { return false; } - $mod = base64_url_decode($matches[1]); $exp = base64_url_decode($matches[2]); @@ -110,6 +113,7 @@ class MagicsigRsaSha256 } $this->keypair = new Crypt_RSA($params); + PEAR::popErrorHandling(); } public function getName()