Merge branch '0.9.x' into 1.0.x

This commit is contained in:
Brion Vibber 2010-11-03 16:09:49 -07:00
commit b716d01a41
81 changed files with 17314 additions and 10641 deletions

View File

@ -55,7 +55,7 @@
Yes
@param status (Required) The URL-encoded text of the status update.
@param source (Optional) The source of the status.
@param source (Optional) The source application name, if using HTTP authentication or an anonymous OAuth consumer.
@param in_reply_to_status_id (Optional) The ID of an existing status that the update is in reply to.
@param lat (Optional) The latitude the status refers to.
@param long (Optional) The longitude the status refers to.
@ -67,7 +67,7 @@
@subsection usagenotes Usage notes
@li The URL pattern is relative to the @ref apiroot.
@li If the @e source parameter is not supplied the source of the status will default to 'api'.
@li If the @e source parameter is not supplied the source of the status will default to 'api'. When authenticated via a registered OAuth application, the application's registered name and URL will always override the source parameter.
@li The XML response uses <a href="http://georss.org/Main_Page">GeoRSS</a>
to encode the latitude and longitude (see example response below <georss:point>).
@li Data uploaded via the @e media parameter should be multipart/form-data encoded.

View File

@ -142,7 +142,7 @@ class InviteAction extends CurrentUserDesignAction
$this->elementStart('ul');
foreach ($this->already as $other) {
// TRANS: Used as list item for already subscribed users (%1$s is nickname, %2$s is e-mail address).
$this->element('li', null, sprintf(_('%1$s (%2$s)'), $other->nickname, $other->email));
$this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email));
}
$this->elementEnd('ul');
}
@ -156,7 +156,7 @@ class InviteAction extends CurrentUserDesignAction
$this->elementStart('ul');
foreach ($this->subbed as $other) {
// TRANS: Used as list item for already registered people (%1$s is nickname, %2$s is e-mail address).
$this->element('li', null, sprintf(_('%1$s (%2$s)'), $other->nickname, $other->email));
$this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email));
}
$this->elementEnd('ul');
}

View File

@ -79,11 +79,7 @@ class OembedAction extends Action
if (empty($profile)) {
$this->serverError(_('Notice has no profile.'), 500);
}
if (!empty($profile->fullname)) {
$authorname = $profile->fullname . ' (' . $profile->nickname . ')';
} else {
$authorname = $profile->nickname;
}
$authorname = $profile->getFancyName();
$oembed['title'] = sprintf(_('%1$s\'s status on %2$s'),
$authorname,
common_exact_date($notice->created));

View File

