code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * A sample module to show best practices for StatusNet plugins
 *
 * PHP version 5
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package   StatusNet
 * @author    James Walker <james@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class MagicEnvelope
{
    const ENCODING = 'base64url';

    const NS = 'http://salmon-protocol.org/ns/magic-env';

    protected $actor = null;    // Profile of user who has signed the envelope

    protected $data      = null;    // When stored here it is _always_ base64url encoded
    protected $data_type = null;
    protected $encoding  = null;
    protected $alg       = null;
    protected $sig       = null;

    /**
     * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
     *
     * @param string XML source
     * @return mixed associative array of envelope data, or false on unrecognized input
     *
     * @fixme will spew errors to logs or output in case of XML parse errors
     * @fixme may give fatal errors if some elements are missing or invalid XML
     * @fixme calling DOMDocument::loadXML statically triggers warnings in strict mode
     */
    public function __construct($xml=null, Profile $actor=null) {
        if (!empty($xml)) {
            $dom = new DOMDocument();
            if (!$dom->loadXML($xml)) {
                throw new ServerException('Tried to load malformed XML as DOM');
            } elseif (!$this->fromDom($dom)) {
                throw new ServerException('Could not load MagicEnvelope from DOM');
            }
        } elseif ($actor instanceof Profile) {
            // So far we only allow setting with _either_ $xml _or_ $actor as that's
            // all our circumstances require. But it may be confusing for new developers.
            // The idea is that feeding XML must be followed by interpretation and then
            // running $magic_env->verify($profile), just as in SalmonAction->prepare(...)
            // and supplying an $actor (which right now has to be a User) will require
            // defining the $data, $data_type etc. attributes manually afterwards before
            // signing the envelope..
            $this->setActor($actor);
        }
    }

    /**
     * Retrieve Salmon keypair first by checking local database, but
     * if it's not found, attempt discovery if it has been requested.
     *
     * @param Profile $profile      The profile we're looking up keys for.
     * @param boolean $discovery    Network discovery if no local cache?
     */
    public function getKeyPair(Profile $profile, $discovery=false) {
        if (!$profile->isLocal()) common_debug('Getting magic-public-key for non-local profile id=='.$profile->getID());
        $magicsig = Magicsig::getKV('user_id', $profile->getID());

        if ($discovery && !$magicsig instanceof Magicsig) {
            if (!$profile->isLocal()) common_debug('magic-public-key not found, will do discovery for profile id=='.$profile->getID());
            // Throws exception on failure, but does not try to _load_ the keypair string.
            $keypair = $this->discoverKeyPair($profile);

            $magicsig = new Magicsig();
            $magicsig->user_id = $profile->getID();
            $magicsig->importKeys($keypair);
            // save the public key for this profile in our database.
            // TODO: If the profile generates a new key remotely, we must be able to replace
            //       this (of course after callback-verification).
            $magicsig->insert();
        } elseif (!$magicsig instanceof Magicsig) { // No discovery request, so we'll give up.
            throw new ServerException(sprintf('No public key found for profile (id==%d)', $profile->id));
        }

        assert($magicsig->publicKey instanceof \phpseclib\Crypt\RSA);

        return $magicsig;
    }

    /**
     * Get the Salmon keypair from a URI, uses XRD Discovery etc. Reasonably
     * you'll only get the public key ;)
     *
     * The string will (hopefully) be formatted as described in Magicsig specification:
     * https://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html
     *
     * @return string formatted as Magicsig keypair
     */
    public function discoverKeyPair(Profile $profile)
    {
        $signer_uri = $profile->getUri();
        if (empty($signer_uri)) {
            throw new ServerException(sprintf('Profile missing URI (id==%d)', $profile->getID()));
        }

        $disco = new Discovery();

        // Throws exception on lookup problems
        try {
            $xrd = $disco->lookup($signer_uri);
        } catch (Exception $e) {
            // Diaspora seems to require us to request the acct: uri
            $xrd = $disco->lookup($profile->getAcctUri());
        }

        common_debug('Will try to find magic-public-key from XRD of profile id=='.$profile->getID());
        $pubkey = null;
        if (Event::handle('MagicsigPublicKeyFromXRD', array($xrd, &$pubkey))) {
            $link = $xrd->get(Magicsig::PUBLICKEYREL);
            if (is_null($link)) {
                // TRANS: Exception.
                throw new Exception(_m('Unable to locate signer public key.'));
            }
            $pubkey = $link->href;
        }
        if (empty($pubkey)) {
            throw new ServerException('Empty Magicsig public key. A bug?');
        }

        // We have a public key element, let's hope it has proper key data.
        $keypair = false;
        $parts = explode(',', $pubkey);
        if (count($parts) == 2) {
            $keypair = $parts[1];
        } else {
            // Backwards compatibility check for separator bug in 0.9.0
            $parts = explode(';', $pubkey);
            if (count($parts) == 2) {
                $keypair = $parts[1];
            }
        }

        if ($keypair === false) {
            // For debugging clarity. Keypair did not pass count()-check above.
            // TRANS: Exception when public key was not properly formatted.
            throw new Exception(_m('Incorrectly formatted public key element.'));
        }

        return $keypair;
    }

    /**
     * The current MagicEnvelope spec as used in StatusNet 0.9.7 and later
     * includes both the original data and some signing metadata fields as
     * the input plaintext for the signature hash.
     *
     * @return string
     */
    public function signingText() {
        return implode('.', array($this->data, // this field is pre-base64'd
                            Magicsig::base64_url_encode($this->data_type),
                            Magicsig::base64_url_encode($this->encoding),
                            Magicsig::base64_url_encode($this->alg)));
    }

    /**
     *
     * @param <type> $text
     * @param <type> $mimetype
     * @param Magicsig $magicsig    Magicsig with private key available.
     *
     * @return MagicEnvelope object with all properties set
     *
     * @throws Exception of various kinds on signing failure
     */
    public function signMessage($text, $mimetype)
    {
        if (!$this->actor instanceof Profile) {
            throw new ServerException('No profile to sign message with is set.');
        } elseif (!$this->actor->isLocal()) {
            throw new ServerException('Cannot sign magic envelopes with remote users since we have no private key.');
        }

        // Find already stored key
        $magicsig = Magicsig::getKV('user_id', $this->actor->getID());
        if (!$magicsig instanceof Magicsig) {
            // and if it doesn't exist, it is time to create one!
            $magicsig = Magicsig::generate($this->actor->getUser());
        }
        assert($magicsig instanceof Magicsig);
        assert($magicsig->privateKey instanceof \phpseclib\Crypt\RSA);

        // Prepare text and metadata for signing
        $this->data = Magicsig::base64_url_encode($text);
        $this->data_type = $mimetype;
        $this->encoding  = self::ENCODING;
        $this->alg       = $magicsig->getName();

        // Get the actual signature
        $this->sig = $magicsig->sign($this->signingText());
    }

    /**
     * Create an <me:env> XML representation of the envelope.
     *
     * @return string representation of XML document
     */
    public function toXML(Profile $target=null, $flavour=null) {
        $xs = new XMLStringer();
        $xs->startXML();    // header, to point out it's not HTML or anything...
        if (Event::handle('StartMagicEnvelopeToXML', array($this, $xs, $flavour, $target))) {
            // fall back to our default, normal Magic Envelope XML.
            // the $xs element _may_ have had elements added, or could get in the end event
            $xs->elementStart('me:env', array('xmlns:me' => self::NS));
            $xs->element('me:data', array('type' => $this->data_type), $this->data);
            $xs->element('me:encoding', null, $this->encoding);
            $xs->element('me:alg', null, $this->alg);
            $xs->element('me:sig', null, $this->getSignature());
            $xs->elementEnd('me:env');

            Event::handle('EndMagicEnvelopeToXML', array($this, $xs, $flavour, $target));
        }
        return $xs->getString();
    }

    /*
     * Extract the contained XML payload, and insert a copy of the envelope
     * signature data as an <me:provenance> section.
     *
     * @return DOMDocument of Atom entry
     *
     * @fixme in case of XML parsing errors, this will spew to the error log or output
     */
    public function getPayload()
    {
        $dom = new DOMDocument();
        if (!$dom->loadXML(Magicsig::base64_url_decode($this->data))) {
            throw new ClientException('Malformed XML in Salmon payload');
        }

        switch ($this->data_type) {
        case 'application/atom+xml':
            if ($dom->documentElement->namespaceURI !== Activity::ATOM
                    || $dom->documentElement->tagName !== 'entry') {
                throw new ServerException(_m('Salmon post must be an Atom entry.'));
            }
            $prov = $dom->createElementNS(self::NS, 'me:provenance');
            $prov->setAttribute('xmlns:me', self::NS);
            $data = $dom->createElementNS(self::NS, 'me:data', $this->data);
            $data->setAttribute('type', $this->data_type);
            $prov->appendChild($data);
            $enc = $dom->createElementNS(self::NS, 'me:encoding', $this->encoding);
            $prov->appendChild($enc);
            $alg = $dom->createElementNS(self::NS, 'me:alg', $this->alg);
            $prov->appendChild($alg);
            $sig = $dom->createElementNS(self::NS, 'me:sig', $this->getSignature());
            $prov->appendChild($sig);

            $dom->documentElement->appendChild($prov);
            break;
        default:
            throw new ClientException('Unknown Salmon payload data type');
        }
        return $dom;
    }

    public function getSignature()
    {
        if (empty($this->sig)) {
            throw new ServerException('You must first call signMessage before getSignature');
        }
        return $this->sig;
    }

    public function getSignatureAlgorithm()
    {
        return $this->alg;
    }

    public function getData()
    {
        return $this->data;
    }

    public function getDataType()
    {
        return $this->data_type;
    }

    public function getEncoding()
    {
        return $this->encoding;
    }

    /**
     * Find the author URI referenced in the payload Atom entry.
     *
     * @return string URI for author
     * @throws ServerException on failure
     */
    public function getAuthorUri() {
        $doc = $this->getPayload();

        $authors = $doc->documentElement->getElementsByTagName('author');
        foreach ($authors as $author) {
            $uris = $author->getElementsByTagName('uri');
            foreach ($uris as $uri) {
                return $uri->nodeValue;
            }
        }
        throw new ServerException('No author URI found in Salmon payload data');
    }

    /**
     * Attempt to verify cryptographic signing for parsed envelope data.
     * Requires network access to retrieve public key referenced by the envelope signer.
     *
     * Details of failure conditions are dumped to output log and not exposed to caller.
     *
     * @param Profile $profile profile used to get locally cached public signature key
     *                         or if necessary perform discovery on.
     *
     * @return boolean
     */
    public function verify(Profile $profile)
    {
        if ($this->alg != 'RSA-SHA256') {
            common_log(LOG_DEBUG, 'Salmon error: bad algorithm: '._ve($this->alg));
            return false;
        }

        if ($this->encoding != self::ENCODING) {
            common_log(LOG_DEBUG, 'Salmon error: bad encoding: '._ve($this->encoding));
            return false;
        }

        try {
            $magicsig = $this->getKeyPair($profile, true);    // Do discovery too if necessary
        } catch (Exception $e) {
            common_log(LOG_DEBUG, "Salmon error: getKeyPair for profile id=={$profile->getID()}: "._ve($e->getMessage()));
            return false;
        }

        if (!$magicsig->verify($this->signingText(), $this->getSignature())) {
            common_log(LOG_INFO, 'Salmon signature verification failed for profile id=='.$profile->getID());
            // TRANS: Client error when incoming salmon slap signature does not verify cryptographically.
            throw new ClientException(_m('Salmon signature verification failed.'));
        }
        common_debug('Salmon signature verification successful for profile id=='.$profile->getID());
        $this->setActor($profile);
        return true;
    }

    /**
     * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
     *
     * @param DOMDocument $dom
     * @return mixed associative array of envelope data, or false on unrecognized input
     *
     * @fixme may give fatal errors if some elements are missing
     */
    protected function fromDom(DOMDocument $dom)
    {
        $env_element = $dom->getElementsByTagNameNS(self::NS, 'env')->item(0);
        if (!$env_element) {
            $env_element = $dom->getElementsByTagNameNS(self::NS, 'provenance')->item(0);
        }

        if (!$env_element) {
            return false;
        }

        $data_element = $env_element->getElementsByTagNameNS(self::NS, 'data')->item(0);
        $sig_element = $env_element->getElementsByTagNameNS(self::NS, 'sig')->item(0);

        $this->data      = preg_replace('/\s/', '', $data_element->nodeValue);
        $this->data_type = $data_element->getAttribute('type');
        $this->encoding  = $env_element->getElementsByTagNameNS(self::NS, 'encoding')->item(0)->nodeValue;
        $this->alg       = $env_element->getElementsByTagNameNS(self::NS, 'alg')->item(0)->nodeValue;
        $this->sig       = preg_replace('/\s/', '', $sig_element->nodeValue);
        return true;
    }

    public function setActor(Profile $actor)
    {
        if ($this->actor instanceof Profile) {
            throw new ServerException('Cannot set a new actor profile for MagicEnvelope object.');
        }
        $this->actor = $actor;
    }

    public function getActor()
    {
        if (!$this->actor instanceof Profile) {
            throw new ServerException('No actor set for this magic envelope.');
        }
        return $this->actor;
    }

    /**
     * Encode the given string as a signed MagicEnvelope XML document,
     * using the keypair for the given local user profile. We can of
     * course not sign a remote profile's slap, since we don't have the
     * private key.
     *
     * Side effects: will create and store a keypair on-demand if one
     * hasn't already been generated for this user. This can be very slow
     * on some systems.
     *
     * @param string $text XML fragment to sign, assumed to be Atom
     * @param User $user User who cryptographically signs $text
     *
     * @return MagicEnvelope object complete with signature
     *
     * @throws Exception on bad profile input or key generation problems
     */
    public static function signAsUser($text, User $user)
    {
        $magic_env = new MagicEnvelope(null, $user->getProfile());
        $magic_env->signMessage($text, 'application/atom+xml');

        return $magic_env;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Prepare WebSub and Salmon distributions for an outgoing message.
 *
 * @package OStatusPlugin
 * @author Brion Vibber <brion@status.net>
 */
class OStatusQueueHandler extends QueueHandler
{
    // If we have more than this many subscribing sites on a single feed,
    // break up the WebSub distribution into smaller batches which will be
    // rolled into the queue progressively. This reduces disruption to
    // other, shorter activities being enqueued while we work.
    const MAX_UNBATCHED = 50;

    // Each batch (a 'hubprep' entry) will have this many items.
    // Selected to provide a balance between queue packet size
    // and number of batches that will end up getting processed.
    // For 20,000 target sites, 1000 should work acceptably.
    const BATCH_SIZE = 1000;

    function transport()
    {
        return 'ostatus';
    }

    function handle($notice)
    {
        assert($notice instanceof Notice);

        $this->notice = $notice;
        $this->user = User::getKV('id', $notice->profile_id);

        try {
            $profile = $this->notice->getProfile();
        } catch (Exception $e) {
            common_log(LOG_ERR, "Can't get profile for notice; skipping: " . $e->getMessage());
            return true;
        }

        foreach ($notice->getAttentionProfiles() as $target) {
            common_debug("OSTATUS [{$this->notice->getID()}]: Attention target profile {$target->getNickname()} ({$target->getID()})");
            if ($target->isGroup()) {
                common_debug("OSTATUS [{$this->notice->getID()}]: {$target->getID()} is a group");
                $oprofile = Ostatus_profile::getKV('group_id', $target->getGroup()->getID());
                if (!$oprofile instanceof Ostatus_profile) {
                    // we don't save profiles like this yet, but in the future
                    $oprofile = Ostatus_profile::getKV('profile_id', $target->getID());
                }
                if ($oprofile instanceof Ostatus_profile) {
                    // remote group
                    if ($notice->isLocal()) {
                        common_debug("OSTATUS [{$this->notice->getID()}]: notice is local and remote group with profile ID {$target->getID()} gets a ping");
                        $this->pingReply($oprofile);
                    }
                } else {
                    common_debug("OSTATUS [{$this->notice->getID()}]: local group with profile id {$target->getID()} gets pushed out");
                    // local group
                    $this->pushGroup($target->getGroup());
                }
            } elseif ($notice->isLocal()) {
                // Notices generated on other sites will have already
                // pinged their reply-targets, so only do these things
                // if the target is not a group and the notice is locally generated

                $oprofile = Ostatus_profile::getKV('profile_id', $target->getID());
                if ($oprofile instanceof Ostatus_profile) {
                    common_debug("OSTATUS [{$this->notice->getID()}]: Notice is local and {$target->getID()} is remote profile, getting pingReply");
                    $this->pingReply($oprofile);
                }
            }
        }

        if ($notice->isLocal()) {
            // Notices generated on remote sites will have already
            // been pushed to user's subscribers by their origin sites.
            $this->pushUser();

            try {
                $parent = $this->notice->getParent();
                foreach($parent->getAttentionProfiles() as $related) {
                    if ($related->isGroup()) {
                        // don't ping groups in parent notices since we might not be a member of them,
                        // though it could be useful if we study this and use it correctly
                        continue;
                    }
                    common_debug("OSTATUS [{$this->notice->getID()}]: parent notice {$parent->getID()} has related profile id=={$related->getID()}");
                    // FIXME: don't ping twice in case someone is in both notice attention spans!
                    $oprofile = Ostatus_profile::getKV('profile_id', $related->getID());
                    if ($oprofile instanceof Ostatus_profile) {
                        $this->pingReply($oprofile);
                    }
                }
            } catch (NoParentNoticeException $e) {
                // nothing to do then
            }

            foreach ($notice->getProfileTags() as $ptag) {
                $oprofile = Ostatus_profile::getKV('peopletag_id', $ptag->id);
                if (!$oprofile) {
                    $this->pushPeopletag($ptag);
                }
            }
        }

        return true;
    }

    function pushUser()
    {
        if ($this->user) {
            common_debug("OSTATUS [{$this->notice->getID()}]: pushing feed for local user {$this->user->getID()}");
            // For local posts, ping the WebSub hub to update their feed.
            // http://identi.ca/api/statuses/user_timeline/1.atom
            $feed = common_local_url('ApiTimelineUser',
                                     array('id' => $this->user->id,
                                           'format' => 'atom'));
            $this->pushFeed($feed, array($this, 'userFeedForNotice'));
        }
    }

    function pushGroup(User_group $group)
    {
        common_debug("OSTATUS [{$this->notice->getID()}]: pushing group '{$group->getNickname()}' profile_id={$group->profile_id}");
        // For a local group, ping the WebSub hub to update its feed.
        // Updates may come from either a local or a remote user.
        $feed = common_local_url('ApiTimelineGroup',
                                 array('id' => $group->getID(),
                                       'format' => 'atom'));
        $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group->getID());
    }

    function pushPeopletag($ptag)
    {
        common_debug("OSTATUS [{$this->notice->getID()}]: pushing peopletag '{$ptag->id}'");
        // For a local people tag, ping the WebSub hub to update its feed.
        // Updates may come from either a local or a remote user.
        $feed = common_local_url('ApiTimelineList',
                                 array('id' => $ptag->id,
                                       'user' => $ptag->tagger,
                                       'format' => 'atom'));
        $this->pushFeed($feed, array($this, 'peopletagFeedForNotice'), $ptag);
    }

    function pingReply(Ostatus_profile $oprofile)
    {
        if ($this->user) {
            common_debug("OSTATUS [{$this->notice->getID()}]: pinging reply to {$oprofile->localProfile()->getNickname()} for local user '{$this->user->getID()}'");
            // For local posts, send a Salmon ping to the mentioned
            // remote user or group.
            // @fixme as an optimization we can skip this if the
            // remote profile is subscribed to the author.
            $oprofile->notifyDeferred($this->notice, $this->user);
        }
    }

    /**
     * @param string $feed URI to the feed
     * @param callable $callback function to generate Atom feed update if needed
     *        any additional params are passed to the callback.
     */
    function pushFeed($feed, $callback)
    {
        // NOTE: external hub pings will not be fixed by
        // our legacy_http thing!
        $hub = common_config('ostatus', 'hub');
        if ($hub) {
            $this->pushFeedExternal($feed, $hub);
        }

        // If we used to be http but now are https, see if we find an http entry for this feed URL
        // and then upgrade it. This self-healing feature needs to be enabled manually in config.
        // This code is based on a patch by @hannes2peer@quitter.se
        if (common_config('fix', 'legacy_http') && parse_url($feed, PHP_URL_SCHEME) === 'https') {
            common_log(LOG_DEBUG, "OSTATUS [{$this->notice->getID()}]: Searching for http scheme instead for HubSub feed topic: "._ve($feed));
            $http_feed = str_replace('https://', 'http://', $feed);
            $sub = new HubSub();
            $sub->topic = $http_feed;
            // If we find it we upgrade the rows in the hubsub table.
            if ($sub->find()) {
                common_log(LOG_INFO, "OSTATUS [{$this->notice->getID()}]: Found topic with http scheme for "._ve($feed).", will update the rows to use https instead!");
                // we found an http:// URL but we use https:// now
                // so let's update the rows to reflect on this!
                while ($sub->fetch()) {
                    common_debug("OSTATUS [{$this->notice->getID()}]: Changing topic URL to https for feed callback "._ve($sub->callback));
                    $orig = clone($sub);
                    $sub->topic = $feed;
                    // hashkey column will be set automagically in HubSub->onUpdateKeys through updateWithKeys
                    $sub->updateWithKeys($orig);
                    unset($orig);
                }
            }
        }

        $sub = new HubSub();
        $sub->topic = $feed;
        if ($sub->find()) {
            $args = array_slice(func_get_args(), 2);
            $atom = call_user_func_array($callback, $args);
            $this->pushFeedInternal($atom, $sub);
        } else {
            common_log(LOG_INFO, "OSTATUS [{$this->notice->getID()}]: No WebSub subscribers for $feed");
        }
    }

    /**
     * Ping external hub about this update.
     * The hub will pull the feed and check for new items later.
     * Not guaranteed safe in an environment with database replication.
     *
     * @param string $feed feed topic URI
     * @param string $hub WebSub hub URI
     * @fixme can consolidate pings for user & group posts
     */
    function pushFeedExternal($feed, $hub)
    {
        $client = new HTTPClient();
        try {
            $data = array('hub.mode' => 'publish',
                          'hub.url' => $feed);
            $response = $client->post($hub, array(), $data);
            if ($response->getStatus() == 204) {
                common_log(LOG_INFO, "WebSub ping to hub $hub for $feed ok");
                return true;
            } else {
                common_log(LOG_ERR, "WebSub ping to hub $hub for $feed failed with HTTP " .
                                    $response->getStatus() . ': ' .
                                    $response->getBody());
            }
        } catch (Exception $e) {
            common_log(LOG_ERR, "WebSub ping to hub $hub for $feed failed: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Queue up direct feed update pushes to subscribers on our internal hub.
     * If there are a large number of subscriber sites, intermediate bulk
     * distribution triggers may be queued.
     *
     * @param string $atom update feed, containing only new/changed items
     * @param HubSub $sub open query of subscribers
     */
    function pushFeedInternal($atom, $sub)
    {
        common_log(LOG_INFO, "Preparing $sub->N WebSub distribution(s) for $sub->topic");
        $n = 0;
        $batch = array();
        while ($sub->fetch()) {
            $n++;
            if ($n < self::MAX_UNBATCHED) {
                $sub->distribute($atom);
            } else {
                $batch[] = $sub->callback;
                if (count($batch) >= self::BATCH_SIZE) {
                    $sub->bulkDistribute($atom, $batch);
                    $batch = array();
                }
            }
        }
        if (count($batch) > 0) {
            $sub->bulkDistribute($atom, $batch);
        }
    }

    /**
     * Build a single-item version of the sending user's Atom feed.
     * @return string
     */
    function userFeedForNotice()
    {
        $atom = new AtomUserNoticeFeed($this->user);
        $atom->addEntryFromNotice($this->notice);
        $feed = $atom->getString();

        return $feed;
    }

    function groupFeedForNotice($group_id)
    {
        $group = User_group::getKV('id', $group_id);

        $atom = new AtomGroupNoticeFeed($group);
        $atom->addEntryFromNotice($this->notice);
        $feed = $atom->getString();

        return $feed;
    }

    function peopletagFeedForNotice($ptag)
    {
        $atom = new AtomListNoticeFeed($ptag);
        $atom->addEntryFromNotice($this->notice);
        $feed = $atom->getString();

        return $feed;
    }
}

repo_name: andremichelle/gnu

license: mit


code:
<?php

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * @package OStatusPlugin
 * @maintainer Mikael Nordfeldth <mmn@hethane.se>
 */

/**
 * Exception indicating we've got a remote reference to a local user,
 * not a remote user!
 *
 * If we can ue a local profile after all, it's available as $e->profile.
 */
class OStatusShadowException extends Exception
{
    public $profile;

    /**
     * @param Profile $profile
     * @param string $message
     */
    function __construct(Profile $profile, $message) {
        $this->profile = $profile;
        parent::__construct($message);
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Process a feed distribution POST from a WebSub (previously PuSH) hub.
 * @package FeedSub
 * @author Brion Vibber <brion@status.net>
 */
class PushInQueueHandler extends QueueHandler
{
    function transport()
    {
        return 'pushin';
    }

    function handle($data)
    {
        assert(is_array($data));

        $feedsub_id = $data['feedsub_id'];
        $post = $data['post'];
        $hmac = $data['hmac'];

        try {
            $feedsub = FeedSub::getByID($feedsub_id);
            $feedsub->receive($post, $hmac);
        } catch(NoResultException $e) {
            common_log(LOG_INFO, "Discarding POST to unknown feed subscription id {$feedsub_id}");
        } catch(Exception $e) {
            if (is_null($feedsub)) {
                common_log(LOG_ERR, "Exception "._ve(get_class($e))." during WebSub push input processing where FeedSub->receive returned null!" . _ve($e->getMessage()));
            } else {
                common_log(LOG_ERR, "Exception "._ve(get_class($e))." during WebSub push input processing for {$feedsub->getUri()}: " . _ve($e->getMessage()));
            }
        }
        return true;
    }
}


code:
<?php
/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Renew an expiring feedsub
 * @package FeedSub
 * @author Stephen Paul Weber <singpolyma@singpolyma.net>
 */
class PushRenewQueueHandler extends QueueHandler
{
    function transport()
    {
        return 'pushrenew';
    }

    function handle($data)
    {
        $feedsub_id = $data['feedsub_id'];
        $feedsub = FeedSub::getKV('id', $feedsub_id);
        if ($feedsub instanceof FeedSub) {
            try {
                common_log(LOG_INFO, "Renewing feed subscription\n\tExp.: {$feedsub->sub_end}\n\tFeed: {$feedsub->uri}\n\tHub:  {$feedsub->huburi}");
                $feedsub->renew();
            } catch(Exception $e) {
                common_log(LOG_ERR, "Exception during WebSub renew processing for $feedsub->uri: " . $e->getMessage());
            }
        } else {
            common_log(LOG_ERR, "Discarding renew for unknown feed subscription id $feedsub_id");
        }
        return true;
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * A sample module to show best practices for StatusNet plugins
 *
 * PHP version 5
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package   StatusNet
 * @author    James Walker <james@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class Salmon
{
    const REL_SALMON = 'salmon';
    const REL_MENTIONED = 'mentioned';

    // XXX: these are deprecated
    const NS_REPLIES = "http://salmon-protocol.org/ns/salmon-replies";
    const NS_MENTIONS = "http://salmon-protocol.org/ns/salmon-mention";

    /**
     * Sign and post the given Atom entry as a Salmon message.
     *
     * Side effects: may generate a keypair on-demand for the given user,
     * which can be very slow on some systems (like those without php5-gmp).
     *
     * @param string $endpoint_uri
     * @param string $xml string representation of payload
     * @param Profile $user profile whose keys we sign with (must be a local user)
     * @return boolean success
     */
    public static function post($endpoint_uri, $xml, Profile $actor, Profile $target=null)
    {
        if (empty($endpoint_uri)) {
            common_debug('No endpoint URI for Salmon post to '.$actor->getUri());
            return false;
        }

        try {
            $magic_env = MagicEnvelope::signAsUser($xml, $actor->getUser());
        } catch (Exception $e) {
            common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
            return false;
        }

        // $target is so far only used in Diaspora, so it can be null
        if (Event::handle('SalmonSlap', array($endpoint_uri, $magic_env, $target))) {
            return false;
            //throw new ServerException('Could not distribute salmon slap as no plugin completed the event.');
        }

        return true;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @package OStatusPlugin
 * @author James Walker <james@status.net>
 */

if (!defined('GNUSOCIAL')) { exit(1); }

class SalmonAction extends Action
{
    protected $needPost = true;

    protected $oprofile = null; // Ostatus_profile of the actor
    protected $actor    = null; // Profile object of the actor

    var $format   = 'text'; // error messages will be printed in plaintext

    var $xml      = null;
    var $activity = null;
    var $target   = null;

    protected function prepare(array $args=array())
    {
        GNUsocial::setApi(true); // Send smaller error pages

        parent::prepare($args);

        if (!isset($_SERVER['CONTENT_TYPE'])) {
            // TRANS: Client error. Do not translate "Content-type"
            throw new ClientException(_m('Salmon requires a Content-type header.'));
        }
        $envxml = null;
        switch ($_SERVER['CONTENT_TYPE']) {
        case 'application/magic-envelope+xml':
            $envxml = file_get_contents('php://input');
            break;
        case 'application/x-www-form-urlencoded':
            $envxml = Magicsig::base64_url_decode($this->trimmed('xml'));
            break;
        default:
            // TRANS: Client error. Do not translate the quoted "application/[type]" strings.
            throw new ClientException(_m('Salmon requires "application/magic-envelope+xml". For Diaspora we also accept "application/x-www-form-urlencoded" with an "xml" parameter.', 415));
        }

        if (empty($envxml)) {
            throw new ClientException('No magic envelope supplied in POST.');
        }
        try {
            $magic_env = new MagicEnvelope($envxml);   // parse incoming XML as a MagicEnvelope

            $entry = $magic_env->getPayload();  // Not cryptographically verified yet!
            $this->activity = new Activity($entry->documentElement);
            if (empty($this->activity->actor->id)) {
                common_log(LOG_ERR, "broken actor: " . var_export($this->activity->actor->id, true));
                common_log(LOG_ERR, "activity with no actor: " . var_export($this->activity, true));
                // TRANS: Exception.
                throw new ClientException(_m('Activity in salmon slap has no actor id.'));
            }
            // ensureProfiles sets $this->actor and $this->oprofile
            $this->ensureProfiles();
        } catch (Exception $e) {
            common_debug('Salmon envelope parsing failed with: '.$e->getMessage());
            // convert exception to ClientException
            throw new ClientException($e->getMessage());
        }

        // Cryptographic verification test, throws exception on failure
        $magic_env->verify($this->actor);

        common_debug('Salmon slap is carrying activity URI=='._ve($this->activity->id));

        return true;
    }

    /**
     * Check the posted activity type and break out to appropriate processing.
     */

    protected function handle()
    {
        parent::handle();

        assert($this->activity instanceof Activity);
        assert($this->target instanceof Profile);

        common_log(LOG_DEBUG, "Got a " . $this->activity->verb);

        try {
            $options = [ 'source' => 'ostatus' ];
            common_debug('Save salmon slap directly with Notice::saveActivity for actor=='.$this->actor->getID());
            $stored = Notice::saveActivity($this->activity, $this->actor, $options);
            common_debug('Save salmon slap finished, notice id=='.$stored->getID());
            return true;
        } catch (AlreadyFulfilledException $e) {
            // The action's results are already fulfilled. Maybe it was a
            // duplicate? Maybe someone's database is out of sync?
            // Let's just accept it and move on.
            common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.');
            return true;
        } catch (NoticeSaveException $e) {
            common_debug('Notice::saveActivity did not save our '._ve($this->activity->verb).' activity, trying old-fashioned salmon saving.');
        }

        try {
            if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) &&
                    Event::handle('StartHandleSalmon', array($this->activity))) {
                switch ($this->activity->verb) {
                case ActivityVerb::POST:
                    $this->handlePost();
                    break;
                case ActivityVerb::SHARE:
                    $this->handleShare();
                    break;
                case ActivityVerb::FOLLOW:
                case ActivityVerb::FRIEND:
                    $this->handleFollow();
                    break;
                case ActivityVerb::UNFOLLOW:
                    $this->handleUnfollow();
                    break;
                case ActivityVerb::JOIN:
                    $this->handleJoin();
                    break;
                case ActivityVerb::LEAVE:
                    $this->handleLeave();
                    break;
                case ActivityVerb::TAG:
                    $this->handleTag();
                    break;
                case ActivityVerb::UNTAG:
                    $this->handleUntag();
                    break;
                case ActivityVerb::UPDATE_PROFILE:
                    $this->handleUpdateProfile();
                    break;
                default:
                    // TRANS: Client exception.
                    throw new ClientException(_m('Unrecognized activity type.'));
                }
                Event::handle('EndHandleSalmon', array($this->activity));
                Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
            }
        } catch (AlreadyFulfilledException $e) {
            // The action's results are already fulfilled. Maybe it was a
            // duplicate? Maybe someone's database is out of sync?
            // Let's just accept it and move on.
            common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.');
        }
    }

    function handlePost()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand posts.'));
    }

    function handleFollow()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand follows.'));
    }

    function handleUnfollow()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand unfollows.'));
    }

    function handleShare()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand share events.'));
    }

    function handleJoin()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand joins.'));
    }

    function handleLeave()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand leave events.'));
    }

    function handleTag()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand list events.'));
    }

    function handleUntag()
    {
        // TRANS: Client exception.
        throw new ClientException(_m('This target does not understand unlist events.'));
    }

    /**
     * Remote user sent us an update to their profile.
     * If we already know them, accept the updates.
     */
    function handleUpdateProfile()
    {
        $oprofile = Ostatus_profile::getActorProfile($this->activity);
        if ($oprofile instanceof Ostatus_profile) {
            common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri");
            $oprofile->updateFromActivityObject($this->activity->actor);
        } else {
            common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->activity->actor->id);
        }
    }

    function ensureProfiles()
    {
        try {
            $this->oprofile = Ostatus_profile::getActorProfile($this->activity);
            if (!$this->oprofile instanceof Ostatus_profile) {
                throw new UnknownUriException($this->activity->actor->id);
            }
        } catch (UnknownUriException $e) {
            // Apparently we didn't find the Profile object based on our URI,
            // so OStatus doesn't have it with this URI in ostatus_profile.
            // Try to look it up again, remote side may have changed from http to https
            // or maybe publish an acct: URI now instead of an http: URL.
            //
            // Steps:
            // 1. Check the newly received URI. Who does it say it is?
            // 2. Compare these alleged identities to our local database.
            // 3. If we found any locally stored identities, ask it about its aliases.
            // 4. Do any of the aliases from our known identity match the recently introduced one?
            //
            // Example: We have stored http://example.com/user/1 but this URI says https://example.com/user/1
            common_debug('No local Profile object found for a magicsigned activity author URI: '.$e->object_uri);
            $disco = new Discovery();
            $xrd = $disco->lookup($e->object_uri);
            // Step 1: We got a bunch of discovery data for https://example.com/user/1 which includes
            //         aliases https://example.com/user and hopefully our original http://example.com/user/1 too
            $all_ids = array_merge(array($xrd->subject), $xrd->aliases);

            if (!in_array($e->object_uri, $all_ids)) {
                common_debug('The activity author URI we got was not listed itself when doing discovery on it.');
                throw $e;
            }

            // Go through each reported alias from lookup to see if we know this already
            foreach ($all_ids as $aliased_uri) {
                $oprofile = Ostatus_profile::getKV('uri', $aliased_uri);
                if (!$oprofile instanceof Ostatus_profile) {
                    continue;   // unknown locally, check the next alias
                }
                // Step 2: We found the alleged http://example.com/user/1 URI in our local database,
                //         but this can't be trusted yet because anyone can publish any alias.
                common_debug('Found a local Ostatus_profile for "'.$e->object_uri.'" with this URI: '.$aliased_uri);

                // We found an existing OStatus profile, but is it really the same? Do a callback to the URI's origin
                // Step 3: lookup our previously known http://example.com/user/1 webfinger etc.
                $xrd = $disco->lookup($oprofile->getUri()); // getUri returns ->uri, which we filtered on earlier
                $doublecheck_aliases = array_merge(array($xrd->subject), $xrd->aliases);
                common_debug('Trying to match known "'.$aliased_uri.'" against its returned aliases: '.implode(' ', $doublecheck_aliases));
                // if we find our original URI here, it is a legitimate alias
                // Step 4: Is the newly introduced https://example.com/user/1 URI in the list of aliases
                //         presented by http://example.com/user/1 (i.e. do they both say they are the same identity?)
                if (in_array($e->object_uri, $doublecheck_aliases)) {
                    $oprofile->updateUriKeys($e->object_uri, DiscoveryHints::fromXRD($xrd));
                    $this->oprofile = $oprofile;
                    break;  // don't iterate through aliases anymore
                }
            }

            // We might end up here after $all_ids is iterated through without a $this->oprofile value,
            if (!$this->oprofile instanceof Ostatus_profile) {
                common_debug("We do not have a local profile to connect to this activity's author. Let's create one.");
                // ensureActivityObjectProfile throws exception on failure
                $this->oprofile = Ostatus_profile::ensureActivityObjectProfile($this->activity->actor);
            }
        }

        assert($this->oprofile instanceof Ostatus_profile);

        $this->actor = $this->oprofile->localProfile();
    }

    function saveNotice()
    {
        if (!$this->oprofile instanceof Ostatus_profile) {
            common_debug('Ostatus_profile missing in ' . get_class(). ' profile: '.var_export($this->profile, true));
        }
        return $this->oprofile->processPost($this->activity, 'salmon');
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Send a Salmon notification in the background.
 * @package OStatusPlugin
 * @author Brion Vibber <brion@status.net>
 */
class SalmonQueueHandler extends QueueHandler
{
    function transport()
    {
        return 'salmon';
    }

    function handle($data)
    {
        assert(is_array($data));
        assert(is_string($data['salmonuri']));
        assert(is_string($data['entry']));

        $actor = Profile::getByID($data['actor']);
        $target = Profile::getByID($data['target']);

        Salmon::post($data['salmonuri'], $data['entry'], $actor, $target);

        // @fixme detect failure and attempt to resend
        return true;
    }
}


code:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-08 18:20+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#. TRANS: String in the gettext speed test script. Unimportant.
#: tests/gettext-speedtest.php:58
msgid "Feeds"
msgstr ""

#. TRANS: Exception.
#: lib/magicenvelope.php:137
msgid "Unable to locate signer public key."
msgstr ""

#. TRANS: Exception when public key was not properly formatted.
#: lib/magicenvelope.php:161
msgid "Incorrectly formatted public key element."
msgstr ""

#: lib/magicenvelope.php:260
msgid "Salmon post must be an Atom entry."
msgstr ""

#. TRANS: Client error when incoming salmon slap signature does not verify cryptographically.
#: lib/magicenvelope.php:362
msgid "Salmon signature verification failed."
msgstr ""

#. TRANS: Client error. Do not translate "Content-type"
#: lib/salmonaction.php:48
msgid "Salmon requires a Content-type header."
msgstr ""

#. TRANS: Exception.
#: lib/salmonaction.php:75
msgid "Activity in salmon slap has no actor id."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:156
msgid "Unrecognized activity type."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:172
msgid "This target does not understand posts."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:178
msgid "This target does not understand follows."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:184
msgid "This target does not understand unfollows."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:190
msgid "This target does not understand share events."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:196
msgid "This target does not understand joins."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:202
msgid "This target does not understand leave events."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:208
msgid "This target does not understand list events."
msgstr ""

#. TRANS: Client exception.
#: lib/salmonaction.php:214
msgid "This target does not understand unlist events."
msgstr ""

#. TRANS: Field label for a field that takes an OStatus user address.
#: actions/ostatussub.php:94
msgid "Subscribe to"
msgstr ""

#. TRANS: Tooltip for field label "Subscribe to".
#: actions/ostatussub.php:97
msgid ""
"OStatus user's address, like nickname@example.com or http://example.net/"
"nickname."
msgstr ""

#. TRANS: Button text.
#. TRANS: Button text to continue joining a remote list.
#: actions/ostatussub.php:101 actions/ostatusgroup.php:83
#: actions/ostatuspeopletag.php:82
msgctxt "BUTTON"
msgid "Continue"
msgstr ""

#. TRANS: Button text.
#: actions/ostatussub.php:136
msgid "Join"
msgstr ""

#. TRANS: Tooltip for button "Join".
#: actions/ostatussub.php:138
msgctxt "BUTTON"
msgid "Join this group"
msgstr ""

#. TRANS: Button text.
#: actions/ostatussub.php:141
msgctxt "BUTTON"
msgid "Confirm"
msgstr ""

#. TRANS: Tooltip for button "Confirm".
#: actions/ostatussub.php:143
msgid "Subscribe to this user"
msgstr ""

#. TRANS: Extra paragraph in remote profile view when already subscribed.
#: actions/ostatussub.php:164
msgid "You are already subscribed to this user."
msgstr ""

#. TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
#. TRANS: and example.net, as these are official standard domain names for use in examples.
#: actions/ostatussub.php:259 actions/ostatussub.php:267
#: actions/ostatussub.php:297 OStatusPlugin.php:252
msgid ""
"Sorry, we could not reach that address. Please make sure that the OStatus "
"address is like nickname@example.com or http://example.net/nickname."
msgstr ""

#. TRANS: Error text.
#: actions/ostatussub.php:271 actions/ostatussub.php:275
#: actions/ostatussub.php:279 actions/ostatussub.php:283
#: actions/ostatussub.php:287
msgid ""
"Sorry, we could not reach that feed. Please try that OStatus address again "
"later."
msgstr ""

#. TRANS: Error text.
#: actions/ostatussub.php:291
msgid "Sorry, that feed is not Pubsubhubub enabled."
msgstr ""

#. TRANS: OStatus remote subscription dialog error.
#. TRANS: OStatus remote group subscription dialog error.
#: actions/ostatussub.php:328 actions/ostatuspeopletag.php:144
msgid "Already subscribed!"
msgstr ""

#. TRANS: OStatus remote subscription dialog error.
#: actions/ostatussub.php:333
msgid "Remote subscription failed!"
msgstr ""

#. TRANS: Client error displayed when the session token does not match or is not given.
#: actions/ostatussub.php:349 actions/ostatusinit.php:68
msgid "There was a problem with your session token. Try again, please."
msgstr ""

#. TRANS: Form title.
#: actions/ostatussub.php:375
msgid "Subscribe to user"
msgstr ""

#. TRANS: Page title for OStatus remote subscription form.
#: actions/ostatussub.php:395
msgid "Confirm"
msgstr ""

#: actions/ostatussub.php:395
msgid "Remote subscription"
msgstr ""

#. TRANS: Instructions.
#: actions/ostatussub.php:407
msgid ""
"You can subscribe to users from other supported sites. Paste their address "
"or profile URI below:"
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:69
msgid ""
"The owner of this salmon endpoint was not in the context of the carried slap."
msgstr ""

#. TRANS: Client exception thrown when an undefied activity is performed.
#: actions/usersalmon.php:95
msgid "Cannot handle that kind of post."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:136 actions/usersalmon.php:168
msgid "Not a person object."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:143
msgid "Unidentified profile being listed."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:148
msgid "This user is not the one being listed."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:158
msgid "The listing could not be saved."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:175
msgid "Unidentified profile being unlisted."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:180
msgid "This user is not the one being unlisted."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:191
msgid "The listing could not be deleted."
msgstr ""

#. TRANS: Client exception.
#: actions/usersalmon.php:212
msgid "Cannot handle that kind of object for liking/faving."
msgstr ""

#. TRANS: Client exception. %s is an object ID.
#: actions/usersalmon.php:219
#, php-format
msgid "Notice with ID %s unknown."
msgstr ""

#. TRANS: Client exception. %1$s is a notice ID, %2$s is a user ID.
#: actions/usersalmon.php:224
#, php-format
msgid "Notice with ID %1$s not posted by %2$s."
msgstr ""

#. TRANS: Client error displayed when trying to list a local object as if it is remote.
#: actions/ostatustag.php:40
msgid "You can use the local list functionality!"
msgstr ""

#. TRANS: Header for listing a remote object. %s is a remote object's name.
#: actions/ostatustag.php:54
#, php-format
msgid "List %s"
msgstr ""

#. TRANS: Button text to list a remote object.
#: actions/ostatustag.php:56
msgctxt "BUTTON"
msgid "Go"
msgstr ""

#. TRANS: Field label.
#: actions/ostatustag.php:68 actions/ostatusinit.php:134
msgid "User nickname"
msgstr ""

#. TRANS: Field title.
#: actions/ostatustag.php:70
msgid "Nickname of the user you want to list."
msgstr ""

#. TRANS: Field label.
#: actions/ostatustag.php:74 actions/ostatusinit.php:144
msgid "Profile Account"
msgstr ""

#. TRANS: Field title.
#: actions/ostatustag.php:76
msgid "Your account id (for example user@example.com)."
msgstr ""

#. TRANS: Client error displayed when remote profile address could not be confirmed.
#. TRANS: Client error.
#: actions/ostatustag.php:103 actions/ostatusinit.php:192
msgid "Could not confirm remote profile address."
msgstr ""

#. TRANS: Title for an OStatus list.
#: actions/ostatustag.php:121
msgid "OStatus list"
msgstr ""

#. TRANS: Client exception.
#: actions/pushhub.php:66
msgid "Publishing outside feeds not supported."
msgstr ""

#. TRANS: Client exception. %s is a mode.
#: actions/pushhub.php:69
#, php-format
msgid "Unrecognized mode \"%s\"."
msgstr ""

#. TRANS: Client exception. %s is a topic.
#: actions/pushhub.php:91
#, php-format
msgid ""
"Unsupported hub.topic %s this hub only serves local user and group Atom "
"feeds."
msgstr ""

#. TRANS: Client exception. %s is the invalid lease value.
#: actions/pushhub.php:98
#, php-format
msgid "Invalid hub.lease \"%s\". It must be empty or positive integer."
msgstr ""

#. TRANS: Client exception. %s is the invalid hub secret.
#: actions/pushhub.php:105
#, php-format
msgid "Invalid hub.secret \"%s\". It must be under 200 bytes."
msgstr ""

#. TRANS: Client exception. %s is a feed URL.
#: actions/pushhub.php:158
#, php-format
msgid "Invalid hub.topic \"%s\". User does not exist."
msgstr ""

#. TRANS: Client exception. %s is a feed URL.
#: actions/pushhub.php:166
#, php-format
msgid "Invalid hub.topic \"%s\". Local_group does not exist."
msgstr ""

#. TRANS: Client exception. %s is a feed URL.
#: actions/pushhub.php:187
#, php-format
msgid "Invalid hub.topic %s; list does not exist."
msgstr ""

#. TRANS: Client exception.
#. TRANS: %1$s is this argument to the method this exception occurs in, %2$s is a URL.
#: actions/pushhub.php:212
#, php-format
msgid "Invalid URL passed for %1$s: \"%2$s\""
msgstr ""

#. TRANS: Client error.
#: actions/ostatusinit.php:44
msgid "You can use the local subscription!"
msgstr ""

#. TRANS: Form title.
#: actions/ostatusinit.php:85
msgctxt "TITLE"
msgid "Subscribe to user"
msgstr ""

#. TRANS: Form legend. %s is a group name.
#: actions/ostatusinit.php:101
#, php-format
msgid "Join group %s"
msgstr ""

#. TRANS: Button text to join a group.
#: actions/ostatusinit.php:103
msgctxt "BUTTON"
msgid "Join"
msgstr ""

#. TRANS: Form legend. %1$s is a list, %2$s is a lister's name.
#: actions/ostatusinit.php:106
#, php-format
msgid "Subscribe to list %1$s by %2$s"
msgstr ""

#. TRANS: Button text to subscribe to a list.
#. TRANS: Button text to subscribe to a profile.
#: actions/ostatusinit.php:108 actions/ostatusinit.php:114
msgctxt "BUTTON"
msgid "Subscribe"
msgstr ""

#. TRANS: Form legend. %s is a nickname.
#: actions/ostatusinit.php:112
#, php-format
msgid "Subscribe to %s"
msgstr ""

#. TRANS: Field label.
#: actions/ostatusinit.php:129
msgid "Group nickname"
msgstr ""

#. TRANS: Field title.
#: actions/ostatusinit.php:131
msgid "Nickname of the group you want to join."
msgstr ""

#. TRANS: Field title.
#: actions/ostatusinit.php:136
msgid "Nickname of the user you want to follow."
msgstr ""

#. TRANS: Tooltip for field label "Profile Account".
#: actions/ostatusinit.php:146
msgid "Your account ID (e.g. user@example.com)."
msgstr ""

#. TRANS: Client error.
#: actions/ostatusinit.php:169
msgid "Must provide a remote profile."
msgstr ""

#. TRANS: Client error.
#: actions/ostatusinit.php:218 actions/ostatusinit.php:232
msgid "No such user."
msgstr ""

#. TRANS: Client error.
#: actions/ostatusinit.php:226 actions/groupsalmon.php:45
msgid "No such group."
msgstr ""

#. TRANS: Client error.
#. TRANS: Client error displayed when referring to a non-existing list.
#: actions/ostatusinit.php:241 actions/peopletagsalmon.php:45
msgid "No such list."
msgstr ""

#. TRANS: Client error.
#: actions/ostatusinit.php:244
msgid "No local user or group nickname provided."
msgstr ""

#. TRANS: Page title.
#: actions/ostatusinit.php:251
msgid "OStatus Connect"
msgstr ""

#. TRANS: Field label.
#: actions/ostatusgroup.php:74
msgid "Join group"
msgstr ""

#. TRANS: Tooltip for field label "Join group". Do not translate the "example.net"
#. TRANS: domain name in the URL, as it is an official standard domain name for examples.
#: actions/ostatusgroup.php:78
msgid "OStatus group's address, like http://example.net/group/nickname."
msgstr ""

#. TRANS: Error text displayed when trying to join a remote group the user is already a member of.
#: actions/ostatusgroup.php:101
msgid "You are already a member of this group."
msgstr ""

#. TRANS: OStatus remote group subscription dialog error.
#: actions/ostatusgroup.php:134
msgid "Already a member!"
msgstr ""

#. TRANS: OStatus remote group subscription dialog error.
#: actions/ostatusgroup.php:144
msgid "Remote group join failed!"
msgstr ""

#. TRANS: Page title for OStatus remote group join form
#: actions/ostatusgroup.php:159
msgid "Confirm joining remote group"
msgstr ""

#. TRANS: Form instructions.
#: actions/ostatusgroup.php:170
msgid ""
"You can subscribe to groups from other supported sites. Paste the group's "
"profile URI below:"
msgstr ""

#. TRANS: Client error.
#. TRANS: Client error displayed trying to perform an action without providing an ID.
#: actions/groupsalmon.php:38 actions/peopletagsalmon.php:38
msgid "No ID."
msgstr ""

#. TRANS: Client error.
#: actions/groupsalmon.php:54
msgid "Cannot accept remote posts for a remote group."
msgstr ""

#. TRANS: Client error.
#: actions/groupsalmon.php:129
msgid "Groups cannot join groups."
msgstr ""

#. TRANS: Client error displayed when trying to join a group the user is blocked from by a group admin.
#: actions/groupsalmon.php:142
msgid "You have been blocked from that group by the admin."
msgstr ""

#. TRANS: Server error. %1$s is a profile URI, %2$s is a group nickname.
#: actions/groupsalmon.php:149
#, php-format
msgid "Could not join remote user %1$s to group %2$s."
msgstr ""

#. TRANS: Client error displayed when trying to have a group join another group.
#: actions/groupsalmon.php:161
msgid "Groups cannot be members of groups"
msgstr ""

#. TRANS: Server error. %1$s is a profile URI, %2$s is a group nickname.
#: actions/groupsalmon.php:170
#, php-format
msgid "Could not remove remote user %1$s from group %2$s."
msgstr ""

#. TRANS: Client error displayed when trying to send a message to a remote list.
#: actions/peopletagsalmon.php:54
msgid "Cannot accept remote posts for a remote list."
msgstr ""

#. TRANS: Client error displayed when trying to subscribe a group to a list.
#. TRANS: Client error displayed when trying to unsubscribe a group from a list.
#: actions/peopletagsalmon.php:91 actions/peopletagsalmon.php:124
msgid "Groups cannot subscribe to lists."
msgstr ""

#. TRANS: Server error displayed when subscribing a remote user to a list fails.
#. TRANS: %1$s is a profile URI, %2$s is a list name.
#: actions/peopletagsalmon.php:109
#, php-format
msgid "Could not subscribe remote user %1$s to list %2$s."
msgstr ""

#. TRANS: Client error displayed when trying to unsubscribe a remote user from a list fails.
#. TRANS: %1$s is a profile URL, %2$s is a list name.
#: actions/peopletagsalmon.php:133
#, php-format
msgid "Could not unsubscribe remote user %1$s from list %2$s."
msgstr ""

#. TRANS: Server exception thrown when referring to a non-existing or empty feed.
#: actions/pushcallback.php:51
msgid "Empty or invalid feed id."
msgstr ""

#. TRANS: Server exception. %s is a feed ID.
#: actions/pushcallback.php:57
#, php-format
msgid "Unknown WebSub subscription feed id %s"
msgstr ""

#. TRANS: Client exception. %s is an invalid feed name.
#: actions/pushcallback.php:96
#, php-format
msgid "Bad hub.topic feed \"%s\"."
msgstr ""

#. TRANS: Client exception. %s is an invalid topic.
#: actions/pushcallback.php:103
#, php-format
msgid "Unexpected subscribe request for %s."
msgstr ""

#. TRANS: Client exception. %s is an invalid topic.
#: actions/pushcallback.php:108
#, php-format
msgid "Unexpected unsubscribe request for %s."
msgstr ""

#. TRANS: Field label.
#: actions/ostatuspeopletag.php:74
msgid "Subscribe to list"
msgstr ""

#. TRANS: Field title.
#: actions/ostatuspeopletag.php:77
msgid "Address of the OStatus list, like http://example.net/user/all/tag."
msgstr ""

#. TRANS: Error text displayed when trying to subscribe to a list already a subscriber to.
#: actions/ostatuspeopletag.php:102
msgid "You are already subscribed to this list."
msgstr ""

#. TRANS: Page title for OStatus remote list subscription form
#: actions/ostatuspeopletag.php:165
msgid "Confirm subscription to remote list"
msgstr ""

#. TRANS: Instructions for OStatus list subscription form.
#: actions/ostatuspeopletag.php:177
msgid ""
"You can subscribe to lists from other supported sites. Paste the list's URI "
"below:"
msgstr ""

#. TRANS: Server exception. %s is a URI
#: classes/Ostatus_profile.php:195 classes/Ostatus_profile.php:218
#, php-format
msgid "Invalid ostatus_profile state: Two or more IDs set for %s."
msgstr ""

#. TRANS: Server exception. %s is a URI
#: classes/Ostatus_profile.php:198 classes/Ostatus_profile.php:221
#, php-format
msgid "Invalid ostatus_profile state: All IDs empty for %s."
msgstr ""

#. TRANS: Server exception.
#: classes/Ostatus_profile.php:419
msgid ""
"Invalid type passed to Ostatus_profile::notify. It must be XML string or "
"Activity entry."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:452
msgid "Unknown feed format."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:473
msgid "RSS feed without a channel."
msgstr ""

#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:645
#, php-format
msgid "Could not reach profile page %s."
msgstr ""

#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:710 scripts/update_ostatus_profiles.php:123
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr ""

#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:822
msgid "Cannot find enough profile information to make a feed."
msgstr ""

#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:896
#, php-format
msgid "Invalid avatar URL %s."
msgstr ""

#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1104
msgid "No author ID URI found."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:1132
msgid "No profile URI."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:1138
msgid "Local user cannot be referenced as remote."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:1143
msgid "Local group cannot be referenced as remote."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:1151
msgid "Local list cannot be referenced as remote."
msgstr ""

#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1206 classes/Ostatus_profile.php:1245
msgid "Cannot save local profile."
msgstr ""

#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1258
msgid "Cannot save local list."
msgstr ""

#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1266
msgid "Cannot save OStatus profile."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:1592
msgid "Not a valid webfinger address (via cache)."
msgstr ""

#. TRANS: Exception.
#: classes/Ostatus_profile.php:1621 scripts/update_ostatus_profiles.php:148
msgid "Not a valid webfinger address."
msgstr ""

#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1700
#, php-format
msgid "Could not save profile for \"%s\"."
msgstr ""

#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1720
#, php-format
msgid "Could not save OStatus profile for \"%s\"."
msgstr ""

#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1728 scripts/update_ostatus_profiles.php:203
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr ""

#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1773
msgid "Could not store HTML content of long post as file."
msgstr ""

#. TRANS: Server exception.
#. TRANS: %1$s is a protocol, %2$s is a URI.
#: classes/Ostatus_profile.php:1807
#, php-format
msgid "Unrecognized URI protocol for profile: %1$s (%2$s)."
msgstr ""

#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1813
#, php-format
msgid "No URI protocol for profile: %s."
msgstr ""

#. TRANS: Server exception.
#: classes/FeedSub.php:285
msgid "Attempting to start WebSub subscription for feed with no hub."
msgstr ""

#. TRANS: Server exception.
#: classes/FeedSub.php:328
msgid "Attempting to end WebSub subscription for feed with no hub."
msgstr ""

#: classes/FeedSub.php:442
#, php-format
msgid "Unexpected HTTP status: %d"
msgstr ""

#. TRANS: Client exception. %s is a HTTP status code.
#: classes/HubSub.php:233
#, php-format
msgid "Hub subscriber verification returned HTTP %s."
msgstr ""

#. TRANS: Link to subscribe to a remote entity.
#. TRANS: Link text for a user to subscribe to an OStatus user.
#: OStatusPlugin.php:196 OStatusPlugin.php:217 OStatusPlugin.php:1325
msgid "Subscribe"
msgstr ""

#. TRANS: Exception in OStatus when invalid URI was entered.
#: OStatusPlugin.php:245
msgid "Invalid URI."
msgstr ""

#. TRANS: Title. %s is a domain name.
#: OStatusPlugin.php:606
#, php-format
msgid "Sent from %s via OStatus"
msgstr ""

#. TRANS: Title for unfollowing a remote profile.
#: OStatusPlugin.php:744
msgctxt "TITLE"
msgid "Unfollow"
msgstr ""

#. TRANS: Success message for unsubscribe from user attempt through OStatus.
#. TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
#: OStatusPlugin.php:747
#, php-format
msgid "%1$s stopped following %2$s."
msgstr ""

#. TRANS: Title for joining a remote groep.
#: OStatusPlugin.php:794
msgctxt "TITLE"
msgid "Join"
msgstr ""

#. TRANS: Success message for subscribe to group attempt through OStatus.
#. TRANS: %1$s is the member name, %2$s is the subscribed group's name.
#: OStatusPlugin.php:797
#, php-format
msgid "%1$s has joined group %2$s."
msgstr ""

#. TRANS: Exception thrown when joining a remote group fails.
#: OStatusPlugin.php:806
msgid "Failed joining remote group."
msgstr ""

#. TRANS: Title for leaving a remote group.
#: OStatusPlugin.php:848
msgctxt "TITLE"
msgid "Leave"
msgstr ""

#. TRANS: Success message for unsubscribe from group attempt through OStatus.
#. TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
#: OStatusPlugin.php:851
#, php-format
msgid "%1$s has left group %2$s."
msgstr ""

#. TRANS: Title for following a remote list.
#: OStatusPlugin.php:894
msgctxt "TITLE"
msgid "Follow list"
msgstr ""

#. TRANS: Success message for remote list follow through OStatus.
#. TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
#: OStatusPlugin.php:897
#, php-format
msgid "%1$s is now following people listed in %2$s by %3$s."
msgstr ""

#. TRANS: Exception thrown when subscription to remote list fails.
#: OStatusPlugin.php:907
msgid "Failed subscribing to remote list."
msgstr ""

#. TRANS: Title for unfollowing a remote list.
#: OStatusPlugin.php:946
msgid "Unfollow list"
msgstr ""

#. TRANS: Success message for remote list unfollow through OStatus.
#. TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
#: OStatusPlugin.php:949
#, php-format
msgid "%1$s stopped following the list %2$s by %3$s."
msgstr ""

#. TRANS: Title for listing a remote profile.
#: OStatusPlugin.php:1025
msgctxt "TITLE"
msgid "List"
msgstr ""

#. TRANS: Success message for remote list addition through OStatus.
#. TRANS: %1$s is the list creator's name, %2$s is the added list member, %3$s is the list name.
#: OStatusPlugin.php:1028
#, php-format
msgid "%1$s listed %2$s in the list %3$s."
msgstr ""

#. TRANS: Title for unlisting a remote profile.
#: OStatusPlugin.php:1076
msgctxt "TITLE"
msgid "Unlist"
msgstr ""

#. TRANS: Success message for remote list removal through OStatus.
#. TRANS: %1$s is the list creator's name, %2$s is the removed list member, %3$s is the list name.
#: OStatusPlugin.php:1079
#, php-format
msgid "%1$s removed %2$s from the list %3$s."
msgstr ""

#. TRANS: Title for unliking a remote notice.
#: OStatusPlugin.php:1126
msgid "Unlike"
msgstr ""

#. TRANS: Success message for remove a favorite notice through OStatus.
#. TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
#: OStatusPlugin.php:1129
#, php-format
msgid "%1$s no longer likes %2$s."
msgstr ""

#. TRANS: Link text for link to remote subscribe.
#: OStatusPlugin.php:1209
msgid "Remote"
msgstr ""

#. TRANS: Title for activity.
#: OStatusPlugin.php:1249
msgid "Profile update"
msgstr ""

#. TRANS: Ping text for remote profile update through OStatus.
#. TRANS: %s is user that updated their profile.
#: OStatusPlugin.php:1252
#, php-format
msgid "%s has updated their profile page."
msgstr ""

#: OStatusPlugin.php:1287
msgid "Pending"
msgstr ""

#: OStatusPlugin.php:1288
msgid "Active"
msgstr ""

#: OStatusPlugin.php:1289
msgid "Polling"
msgstr ""

#: OStatusPlugin.php:1290
msgid "Inactive"
msgstr ""

#: OStatusPlugin.php:1293
msgid "WebSub"
msgstr ""

#. TRANS: Link text for a user to list an OStatus user.
#: OStatusPlugin.php:1334
msgid "List"
msgstr ""

#. TRANS: Plugin description.
#: OStatusPlugin.php:1359
msgid ""
"Follow people across social networks that implement <a href=\"http://ostatus."
"org/\">OStatus</a>."
msgstr ""