@ -68,12 +68,7 @@ class ShowgroupAction extends GroupDesignAction
*/
function title()
{
if (!empty($this->group->fullname)) {
// @todo FIXME: Needs proper i18n. Maybe use a generic method for this?
$base = $this->group->fullname . ' (' . $this->group->nickname . ')';
} else {
$base = $this->group->nickname;
}
$base = $this->group->getFancyName();
if ($this->page == 1) {
// TRANS: Page title for first group page. %s is a group name.

View File

@ -167,11 +167,7 @@ class ShownoticeAction extends OwnerDesignAction
function title()
{
if (!empty($this->profile->fullname)) {
$base = $this->profile->fullname . ' (' . $this->profile->nickname . ')';
} else {
$base = $this->profile->nickname;
}
$base = $this->profile->getFancyName();
return sprintf(_('%1$s\'s status on %2$s'),
$base,

View File

@ -66,17 +66,19 @@ class ShowstreamAction extends ProfileAction
$base = $this->profile->getFancyName();
if (!empty($this->tag)) {
if ($this->page == 1) {
// TRANS: Page title showing tagged notices in one user's stream. Param 1 is the username, 2 is the hash tag.
// TRANS: Page title showing tagged notices in one user's stream. %1$s is the username, %2$s is the hash tag.
return sprintf(_('%1$s tagged %2$s'), $base, $this->tag);
} else {
// TRANS: Page title showing tagged notices in one user's stream. Param 1 is the username, 2 is the hash tag, 3 is the page number.
// TRANS: Page title showing tagged notices in one user's stream.
// TRANS: %1$s is the username, %2$s is the hash tag, %1$d is the page number.
return sprintf(_('%1$s tagged %2$s, page %3$d'), $base, $this->tag, $this->page);
}
} else {
if ($this->page == 1) {
return $base;
} else {
// TRANS: Extended page title showing tagged notices in one user's stream. Param 1 is the username, param 2 is the page number.
// TRANS: Extended page title showing tagged notices in one user's stream.
// TRANS: %1$s is the username, %2$d is the page number.
return sprintf(_('%1$s, page %2$d'),
$base,
$this->page);
@ -120,6 +122,8 @@ class ShowstreamAction extends ProfileAction
common_local_url('userrss',
array('nickname' => $this->user->nickname,
'tag' => $this->tag)),
// TRANS: Title for link to notice feed.
// TRANS: %1$s is a user nickname, %2$s is a hashtag.
sprintf(_('Notice feed for %1$s tagged %2$s (RSS 1.0)'),
$this->user->nickname, $this->tag)));
}
@ -127,6 +131,8 @@ class ShowstreamAction extends ProfileAction
return array(new Feed(Feed::RSS1,
common_local_url('userrss',
array('nickname' => $this->user->nickname)),
// TRANS: Title for link to notice feed.
// TRANS: %s is a user nickname.
sprintf(_('Notice feed for %s (RSS 1.0)'),
$this->user->nickname)),
new Feed(Feed::RSS2,
@ -134,6 +140,8 @@ class ShowstreamAction extends ProfileAction
array(
'id' => $this->user->id,
'format' => 'rss')),
// TRANS: Title for link to notice feed.
// TRANS: %s is a user nickname.
sprintf(_('Notice feed for %s (RSS 2.0)'),
$this->user->nickname)),
new Feed(Feed::ATOM,
@ -146,6 +154,8 @@ class ShowstreamAction extends ProfileAction
new Feed(Feed::FOAF,
common_local_url('foaf', array('nickname' =>
$this->user->nickname)),
// TRANS: Title for link to notice feed. FOAF stands for Friend of a Friend.
// TRANS: More information at http://www.foaf-project.org. %s is a user nickname.
sprintf(_('FOAF for %s'), $this->user->nickname)));
}
@ -191,17 +201,23 @@ class ShowstreamAction extends ProfileAction
function showEmptyListMessage()
{
$message = sprintf(_('This is the timeline for %1$s but %2$s hasn\'t posted anything yet.'), $this->user->nickname, $this->user->nickname) . ' ';
// TRANS: First sentence of empty list message for a stream. $1%s is a user nickname.
$message = sprintf(_('This is the timeline for %1$s, but %1$s hasn\'t posted anything yet.'), $this->user->nickname) . ' ';
if (common_logged_in()) {
$current_user = common_current_user();
if ($this->user->id === $current_user->id) {
// TRANS: Second sentence of empty list message for a stream for the user themselves.
$message .= _('Seen anything interesting recently? You haven\'t posted any notices yet, now would be a good time to start :)');
} else {
// TRANS: Second sentence of empty list message for a non-self stream. %1$s is a user nickname, %2$s is a part of a URL.
// TRANS: This message contains a Markdown link. Keep "](" together.
$message .= sprintf(_('You can try to nudge %1$s or [post something to them](%%%%action.newnotice%%%%?status_textarea=%2$s).'), $this->user->nickname, '@' . $this->user->nickname);
}
}
else {
// TRANS: Second sentence of empty message for anonymous users. %s is a user nickname.
// TRANS: This message contains a Markdown link. Keep "](" together.
$message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to them.'), $this->user->nickname);
}
@ -237,11 +253,15 @@ class ShowstreamAction extends ProfileAction
function showAnonymousMessage()
{
if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
// TRANS: Announcement for anonymous users showing a stream if site registrations are open.
// TRANS: This message contains a Markdown link. Keep "](" together.
$m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. ' .
'[Join now](%%%%action.register%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'),
$this->user->nickname, $this->user->nickname);
} else {
// TRANS: Announcement for anonymous users showing a stream if site registrations are closed or invite only.
// TRANS: This message contains a Markdown link. Keep "](" together.
$m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. '),
$this->user->nickname, $this->user->nickname);
@ -281,7 +301,6 @@ class ProfileNoticeListItem extends DoFollowListItem
*
* @return void
*/
function showRepeat()
{
if (!empty($this->repeat)) {
@ -292,13 +311,14 @@ class ProfileNoticeListItem extends DoFollowListItem
'class' => 'url');
if (!empty($this->profile->fullname)) {
$attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ')';
$attrs['title'] = $this->getFancyName();
}
$this->out->elementStart('span', 'repeat');
$text_link = XMLStringer::estring('a', $attrs, $this->profile->nickname);
// TRANS: Link to the author of a repeated notice. %s is a linked nickname.
$this->out->raw(sprintf(_('Repeat of %s'), $text_link));
$this->out->elementEnd('span');

View File

@ -161,7 +161,7 @@ class Profile extends Memcached_DataObject
{
if ($this->fullname) {
// TRANS: Full name of a profile or group followed by nickname in parens
return sprintf(_('%1$s (%2$s)'), $this->fullname, $this->nickname);
return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
} else {
return $this->nickname;
}

View File

@ -234,6 +234,22 @@ class User_group extends Memcached_DataObject
return ($this->fullname) ? $this->fullname : $this->nickname;
}
/**
* Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone
* if no fullname is provided.
*
* @return string
*/
function getFancyName()
{
if ($this->fullname) {
// TRANS: Full name of a profile or group followed by nickname in parens
return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
} else {
return $this->nickname;
}
}
function getAliases()
{
$aliases = array();

View File

@ -873,16 +873,17 @@ class Action extends HTMLOutputter // lawsuit
// TRANS: Secondary navigation menu option leading to privacy policy.
_('Privacy'));
$this->menuItem(common_local_url('doc', array('title' => 'source')),
// TRANS: Secondary navigation menu option.
// TRANS: Secondary navigation menu option. Leads to information about StatusNet and its license.
_('Source'));
$this->menuItem(common_local_url('version'),
// TRANS: Secondary navigation menu option leading to version information on the StatusNet site.
_('Version'));
$this->menuItem(common_local_url('doc', array('title' => 'contact')),
// TRANS: Secondary navigation menu option leading to contact information on the StatusNet site.
// TRANS: Secondary navigation menu option leading to e-mail contact information on the
// TRANS: StatusNet site, where to report bugs, ...
_('Contact'));
$this->menuItem(common_local_url('doc', array('title' => 'badge')),
// TRANS: Secondary navigation menu option.
// TRANS: Secondary navigation menu option. Leads to information about embedding a timeline widget.
_('Badge'));
Event::handle('EndSecondaryNav', array($this));
}

View File

@ -423,7 +423,7 @@ class WhoisCommand extends Command
// TRANS: Whois output.
// TRANS: %1$s nickname of the queried user, %2$s is their profile URL.
$whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname,
$whois = sprintf(_m('WHOIS',"%1\$s (%2\$s)"), $recipient->nickname,
$recipient->profileurl);
if ($recipient->fullname) {
// TRANS: Whois output. %s is the full name of the queried user.

View File

@ -593,6 +593,10 @@ function mail_notify_fave($other, $user, $notice)
}
$profile = $user->getProfile();
if ($other->hasBlocked($profile)) {
// If the author has blocked us, don't spam them with a notification.
return;
}
$bestname = $profile->getBestName();

View File

@ -306,7 +306,7 @@ class NoticeListItem extends Widget
$attrs = array('href' => $this->profile->profileurl,
'class' => 'url');
if (!empty($this->profile->fullname)) {
$attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ')';
$attrs['title'] = $this->profile->getFancyName();
}
$this->out->elementStart('a', $attrs);
$this->showAvatar();

View File

@ -87,8 +87,11 @@ class PersonalGroupNav extends Widget
if ($nickname) {
$user = User::staticGet('nickname', $nickname);
$user_profile = $user->getProfile();
$name = $user_profile->getBestName();
} else {
// @fixme can this happen? is this valid?
$user_profile = false;
$name = $nickname;
}
$this->out->elementStart('ul', array('class' => 'nav'));
@ -97,22 +100,22 @@ class PersonalGroupNav extends Widget
$this->out->menuItem(common_local_url('all', array('nickname' =>
$nickname)),
_('Personal'),
sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
sprintf(_('%s and friends'), $name),
$action == 'all', 'nav_timeline_personal');
$this->out->menuItem(common_local_url('replies', array('nickname' =>
$nickname)),
_('Replies'),
sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
sprintf(_('Replies to %s'), $name),
$action == 'replies', 'nav_timeline_replies');
$this->out->menuItem(common_local_url('showstream', array('nickname' =>
$nickname)),
_('Profile'),
($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname,
$name,
$action == 'showstream', 'nav_profile');
$this->out->menuItem(common_local_url('showfavorites', array('nickname' =>
$nickname)),
_('Favorites'),
sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')),
sprintf(_('%s\'s favorite notices'), ($user_profile) ? $name : _('User')),
$action == 'showfavorites', 'nav_timeline_favorites');
$cur = common_current_user();

View File

@ -1038,7 +1038,7 @@ function common_group_link($sender_id, $nickname)
$attrs = array('href' => $group->permalink(),
'class' => 'url');
if (!empty($group->fullname)) {
$attrs['title'] = $group->fullname . ' (' . $group->nickname . ')';
$attrs['title'] = $group->getFancyName();
}
$xs = new XMLStringer();
$xs->elementStart('span', 'vcard');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -41,12 +41,7 @@ class GroupFavoritedAction extends ShowgroupAction
*/
function title()
{
if (!empty($this->group->fullname)) {
// @todo Create a core method to create this properly. i18n issue.
$base = $this->group->fullname . ' (' . $this->group->nickname . ')';
} else {
$base = $this->group->nickname;
}
$base = $this->group->getFancyName();
if ($this->page == 1) {
// TRANS: %s is a group name.

View File

@ -61,12 +61,7 @@ class AllmapAction extends MapAction
function title()
{
if (!empty($this->profile->fullname)) {
// @todo FIXME: Bad i18n. Should be "%1$s (%2$s)".
$base = $this->profile->fullname . ' (' . $this->user->nickname . ') ';
} else {
$base = $this->user->nickname;
}
$base = $this->profile->getFancyName();
if ($this->page == 1) {
// TRANS: Page title.

View File

@ -58,12 +58,7 @@ class UsermapAction extends MapAction
function title()
{
if (!empty($this->profile->fullname)) {
// @todo FIXME: Bad i18n. Should be '%1$s (%2$s)'
$base = $this->profile->fullname . ' (' . $this->user->nickname . ')';
} else {
$base = $this->user->nickname;
}
$base = $this->profile->getFancyName();
if ($this->page == 1) {
// @todo CHECKME: inconsisten with paged variant below. " map" missing.

View File

@ -150,10 +150,10 @@ class Ostatus_profile extends Managed_DataObject
} else if ($this->group_id && !$this->profile_id) {
return true;
} else if ($this->group_id && $this->profile_id) {
// TRANS: Server exception.
// TRANS: Server exception. %s is a URI.
throw new ServerException(sprintf(_m('Invalid ostatus_profile state: both group and profile IDs set for %s.'),$this->uri));
} else {
// TRANS: Server exception.
// TRANS: Server exception. %s is a URI.
throw new ServerException(sprintf(_m('Invalid ostatus_profile state: both group and profile IDs empty for %s.'),$this->uri));
}
}
@ -367,6 +367,7 @@ class Ostatus_profile extends Managed_DataObject
} else if ($feed->localName == 'rss') { // @fixme check namespace
$this->processRssFeed($feed, $source);
} else {
// TRANS: Exception.
throw new Exception(_m('Unknown feed format.'));
}
}
@ -390,6 +391,7 @@ class Ostatus_profile extends Managed_DataObject
$channels = $rss->getElementsByTagName('channel');
if ($channels->length == 0) {
// TRANS: Exception.
throw new Exception(_m('RSS feed without a channel.'));
} else if ($channels->length > 1) {
common_log(LOG_WARNING, __METHOD__ . ": more than one channel in an RSS feed");
@ -517,7 +519,7 @@ class Ostatus_profile extends Managed_DataObject
$sourceContent = $note->title;
} else {
// @fixme fetch from $sourceUrl?
// TRANS: Client exception. %s is a source URL.
// TRANS: Client exception. %s is a source URI.
throw new ClientException(sprintf(_m('No content for notice %s.'),$sourceUri));
}
@ -551,7 +553,8 @@ class Ostatus_profile extends Managed_DataObject
// so we can fold-out the full version inline.
// @fixme I18N this tooltip will be saved with the site's default language
// TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime this will usually be replaced with localized text from StatusNet core messages.
// TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
// TRANS: this will usually be replaced with localised text from StatusNet core messages.
$showMoreText = _m('Show more');
$attachUrl = common_local_url('attachment',
array('attachment' => $attachment->id));
@ -802,7 +805,7 @@ class Ostatus_profile extends Managed_DataObject
return self::ensureFeedURL($feedurl, $hints);
}
// TRANS: Exception.
// TRANS: Exception. %s is a URL.
throw new Exception(sprintf(_m('Could not find a feed URL for profile page %s.'),$finalUrl));
}
@ -940,6 +943,7 @@ class Ostatus_profile extends Managed_DataObject
}
// XXX: make some educated guesses here
// TRANS: Feed sub exception.
throw new FeedSubException(_m('Can\'t find enough profile information to make a feed.'));
}
@ -999,6 +1003,7 @@ class Ostatus_profile extends Managed_DataObject
return;
}
if (!common_valid_http_url($url)) {
// TRANS: Server exception. %s is a URL.
throw new ServerException(sprintf(_m("Invalid avatar URL %s."), $url));
}
@ -1009,6 +1014,7 @@ class Ostatus_profile extends Managed_DataObject
}
if (!$self) {
throw new ServerException(sprintf(
// TRANS: Server exception. %s is a URI.
_m("Tried to update avatar for unsaved remote profile %s."),
$this->uri));
}
@ -1018,6 +1024,7 @@ class Ostatus_profile extends Managed_DataObject
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
try {
if (!copy($url, $temp_filename)) {
// TRANS: Server exception. %s is a URL.
throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url));
}
@ -1300,7 +1307,7 @@ class Ostatus_profile extends Managed_DataObject
$oprofile->profile_id = $profile->insert();
if (!$oprofile->profile_id) {
// TRANS: Exception.
// TRANS: Server exception.
throw new ServerException(_m('Can\'t save local profile.'));
}
} else {
@ -1311,7 +1318,7 @@ class Ostatus_profile extends Managed_DataObject
$oprofile->group_id = $group->insert();
if (!$oprofile->group_id) {
// TRANS: Exception.
// TRANS: Server exception.
throw new ServerException(_m('Can\'t save local profile.'));
}
}
@ -1319,7 +1326,7 @@ class Ostatus_profile extends Managed_DataObject
$ok = $oprofile->insert();
if (!$ok) {
// TRANS: Exception.
// TRANS: Server exception.
throw new ServerException(_m('Can\'t save OStatus profile.'));
}
@ -1758,6 +1765,7 @@ class Ostatus_profile extends Managed_DataObject
if ($file_id === false) {
common_log_db_error($file, "INSERT", __FILE__);
// TRANS: Server exception.
throw new ServerException(_m('Could not store HTML content of long post as file.'));
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-10-27 23:43+0000\n"
"POT-Creation-Date: 2010-11-02 22:51+0000\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"
@ -119,13 +119,13 @@ msgstr ""
msgid "Attempting to end PuSH subscription for feed with no hub."
msgstr ""
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:192
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs set for %s."
msgstr ""
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:195
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs empty for %s."
@ -145,105 +145,113 @@ msgid ""
"Activity entry."
msgstr ""
#: classes/Ostatus_profile.php:408
#. TRANS: Exception.
#: classes/Ostatus_profile.php:409
msgid "Unknown feed format."
msgstr ""
#: classes/Ostatus_profile.php:431
#. TRANS: Exception.
#: classes/Ostatus_profile.php:433
msgid "RSS feed without a channel."
msgstr ""
#. TRANS: Client exception.
#: classes/Ostatus_profile.php:476
#: classes/Ostatus_profile.php:478
msgid "Can't handle that kind of post."
msgstr ""
#. TRANS: Client exception. %s is a source URL.
#: classes/Ostatus_profile.php:559
#. TRANS: Client exception. %s is a source URI.
#: classes/Ostatus_profile.php:561
#, php-format
msgid "No content for notice %s."
msgstr ""
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present.
#: classes/Ostatus_profile.php:592
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
#. TRANS: this will usually be replaced with localised text from StatusNet core messages.
#: classes/Ostatus_profile.php:596
msgid "Show more"
msgstr ""
#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:785
#: classes/Ostatus_profile.php:789
#, php-format
msgid "Could not reach profile page %s."
msgstr ""
#. TRANS: Exception.
#: classes/Ostatus_profile.php:843
#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:847
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr ""
#: classes/Ostatus_profile.php:980
#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:985
msgid "Can't find enough profile information to make a feed."
msgstr ""
#: classes/Ostatus_profile.php:1039
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1045
#, php-format
msgid "Invalid avatar URL %s."
msgstr ""
#: classes/Ostatus_profile.php:1049
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1056
#, php-format
msgid "Tried to update avatar for unsaved remote profile %s."
msgstr ""
#: classes/Ostatus_profile.php:1058
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1066
#, php-format
msgid "Unable to fetch avatar from %s."
msgstr ""
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1284
#: classes/Ostatus_profile.php:1292
msgid "Local user can't be referenced as remote."
msgstr ""
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1289
#: classes/Ostatus_profile.php:1297
msgid "Local group can't be referenced as remote."
msgstr ""
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1341 classes/Ostatus_profile.php:1352
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1349 classes/Ostatus_profile.php:1360
msgid "Can't save local profile."
msgstr ""
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1360
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1368
msgid "Can't save OStatus profile."
msgstr ""
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1619 classes/Ostatus_profile.php:1647
#: classes/Ostatus_profile.php:1627 classes/Ostatus_profile.php:1655
msgid "Not a valid webfinger address."
msgstr ""
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1729
#: classes/Ostatus_profile.php:1737
#, php-format
msgid "Couldn't save profile for \"%s\"."
msgstr ""
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1748
#: classes/Ostatus_profile.php:1756
#, php-format
msgid "Couldn't save ostatus_profile for \"%s\"."
msgstr ""
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1756
#: classes/Ostatus_profile.php:1764
#, php-format
msgid "Couldn't find a valid profile for \"%s\"."
msgstr ""
#: classes/Ostatus_profile.php:1798
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1807
msgid "Could not store HTML content of long post as file."
msgstr ""

View File

@ -10,13 +10,13 @@ msgid ""
msgstr ""
"Project-Id-Version: StatusNet - OStatus\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-10-27 23:43+0000\n"
"PO-Revision-Date: 2010-10-27 23:47:14+0000\n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:51+0000\n"
"Language-Team: French <http://translatewiki.net/wiki/Portal:fr>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-10-23 19:00:35+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75596); Translate extension (2010-09-17)\n"
"X-POT-Import-Date: 2010-10-29 16:13:55+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: fr\n"
"X-Message-Group: #out-statusnet-plugin-ostatus\n"
@ -131,7 +131,7 @@ msgstr ""
"Tente darrêter linscription PuSH à un flux dinformation sans "
"concentrateur."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:192
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs set for %s."
@ -139,7 +139,7 @@ msgstr ""
"État invalide du profil OStatus : identifiants à la fois de groupe et de "
"profil définis pour « %s »."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:195
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs empty for %s."
@ -163,111 +163,119 @@ msgstr ""
"Type invalide passé à la méthode « Ostatus_profile::notify ». Ce doit être "
"une chaîne XML ou une entrée « Activity »."
#: classes/Ostatus_profile.php:408
#. TRANS: Exception.
#: classes/Ostatus_profile.php:409
msgid "Unknown feed format."
msgstr "Format de flux dinformation inconnu."
#: classes/Ostatus_profile.php:431
#. TRANS: Exception.
#: classes/Ostatus_profile.php:433
msgid "RSS feed without a channel."
msgstr "Flux RSS sans canal."
#. TRANS: Client exception.
#: classes/Ostatus_profile.php:476
#: classes/Ostatus_profile.php:478
msgid "Can't handle that kind of post."
msgstr "Impossible de gérer cette sorte de publication."
#. TRANS: Client exception. %s is a source URL.
#: classes/Ostatus_profile.php:559
#. TRANS: Client exception. %s is a source URI.
#: classes/Ostatus_profile.php:561
#, php-format
msgid "No content for notice %s."
msgstr "Aucun contenu dans lavis « %s »."
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present.
#: classes/Ostatus_profile.php:592
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
#. TRANS: this will usually be replaced with localised text from StatusNet core messages.
#: classes/Ostatus_profile.php:596
msgid "Show more"
msgstr "Voir davantage"
#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:785
#: classes/Ostatus_profile.php:789
#, php-format
msgid "Could not reach profile page %s."
msgstr "Impossible datteindre la page de profil « %s »."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:843
#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:847
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr ""
"Impossible de trouver une adresse URL de flux dinformation pour la page de "
"profil « %s »."
#: classes/Ostatus_profile.php:980
#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:985
msgid "Can't find enough profile information to make a feed."
msgstr ""
"Impossible de trouver assez dinformations de profil pour créer un flux "
"dinformation."
#: classes/Ostatus_profile.php:1039
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1045
#, php-format
msgid "Invalid avatar URL %s."
msgstr "Adresse URL davatar « %s » invalide."
#: classes/Ostatus_profile.php:1049
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1056
#, php-format
msgid "Tried to update avatar for unsaved remote profile %s."
msgstr ""
"Tente de mettre à jour lavatar associé au profil distant non sauvegardé « %s "
"»."
#: classes/Ostatus_profile.php:1058
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1066
#, php-format
msgid "Unable to fetch avatar from %s."
msgstr "Impossible de récupérer lavatar depuis « %s »."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1284
#: classes/Ostatus_profile.php:1292
msgid "Local user can't be referenced as remote."
msgstr "Lutilisateur local ne peut être référencé comme distant."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1289
#: classes/Ostatus_profile.php:1297
msgid "Local group can't be referenced as remote."
msgstr "Le groupe local ne peut être référencé comme distant."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1341 classes/Ostatus_profile.php:1352
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1349 classes/Ostatus_profile.php:1360
msgid "Can't save local profile."
msgstr "Impossible de sauvegarder le profil local."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1360
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1368
msgid "Can't save OStatus profile."
msgstr "Impossible de sauvegarder le profil OStatus."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1619 classes/Ostatus_profile.php:1647
#: classes/Ostatus_profile.php:1627 classes/Ostatus_profile.php:1655
msgid "Not a valid webfinger address."
msgstr "Ce nest pas une adresse « webfinger » valide."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1729
#: classes/Ostatus_profile.php:1737
#, php-format
msgid "Couldn't save profile for \"%s\"."
msgstr "Impossible de sauvegarder le profil pour « %s »."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1748
#: classes/Ostatus_profile.php:1756
#, php-format
msgid "Couldn't save ostatus_profile for \"%s\"."
msgstr "Impossible denregistrer le profil OStatus pour « %s »."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1756
#: classes/Ostatus_profile.php:1764
#, php-format
msgid "Couldn't find a valid profile for \"%s\"."
msgstr "Impossible de trouver un profil valide pour « %s »."
#: classes/Ostatus_profile.php:1798
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1807
msgid "Could not store HTML content of long post as file."
msgstr ""
"Impossible de stocker le contenu HTML dune longue publication en un fichier."

View File

@ -9,13 +9,13 @@ msgid ""
msgstr ""
"Project-Id-Version: StatusNet - OStatus\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-10-27 23:43+0000\n"
"PO-Revision-Date: 2010-10-27 23:47:15+0000\n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:51+0000\n"
"Language-Team: Interlingua <http://translatewiki.net/wiki/Portal:ia>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-10-23 19:00:35+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75596); Translate extension (2010-09-17)\n"
"X-POT-Import-Date: 2010-10-29 16:13:55+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: ia\n"
"X-Message-Group: #out-statusnet-plugin-ostatus\n"
@ -126,14 +126,14 @@ msgstr "Tentativa de comenciar subscription PuSH pro syndication sin centro."
msgid "Attempting to end PuSH subscription for feed with no hub."
msgstr "Tentativa de terminar subscription PuSH pro syndication sin centro."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:192
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs set for %s."
msgstr ""
"Stato ostatus_profile invalide: IDs e de gruppo e de profilo definite pro %s."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:195
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs empty for %s."
@ -156,106 +156,114 @@ msgstr ""
"Typo invalide passate a Ostatos_profile::notify. Illo debe esser catena XML "
"o entrata Activity."
#: classes/Ostatus_profile.php:408
#. TRANS: Exception.
#: classes/Ostatus_profile.php:409
msgid "Unknown feed format."
msgstr "Formato de syndication incognite."
#: classes/Ostatus_profile.php:431
#. TRANS: Exception.
#: classes/Ostatus_profile.php:433
msgid "RSS feed without a channel."
msgstr "Syndication RSS sin canal."
#. TRANS: Client exception.
#: classes/Ostatus_profile.php:476
#: classes/Ostatus_profile.php:478
msgid "Can't handle that kind of post."
msgstr "Non pote tractar iste typo de message."
#. TRANS: Client exception. %s is a source URL.
#: classes/Ostatus_profile.php:559
#. TRANS: Client exception. %s is a source URI.
#: classes/Ostatus_profile.php:561
#, php-format
msgid "No content for notice %s."
msgstr "Nulle contento pro nota %s."
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present.
#: classes/Ostatus_profile.php:592
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
#. TRANS: this will usually be replaced with localised text from StatusNet core messages.
#: classes/Ostatus_profile.php:596
msgid "Show more"
msgstr "Monstrar plus"
#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:785
#: classes/Ostatus_profile.php:789
#, php-format
msgid "Could not reach profile page %s."
msgstr "Non poteva attinger pagina de profilo %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:843
#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:847
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr "Non poteva trovar un URL de syndication pro pagina de profilo %s."
#: classes/Ostatus_profile.php:980
#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:985
msgid "Can't find enough profile information to make a feed."
msgstr ""
"Non pote trovar satis de information de profilo pro facer un syndication."
#: classes/Ostatus_profile.php:1039
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1045
#, php-format
msgid "Invalid avatar URL %s."
msgstr "URL de avatar %s invalide."
#: classes/Ostatus_profile.php:1049
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1056
#, php-format
msgid "Tried to update avatar for unsaved remote profile %s."
msgstr "Tentava actualisar avatar pro profilo remote non salveguardate %s."
#: classes/Ostatus_profile.php:1058
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1066
#, php-format
msgid "Unable to fetch avatar from %s."
msgstr "Incapace de obtener avatar ab %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1284
#: classes/Ostatus_profile.php:1292
msgid "Local user can't be referenced as remote."
msgstr "Usator local non pote esser referentiate como remote."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1289
#: classes/Ostatus_profile.php:1297
msgid "Local group can't be referenced as remote."
msgstr "Gruppo local non pote esser referentiate como remote."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1341 classes/Ostatus_profile.php:1352
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1349 classes/Ostatus_profile.php:1360
msgid "Can't save local profile."
msgstr "Non pote salveguardar profilo local."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1360
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1368
msgid "Can't save OStatus profile."
msgstr "Non pote salveguardar profilo OStatus."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1619 classes/Ostatus_profile.php:1647
#: classes/Ostatus_profile.php:1627 classes/Ostatus_profile.php:1655
msgid "Not a valid webfinger address."
msgstr "Adresse webfinger invalide."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1729
#: classes/Ostatus_profile.php:1737
#, php-format
msgid "Couldn't save profile for \"%s\"."
msgstr "Non poteva salveguardar profilo pro \"%s\"."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1748
#: classes/Ostatus_profile.php:1756
#, php-format
msgid "Couldn't save ostatus_profile for \"%s\"."
msgstr "Non poteva salveguardar osatus_profile pro %s."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1756
#: classes/Ostatus_profile.php:1764
#, php-format
msgid "Couldn't find a valid profile for \"%s\"."
msgstr "Non poteva trovar un profilo valide pro \"%s\"."
#: classes/Ostatus_profile.php:1798
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1807
msgid "Could not store HTML content of long post as file."
msgstr "Non poteva immagazinar contento HTML de longe message como file."

View File

@ -9,13 +9,13 @@ msgid ""
msgstr ""
"Project-Id-Version: StatusNet - OStatus\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-10-27 23:43+0000\n"
"PO-Revision-Date: 2010-10-27 23:47:15+0000\n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:51+0000\n"
"Language-Team: Macedonian <http://translatewiki.net/wiki/Portal:mk>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-10-23 19:00:35+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75596); Translate extension (2010-09-17)\n"
"X-POT-Import-Date: 2010-10-29 16:13:55+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: mk\n"
"X-Message-Group: #out-statusnet-plugin-ostatus\n"
@ -127,7 +127,7 @@ msgid "Attempting to end PuSH subscription for feed with no hub."
msgstr ""
"Се обидувам да ставам крај на PuSH-претплатата за емитување без средиште."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:192
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs set for %s."
@ -135,7 +135,7 @@ msgstr ""
"Неважечка ostatus_profile-состојба: назнаките (ID) на групата и профилот се "
"наместени за %s."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:195
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs empty for %s."
@ -159,106 +159,114 @@ msgstr ""
"На Ostatus_profile::notify е пренесен неважечки тип. Мора да биде XML-низа "
"или ставка во Activity."
#: classes/Ostatus_profile.php:408
#. TRANS: Exception.
#: classes/Ostatus_profile.php:409
msgid "Unknown feed format."
msgstr "Непознат формат на каналско емитување."
#: classes/Ostatus_profile.php:431
#. TRANS: Exception.
#: classes/Ostatus_profile.php:433
msgid "RSS feed without a channel."
msgstr "RSS-емитување без канал."
#. TRANS: Client exception.
#: classes/Ostatus_profile.php:476
#: classes/Ostatus_profile.php:478
msgid "Can't handle that kind of post."
msgstr "Не можам да работам со таква објава."
#. TRANS: Client exception. %s is a source URL.
#: classes/Ostatus_profile.php:559
#. TRANS: Client exception. %s is a source URI.
#: classes/Ostatus_profile.php:561
#, php-format
msgid "No content for notice %s."
msgstr "Нема содржина за забелешката %s."
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present.
#: classes/Ostatus_profile.php:592
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
#. TRANS: this will usually be replaced with localised text from StatusNet core messages.
#: classes/Ostatus_profile.php:596
msgid "Show more"
msgstr "Повеќе"
#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:785
#: classes/Ostatus_profile.php:789
#, php-format
msgid "Could not reach profile page %s."
msgstr "Не можев да ја добијам профилната страница %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:843
#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:847
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr "Не можев да пронајдам каналска URL-адреса за профилната страница %s."
#: classes/Ostatus_profile.php:980
#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:985
msgid "Can't find enough profile information to make a feed."
msgstr "Не можев да најдам доволно профилни податоци за да направам канал."
#: classes/Ostatus_profile.php:1039
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1045
#, php-format
msgid "Invalid avatar URL %s."
msgstr "Неважечка URL-адреса за аватарот: %s."
#: classes/Ostatus_profile.php:1049
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1056
#, php-format
msgid "Tried to update avatar for unsaved remote profile %s."
msgstr ""
"Се обидов да го подновам аватарот за незачуваниот далечински профил %s."
#: classes/Ostatus_profile.php:1058
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1066
#, php-format
msgid "Unable to fetch avatar from %s."
msgstr "Не можам да го добијам аватарот од %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1284
#: classes/Ostatus_profile.php:1292
msgid "Local user can't be referenced as remote."
msgstr "Локалниот корисник не може да се наведе како далечински."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1289
#: classes/Ostatus_profile.php:1297
msgid "Local group can't be referenced as remote."
msgstr "Локалната група не може да се наведе како далечинска."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1341 classes/Ostatus_profile.php:1352
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1349 classes/Ostatus_profile.php:1360
msgid "Can't save local profile."
msgstr "Не можам да го зачувам локалниот профил."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1360
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1368
msgid "Can't save OStatus profile."
msgstr "Не можам да го зачувам профилот од OStatus."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1619 classes/Ostatus_profile.php:1647
#: classes/Ostatus_profile.php:1627 classes/Ostatus_profile.php:1655
msgid "Not a valid webfinger address."
msgstr "Ова не е важечка Webfinger-адреса"
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1729
#: classes/Ostatus_profile.php:1737
#, php-format
msgid "Couldn't save profile for \"%s\"."
msgstr "Не можам да го зачувам профилот за „%s“."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1748
#: classes/Ostatus_profile.php:1756
#, php-format
msgid "Couldn't save ostatus_profile for \"%s\"."
msgstr "Не можам да го зачувам ostatus_profile за „%s“."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1756
#: classes/Ostatus_profile.php:1764
#, php-format
msgid "Couldn't find a valid profile for \"%s\"."
msgstr "Не можев да пронајдам важечки профил за „%s“."
#: classes/Ostatus_profile.php:1798
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1807
msgid "Could not store HTML content of long post as file."
msgstr ""
"Не можам да ја складирам HTML-содржината на долгата објава како податотека."

View File

@ -10,13 +10,13 @@ msgid ""
msgstr ""
"Project-Id-Version: StatusNet - OStatus\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-10-27 23:43+0000\n"
"PO-Revision-Date: 2010-10-27 23:47:16+0000\n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:51+0000\n"
"Language-Team: Dutch <http://translatewiki.net/wiki/Portal:nl>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-10-23 19:00:35+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75596); Translate extension (2010-09-17)\n"
"X-POT-Import-Date: 2010-10-29 16:13:55+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: nl\n"
"X-Message-Group: #out-statusnet-plugin-ostatus\n"
@ -133,7 +133,7 @@ msgid "Attempting to end PuSH subscription for feed with no hub."
msgstr ""
"Aan het proberen een PuSH-abonnement te verwijderen voor een feed zonder hub."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:192
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs set for %s."
@ -141,7 +141,7 @@ msgstr ""
"Ongeldige ostatus_profile status: het ID voor zowel de groep als het profiel "
"voor %s is ingesteld."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:195
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs empty for %s."
@ -165,112 +165,120 @@ msgstr ""
"Ongeldig type doorgegeven aan Ostatus_profile::notify. Het moet een XML-"
"string of Activity zijn."
#: classes/Ostatus_profile.php:408
#. TRANS: Exception.
#: classes/Ostatus_profile.php:409
msgid "Unknown feed format."
msgstr "Onbekend feedformaat"
#: classes/Ostatus_profile.php:431
#. TRANS: Exception.
#: classes/Ostatus_profile.php:433
msgid "RSS feed without a channel."
msgstr "RSS-feed zonder kanaal."
#. TRANS: Client exception.
#: classes/Ostatus_profile.php:476
#: classes/Ostatus_profile.php:478
msgid "Can't handle that kind of post."
msgstr "Dat type post kan niet verwerkt worden."
#. TRANS: Client exception. %s is a source URL.
#: classes/Ostatus_profile.php:559
#. TRANS: Client exception. %s is a source URI.
#: classes/Ostatus_profile.php:561
#, php-format
msgid "No content for notice %s."
msgstr "Geen inhoud voor mededeling %s."
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present.
#: classes/Ostatus_profile.php:592
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
#. TRANS: this will usually be replaced with localised text from StatusNet core messages.
#: classes/Ostatus_profile.php:596
msgid "Show more"
msgstr "Meer weergeven"
#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:785
#: classes/Ostatus_profile.php:789
#, php-format
msgid "Could not reach profile page %s."
msgstr "Het was niet mogelijk de profielpagina %s te bereiken."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:843
#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:847
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr "Het was niet mogelijk de feed-URL voor de profielpagina %s te vinden."
#: classes/Ostatus_profile.php:980
#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:985
msgid "Can't find enough profile information to make a feed."
msgstr ""
"Het was niet mogelijk voldoende profielinformatie te vinden om een feed te "
"maken."
#: classes/Ostatus_profile.php:1039
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1045
#, php-format
msgid "Invalid avatar URL %s."
msgstr "Ongeldige avatar-URL %s."
#: classes/Ostatus_profile.php:1049
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1056
#, php-format
msgid "Tried to update avatar for unsaved remote profile %s."
msgstr ""
"Geprobeerd om een avatar bij te werken voor het niet opgeslagen profiel %s."
#: classes/Ostatus_profile.php:1058
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1066
#, php-format
msgid "Unable to fetch avatar from %s."
msgstr "Het was niet mogelijk de avatar op te halen van %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1284
#: classes/Ostatus_profile.php:1292
msgid "Local user can't be referenced as remote."
msgstr ""
"Naar een lokale gebruiker kan niet verwezen worden alsof die zich bij een "
"andere dienst bevindt."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1289
#: classes/Ostatus_profile.php:1297
msgid "Local group can't be referenced as remote."
msgstr ""
"Naar een lokale groep kan niet verwezen worden alsof die zich bij een andere "
"dienst bevindt."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1341 classes/Ostatus_profile.php:1352
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1349 classes/Ostatus_profile.php:1360
msgid "Can't save local profile."
msgstr "Het was niet mogelijk het lokale profiel op te slaan."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1360
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1368
msgid "Can't save OStatus profile."
msgstr "Het was niet mogelijk het Ostatusprofiel op te slaan."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1619 classes/Ostatus_profile.php:1647
#: classes/Ostatus_profile.php:1627 classes/Ostatus_profile.php:1655
msgid "Not a valid webfinger address."
msgstr "Geen geldig webfingeradres."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1729
#: classes/Ostatus_profile.php:1737
#, php-format
msgid "Couldn't save profile for \"%s\"."
msgstr "Het was niet mogelijk het profiel voor \"%s\" op te slaan."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1748
#: classes/Ostatus_profile.php:1756
#, php-format
msgid "Couldn't save ostatus_profile for \"%s\"."
msgstr "Het was niet mogelijk het ostatus_profile voor \"%s\" op te slaan."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1756
#: classes/Ostatus_profile.php:1764
#, php-format
msgid "Couldn't find a valid profile for \"%s\"."
msgstr "Er is geen geldig profiel voor \"%s\" gevonden."
#: classes/Ostatus_profile.php:1798
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1807
msgid "Could not store HTML content of long post as file."
msgstr ""
"Het was niet mogelijk de HTML-inhoud van het lange bericht als bestand op te "

View File

@ -9,13 +9,13 @@ msgid ""
msgstr ""
"Project-Id-Version: StatusNet - OStatus\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-10-27 23:43+0000\n"
"PO-Revision-Date: 2010-10-27 23:47:17+0000\n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:51+0000\n"
"Language-Team: Ukrainian <http://translatewiki.net/wiki/Portal:uk>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-10-23 19:00:35+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75596); Translate extension (2010-09-17)\n"
"X-POT-Import-Date: 2010-10-29 16:13:55+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: uk\n"
"X-Message-Group: #out-statusnet-plugin-ostatus\n"
@ -130,7 +130,7 @@ msgstr ""
"Спроба скасувати підписку за допомогою PuSH до веб-стрічки, котра не має "
"вузла."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:192
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs set for %s."
@ -138,7 +138,7 @@ msgstr ""
"Невірний стан параметру ostatus_profile: як групові, так і персональні "
"ідентифікатори встановлено для %s."
#. TRANS: Server exception.
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:195
#, php-format
msgid "Invalid ostatus_profile state: both group and profile IDs empty for %s."
@ -162,106 +162,114 @@ msgstr ""
"До параметру Ostatus_profile::notify передано невірний тип. Це має бути або "
"рядок у форматі XML, або запис активності."
#: classes/Ostatus_profile.php:408
#. TRANS: Exception.
#: classes/Ostatus_profile.php:409
msgid "Unknown feed format."
msgstr "Невідомий формат веб-стрічки."
#: classes/Ostatus_profile.php:431
#. TRANS: Exception.
#: classes/Ostatus_profile.php:433
msgid "RSS feed without a channel."
msgstr "RSS-стрічка не має каналу."
#. TRANS: Client exception.
#: classes/Ostatus_profile.php:476
#: classes/Ostatus_profile.php:478
msgid "Can't handle that kind of post."
msgstr "Не вдається обробити такий тип допису."
#. TRANS: Client exception. %s is a source URL.
#: classes/Ostatus_profile.php:559
#. TRANS: Client exception. %s is a source URI.
#: classes/Ostatus_profile.php:561
#, php-format
msgid "No content for notice %s."
msgstr "Допис %s не має змісту."
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present.
#: classes/Ostatus_profile.php:592
#. TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime
#. TRANS: this will usually be replaced with localised text from StatusNet core messages.
#: classes/Ostatus_profile.php:596
msgid "Show more"
msgstr "Розгорнути"
#. TRANS: Exception. %s is a profile URL.
#: classes/Ostatus_profile.php:785
#: classes/Ostatus_profile.php:789
#, php-format
msgid "Could not reach profile page %s."
msgstr "Не вдалося досягти сторінки профілю %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:843
#. TRANS: Exception. %s is a URL.
#: classes/Ostatus_profile.php:847
#, php-format
msgid "Could not find a feed URL for profile page %s."
msgstr "Не вдалося знайти URL веб-стрічки для сторінки профілю %s."
#: classes/Ostatus_profile.php:980
#. TRANS: Feed sub exception.
#: classes/Ostatus_profile.php:985
msgid "Can't find enough profile information to make a feed."
msgstr ""
"Не можу знайти достатньо інформації про профіль, аби сформувати веб-стрічку."
#: classes/Ostatus_profile.php:1039
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1045
#, php-format
msgid "Invalid avatar URL %s."
msgstr "Невірна URL-адреса аватари %s."
#: classes/Ostatus_profile.php:1049
#. TRANS: Server exception. %s is a URI.
#: classes/Ostatus_profile.php:1056
#, php-format
msgid "Tried to update avatar for unsaved remote profile %s."
msgstr "Намагаюся оновити аватару для не збереженого віддаленого профілю %s."
#: classes/Ostatus_profile.php:1058
#. TRANS: Server exception. %s is a URL.
#: classes/Ostatus_profile.php:1066
#, php-format
msgid "Unable to fetch avatar from %s."
msgstr "Неможливо завантажити аватару з %s."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1284
#: classes/Ostatus_profile.php:1292
msgid "Local user can't be referenced as remote."
msgstr "Місцевий користувач не може бути зазначеним у якості віддаленого."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1289
#: classes/Ostatus_profile.php:1297
msgid "Local group can't be referenced as remote."
msgstr "Локальну спільноту не можна зазначити у якості віддаленої."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1341 classes/Ostatus_profile.php:1352
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1349 classes/Ostatus_profile.php:1360
msgid "Can't save local profile."
msgstr "Не вдається зберегти місцевий профіль."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1360
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1368
msgid "Can't save OStatus profile."
msgstr "Не вдається зберегти профіль OStatus."
#. TRANS: Exception.
#: classes/Ostatus_profile.php:1619 classes/Ostatus_profile.php:1647
#: classes/Ostatus_profile.php:1627 classes/Ostatus_profile.php:1655
msgid "Not a valid webfinger address."
msgstr "Це недійсна адреса для протоколу WebFinger."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1729
#: classes/Ostatus_profile.php:1737
#, php-format
msgid "Couldn't save profile for \"%s\"."
msgstr "Не можу зберегти профіль для «%s»."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1748
#: classes/Ostatus_profile.php:1756
#, php-format
msgid "Couldn't save ostatus_profile for \"%s\"."
msgstr "Не можу зберегти профіль OStatus для «%s»."
#. TRANS: Exception. %s is a webfinger address.
#: classes/Ostatus_profile.php:1756
#: classes/Ostatus_profile.php:1764
#, php-format
msgid "Couldn't find a valid profile for \"%s\"."
msgstr "не можу знайти відповідний й профіль для «%s»."
#: classes/Ostatus_profile.php:1798
#. TRANS: Server exception.
#: classes/Ostatus_profile.php:1807
msgid "Could not store HTML content of long post as file."
msgstr "Не можу зберегти HTML місткого допису у якості файлу."

View File

@ -0,0 +1,58 @@
# Translation of StatusNet - Realtime to Interlingua (Interlingua)
# Expored from translatewiki.net
#
# Author: McDutchie
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - Realtime\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:57+0000\n"
"Language-Team: Interlingua <http://translatewiki.net/wiki/Portal:ia>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-11-02 19:54:39+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: ia\n"
"X-Message-Group: #out-statusnet-plugin-realtime\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. TRANS: Text label for realtime view "play" button, usually replaced by an icon.
#: RealtimePlugin.php:339
msgctxt "BUTTON"
msgid "Play"
msgstr "Reproducer"
#. TRANS: Tooltip for realtime view "play" button.
#: RealtimePlugin.php:341
msgctxt "TOOLTIP"
msgid "Play"
msgstr "Reproducer"
#. TRANS: Text label for realtime view "pause" button
#: RealtimePlugin.php:343
msgctxt "BUTTON"
msgid "Pause"
msgstr "Pausar"
#. TRANS: Tooltip for realtime view "pause" button
#: RealtimePlugin.php:345
msgctxt "TOOLTIP"
msgid "Pause"
msgstr "Pausar"
#. TRANS: Text label for realtime view "popup" button, usually replaced by an icon.
#: RealtimePlugin.php:347
msgctxt "BUTTON"
msgid "Pop up"
msgstr "Fenestra"
#. TRANS: Tooltip for realtime view "popup" button.
#: RealtimePlugin.php:349
msgctxt "TOOLTIP"
msgid "Pop up in a window"
msgstr "Aperir le reproductor in un nove fenestra"

View File

@ -0,0 +1,58 @@
# Translation of StatusNet - Realtime to Macedonian (Македонски)
# Expored from translatewiki.net
#
# Author: Bjankuloski06
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - Realtime\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:57+0000\n"
"Language-Team: Macedonian <http://translatewiki.net/wiki/Portal:mk>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-11-02 19:54:39+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: mk\n"
"X-Message-Group: #out-statusnet-plugin-realtime\n"
"Plural-Forms: nplurals=2; plural=(n == 1 || n%10 == 1) ? 0 : 1;\n"
#. TRANS: Text label for realtime view "play" button, usually replaced by an icon.
#: RealtimePlugin.php:339
msgctxt "BUTTON"
msgid "Play"
msgstr "Пушти"
#. TRANS: Tooltip for realtime view "play" button.
#: RealtimePlugin.php:341
msgctxt "TOOLTIP"
msgid "Play"
msgstr "Пушти"
#. TRANS: Text label for realtime view "pause" button
#: RealtimePlugin.php:343
msgctxt "BUTTON"
msgid "Pause"
msgstr "Паузирај"
#. TRANS: Tooltip for realtime view "pause" button
#: RealtimePlugin.php:345
msgctxt "TOOLTIP"
msgid "Pause"
msgstr "Паузирај"
#. TRANS: Text label for realtime view "popup" button, usually replaced by an icon.
#: RealtimePlugin.php:347
msgctxt "BUTTON"
msgid "Pop up"
msgstr "Прозорче"
#. TRANS: Tooltip for realtime view "popup" button.
#: RealtimePlugin.php:349
msgctxt "TOOLTIP"
msgid "Pop up in a window"
msgstr "Прикажи во прозорче"

View File

@ -0,0 +1,58 @@
# Translation of StatusNet - Realtime to Dutch (Nederlands)
# Expored from translatewiki.net
#
# Author: Siebrand
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - Realtime\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:54:57+0000\n"
"Language-Team: Dutch <http://translatewiki.net/wiki/Portal:nl>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-11-02 19:54:39+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: nl\n"
"X-Message-Group: #out-statusnet-plugin-realtime\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. TRANS: Text label for realtime view "play" button, usually replaced by an icon.
#: RealtimePlugin.php:339
msgctxt "BUTTON"
msgid "Play"
msgstr "Afspelen"
#. TRANS: Tooltip for realtime view "play" button.
#: RealtimePlugin.php:341
msgctxt "TOOLTIP"
msgid "Play"
msgstr "Afspelen"
#. TRANS: Text label for realtime view "pause" button
#: RealtimePlugin.php:343
msgctxt "BUTTON"
msgid "Pause"
msgstr "Pauzeren"
#. TRANS: Tooltip for realtime view "pause" button
#: RealtimePlugin.php:345
msgctxt "TOOLTIP"
msgid "Pause"
msgstr "Pauzeren"
#. TRANS: Text label for realtime view "popup" button, usually replaced by an icon.
#: RealtimePlugin.php:347
msgctxt "BUTTON"
msgid "Pop up"
msgstr "Pop-up"
#. TRANS: Tooltip for realtime view "popup" button.
#: RealtimePlugin.php:349
msgctxt "TOOLTIP"
msgid "Pop up in a window"
msgstr "In nieuw venstertje weergeven"

View File

@ -200,8 +200,15 @@ class TwitterBridgePlugin extends Plugin
return false;
case 'TwitterOAuthClient':
case 'TwitterQueueHandler':
case 'TwitterImport':
case 'JsonStreamReader':
case 'TwitterStreamReader':
include_once $dir . '/' . strtolower($cls) . '.php';
return false;
case 'TwitterSiteStream':
case 'TwitterUserStream':
include_once $dir . '/twitterstreamreader.php';
return false;
case 'Notice_to_status':
case 'Twitter_synch_status':
include_once $dir . '/' . $cls . '.php';
@ -267,7 +274,11 @@ class TwitterBridgePlugin extends Plugin
function onEndInitializeQueueManager($manager)
{
if (self::hasKeys()) {
// Outgoing notices -> twitter
$manager->connect('twitter', 'TwitterQueueHandler');
// Incoming statuses <- twitter
$manager->connect('tweetin', 'TweetInQueueHandler');
}
return true;
}

View File

@ -0,0 +1,314 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-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/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$shortoptions = 'fi::a';
$longoptions = array('id::', 'foreground', 'all');
$helptext = <<<END_OF_XMPP_HELP
Daemon script for receiving new notices from Twitter users.
-i --id Identity (default none)
-a --all Handle Twitter for all local sites
(requires Stomp queue handler, status_network setup)
-f --foreground Stay in the foreground (default background)
END_OF_XMPP_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
require_once INSTALLDIR . '/lib/jabber.php';
class TwitterDaemon extends SpawningDaemon
{
protected $allsites = false;
function __construct($id=null, $daemonize=true, $threads=1, $allsites=false)
{
if ($threads != 1) {
// This should never happen. :)
throw new Exception("TwitterDaemon must run single-threaded");
}
parent::__construct($id, $daemonize, $threads);
$this->allsites = $allsites;
}
function runThread()
{
common_log(LOG_INFO, 'Waiting to listen to Twitter and queues');
$master = new TwitterMaster($this->get_id(), $this->processManager());
$master->init($this->allsites);
$master->service();
common_log(LOG_INFO, 'terminating normally');
return $master->respawn ? self::EXIT_RESTART : self::EXIT_SHUTDOWN;
}
}
class TwitterMaster extends IoMaster
{
protected $processManager;
function __construct($id, $processManager)
{
parent::__construct($id);
$this->processManager = $processManager;
}
/**
* Initialize IoManagers for the currently configured site
* which are appropriate to this instance.
*/
function initManagers()
{
$qm = QueueManager::get();
$qm->setActiveGroup('twitter');
$this->instantiate($qm);
$this->instantiate(new TwitterManager());
$this->instantiate($this->processManager);
}
}
class TwitterManager extends IoManager
{
// Recommended resource limits from http://dev.twitter.com/pages/site_streams
const MAX_STREAMS = 1000;
const USERS_PER_STREAM = 100;
const STREAMS_PER_SECOND = 20;
protected $streams;
protected $users;
/**
* Pull the site's active Twitter-importing users and start spawning
* some data streams for them!
*
* @fixme check their last-id and check whether we'll need to do a manual pull.
* @fixme abstract out the fetching so we can work over multiple sites.
*/
protected function initStreams()
{
common_log(LOG_INFO, 'init...');
// Pull Twitter user IDs for all users we want to pull data for
$flink = new Foreign_link();
$flink->service = TWITTER_SERVICE;
// @fixme probably should do the bitfield check in a whereAdd but it's ugly :D
$flink->find();
$userIds = array();
while ($flink->fetch()) {
if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
FOREIGN_NOTICE_RECV) {
$userIds[] = $flink->foreign_id;
if (count($userIds) >= self::USERS_PER_STREAM) {
$this->spawnStream($userIds);
$userIds = array();
}
}
}
if (count($userIds)) {
$this->spawnStream($userIds);
}
}
/**
* Prepare a Site Stream connection for the given chunk of users.
* The actual connection will be opened later.
*
* @param $userIds array of Twitter-side user IDs
*/
protected function spawnStream($userIds)
{
$stream = $this->initSiteStream();
$stream->followUsers($userIds);
// Slip the stream reader into our list of active streams.
// We'll manage its actual connection on the next go-around.
$this->streams[] = $stream;
// Record the user->stream mappings; this makes it easier for us to know
// later if we need to kill something.
foreach ($userIds as $id) {
$this->users[$id] = $stream;
}
}
/**
* Initialize a generic site streams connection object.
* All our connections will look like this, then we'll add users to them.
*
* @return TwitterStreamReader
*/
protected function initSiteStream()
{
$auth = $this->siteStreamAuth();
$stream = new TwitterSiteStream($auth);
// Add our event handler callbacks. Whee!
$this->setupEvents($stream);
return $stream;
}
/**
* Fetch the Twitter OAuth credentials to use to connect to the Site Streams API.
*
* This will use the locally-stored credentials for the applictation's owner account
* from the site configuration. These should be configured through the administration
* panels or manually in the config file.
*
* Will throw an exception if no credentials can be found -- but beware that invalid
* credentials won't cause breakage until later.
*
* @return TwitterOAuthClient
*/
protected function siteStreamAuth()
{
$token = common_config('twitter', 'stream_token');
$secret = common_config('twitter', 'stream_secret');
if (empty($token) || empty($secret)) {
throw new ServerException('Twitter site streams have not been correctly configured. Configure the app owner account via the admin panel.');
}
return new TwitterOAuthClient($token, $secret);
}
/**
* Collect the sockets for all active connections for i/o monitoring.
*
* @return array of resources
*/
public function getSockets()
{
$sockets = array();
foreach ($this->streams as $stream) {
foreach ($stream->getSockets() as $socket) {
$sockets[] = $socket;
}
}
return $sockets;
}
/**
* We're ready to process input from one of our data sources! Woooooo!
* @fixme is there an easier way to map from socket back to owning module? :(
*
* @param resource $socket
* @return boolean success
*/
public function handleInput($socket)
{
foreach ($this->streams as $stream) {
foreach ($stream->getSockets() as $aSocket) {
if ($socket === $aSocket) {
$stream->handleInput($socket);
}
}
}
return true;
}
/**
* Start the i/o system up! Prepare our connections and start opening them.
*
* @fixme do some rate-limiting on the stream setup
* @fixme do some sensible backoff on failure etc
*/
public function start()
{
$this->initStreams();
foreach ($this->streams as $stream) {
$stream->connect();
}
return true;
}
/**
* Close down our connections when the daemon wraps up for business.
*/
public function finish()
{
foreach ($this->streams as $index => $stream) {
$stream->close();
unset($this->streams[$index]);
}
return true;
}
public static function get()
{
throw new Exception('not a singleton');
}
/**
* Set up event handlers on the streaming interface.
*
* @fixme add more event types as we add handling for them
*/
protected function setupEvents(TwitterStreamReader $stream)
{
$handlers = array(
'status',
);
foreach ($handlers as $event) {
$stream->hookEvent($event, array($this, 'onTwitter' . ucfirst($event)));
}
}
/**
* Event callback notifying that a user has a new message in their home timeline.
* We store the incoming message into the queues for processing, keeping our own
* daemon running as shiny-fast as possible.
*
* @param object $status JSON data: Twitter status update
* @fixme in all-sites mode we may need to route queue items into another site's
* destination queues, or multiple sites.
*/
protected function onTwitterStatus($status, $context)
{
$data = array(
'status' => $status,
'for_user' => $context->for_user,
);
$qm = QueueManager::get();
$qm->enqueue($data, 'tweetin');
}
}
if (have_option('i', 'id')) {
$id = get_option_value('i', 'id');
} else if (count($args) > 0) {
$id = $args[0];
} else {
$id = null;
}
$foreground = have_option('f', 'foreground');
$all = have_option('a') || have_option('--all');
$daemon = new TwitterDaemon($id, !$foreground, 1, $all);
$daemon->runOnce();

View File

@ -192,25 +192,12 @@ class TwitterStatusFetcher extends ParallelizingDaemon
common_debug(LOG_INFO, $this->name() . ' - Retrieved ' . sizeof($timeline) . ' statuses from Twitter.');
$importer = new TwitterImport();
// Reverse to preserve order
foreach (array_reverse($timeline) as $status) {
// Hacktastic: filter out stuff coming from this StatusNet
$source = mb_strtolower(common_config('integration', 'source'));
if (preg_match("/$source/", mb_strtolower($status->source))) {
common_debug($this->name() . ' - Skipping import of status ' .
$status->id . ' with source ' . $source);
continue;
}
// Don't save it if the user is protected
// FIXME: save it but treat it as private
if ($status->user->protected) {
continue;
}
$notice = $this->saveStatus($status);
$notice = $importer->importStatus($status);
if (!empty($notice)) {
Inbox::insertNotice($flink->user_id, $notice->id);
@ -226,578 +213,6 @@ class TwitterStatusFetcher extends ParallelizingDaemon
$flink->last_noticesync = common_sql_now();
$flink->update();
}
function saveStatus($status)
{
$profile = $this->ensureProfile($status->user);
if (empty($profile)) {
common_log(LOG_ERR, $this->name() .
' - Problem saving notice. No associated Profile.');
return null;
}
$statusUri = $this->makeStatusURI($status->user->screen_name, $status->id);
// check to see if we've already imported the status
$n2s = Notice_to_status::staticGet('status_id', $status->id);
if (!empty($n2s)) {
common_log(
LOG_INFO,
$this->name() .
" - Ignoring duplicate import: {$status->id}"
);
return Notice::staticGet('id', $n2s->notice_id);
}
// If it's a retweet, save it as a repeat!
if (!empty($status->retweeted_status)) {
common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}.");
$original = $this->saveStatus($status->retweeted_status);
if (empty($original)) {
return null;
} else {
$author = $original->getProfile();
// TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'.
// TRANS: %1$s is the repeated user's name, %2$s is the repeated notice.
$content = sprintf(_m('RT @%1$s %2$s'),
$author->nickname,
$original->content);
if (Notice::contentTooLong($content)) {
$contentlimit = Notice::maxContent();
$content = mb_substr($content, 0, $contentlimit - 4) . ' ...';
}
$repeat = Notice::saveNew($profile->id,
$content,
'twitter',
array('repeat_of' => $original->id,
'uri' => $statusUri,
'is_local' => Notice::GATEWAY));
common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}");
Notice_to_status::saveNew($repeat->id, $status->id);
return $repeat;
}
}
$notice = new Notice();
$notice->profile_id = $profile->id;
$notice->uri = $statusUri;
$notice->url = $statusUri;
$notice->created = strftime(
'%Y-%m-%d %H:%M:%S',
strtotime($status->created_at)
);
$notice->source = 'twitter';
$notice->reply_to = null;
if (!empty($status->in_reply_to_status_id)) {
common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}");
$n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id);
if (empty($n2s)) {
common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
} else {
$reply = Notice::staticGet('id', $n2s->notice_id);
if (empty($reply)) {
common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
} else {
common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}");
$notice->reply_to = $reply->id;
$notice->conversation = $reply->conversation;
}
}
}
if (empty($notice->conversation)) {
$conv = Conversation::create();
$notice->conversation = $conv->id;
common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}.");
}
$notice->is_local = Notice::GATEWAY;
$notice->content = html_entity_decode($status->text, ENT_QUOTES, 'UTF-8');
$notice->rendered = $this->linkify($status);
if (Event::handle('StartNoticeSave', array(&$notice))) {
$id = $notice->insert();
if (!$id) {
common_log_db_error($notice, 'INSERT', __FILE__);
common_log(LOG_ERR, $this->name() .
' - Problem saving notice.');
}
Event::handle('EndNoticeSave', array($notice));
}
Notice_to_status::saveNew($notice->id, $status->id);
$this->saveStatusMentions($notice, $status);
$notice->blowOnInsert();
return $notice;
}
/**
* Make an URI for a status.
*
* @param object $status status object
*
* @return string URI
*/
function makeStatusURI($username, $id)
{
return 'http://twitter.com/'
. $username
. '/status/'
. $id;
}
/**
* Look up a Profile by profileurl field. Profile::staticGet() was
* not working consistently.
*
* @param string $nickname local nickname of the Twitter user
* @param string $profileurl the profile url
*
* @return mixed value the first Profile with that url, or null
*/
function getProfileByUrl($nickname, $profileurl)
{
$profile = new Profile();
$profile->nickname = $nickname;
$profile->profileurl = $profileurl;
$profile->limit(1);
if ($profile->find()) {
$profile->fetch();
return $profile;
}
return null;
}
/**
* Check to see if this Twitter status has already been imported
*
* @param Profile $profile Twitter user's local profile
* @param string $statusUri URI of the status on Twitter
*
* @return mixed value a matching Notice or null
*/
function checkDupe($profile, $statusUri)
{
$notice = new Notice();
$notice->uri = $statusUri;
$notice->profile_id = $profile->id;
$notice->limit(1);
if ($notice->find()) {
$notice->fetch();
return $notice;
}
return null;
}
function ensureProfile($user)
{
// check to see if there's already a profile for this user
$profileurl = 'http://twitter.com/' . $user->screen_name;
$profile = $this->getProfileByUrl($user->screen_name, $profileurl);
if (!empty($profile)) {
common_debug($this->name() .
" - Profile for $profile->nickname found.");
// Check to see if the user's Avatar has changed
$this->checkAvatar($user, $profile);
return $profile;
} else {
common_debug($this->name() . ' - Adding profile and remote profile ' .
"for Twitter user: $profileurl.");
$profile = new Profile();
$profile->query("BEGIN");
$profile->nickname = $user->screen_name;
$profile->fullname = $user->name;
$profile->homepage = $user->url;
$profile->bio = $user->description;
$profile->location = $user->location;
$profile->profileurl = $profileurl;
$profile->created = common_sql_now();
try {
$id = $profile->insert();
} catch(Exception $e) {
common_log(LOG_WARNING, $this->name . ' Couldn\'t insert profile - ' . $e->getMessage());
}
if (empty($id)) {
common_log_db_error($profile, 'INSERT', __FILE__);
$profile->query("ROLLBACK");
return false;
}
// check for remote profile
$remote_pro = Remote_profile::staticGet('uri', $profileurl);
if (empty($remote_pro)) {
$remote_pro = new Remote_profile();
$remote_pro->id = $id;
$remote_pro->uri = $profileurl;
$remote_pro->created = common_sql_now();
try {
$rid = $remote_pro->insert();
} catch (Exception $e) {
common_log(LOG_WARNING, $this->name() . ' Couldn\'t save remote profile - ' . $e->getMessage());
}
if (empty($rid)) {
common_log_db_error($profile, 'INSERT', __FILE__);
$profile->query("ROLLBACK");
return false;
}
}
$profile->query("COMMIT");
$this->saveAvatars($user, $id);
return $profile;
}
}
function checkAvatar($twitter_user, $profile)
{
global $config;
$path_parts = pathinfo($twitter_user->profile_image_url);
$newname = 'Twitter_' . $twitter_user->id . '_' .
$path_parts['basename'];
$oldname = $profile->getAvatar(48)->filename;
if ($newname != $oldname) {
common_debug($this->name() . ' - Avatar for Twitter user ' .
"$profile->nickname has changed.");
common_debug($this->name() . " - old: $oldname new: $newname");
$this->updateAvatars($twitter_user, $profile);
}
if ($this->missingAvatarFile($profile)) {
common_debug($this->name() . ' - Twitter user ' .
$profile->nickname .
' is missing one or more local avatars.');
common_debug($this->name() ." - old: $oldname new: $newname");
$this->updateAvatars($twitter_user, $profile);
}
}
function updateAvatars($twitter_user, $profile) {
global $config;
$path_parts = pathinfo($twitter_user->profile_image_url);
$img_root = substr($path_parts['basename'], 0, -11);
$ext = $path_parts['extension'];
$mediatype = $this->getMediatype($ext);
foreach (array('mini', 'normal', 'bigger') as $size) {
$url = $path_parts['dirname'] . '/' .
$img_root . '_' . $size . ".$ext";
$filename = 'Twitter_' . $twitter_user->id . '_' .
$img_root . "_$size.$ext";
$this->updateAvatar($profile->id, $size, $mediatype, $filename);
$this->fetchAvatar($url, $filename);
}
}
function missingAvatarFile($profile) {
foreach (array(24, 48, 73) as $size) {
$filename = $profile->getAvatar($size)->filename;
$avatarpath = Avatar::path($filename);
if (file_exists($avatarpath) == FALSE) {
return true;
}
}
return false;
}
function getMediatype($ext)
{
$mediatype = null;
switch (strtolower($ext)) {
case 'jpg':
$mediatype = 'image/jpg';
break;
case 'gif':
$mediatype = 'image/gif';
break;
default:
$mediatype = 'image/png';
}
return $mediatype;
}
function saveAvatars($user, $id)
{
global $config;
$path_parts = pathinfo($user->profile_image_url);
$ext = $path_parts['extension'];
$end = strlen('_normal' . $ext);
$img_root = substr($path_parts['basename'], 0, -($end+1));
$mediatype = $this->getMediatype($ext);
foreach (array('mini', 'normal', 'bigger') as $size) {
$url = $path_parts['dirname'] . '/' .
$img_root . '_' . $size . ".$ext";
$filename = 'Twitter_' . $user->id . '_' .
$img_root . "_$size.$ext";
if ($this->fetchAvatar($url, $filename)) {
$this->newAvatar($id, $size, $mediatype, $filename);
} else {
common_log(LOG_WARNING, $id() .
" - Problem fetching Avatar: $url");
}
}
}
function updateAvatar($profile_id, $size, $mediatype, $filename) {
common_debug($this->name() . " - Updating avatar: $size");
$profile = Profile::staticGet($profile_id);
if (empty($profile)) {
common_debug($this->name() . " - Couldn't get profile: $profile_id!");
return;
}
$sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
$avatar = $profile->getAvatar($sizes[$size]);
// Delete the avatar, if present
if ($avatar) {
$avatar->delete();
}
$this->newAvatar($profile->id, $size, $mediatype, $filename);
}
function newAvatar($profile_id, $size, $mediatype, $filename)
{
global $config;
$avatar = new Avatar();
$avatar->profile_id = $profile_id;
switch($size) {
case 'mini':
$avatar->width = 24;
$avatar->height = 24;
break;
case 'normal':
$avatar->width = 48;
$avatar->height = 48;
break;
default:
// Note: Twitter's big avatars are a different size than
// StatusNet's (StatusNet's = 96)
$avatar->width = 73;
$avatar->height = 73;
}
$avatar->original = 0; // we don't have the original
$avatar->mediatype = $mediatype;
$avatar->filename = $filename;
$avatar->url = Avatar::url($filename);
$avatar->created = common_sql_now();
try {
$id = $avatar->insert();
} catch (Exception $e) {
common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert avatar - ' . $e->getMessage());
}
if (empty($id)) {
common_log_db_error($avatar, 'INSERT', __FILE__);
return null;
}
common_debug($this->name() .
" - Saved new $size avatar for $profile_id.");
return $id;
}
/**
* Fetch a remote avatar image and save to local storage.
*
* @param string $url avatar source URL
* @param string $filename bare local filename for download
* @return bool true on success, false on failure
*/
function fetchAvatar($url, $filename)
{
common_debug($this->name() . " - Fetching Twitter avatar: $url");
$request = HTTPClient::start();
$response = $request->get($url);
if ($response->isOk()) {
$avatarfile = Avatar::path($filename);
$ok = file_put_contents($avatarfile, $response->getBody());
if (!$ok) {
common_log(LOG_WARNING, $this->name() .
" - Couldn't open file $filename");
return false;
}
} else {
return false;
}
return true;
}
const URL = 1;
const HASHTAG = 2;
const MENTION = 3;
function linkify($status)
{
$text = $status->text;
if (empty($status->entities)) {
common_log(LOG_WARNING, "No entities data for {$status->id}; trying to fake up links ourselves.");
$text = common_replace_urls_callback($text, 'common_linkify');
$text = preg_replace('/(^|\&quot\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.TwitterStatusFetcher::tagLink('\\2')", $text);
$text = preg_replace('/(^|\s+)@([a-z0-9A-Z_]{1,64})/e', "'\\1@'.TwitterStatusFetcher::atLink('\\2')", $text);
return $text;
}
// Move all the entities into order so we can
// replace them in reverse order and thus
// not mess up their indices
$toReplace = array();
if (!empty($status->entities->urls)) {
foreach ($status->entities->urls as $url) {
$toReplace[$url->indices[0]] = array(self::URL, $url);
}
}
if (!empty($status->entities->hashtags)) {
foreach ($status->entities->hashtags as $hashtag) {
$toReplace[$hashtag->indices[0]] = array(self::HASHTAG, $hashtag);
}
}
if (!empty($status->entities->user_mentions)) {
foreach ($status->entities->user_mentions as $mention) {
$toReplace[$mention->indices[0]] = array(self::MENTION, $mention);
}
}
// sort in reverse order by key
krsort($toReplace);
foreach ($toReplace as $part) {
list($type, $object) = $part;
switch($type) {
case self::URL:
$linkText = $this->makeUrlLink($object);
break;
case self::HASHTAG:
$linkText = $this->makeHashtagLink($object);
break;
case self::MENTION:
$linkText = $this->makeMentionLink($object);
break;
default:
continue;
}
$text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]);
}
return $text;
}
function makeUrlLink($object)
{
return "<a href='{$object->url}' class='extlink'>{$object->url}</a>";
}
function makeHashtagLink($object)
{
return "#" . self::tagLink($object->text);
}
function makeMentionLink($object)
{
return "@".self::atLink($object->screen_name, $object->name);
}
static function tagLink($tag)
{
return "<a href='https://twitter.com/search?q=%23{$tag}' class='hashtag'>{$tag}</a>";
}
static function atLink($screenName, $fullName=null)
{
if (!empty($fullName)) {
return "<a href='http://twitter.com/{$screenName}' title='{$fullName}'>{$screenName}</a>";
} else {
return "<a href='http://twitter.com/{$screenName}'>{$screenName}</a>";
}
}
function saveStatusMentions($notice, $status)
{
$mentions = array();
if (empty($status->entities) || empty($status->entities->user_mentions)) {
return;
}
foreach ($status->entities->user_mentions as $mention) {
$flink = Foreign_link::getByForeignID($mention->id, TWITTER_SERVICE);
if (!empty($flink)) {
$user = User::staticGet('id', $flink->user_id);
if (!empty($user)) {
$reply = new Reply();
$reply->notice_id = $notice->id;
$reply->profile_id = $user->id;
common_log(LOG_INFO, __METHOD__ . ": saving reply: notice {$notice->id} to profile {$user->id}");
$id = $reply->insert();
}
}
}
}
}
$id = null;

View File

@ -0,0 +1,265 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* PHP version 5
*
* LICENCE: 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/>.
*
* @category Plugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class OAuthData
{
public $consumer_key, $consumer_secret, $token, $token_secret;
}
/**
*
*/
abstract class JsonStreamReader
{
const CRLF = "\r\n";
public $id;
protected $socket = null;
protected $state = 'init'; // 'init', 'connecting', 'waiting', 'headers', 'active'
public function __construct()
{
$this->id = get_class($this) . '.' . substr(md5(mt_rand()), 0, 8);
}
/**
* Starts asynchronous connect operation...
*
* @fixme Can we do the open-socket fully async to? (need write select infrastructure)
*
* @param string $url
*/
public function connect($url)
{
common_log(LOG_DEBUG, "$this->id opening connection to $url");
$scheme = parse_url($url, PHP_URL_SCHEME);
if ($scheme == 'http') {
$rawScheme = 'tcp';
} else if ($scheme == 'https') {
$rawScheme = 'ssl';
} else {
throw new ServerException('Invalid URL scheme for HTTP stream reader');
}
$host = parse_url($url, PHP_URL_HOST);
$port = parse_url($url, PHP_URL_PORT);
if (!$port) {
if ($scheme == 'https') {
$port = 443;
} else {
$port = 80;
}
}
$path = parse_url($url, PHP_URL_PATH);
$query = parse_url($url, PHP_URL_QUERY);
if ($query) {
$path .= '?' . $query;
}
$errno = $errstr = null;
$timeout = 5;
//$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
$flags = STREAM_CLIENT_CONNECT;
// @fixme add SSL params
$this->socket = stream_socket_client("$rawScheme://$host:$port", $errno, $errstr, $timeout, $flags);
$this->send($this->httpOpen($host, $path));
stream_set_blocking($this->socket, false);
$this->state = 'waiting';
}
/**
* Send some fun data off to the server.
*
* @param string $buffer
*/
function send($buffer)
{
fwrite($this->socket, $buffer);
}
/**
* Read next packet of data from the socket.
*
* @return string
*/
function read()
{
$buffer = fread($this->socket, 65536);
return $buffer;
}
/**
* Build HTTP request headers.
*
* @param string $host
* @param string $path
* @return string
*/
protected function httpOpen($host, $path)
{
$lines = array(
"GET $path HTTP/1.1",
"Host: $host",
"User-Agent: StatusNet/" . STATUSNET_VERSION . " (TwitterBridgePlugin)",
"Connection: close",
"",
""
);
return implode(self::CRLF, $lines);
}
/**
* Close the current connection, if open.
*/
public function close()
{
if ($this->isConnected()) {
common_log(LOG_DEBUG, "$this->id closing connection.");
fclose($this->socket);
$this->socket = null;
}
}
/**
* Are we currently connected?
*
* @return boolean
*/
public function isConnected()
{
return $this->socket !== null;
}
/**
* Send any sockets we're listening on to the IO manager
* to wait for input.
*
* @return array of resources
*/
public function getSockets()
{
if ($this->isConnected()) {
return array($this->socket);
}
return array();
}
/**
* Take a chunk of input over the horn and go go go! :D
*
* @param string $buffer
*/
public function handleInput($socket)
{
if ($this->socket !== $socket) {
throw new Exception('Got input from unexpected socket!');
}
try {
$buffer = $this->read();
$lines = explode(self::CRLF, $buffer);
foreach ($lines as $line) {
$this->handleLine($line);
}
} catch (Exception $e) {
common_log(LOG_ERR, "$this->id aborting connection due to error: " . $e->getMessage());
fclose($this->socket);
throw $e;
}
}
protected function handleLine($line)
{
switch ($this->state)
{
case 'waiting':
$this->handleLineWaiting($line);
break;
case 'headers':
$this->handleLineHeaders($line);
break;
case 'active':
$this->handleLineActive($line);
break;
default:
throw new Exception('Invalid state in handleLine: ' . $this->state);
}
}
/**
*
* @param <type> $line
*/
protected function handleLineWaiting($line)
{
$bits = explode(' ', $line, 3);
if (count($bits) != 3) {
throw new Exception("Invalid HTTP response line: $line");
}
list($http, $status, $text) = $bits;
if (substr($http, 0, 5) != 'HTTP/') {
throw new Exception("Invalid HTTP response line chunk '$http': $line");
}
if ($status != '200') {
throw new Exception("Bad HTTP response code $status: $line");
}
common_log(LOG_DEBUG, "$this->id $line");
$this->state = 'headers';
}
protected function handleLineHeaders($line)
{
if ($line == '') {
$this->state = 'active';
common_log(LOG_DEBUG, "$this->id connection is active!");
} else {
common_log(LOG_DEBUG, "$this->id read HTTP header: $line");
$this->responseHeaders[] = $line;
}
}
protected function handleLineActive($line)
{
if ($line == "") {
// Server sends empty lines as keepalive.
return;
}
$data = json_decode($line);
if ($data) {
$this->handleJson($data);
} else {
common_log(LOG_ERR, "$this->id received bogus JSON data: " . var_export($line, true));
}
}
abstract protected function handleJson(stdClass $data);
}

View File

@ -0,0 +1,147 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* PHP version 5
*
* LICENCE: 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/>.
*
* @category Plugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$shortoptions = 'n:';
$longoptions = array('nick=','import','all');
$helptext = <<<ENDOFHELP
USAGE: fakestream.php -n <username>
-n --nick=<username> Local user whose Twitter timeline to watch
--import Experimental: run incoming messages through import
--all Experimental: run multiuser; requires nick be the app owner
Attempts a User Stream connection to Twitter as the given user, dumping
data as it comes.
ENDOFHELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (have_option('n')) {
$nickname = get_option_value('n');
} else if (have_option('nick')) {
$nickname = get_option_value('nickname');
} else if (have_option('all')) {
$nickname = null;
} else {
show_help($helptext);
exit(0);
}
/**
*
* @param User $user
* @return TwitterOAuthClient
*/
function twitterAuthForUser(User $user)
{
$flink = Foreign_link::getByUserID($user->id,
TWITTER_SERVICE);
if (!$flink) {
throw new ServerException("No Twitter config for this user.");
}
$token = TwitterOAuthClient::unpackToken($flink->credentials);
if (!$token) {
throw new ServerException("No Twitter OAuth credentials for this user.");
}
return new TwitterOAuthClient($token->key, $token->secret);
}
/**
* Emulate the line-by-line output...
*
* @param Foreign_link $flink
* @param mixed $data
*/
function dumpMessage($flink, $data)
{
$msg = prepMessage($flink, $data);
print json_encode($msg) . "\r\n";
}
function prepMessage($flink, $data)
{
$msg->for_user = $flink->foreign_id;
$msg->message = $data;
return $msg;
}
if (have_option('all')) {
$users = array();
$flink = new Foreign_link();
$flink->service = TWITTER_SERVICE;
$flink->find();
while ($flink->fetch()) {
if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
FOREIGN_NOTICE_RECV) {
$users[] = $flink->user_id;
}
}
} else {
$user = User::staticGet('nickname', $nickname);
$users = array($user->id);
}
$output = array();
foreach ($users as $id) {
$user = User::staticGet('id', $id);
if (!$user) {
throw new Exception("No user for id $id");
}
$auth = twitterAuthForUser($user);
$flink = Foreign_link::getByUserID($user->id,
TWITTER_SERVICE);
$friends->friends = $auth->friendsIds();
dumpMessage($flink, $friends);
$timeline = $auth->statusesHomeTimeline();
foreach ($timeline as $status) {
$output[] = prepMessage($flink, $status);
}
}
usort($output, function($a, $b) {
if ($a->message->id < $b->message->id) {
return -1;
} else if ($a->message->id == $b->message->id) {
return 0;
} else {
return 1;
}
});
foreach ($output as $msg) {
print json_encode($msg) . "\r\n";
}

View File

@ -0,0 +1,244 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* PHP version 5
*
* LICENCE: 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/>.
*
* @category Plugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$shortoptions = 'n:';
$longoptions = array('nick=','import','all','apiroot=');
$helptext = <<<ENDOFHELP
USAGE: streamtest.php -n <username>
-n --nick=<username> Local user whose Twitter timeline to watch
--import Experimental: run incoming messages through import
--all Experimental: run multiuser; requires nick be the app owner
--apiroot=<url> Provide alternate streaming API root URL
Attempts a User Stream connection to Twitter as the given user, dumping
data as it comes.
ENDOFHELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
require_once dirname(dirname(__FILE__)) . '/jsonstreamreader.php';
require_once dirname(dirname(__FILE__)) . '/twitterstreamreader.php';
if (have_option('n')) {
$nickname = get_option_value('n');
} else if (have_option('nick')) {
$nickname = get_option_value('nickname');
} else {
show_help($helptext);
exit(0);
}
/**
*
* @param User $user
* @return TwitterOAuthClient
*/
function twitterAuthForUser(User $user)
{
$flink = Foreign_link::getByUserID($user->id,
TWITTER_SERVICE);
if (!$flink) {
throw new ServerException("No Twitter config for this user.");
}
$token = TwitterOAuthClient::unpackToken($flink->credentials);
if (!$token) {
throw new ServerException("No Twitter OAuth credentials for this user.");
}
return new TwitterOAuthClient($token->key, $token->secret);
}
function homeStreamForUser(User $user)
{
$auth = twitterAuthForUser($user);
return new TwitterUserStream($auth);
}
function siteStreamForOwner(User $user)
{
// The user we auth as must be the owner of the application.
$auth = twitterAuthForUser($user);
if (have_option('apiroot')) {
$stream = new TwitterSiteStream($auth, get_option_value('apiroot'));
} else {
$stream = new TwitterSiteStream($auth);
}
// Pull Twitter user IDs for all users we want to pull data for
$userIds = array();
$flink = new Foreign_link();
$flink->service = TWITTER_SERVICE;
$flink->find();
while ($flink->fetch()) {
if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
FOREIGN_NOTICE_RECV) {
$userIds[] = $flink->foreign_id;
}
}
$stream->followUsers($userIds);
return $stream;
}
$user = User::staticGet('nickname', $nickname);
global $myuser;
$myuser = $user;
if (have_option('all')) {
$stream = siteStreamForOwner($user);
} else {
$stream = homeStreamForUser($user);
}
$stream->hookEvent('raw', function($data, $context) {
common_log(LOG_INFO, json_encode($data) . ' for ' . json_encode($context));
});
$stream->hookEvent('friends', function($data, $context) {
printf("Friend list: %s\n", implode(', ', $data->friends));
});
$stream->hookEvent('favorite', function($data, $context) {
printf("%s favorited %s's notice: %s\n",
$data->source->screen_name,
$data->target->screen_name,
$data->target_object->text);
});
$stream->hookEvent('unfavorite', function($data, $context) {
printf("%s unfavorited %s's notice: %s\n",
$data->source->screen_name,
$data->target->screen_name,
$data->target_object->text);
});
$stream->hookEvent('follow', function($data, $context) {
printf("%s friended %s\n",
$data->source->screen_name,
$data->target->screen_name);
});
$stream->hookEvent('unfollow', function($data, $context) {
printf("%s unfriended %s\n",
$data->source->screen_name,
$data->target->screen_name);
});
$stream->hookEvent('delete', function($data, $context) {
printf("Deleted status notification: %s\n",
$data->status->id);
});
$stream->hookEvent('scrub_geo', function($data, $context) {
printf("Req to scrub geo data for user id %s up to status ID %s\n",
$data->user_id,
$data->up_to_status_id);
});
$stream->hookEvent('status', function($data, $context) {
printf("Received status update from %s: %s\n",
$data->user->screen_name,
$data->text);
if (have_option('import')) {
$importer = new TwitterImport();
printf("\timporting...");
$notice = $importer->importStatus($data);
if ($notice) {
global $myuser;
Inbox::insertNotice($myuser->id, $notice->id);
printf(" %s\n", $notice->id);
} else {
printf(" FAIL\n");
}
}
});
$stream->hookEvent('direct_message', function($data) {
printf("Direct message from %s to %s: %s\n",
$data->sender->screen_name,
$data->recipient->screen_name,
$data->text);
});
class TwitterManager extends IoManager
{
function __construct(TwitterStreamReader $stream)
{
$this->stream = $stream;
}
function getSockets()
{
return $this->stream->getSockets();
}
function handleInput($data)
{
$this->stream->handleInput($data);
return true;
}
function start()
{
$this->stream->connect();
return true;
}
function finish()
{
$this->stream->close();
return true;
}
public static function get()
{
throw new Exception('not a singleton');
}
}
class TwitterStreamMaster extends IoMaster
{
function __construct($id, $ioManager)
{
parent::__construct($id);
$this->ioManager = $ioManager;
}
/**
* Initialize IoManagers which are appropriate to this instance.
*/
function initManagers()
{
$this->instantiate($this->ioManager);
}
}
$master = new TwitterStreamMaster('TwitterStream', new TwitterManager($stream));
$master->init();
$master->service();

View File

@ -0,0 +1,59 @@
<?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') && !defined('LACONICA')) { exit(1); }
require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
/**
* Queue handler to deal with incoming Twitter status updates, as retrieved by
* TwitterDaemon (twitterdaemon.php).
*
* The queue handler passes the status through TwitterImporter for import into the
* local database (if necessary), then adds the imported notice to the local inbox
* of the attached Twitter user.
*
* Warning: the way we do inbox distribution manually means that realtime, XMPP, etc
* don't work on Twitter-borne messages. When TwitterImporter is changed to handle
* that correctly, we'll only need to do this once...?
*/
class TweetCtlQueueHandler extends QueueHandler
{
function transport()
{
return 'tweetctl';
}
function handle($data)
{
// A user has activated or deactivated their Twitter bridge
// import status.
$action = $data['action'];
$userId = $data['for_user'];
$tm = TwitterManager::get();
if ($action == 'start') {
$tm->startTwitterUser($userId);
} else if ($action == 'stop') {
$tm->stopTwitterUser($userId);
}
return true;
}
}

View File

@ -0,0 +1,63 @@
<?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') && !defined('LACONICA')) { exit(1); }
require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
/**
* Queue handler to deal with incoming Twitter status updates, as retrieved by
* TwitterDaemon (twitterdaemon.php).
*
* The queue handler passes the status through TwitterImporter for import into the
* local database (if necessary), then adds the imported notice to the local inbox
* of the attached Twitter user.
*
* Warning: the way we do inbox distribution manually means that realtime, XMPP, etc
* don't work on Twitter-borne messages. When TwitterImporter is changed to handle
* that correctly, we'll only need to do this once...?
*/
class TweetInQueueHandler extends QueueHandler
{
function transport()
{
return 'tweetin';
}
function handle($data)
{
// JSON object with Twitter data
$status = $data['status'];
// Twitter user ID this incoming data belongs to.
$receiver = $data['for_user'];
$importer = new TwitterImport();
$notice = $importer->importStatus($status);
if ($notice) {
$flink = Foreign_link::getByForeignID(TWITTER_SERVICE, $receiver);
if ($flink) {
// @fixme this should go through more regular channels?
Inbox::insertNotice($flink->user_id, $notice->id);
}
}
return true;
}
}

View File

@ -0,0 +1,651 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* PHP version 5
*
* LICENCE: 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/>.
*
* @category Plugin
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @author Julien C <chaumond@gmail.com>
* @author Brion Vibber <brion@status.net>
* @copyright 2009-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
/**
* Encapsulation of the Twitter status -> notice incoming bridge import.
* Is used by both the polling twitterstatusfetcher.php daemon, and the
* in-progress streaming import.
*
* @category Plugin
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @author Julien C <chaumond@gmail.com>
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @link http://twitter.com/
*/
class TwitterImport
{
public function importStatus($status)
{
// Hacktastic: filter out stuff coming from this StatusNet
$source = mb_strtolower(common_config('integration', 'source'));
if (preg_match("/$source/", mb_strtolower($status->source))) {
common_debug($this->name() . ' - Skipping import of status ' .
$status->id . ' with source ' . $source);
return null;
}
// Don't save it if the user is protected
// FIXME: save it but treat it as private
if ($status->user->protected) {
return null;
}
$notice = $this->saveStatus($status);
return $notice;
}
function name()
{
return get_class($this);
}
function saveStatus($status)
{
$profile = $this->ensureProfile($status->user);
if (empty($profile)) {
common_log(LOG_ERR, $this->name() .
' - Problem saving notice. No associated Profile.');
return null;
}
$statusUri = $this->makeStatusURI($status->user->screen_name, $status->id);
// check to see if we've already imported the status
$n2s = Notice_to_status::staticGet('status_id', $status->id);
if (!empty($n2s)) {
common_log(
LOG_INFO,
$this->name() .
" - Ignoring duplicate import: {$status->id}"
);
return Notice::staticGet('id', $n2s->notice_id);
}
// If it's a retweet, save it as a repeat!
if (!empty($status->retweeted_status)) {
common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}.");
$original = $this->saveStatus($status->retweeted_status);
if (empty($original)) {
return null;
} else {
$author = $original->getProfile();
// TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'.
// TRANS: %1$s is the repeated user's name, %2$s is the repeated notice.
$content = sprintf(_m('RT @%1$s %2$s'),
$author->nickname,
$original->content);
if (Notice::contentTooLong($content)) {
$contentlimit = Notice::maxContent();
$content = mb_substr($content, 0, $contentlimit - 4) . ' ...';
}
$repeat = Notice::saveNew($profile->id,
$content,
'twitter',
array('repeat_of' => $original->id,
'uri' => $statusUri,
'is_local' => Notice::GATEWAY));
common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}");
Notice_to_status::saveNew($repeat->id, $status->id);
return $repeat;
}
}
$notice = new Notice();
$notice->profile_id = $profile->id;
$notice->uri = $statusUri;
$notice->url = $statusUri;
$notice->created = strftime(
'%Y-%m-%d %H:%M:%S',
strtotime($status->created_at)
);
$notice->source = 'twitter';
$notice->reply_to = null;
if (!empty($status->in_reply_to_status_id)) {
common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}");
$n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id);
if (empty($n2s)) {
common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
} else {
$reply = Notice::staticGet('id', $n2s->notice_id);
if (empty($reply)) {
common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
} else {
common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}");
$notice->reply_to = $reply->id;
$notice->conversation = $reply->conversation;
}
}
}
if (empty($notice->conversation)) {
$conv = Conversation::create();
$notice->conversation = $conv->id;
common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}.");
}
$notice->is_local = Notice::GATEWAY;
$notice->content = html_entity_decode($status->text, ENT_QUOTES, 'UTF-8');
$notice->rendered = $this->linkify($status);
if (Event::handle('StartNoticeSave', array(&$notice))) {
$id = $notice->insert();
if (!$id) {
common_log_db_error($notice, 'INSERT', __FILE__);
common_log(LOG_ERR, $this->name() .
' - Problem saving notice.');
}
Event::handle('EndNoticeSave', array($notice));
}
Notice_to_status::saveNew($notice->id, $status->id);
$this->saveStatusMentions($notice, $status);
$notice->blowOnInsert();
return $notice;
}
/**
* Make an URI for a status.
*
* @param object $status status object
*
* @return string URI
*/
function makeStatusURI($username, $id)
{
return 'http://twitter.com/'
. $username
. '/status/'
. $id;
}
/**
* Look up a Profile by profileurl field. Profile::staticGet() was
* not working consistently.
*
* @param string $nickname local nickname of the Twitter user
* @param string $profileurl the profile url
*
* @return mixed value the first Profile with that url, or null
*/
function getProfileByUrl($nickname, $profileurl)
{
$profile = new Profile();
$profile->nickname = $nickname;
$profile->profileurl = $profileurl;
$profile->limit(1);
if ($profile->find()) {
$profile->fetch();
return $profile;
}
return null;
}
/**
* Check to see if this Twitter status has already been imported
*
* @param Profile $profile Twitter user's local profile
* @param string $statusUri URI of the status on Twitter
*
* @return mixed value a matching Notice or null
*/
function checkDupe($profile, $statusUri)
{
$notice = new Notice();
$notice->uri = $statusUri;
$notice->profile_id = $profile->id;
$notice->limit(1);
if ($notice->find()) {
$notice->fetch();
return $notice;
}
return null;
}
function ensureProfile($user)
{
// check to see if there's already a profile for this user
$profileurl = 'http://twitter.com/' . $user->screen_name;
$profile = $this->getProfileByUrl($user->screen_name, $profileurl);
if (!empty($profile)) {
common_debug($this->name() .
" - Profile for $profile->nickname found.");
// Check to see if the user's Avatar has changed
$this->checkAvatar($user, $profile);
return $profile;
} else {
common_debug($this->name() . ' - Adding profile and remote profile ' .
"for Twitter user: $profileurl.");
$profile = new Profile();
$profile->query("BEGIN");
$profile->nickname = $user->screen_name;
$profile->fullname = $user->name;
$profile->homepage = $user->url;
$profile->bio = $user->description;
$profile->location = $user->location;
$profile->profileurl = $profileurl;
$profile->created = common_sql_now();
try {
$id = $profile->insert();
} catch(Exception $e) {
common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert profile - ' . $e->getMessage());
}
if (empty($id)) {
common_log_db_error($profile, 'INSERT', __FILE__);
$profile->query("ROLLBACK");
return false;
}
// check for remote profile
$remote_pro = Remote_profile::staticGet('uri', $profileurl);
if (empty($remote_pro)) {
$remote_pro = new Remote_profile();
$remote_pro->id = $id;
$remote_pro->uri = $profileurl;
$remote_pro->created = common_sql_now();
try {
$rid = $remote_pro->insert();
} catch (Exception $e) {
common_log(LOG_WARNING, $this->name() . ' Couldn\'t save remote profile - ' . $e->getMessage());
}
if (empty($rid)) {
common_log_db_error($profile, 'INSERT', __FILE__);
$profile->query("ROLLBACK");
return false;
}
}
$profile->query("COMMIT");
$this->saveAvatars($user, $id);
return $profile;
}
}
function checkAvatar($twitter_user, $profile)
{
global $config;
$path_parts = pathinfo($twitter_user->profile_image_url);
$newname = 'Twitter_' . $twitter_user->id . '_' .
$path_parts['basename'];
$oldname = $profile->getAvatar(48)->filename;
if ($newname != $oldname) {
common_debug($this->name() . ' - Avatar for Twitter user ' .
"$profile->nickname has changed.");
common_debug($this->name() . " - old: $oldname new: $newname");
$this->updateAvatars($twitter_user, $profile);
}
if ($this->missingAvatarFile($profile)) {
common_debug($this->name() . ' - Twitter user ' .
$profile->nickname .
' is missing one or more local avatars.');
common_debug($this->name() ." - old: $oldname new: $newname");
$this->updateAvatars($twitter_user, $profile);
}
}
function updateAvatars($twitter_user, $profile) {
global $config;
$path_parts = pathinfo($twitter_user->profile_image_url);
$img_root = substr($path_parts['basename'], 0, -11);
$ext = $path_parts['extension'];
$mediatype = $this->getMediatype($ext);
foreach (array('mini', 'normal', 'bigger') as $size) {
$url = $path_parts['dirname'] . '/' .
$img_root . '_' . $size . ".$ext";
$filename = 'Twitter_' . $twitter_user->id . '_' .
$img_root . "_$size.$ext";
$this->updateAvatar($profile->id, $size, $mediatype, $filename);
$this->fetchAvatar($url, $filename);
}
}
function missingAvatarFile($profile) {
foreach (array(24, 48, 73) as $size) {
$filename = $profile->getAvatar($size)->filename;
$avatarpath = Avatar::path($filename);
if (file_exists($avatarpath) == FALSE) {
return true;
}
}
return false;
}
function getMediatype($ext)
{
$mediatype = null;
switch (strtolower($ext)) {
case 'jpg':
$mediatype = 'image/jpg';
break;
case 'gif':
$mediatype = 'image/gif';
break;
default:
$mediatype = 'image/png';
}
return $mediatype;
}
function saveAvatars($user, $id)
{
global $config;
$path_parts = pathinfo($user->profile_image_url);
$ext = $path_parts['extension'];
$end = strlen('_normal' . $ext);
$img_root = substr($path_parts['basename'], 0, -($end+1));
$mediatype = $this->getMediatype($ext);
foreach (array('mini', 'normal', 'bigger') as $size) {
$url = $path_parts['dirname'] . '/' .
$img_root . '_' . $size . ".$ext";
$filename = 'Twitter_' . $user->id . '_' .
$img_root . "_$size.$ext";
if ($this->fetchAvatar($url, $filename)) {
$this->newAvatar($id, $size, $mediatype, $filename);
} else {
common_log(LOG_WARNING, $id() .
" - Problem fetching Avatar: $url");
}
}
}
function updateAvatar($profile_id, $size, $mediatype, $filename) {
common_debug($this->name() . " - Updating avatar: $size");
$profile = Profile::staticGet($profile_id);
if (empty($profile)) {
common_debug($this->name() . " - Couldn't get profile: $profile_id!");
return;
}
$sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
$avatar = $profile->getAvatar($sizes[$size]);
// Delete the avatar, if present
if ($avatar) {
$avatar->delete();
}
$this->newAvatar($profile->id, $size, $mediatype, $filename);
}
function newAvatar($profile_id, $size, $mediatype, $filename)
{
global $config;
$avatar = new Avatar();
$avatar->profile_id = $profile_id;
switch($size) {
case 'mini':
$avatar->width = 24;
$avatar->height = 24;
break;
case 'normal':
$avatar->width = 48;
$avatar->height = 48;
break;
default:
// Note: Twitter's big avatars are a different size than
// StatusNet's (StatusNet's = 96)
$avatar->width = 73;
$avatar->height = 73;
}
$avatar->original = 0; // we don't have the original
$avatar->mediatype = $mediatype;
$avatar->filename = $filename;
$avatar->url = Avatar::url($filename);
$avatar->created = common_sql_now();
try {
$id = $avatar->insert();
} catch (Exception $e) {
common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert avatar - ' . $e->getMessage());
}
if (empty($id)) {
common_log_db_error($avatar, 'INSERT', __FILE__);
return null;
}
common_debug($this->name() .
" - Saved new $size avatar for $profile_id.");
return $id;
}
/**
* Fetch a remote avatar image and save to local storage.
*
* @param string $url avatar source URL
* @param string $filename bare local filename for download
* @return bool true on success, false on failure
*/
function fetchAvatar($url, $filename)
{
common_debug($this->name() . " - Fetching Twitter avatar: $url");
$request = HTTPClient::start();
$response = $request->get($url);
if ($response->isOk()) {
$avatarfile = Avatar::path($filename);
$ok = file_put_contents($avatarfile, $response->getBody());
if (!$ok) {
common_log(LOG_WARNING, $this->name() .
" - Couldn't open file $filename");
return false;
}
} else {
return false;
}
return true;
}
const URL = 1;
const HASHTAG = 2;
const MENTION = 3;
function linkify($status)
{
$text = $status->text;
if (empty($status->entities)) {
common_log(LOG_WARNING, "No entities data for {$status->id}; trying to fake up links ourselves.");
$text = common_replace_urls_callback($text, 'common_linkify');
$text = preg_replace('/(^|\&quot\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.TwitterStatusFetcher::tagLink('\\2')", $text);
$text = preg_replace('/(^|\s+)@([a-z0-9A-Z_]{1,64})/e', "'\\1@'.TwitterStatusFetcher::atLink('\\2')", $text);
return $text;
}
// Move all the entities into order so we can
// replace them in reverse order and thus
// not mess up their indices
$toReplace = array();
if (!empty($status->entities->urls)) {
foreach ($status->entities->urls as $url) {
$toReplace[$url->indices[0]] = array(self::URL, $url);
}
}
if (!empty($status->entities->hashtags)) {
foreach ($status->entities->hashtags as $hashtag) {
$toReplace[$hashtag->indices[0]] = array(self::HASHTAG, $hashtag);
}
}
if (!empty($status->entities->user_mentions)) {
foreach ($status->entities->user_mentions as $mention) {
$toReplace[$mention->indices[0]] = array(self::MENTION, $mention);
}
}
// sort in reverse order by key
krsort($toReplace);
foreach ($toReplace as $part) {
list($type, $object) = $part;
switch($type) {
case self::URL:
$linkText = $this->makeUrlLink($object);
break;
case self::HASHTAG:
$linkText = $this->makeHashtagLink($object);
break;
case self::MENTION:
$linkText = $this->makeMentionLink($object);
break;
default:
continue;
}
$text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]);
}
return $text;
}
function makeUrlLink($object)
{
return "<a href='{$object->url}' class='extlink'>{$object->url}</a>";
}
function makeHashtagLink($object)
{
return "#" . self::tagLink($object->text);
}
function makeMentionLink($object)
{
return "@".self::atLink($object->screen_name, $object->name);
}
static function tagLink($tag)
{
return "<a href='https://twitter.com/search?q=%23{$tag}' class='hashtag'>{$tag}</a>";
}
static function atLink($screenName, $fullName=null)
{
if (!empty($fullName)) {
return "<a href='http://twitter.com/{$screenName}' title='{$fullName}'>{$screenName}</a>";
} else {
return "<a href='http://twitter.com/{$screenName}'>{$screenName}</a>";
}
}
function saveStatusMentions($notice, $status)
{
$mentions = array();
if (empty($status->entities) || empty($status->entities->user_mentions)) {
return;
}
foreach ($status->entities->user_mentions as $mention) {
$flink = Foreign_link::getByForeignID($mention->id, TWITTER_SERVICE);
if (!empty($flink)) {
$user = User::staticGet('id', $flink->user_id);
if (!empty($user)) {
$reply = new Reply();
$reply->notice_id = $notice->id;
$reply->profile_id = $user->id;
common_log(LOG_INFO, __METHOD__ . ": saving reply: notice {$notice->id} to profile {$user->id}");
$id = $reply->insert();
}
}
}
}
}

View File

@ -285,6 +285,7 @@ class TwittersettingsAction extends ConnectSettingsAction
}
$original = clone($flink);
$wasReceiving = (bool)($original->noticesync & FOREIGN_NOTICE_RECV);
$flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$result = $flink->update($original);
@ -294,6 +295,19 @@ class TwittersettingsAction extends ConnectSettingsAction
return;
}
if ($wasReceiving xor $noticerecv) {
$this->notifyDaemon($flink->foreign_id, $noticerecv);
}
$this->showForm(_m('Twitter preferences saved.'), true);
}
/**
* Tell the import daemon that we've updated a user's receive status.
*/
function notifyDaemon($twitterUserId, $receiving)
{
// todo... should use control signals rather than queues
}
}

View File

@ -0,0 +1,285 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* PHP version 5
*
* LICENCE: 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/>.
*
* @category Plugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
/**
* Base class for reading Twitter's User Streams and Site Streams
* real-time streaming APIs.
*
* Caller can hook event callbacks for various types of messages;
* the data from the stream and some context info will be passed
* on to the callbacks.
*/
abstract class TwitterStreamReader extends JsonStreamReader
{
protected $callbacks = array();
function __construct(TwitterOAuthClient $auth, $baseUrl)
{
$this->baseUrl = $baseUrl;
$this->oauth = $auth;
}
public function connect($method, $params=array())
{
$url = $this->oAuthUrl($this->baseUrl . '/' . $method, $params);
return parent::connect($url);
}
/**
* Sign our target URL with OAuth auth stuff.
*
* @param string $url
* @param array $params
* @return string
*/
protected function oAuthUrl($url, $params=array())
{
// In an ideal world this would be better encapsulated. :)
$request = OAuthRequest::from_consumer_and_token($this->oauth->consumer,
$this->oauth->token, 'GET', $url, $params);
$request->sign_request($this->oauth->sha1_method,
$this->oauth->consumer, $this->oauth->token);
return $request->to_url();
}
/**
* Add an event callback to receive notifications when things come in
* over the wire.
*
* Callbacks should be in the form: function(object $data, array $context)
* where $context may list additional data on some streams, such as the
* user to whom the message should be routed.
*
* Available events:
*
* Messaging:
*
* 'status': $data contains a status update in standard Twitter JSON format.
* $data->user: sending user in standard Twitter JSON format.
* $data->text... etc
*
* 'direct_message': $data contains a direct message in standard Twitter JSON format.
* $data->sender: sending user in standard Twitter JSON format.
* $data->recipient: receiving user in standard Twitter JSON format.
* $data->text... etc
*
*
* Out of band events:
*
* 'follow': User has either started following someone, or is being followed.
* $data->source: following user in standard Twitter JSON format.
* $data->target: followed user in standard Twitter JSON format.
*
* 'favorite': Someone has favorited a status update.
* $data->source: user doing the favoriting, in standard Twitter JSON format.
* $data->target: user whose status was favorited, in standard Twitter JSON format.
* $data->target_object: the favorited status update in standard Twitter JSON format.
*
* 'unfavorite': Someone has unfavorited a status update.
* $data->source: user doing the unfavoriting, in standard Twitter JSON format.
* $data->target: user whose status was unfavorited, in standard Twitter JSON format.
* $data->target_object: the unfavorited status update in standard Twitter JSON format.
*
*
* Meta information:
*
* 'friends':
* $data->friends: array of user IDs of the current user's friends.
*
* 'delete': Advisory that a Twitter status has been deleted; nice clients
* should follow suit.
* $data->id: ID of status being deleted
* $data->user_id: ID of its owning user
*
* 'scrub_geo': Advisory that a user is clearing geo data from their status
* stream; nice clients should follow suit.
* $data->user_id: ID of user
* $data->up_to_status_id: any notice older than this should be scrubbed.
*
* 'limit': Advisory that tracking has hit a resource limit.
* $data->track
*
* 'raw': receives the full JSON data for all message types.
*
* @param string $event
* @param callable $callback
*/
public function hookEvent($event, $callback)
{
$this->callbacks[$event][] = $callback;
}
/**
* Call event handler callbacks for the given event.
*
* @param string $event
* @param mixed $arg1 ... one or more params to pass on
*/
protected function fireEvent($event, $arg1)
{
if (array_key_exists($event, $this->callbacks)) {
$args = array_slice(func_get_args(), 1);
foreach ($this->callbacks[$event] as $callback) {
call_user_func_array($callback, $args);
}
}
}
protected function handleJson(stdClass $data)
{
$this->routeMessage($data);
}
abstract protected function routeMessage(stdClass $data);
/**
* Send the decoded JSON object out to any event listeners.
*
* @param array $data
* @param array $context optional additional context data to pass on
*/
protected function handleMessage(stdClass $data, array $context=array())
{
$this->fireEvent('raw', $data, $context);
if (isset($data->text)) {
$this->fireEvent('status', $data, $context);
return;
}
if (isset($data->event)) {
$this->fireEvent($data->event, $data, $context);
return;
}
if (isset($data->friends)) {
$this->fireEvent('friends', $data, $context);
}
$knownMeta = array('delete', 'scrub_geo', 'limit', 'direct_message');
foreach ($knownMeta as $key) {
if (isset($data->$key)) {
$this->fireEvent($key, $data->$key, $context);
return;
}
}
}
}
/**
* Multiuser stream listener for Twitter Site Streams API
* http://dev.twitter.com/pages/site_streams
*
* The site streams API allows listening to updates for multiple users.
* Pass in the user IDs to listen to in via followUser() -- note they
* must each have a valid OAuth token for the application ID we're
* connecting as.
*
* You'll need to be connecting with the auth keys for the user who
* owns the application registration.
*
* The user each message is destined for will be passed to event handlers
* in $context['for_user_id'].
*/
class TwitterSiteStream extends TwitterStreamReader
{
protected $userIds;
public function __construct(TwitterOAuthClient $auth, $baseUrl='http://betastream.twitter.com')
{
parent::__construct($auth, $baseUrl);
}
public function connect($method='2b/site.json')
{
$params = array();
if ($this->userIds) {
$params['follow'] = implode(',', $this->userIds);
}
return parent::connect($method, $params);
}
/**
* Set the users whose home streams should be pulled.
* They all must have valid oauth tokens for this application.
*
* Must be called before connect().
*
* @param array $userIds
*/
function followUsers($userIds)
{
$this->userIds = $userIds;
}
/**
* Each message in the site stream tells us which user ID it should be
* routed to; we'll need that to let the caller know what to do.
*
* @param array $data
*/
function routeMessage(stdClass $data)
{
$context = array(
'source' => 'sitestream',
'for_user' => $data->for_user
);
parent::handleMessage($data->message, $context);
}
}
/**
* Stream listener for Twitter User Streams API
* http://dev.twitter.com/pages/user_streams
*
* This will pull the home stream and additional events just for the user
* we've authenticated as.
*/
class TwitterUserStream extends TwitterStreamReader
{
public function __construct(TwitterOAuthClient $auth, $baseUrl='https://userstream.twitter.com')
{
parent::__construct($auth, $baseUrl);
}
public function connect($method='2/user.json')
{
return parent::connect($method);
}
/**
* Each message in the user stream is just ready to go.
*
* @param array $data
*/
function routeMessage(stdClass $data)
{
$context = array(
'source' => 'userstream'
);
parent::handleMessage($data, $context);
}
}

View File

@ -128,25 +128,9 @@ class UserFlagPlugin extends Plugin
*/
function onEndProfilePageActionsElements(&$action, $profile)
{
$user = common_current_user();
if (!empty($user) && ($user->id != $profile->id)) {
$action->elementStart('li', 'entity_flag');
if (User_flag_profile::exists($profile->id, $user->id)) {
// @todo FIXME: Add a title explaining what 'flagged' means?
// TRANS: Message added to a profile if it has been flagged for review.
$action->element('p', 'flagged', _('Flagged'));
} else {
$form = new FlagProfileForm($action, $profile,
array('action' => 'showstream',
'nickname' => $profile->nickname));
$form->show();
}
$action->elementEnd('li');
}
$this->showFlagButton($action, $profile,
array('action' => 'showstream',
'nickname' => $profile->nickname));
return true;
}
@ -160,24 +144,42 @@ class UserFlagPlugin extends Plugin
*/
function onEndProfileListItemActionElements($item)
{
$user = common_current_user();
if (!empty($user)) {
list($action, $args) = $item->action->returnToArgs();
$args['action'] = $action;
$form = new FlagProfileForm($item->action, $item->profile, $args);
$item->action->elementStart('li', 'entity_flag');
$form->show();
$item->action->elementEnd('li');
}
list($action, $args) = $item->action->returnToArgs();
$args['action'] = $action;
$this->showFlagButton($item->action, $item->profile, $args);
return true;
}
/**
* Actually output a flag button. If the target profile has already been
* flagged by the current user, a null-action faux button is shown.
*
* @param Action $action
* @param Profile $profile
* @param array $returnToArgs
*/
protected function showFlagButton($action, $profile, $returnToArgs)
{
$user = common_current_user();
if (!empty($user) && ($user->id != $profile->id)) {
$action->elementStart('li', 'entity_flag');
if (User_flag_profile::exists($profile->id, $user->id)) {
// @todo FIXME: Add a title explaining what 'flagged' means?
// TRANS: Message added to a profile if it has been flagged for review.
$action->element('p', 'flagged', _m('Flagged'));
} else {
$form = new FlagProfileForm($action, $profile, $returnToArgs);
$form->show();
}
$action->elementEnd('li');
}
}
/**
* Initialize any flagging buttons on the page
*

View File

@ -79,21 +79,36 @@ class User_flag_profile extends Memcached_DataObject
/**
* return key definitions for DB_DataObject
*
* @return array key definitions
* @return array of key names
*/
function keys()
{
return array('profile_id' => 'K', 'user_id' => 'K');
return array_keys($this->keyTypes());
}
/**
* return key definitions for DB_DataObject
*
* @return array key definitions
* @return array map of key definitions
*/
function keyTypes()
{
return $this->keys();
return array('profile_id' => 'K', 'user_id' => 'K');
}
/**
* Magic formula for non-autoincrementing integer primary keys
*
* If a table has a single integer column as its primary key, DB_DataObject
* assumes that the column is auto-incrementing and makes a sequence table
* to do this incrementation. Since we don't need this for our class, we
* overload this method and return the magic formula that DB_DataObject needs.
*
* @return array magic three-false array that stops auto-incrementing.
*/
function sequenceKey()
{
return array(false, false, false);
}
/**

View File

@ -60,13 +60,6 @@ class FlagprofileAction extends ProfileFormAction
assert(!empty($user)); // checked above
assert(!empty($this->profile)); // checked above
if (User_flag_profile::exists($this->profile->id,
$user->id)) {
// TRANS: Client error when setting flag that has already been set for a profile.
$this->clientError(_m('Flag already exists.'));
return false;
}
return true;
}
@ -104,7 +97,13 @@ class FlagprofileAction extends ProfileFormAction
// throws an exception on error
User_flag_profile::create($user->id, $this->profile->id);
if (User_flag_profile::exists($this->profile->id,
$user->id)) {
// We'll return to the profile page (or return the updated AJAX form)
// showing the current state, so no harm done.
} else {
User_flag_profile::create($user->id, $this->profile->id);
}
if ($this->boolean('ajax')) {
$this->ajaxResults();

View File

@ -0,0 +1,26 @@
# Translation of StatusNet - UserLimit to Finnish (Suomi)
# Expored from translatewiki.net
#
# Author: Centerlink
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - UserLimit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-11-02 22:51+0000\n"
"PO-Revision-Date: 2010-11-02 22:55:20+0000\n"
"Language-Team: Finnish <http://translatewiki.net/wiki/Portal:fi>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2010-10-29 16:14:14+0000\n"
"X-Generator: MediaWiki 1.17alpha (r75875); Translate extension (2010-09-17)\n"
"X-Translation-Project: translatewiki.net at http://translatewiki.net\n"
"X-Language-Code: fi\n"
"X-Message-Group: #out-statusnet-plugin-userlimit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: UserLimitPlugin.php:89
msgid "Limit the number of users who can register."
msgstr "Rajoita niiden käyttäjien lukumäärää, jotka voivat rekisteröityä."

View File

@ -57,7 +57,8 @@ function getActivityStreamDocument()
throw new Exception("File '$filename' not readable.");
}
printfv(_("Getting backup from file '$filename'.")."\n");
// TRANS: Commandline script output. %s is the filename that contains a backup for a user.
printfv(_("Getting backup from file '%s'.")."\n",$filename);
$xml = file_get_contents($filename);
@ -79,19 +80,22 @@ function importActivityStream($user, $doc)
if (!empty($subjectEl)) {
$subject = new ActivityObject($subjectEl);
printfv(_("Backup file for user %s (%s)")."\n", $subject->id, Ostatus_profile::getActivityObjectNickname($subject));
// TRANS: Commandline script output. %1$s is the subject ID, %2$s is the subject nickname.
printfv(_("Backup file for user %1$s (%2$s)")."\n", $subject->id, Ostatus_profile::getActivityObjectNickname($subject));
} else {
throw new Exception("Feed doesn't have an <activity:subject> element.");
}
if (is_null($user)) {
// TRANS: Commandline script output.
printfv(_("No user specified; using backup user.")."\n");
$user = userFromSubject($subject);
}
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
printfv(_("%d entries in backup.")."\n", $entries->length);
// TRANS: Commandline script output. %d is the number of entries in the activity stream in backup; used for plural.
printfv(_m("%d entry in backup.","%d entries in backup.",$entries->length)."\n", $entries->length);
for ($i = $entries->length - 1; $i >= 0; $i--) {
try {