Merge branch 'master' into 1.0.x

This commit is contained in:
Evan Prodromou
2010-08-13 14:33:41 -07:00
78 changed files with 11330 additions and 2668 deletions

View File

@@ -30,11 +30,13 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
class GravatarPlugin extends Plugin
{
function onInitializePlugin() {
function onInitializePlugin()
{
return true;
}
function onStartAvatarFormData($action) {
function onStartAvatarFormData($action)
{
$user = common_current_user();
$hasGravatar = $this->hasGravatar($user->id);
@@ -43,7 +45,8 @@ class GravatarPlugin extends Plugin
}
}
function onEndAvatarFormData(&$action) {
function onEndAvatarFormData($action)
{
$user = common_current_user();
$hasGravatar = $this->hasGravatar($user->id);
@@ -89,7 +92,8 @@ class GravatarPlugin extends Plugin
}
}
function onStartAvatarSaveForm($action) {
function onStartAvatarSaveForm($action)
{
if ($action->arg('add')) {
$result = $this->gravatar_save();
@@ -178,7 +182,8 @@ class GravatarPlugin extends Plugin
'success' => true);
}
function gravatar_url($email, $size) {
function gravatar_url($email, $size)
{
$url = "http://www.gravatar.com/avatar.php?gravatar_id=".
md5(strtolower($email)).
"&default=".urlencode(Avatar::defaultImage($size)).
@@ -197,4 +202,4 @@ class GravatarPlugin extends Plugin
return true;
}
}
}

View File

@@ -28,6 +28,15 @@ set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/ext
class FeedSubException extends Exception
{
function __construct($msg=null)
{
$type = get_class($this);
if ($msg) {
parent::__construct("$type: $msg");
} else {
parent::__construct($type);
}
}
}
class OStatusPlugin extends Plugin
@@ -479,6 +488,24 @@ class OStatusPlugin extends Plugin
}
}
/**
* Tell the FeedSub infrastructure whether we have any active OStatus
* usage for the feed; if not it'll be able to garbage-collect the
* feed subscription.
*
* @param FeedSub $feedsub
* @param integer $count in/out
* @return mixed hook return code
*/
function onFeedSubSubscriberCount($feedsub, &$count)
{
$oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
if ($oprofile) {
$count += $oprofile->subscriberCount();
}
return true;
}
/**
* When about to subscribe to a remote user, start a server-to-server
* PuSH subscription if needed. If we can't establish that, abort.

View File

@@ -1,18 +1,42 @@
Plugin to support importing updates from external RSS and Atom feeds into your timeline.
Plugin to support importing and exporting notices through Atom and RSS feeds.
The OStatus plugin concentrates on user-to-user cases for federating StatusNet
and similar social networking / microblogging / blogging sites, but includes
low-level feed subscription systems which are used by some other plugins.
Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be
subscribed unless an external PuSH hub proxy is used.
Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed.
Configuration options available:
$config['ostatus']['hub']
(default internal hub)
Set to URL of an external PuSH hub to use it instead of our internal hub.
Set to URL of an external PuSH hub to use it instead of our internal hub
for sending outgoing updates in user and group feeds.
$config['ostatus']['hub_retries']
(default 0)
Number of times to retry a PuSH send to consumers if using internal hub
Settings controlling incoming feed subscription:
$config['feedsub']['fallback_hub']
To subscribe to feeds that don't have a hub, an external PuSH proxy hub
such as Superfeedr may be used. Any feed without a hub of its own will
be subscribed through the specified hub URL instead. If the external hub
has usage charges, be aware that there is no restriction placed to how
many feeds may be subscribed!
$config['feedsub']['fallback_hub'] = 'https://superfeedr.com/hubbub';
$config['feedsub']['hub_user']
$config['feedsub']['hub_password']
If using the fallback hub mode, these settings may be used to provide
HTTP authentication credentials for contacting the hub. Default hubs
specified from feeds are assumed to not require
For testing, shouldn't be used in production:
$config['ostatus']['skip_signatures']
@@ -23,12 +47,11 @@ $config['feedsub']['nohub']
(default require hub)
Allow low-level feed subscription setup for feeds without hubs.
Not actually usable at this stage, OStatus will check for hubs too
and we have no polling backend.
and we have no polling backend. (The fallback hub option can be used
with a 3rd-party service to provide such polling.)
Todo:
* fully functional l10n
* redo non-OStatus feed support
** rssCloud support?
** possibly a polling daemon to support non-PuSH feeds?
* better support for feeds that aren't natively oriented at social networking
* make use of tags/categories from feeds
* better repeat handling

View File

@@ -61,7 +61,7 @@ class GroupsalmonAction extends SalmonAction
function handlePost()
{
// @fixme process all objects?
switch ($this->act->objects[0]->type) {
switch ($this->activity->objects[0]->type) {
case ActivityObject::ARTICLE:
case ActivityObject::BLOGENTRY:
case ActivityObject::NOTE:
@@ -74,7 +74,7 @@ class GroupsalmonAction extends SalmonAction
// Notice must be to the attention of this group
$context = $this->act->context;
$context = $this->activity->context;
if (empty($context->attention)) {
throw new ClientException("Not to the attention of anyone.");

View File

@@ -55,10 +55,10 @@ class UsersalmonAction extends SalmonAction
*/
function handlePost()
{
common_log(LOG_INFO, "Received post of '{$this->act->objects[0]->id}' from '{$this->act->actor->id}'");
common_log(LOG_INFO, "Received post of '{$this->activity->objects[0]->id}' from '{$this->activity->actor->id}'");
// @fixme: process all activity objects?
switch ($this->act->objects[0]->type) {
switch ($this->activity->objects[0]->type) {
case ActivityObject::ARTICLE:
case ActivityObject::BLOGENTRY:
case ActivityObject::NOTE:
@@ -72,7 +72,7 @@ class UsersalmonAction extends SalmonAction
// Notice must either be a) in reply to a notice by this user
// or b) to the attention of this user
$context = $this->act->context;
$context = $this->activity->context;
if (!empty($context->replyToID)) {
$notice = Notice::staticGet('uri', $context->replyToID);
@@ -92,7 +92,7 @@ class UsersalmonAction extends SalmonAction
throw new ClientException("Not to anyone in reply to anything!");
}
$existing = Notice::staticGet('uri', $this->act->objects[0]->id);
$existing = Notice::staticGet('uri', $this->activity->objects[0]->id);
if (!empty($existing)) {
common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists.");
@@ -143,7 +143,7 @@ class UsersalmonAction extends SalmonAction
function handleFavorite()
{
$notice = $this->getNotice($this->act->objects[0]);
$notice = $this->getNotice($this->activity->objects[0]);
$profile = $this->ensureProfile()->localProfile();
$old = Fave::pkeyGet(array('user_id' => $profile->id,
@@ -164,7 +164,7 @@ class UsersalmonAction extends SalmonAction
*/
function handleUnfavorite()
{
$notice = $this->getNotice($this->act->objects[0]);
$notice = $this->getNotice($this->activity->objects[0]);
$profile = $this->ensureProfile()->localProfile();
$fave = Fave::pkeyGet(array('user_id' => $profile->id,

View File

@@ -207,8 +207,8 @@ class FeedSub extends Memcached_DataObject
$discover = new FeedDiscovery();
$discover->discoverFromFeedURL($feeduri);
$huburi = $discover->getAtomLink('hub');
if (!$huburi) {
$huburi = $discover->getHubLink();
if (!$huburi && !common_config('feedsub', 'fallback_hub')) {
throw new FeedSubNoHubException();
}
@@ -241,8 +241,12 @@ class FeedSub extends Memcached_DataObject
common_log(LOG_WARNING, "Attempting to (re)start PuSH subscription to $this->uri in unexpected state $this->sub_state");
}
if (empty($this->huburi)) {
if (common_config('feedsub', 'nohub')) {
if (common_config('feedsub', 'fallback_hub')) {
// No native hub on this feed?
// Use our fallback hub, which handles polling on our behalf.
} else if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
// We'll never actually get updates in this mode.
return true;
} else {
throw new ServerException("Attempting to start PuSH subscription for feed with no hub");
@@ -255,6 +259,9 @@ class FeedSub extends Memcached_DataObject
/**
* Send a PuSH unsubscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
* Warning: this will cancel the subscription even if someone else in
* the system is using it. Most callers will want garbageCollect() instead,
* which confirms there's no uses left.
*
* @return bool true on success, false on failure
* @throws ServerException if feed state is not valid
@@ -264,8 +271,12 @@ class FeedSub extends Memcached_DataObject
common_log(LOG_WARNING, "Attempting to (re)end PuSH subscription to $this->uri in unexpected state $this->sub_state");
}
if (empty($this->huburi)) {
if (common_config('feedsub', 'nohub')) {
if (common_config('feedsub', 'fallback_hub')) {
// No native hub on this feed?
// Use our fallback hub, which handles polling on our behalf.
} else if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
// We'll never actually get updates in this mode.
return true;
} else {
throw new ServerException("Attempting to end PuSH subscription for feed with no hub");
@@ -275,6 +286,33 @@ class FeedSub extends Memcached_DataObject
return $this->doSubscribe('unsubscribe');
}
/**
* Check if there are any active local uses of this feed, and if not then
* make sure it's inactive, unsubscribing if necessary.
*
* @return boolean true if the subscription is now inactive, false if still active.
*/
public function garbageCollect()
{
if ($this->sub_state == '' || $this->sub_state == 'inactive') {
// No active PuSH subscription, we can just leave it be.
return true;
} else {
// PuSH subscription is either active or in an indeterminate state.
// Check if we're out of subscribers, and if so send an unsubscribe.
$count = 0;
Event::handle('FeedSubSubscriberCount', array($this, &$count));
if ($count) {
common_log(LOG_INFO, __METHOD__ . ': ok, ' . $count . ' user(s) left for ' . $this->uri);
return false;
} else {
common_log(LOG_INFO, __METHOD__ . ': unsubscribing, no users left for ' . $this->uri);
return $this->unsubscribe();
}
}
}
protected function doSubscribe($mode)
{
$orig = clone($this);
@@ -296,7 +334,21 @@ class FeedSub extends Memcached_DataObject
'hub.secret' => $this->secret,
'hub.topic' => $this->uri);
$client = new HTTPClient();
$response = $client->post($this->huburi, $headers, $post);
if ($this->huburi) {
$hub = $this->huburi;
} else {
if (common_config('feedsub', 'fallback_hub')) {
$hub = common_config('feedsub', 'fallback_hub');
if (common_config('feedsub', 'hub_user')) {
$u = common_config('feedsub', 'hub_user');
$p = common_config('feedsub', 'hub_pass');
$client->setAuth($u, $p);
}
} else {
throw new FeedSubException('WTF?');
}
}
$response = $client->post($hub, $headers, $post);
$status = $response->getStatus();
if ($status == 202) {
common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');

View File

@@ -215,22 +215,13 @@ class Ostatus_profile extends Memcached_DataObject
}
/**
* Send a PuSH unsubscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
* Check if this remote profile has any active local subscriptions, and
* if not drop the PuSH subscription feed.
*
* @return bool true on success, false on failure
* @throws ServerException if feed state is not valid
*/
public function unsubscribe() {
$feedsub = FeedSub::staticGet('uri', $this->feeduri);
if (!$feedsub || $feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') {
// No active PuSH subscription, we can just leave it be.
return true;
} else {
// PuSH subscription is either active or in an indeterminate state.
// Send an unsubscribe.
return $feedsub->unsubscribe();
}
$this->garbageCollect();
}
/**
@@ -240,6 +231,21 @@ class Ostatus_profile extends Memcached_DataObject
* @return boolean
*/
public function garbageCollect()
{
$feedsub = FeedSub::staticGet('uri', $this->feeduri);
return $feedsub->garbageCollect();
}
/**
* Check if this remote profile has any active local subscriptions, so the
* PuSH subscription layer can decide if it can drop the feed.
*
* This gets called via the FeedSubSubscriberCount event when running
* FeedSub::garbageCollect().
*
* @return int
*/
public function subscriberCount()
{
if ($this->isGroup()) {
$members = $this->localGroup()->getMembers(0, 1);
@@ -247,13 +253,14 @@ class Ostatus_profile extends Memcached_DataObject
} else {
$count = $this->localProfile()->subscriberCount();
}
if ($count == 0) {
common_log(LOG_INFO, "Unsubscribing from now-unused remote feed $this->feeduri");
$this->unsubscribe();
return true;
} else {
return false;
}
common_log(LOG_INFO, __METHOD__ . " SUB COUNT BEFORE: $count");
// Other plugins may be piggybacking on OStatus without having
// an active group or user-to-user subscription we know about.
Event::handle('Ostatus_profileSubscriberCount', array($this, &$count));
common_log(LOG_INFO, __METHOD__ . " SUB COUNT AFTER: $count");
return $count;
}
/**
@@ -438,26 +445,31 @@ class Ostatus_profile extends Memcached_DataObject
* @param DOMElement $feed for context
* @param string $source identifier ("push" or "salmon")
*/
public function processEntry($entry, $feed, $source)
{
$activity = new Activity($entry, $feed);
// @todo process all activity objects
switch ($activity->objects[0]->type) {
case ActivityObject::ARTICLE:
case ActivityObject::BLOGENTRY:
case ActivityObject::NOTE:
case ActivityObject::STATUS:
case ActivityObject::COMMENT:
break;
default:
throw new ClientException("Can't handle that kind of post.");
}
if (Event::handle('StartHandleFeedEntry', array($activity))) {
if ($activity->verb == ActivityVerb::POST) {
$this->processPost($activity, $source);
} else {
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
// @todo process all activity objects
switch ($activity->objects[0]->type) {
case ActivityObject::ARTICLE:
case ActivityObject::BLOGENTRY:
case ActivityObject::NOTE:
case ActivityObject::STATUS:
case ActivityObject::COMMENT:
if ($activity->verb == ActivityVerb::POST) {
$this->processPost($activity, $source);
} else {
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
}
break;
default:
throw new ClientException("Can't handle that kind of post.");
}
Event::handle('EndHandleFeedEntry', array($activity));
}
}
@@ -486,8 +498,14 @@ class Ostatus_profile extends Memcached_DataObject
// OK here! assume the default
} else if ($actor->id == $this->uri || $actor->link == $this->uri) {
$this->updateFromActivityObject($actor);
} else {
} else if ($actor->id) {
// We have an ActivityStreams actor with an explicit ID that doesn't match the feed owner.
// This isn't what we expect from mainline OStatus person feeds!
// Group feeds go down another path, with different validation.
throw new Exception("Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}");
} else {
// Plain <author> without ActivityStreams actor info.
// We'll just ignore this info for now and save the update under the feed's identity.
}
$oprofile = $this;
@@ -668,7 +686,7 @@ class Ostatus_profile extends Memcached_DataObject
common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $attention_uris));
$groups = array();
$replies = array();
foreach ($attention_uris as $recipient) {
foreach (array_unique($attention_uris) as $recipient) {
// Is the recipient a local user?
$user = User::staticGet('uri', $recipient);
if ($user) {
@@ -862,12 +880,12 @@ class Ostatus_profile extends Memcached_DataObject
$feeduri = $discover->discoverFromFeedURL($feed_url);
$hints['feedurl'] = $feeduri;
$huburi = $discover->getAtomLink('hub');
$huburi = $discover->getHubLink();
$hints['hub'] = $huburi;
$salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES);
$hints['salmon'] = $salmonuri;
if (!$huburi) {
if (!$huburi && !common_config('feedsub', 'fallback_hub')) {
// We can only deal with folks with a PuSH hub
throw new FeedSubNoHubException();
}
@@ -1263,10 +1281,10 @@ class Ostatus_profile extends Memcached_DataObject
$discover = new FeedDiscovery();
$discover->discoverFromFeedURL($hints['feedurl']);
}
$huburi = $discover->getAtomLink('hub');
$huburi = $discover->getHubLink();
}
if (!$huburi) {
if (!$huburi && !common_config('feedsub', 'fallback_hub')) {
// We can only deal with folks with a PuSH hub
throw new FeedSubNoHubException();
}

View File

@@ -87,6 +87,16 @@ class FeedDiscovery
return ActivityUtils::getLink($this->root, $rel, $type);
}
/**
* Get the referenced PuSH hub link from an Atom feed.
*
* @return mixed string or false
*/
public function getHubLink()
{
return $this->getAtomLink('hub');
}
/**
* @param string $url
* @param bool $htmlOk pass false here if you don't want to follow web pages.

View File

@@ -210,13 +210,13 @@ class MagicEnvelope
}
$data_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'data')->item(0);
$sig_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0);
return array(
'data' => trim($data_element->nodeValue),
'data' => preg_replace('/\s/', '', $data_element->nodeValue),
'data_type' => $data_element->getAttribute('type'),
'encoding' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'encoding')->item(0)->nodeValue,
'alg' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'alg')->item(0)->nodeValue,
'sig' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0)->nodeValue,
'sig' => preg_replace('/\s/', '', $sig_element->nodeValue),
);
}

View File

@@ -47,7 +47,6 @@ class SalmonAction extends Action
$xml = file_get_contents('php://input');
// Check the signature
$salmon = new Salmon;
if (!$salmon->verifyMagicEnv($xml)) {
@@ -58,7 +57,6 @@ class SalmonAction extends Action
$env = $magic_env->parse($xml);
$xml = $magic_env->unfold($env);
}
$dom = DOMDocument::loadXML($xml);
if ($dom->documentElement->namespaceURI != Activity::ATOM ||
@@ -67,7 +65,7 @@ class SalmonAction extends Action
$this->clientError(_m('Salmon post must be an Atom entry.'));
}
$this->act = new Activity($dom->documentElement);
$this->activity = new Activity($dom->documentElement);
return true;
}
@@ -79,9 +77,9 @@ class SalmonAction extends Action
{
StatusNet::setApi(true); // Send smaller error pages
common_log(LOG_DEBUG, "Got a " . $this->act->verb);
common_log(LOG_DEBUG, "Got a " . $this->activity->verb);
if (Event::handle('StartHandleSalmon', array($this->activity))) {
switch ($this->act->verb)
switch ($this->activity->verb)
{
case ActivityVerb::POST:
$this->handlePost();
@@ -164,12 +162,12 @@ class SalmonAction extends Action
*/
function handleUpdateProfile()
{
$oprofile = Ostatus_profile::getActorProfile($this->act);
$oprofile = Ostatus_profile::getActorProfile($this->activity);
if ($oprofile) {
common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri");
$oprofile->updateFromActivityObject($this->act->actor);
$oprofile->updateFromActivityObject($this->activity->actor);
} else {
common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->act->actor->id);
common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->activity->actor->id);
}
}
@@ -178,10 +176,10 @@ class SalmonAction extends Action
*/
function ensureProfile()
{
$actor = $this->act->actor;
$actor = $this->activity->actor;
if (empty($actor->id)) {
common_log(LOG_ERR, "broken actor: " . var_export($actor, true));
common_log(LOG_ERR, "activity with no actor: " . var_export($this->act, true));
common_log(LOG_ERR, "activity with no actor: " . var_export($this->activity, true));
throw new Exception("Received a salmon slap from unidentified actor.");
}
@@ -191,6 +189,6 @@ class SalmonAction extends Action
function saveNotice()
{
$oprofile = $this->ensureProfile();
return $oprofile->processPost($this->act, 'salmon');
return $oprofile->processPost($this->activity, 'salmon');
}
}

View File

@@ -55,7 +55,7 @@ print "Re-running feed discovery for profile URL $oprofile->uri\n";
// @fixme will bork where the URI isn't the profile URL for now
$discover = new FeedDiscovery();
$feedurl = $discover->discoverFromURL($oprofile->uri);
$huburi = $discover->getAtomLink('hub');
$huburi = $discover->getHubLink();
$salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES);
print " Feed URL: $feedurl\n";

View File

@@ -10,7 +10,7 @@ define('STATUSNET', true);
define('LACONICA', true);
require_once INSTALLDIR . '/lib/common.php';
require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php';
require_once INSTALLDIR . '/plugins/OStatus/lib/feeddiscovery.php';
class FeedDiscoveryTest extends PHPUnit_Framework_TestCase
{

View File

@@ -53,6 +53,8 @@ class SitemapAction extends Action
function handle($args)
{
parent::handle($args);
header('Content-Type: text/xml; charset=UTF-8');
$this->startXML();
@@ -67,6 +69,27 @@ class SitemapAction extends Action
$this->endXML();
}
function lastModified()
{
$y = $this->trimmed('year');
$m = $this->trimmed('month');
$d = $this->trimmed('day');
$y += 0;
$m += 0;
$d += 0;
$begdate = strtotime("$y-$m-$d 00:00:00");
$enddate = $begdate + (24 * 60 * 60);
if ($enddate < time()) {
return $enddate;
} else {
return null;
}
}
function showUrl($url, $lastMod=null, $changeFreq=null, $priority=null)
{
$this->elementStart('url');

View File

@@ -0,0 +1,168 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @package SubMirrorPlugin
* @maintainer Brion Vibber <brion@status.net>
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
class SubMirrorPlugin extends Plugin
{
/**
* Hook for RouterInitialized event.
*
* @param Net_URL_Mapper $m path-to-action mapper
* @return boolean hook return
*/
function onRouterInitialized($m)
{
$m->connect('settings/mirror',
array('action' => 'mirrorsettings'));
$m->connect('settings/mirror/add',
array('action' => 'addmirror'));
$m->connect('settings/mirror/edit',
array('action' => 'editmirror'));
return true;
}
/**
* Automatically load the actions and libraries used by the plugin
*
* @param Class $cls the class
*
* @return boolean hook return
*
*/
function onAutoload($cls)
{
$base = dirname(__FILE__);
$lower = strtolower($cls);
$files = array("$base/lib/$lower.php",
"$base/classes/$cls.php");
if (substr($lower, -6) == 'action') {
$files[] = "$base/actions/" . substr($lower, 0, -6) . ".php";
}
foreach ($files as $file) {
if (file_exists($file)) {
include_once $file;
return false;
}
}
return true;
}
function handle($notice)
{
// Is anybody mirroring?
$mirror = new SubMirror();
$mirror->subscribed = $notice->profile_id;
if ($mirror->find()) {
while ($mirror->fetch()) {
$mirror->repeat($notice);
}
}
}
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'SubMirror',
'version' => STATUSNET_VERSION,
'author' => 'Brion Vibber',
'homepage' => 'http://status.net/wiki/Plugin:SubMirror',
'rawdescription' =>
_m('Pull feeds into your timeline!'));
return true;
}
/**
* Menu item for settings
*
* @param Action &$action Action being executed
*
* @return boolean hook return
*/
function onEndAccountSettingsNav(&$action)
{
$action_name = $action->trimmed('action');
$action->menuItem(common_local_url('mirrorsettings'),
// TRANS: SubMirror plugin menu item on user settings page.
_m('MENU', 'Mirroring'),
// TRANS: SubMirror plugin tooltip for user settings menu item.
_m('Configure mirroring of posts from other feeds'),
$action_name === 'mirrorsettings');
return true;
}
function onCheckSchema()
{
$schema = Schema::get();
$schema->ensureTable('submirror', SubMirror::schemaDef());
// @hack until key definition support is merged
SubMirror::fixIndexes($schema);
return true;
}
/**
* Set up queue handlers for outgoing hub pushes
* @param QueueManager $qm
* @return boolean hook return
*/
function onEndInitializeQueueManager(QueueManager $qm)
{
// After each notice save, check if there's any repeat mirrors.
$qm->connect('mirror', 'MirrorQueueHandler');
return true;
}
function onStartEnqueueNotice($notice, &$transports)
{
$transports[] = 'mirror';
}
/**
* Let the OStatus subscription garbage collection know if we're
* making use of a remote feed, so it doesn't get dropped out
* from under us.
*
* @param Ostatus_profile $oprofile
* @param int $count in/out
* @return mixed hook return value
*/
function onOstatus_profileSubscriberCount($oprofile, &$count)
{
if ($oprofile->profile_id) {
$mirror = new SubMirror();
$mirror->subscribed = $oprofile->profile_id;
if ($mirror->find()) {
while ($mirror->fetch()) {
$count++;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,77 @@
<?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/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Takes parameters:
*
* - feed: a profile ID
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class AddMirrorAction extends BaseMirrorAction
{
var $feedurl;
/**
* Check pre-requisites and instantiate attributes
*
* @param Array $args array of arguments (URL, GET, POST)
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
$this->feedurl = $this->validateFeedUrl($this->trimmed('feedurl'));
$this->profile = $this->profileForFeed($this->feedurl);
return true;
}
function saveMirror()
{
if ($this->oprofile->subscribe()) {
SubMirror::saveMirror($this->user, $this->profile);
} else {
$this->serverError(_m("Could not subscribe to feed."));
}
}
}

View File

@@ -0,0 +1,169 @@
<?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/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Takes parameters:
*
* - feed: a profile ID
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
abstract class BaseMirrorAction extends Action
{
var $user;
var $profile;
/**
* Check pre-requisites and instantiate attributes
*
* @param Array $args array of arguments (URL, GET, POST)
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
return $this->sharedBoilerplate();
}
protected function validateFeedUrl($url)
{
if (common_valid_http_url($url)) {
return $url;
} else {
$this->clientError(_m("Invalid feed URL."));
}
}
protected function validateProfile($id)
{
$id = intval($id);
$profile = Profile::staticGet('id', $id);
if ($profile && $profile->id != $this->user->id) {
return $profile;
}
// TRANS: Error message returned to user when setting up feed mirroring, but we were unable to resolve the given URL to a working feed.
$this->clientError(_m("Invalid profile for mirroring."));
}
/**
*
* @param string $url
* @return Profile
*/
protected function profileForFeed($url)
{
$oprofile = Ostatus_profile::ensureProfileURL($url);
if ($oprofile->isGroup()) {
$this->clientError(_m("Can't mirror a StatusNet group at this time."));
}
$this->oprofile = $oprofile; // @fixme ugly side effect :D
return $oprofile->localProfile();
}
/**
* @fixme none of this belongs in end classes
* this stuff belongs in shared code!
*/
function sharedBoilerplate()
{
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
$this->clientError(_('This action only accepts POST requests.'));
return false;
}
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
$this->clientError(_('There was a problem with your session token.'.
' Try again, please.'));
return false;
}
// Only for logged-in users
$this->user = common_current_user();
if (empty($this->user)) {
$this->clientError(_('Not logged in.'));
return false;
}
return true;
}
/**
* Handle request
*
* Does the subscription and returns results.
*
* @param Array $args unused.
*
* @return void
*/
function handle($args)
{
// Throws exception on error
$this->saveMirror();
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
$this->element('title', null, _('Subscribed'));
$this->elementEnd('head');
$this->elementStart('body');
$unsubscribe = new EditMirrorForm($this, $this->profile);
$unsubscribe->show();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$url = common_local_url('mirrorsettings');
common_redirect($url, 303);
}
}
abstract function saveMirror();
}

View File

@@ -0,0 +1,112 @@
<?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/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Takes parameters:
*
* - feed: a profile ID
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class EditMirrorAction extends BaseMirrorAction
{
/**
* Check pre-requisites and instantiate attributes
*
* @param Array $args array of arguments (URL, GET, POST)
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
$this->profile = $this->validateProfile($this->trimmed('profile'));
$this->mirror = SubMirror::pkeyGet(array('subscriber' => $this->user->id,
'subscribed' => $this->profile->id));
if (!$this->mirror) {
$this->clientError(_m("Requested invalid profile to edit."));
}
$this->style = $this->validateStyle($this->trimmed('style'));
// DO NOT change to $this->boolean(), it will be wrong.
// We're checking for the presence of the setting, not its value.
$this->delete = (bool)$this->arg('delete');
return true;
}
protected function validateStyle($style)
{
$allowed = array('repeat', 'copy');
if (in_array($style, $allowed)) {
return $style;
} else {
$this->clientError(_m("Bad form data."));
}
}
function saveMirror()
{
$mirror = SubMirror::getMirror($this->user, $this->profile);
if (!$mirror) {
$this->clientError(_m('Requested edit of missing mirror'));
}
if ($this->delete) {
$mirror->delete();
$oprofile = Ostatus_profile::staticGet('profile_id', $this->profile->id);
if ($oprofile) {
$oprofile->garbageCollect();
}
} else if ($this->style != $mirror->style) {
$orig = clone($mirror);
$mirror->style = $this->style;
$mirror->modified = common_sql_now();
$mirror->update($orig);
}
}
}

View File

@@ -0,0 +1,106 @@
<?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 Plugins
* @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/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
class MirrorSettingsAction extends AccountSettingsAction
{
/**
* Title of the page
*
* @return string Page title
*/
function title()
{
return _m('Feed mirror settings');
}
/**
* Instructions for use
*
* @return string Instructions for use
*/
function getInstructions()
{
return _m('You can mirror updates from many RSS and Atom feeds ' .
'into your StatusNet timeline!');
}
/**
* Show the form for OpenID management
*
* We have one form with a few different submit buttons to do different things.
*
* @return void
*/
function showContent()
{
$user = common_current_user();
$this->showAddFeedForm();
$mirror = new SubMirror();
$mirror->subscriber = $user->id;
if ($mirror->find()) {
while ($mirror->fetch()) {
$this->showFeedForm($mirror);
}
}
}
function showFeedForm($mirror)
{
$profile = Profile::staticGet('id', $mirror->subscribed);
if ($profile) {
$form = new EditMirrorForm($this, $profile);
$form->show();
}
}
function showAddFeedForm()
{
$form = new AddMirrorForm($this);
$form->show();
}
/**
* Handle a POST request
*
* Muxes to different sub-functions based on which button was pushed
*
* @return void
*/
function handlePost()
{
}
}

View File

@@ -0,0 +1,229 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @package SubMirrorPlugin
* @maintainer Brion Vibber <brion@status.net>
*/
class SubMirror extends Memcached_DataObject
{
public $__table = 'submirror';
public $subscriber;
public $subscribed;
public $style;
public $created;
public $modified;
public /*static*/ function staticGet($k, $v=null)
{
return parent::staticGet(__CLASS__, $k, $v);
}
/**
* return table definition for DB_DataObject
*
* DB_DataObject needs to know something about the table to manipulate
* instances. This method provides all the DB_DataObject needs to know.
*
* @return array array of column definitions
*/
function table()
{
return array('subscriber' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'subscribed' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'style' => DB_DATAOBJECT_STR,
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
}
static function schemaDef()
{
// @fixme need a reverse key on (subscribed, subscriber) as well
return array(new ColumnDef('subscriber', 'integer',
null, false, 'PRI'),
new ColumnDef('subscribed', 'integer',
null, false, 'PRI'),
new ColumnDef('style', 'varchar',
16, true),
new ColumnDef('created', 'datetime',
null, false),
new ColumnDef('modified', 'datetime',
null, false));
}
/**
* Temporary hack to set up the compound index, since we can't do
* it yet through regular Schema interface. (Coming for 1.0...)
*
* @param Schema $schema
* @return void
*/
static function fixIndexes($schema)
{
try {
$schema->createIndex('submirror', array('subscribed', 'subscriber'));
} catch (Exception $e) {
common_log(LOG_ERR, __METHOD__ . ': ' . $e->getMessage());
}
}
/**
* return key definitions for DB_DataObject
*
* DB_DataObject needs to know about keys that the table has; this function
* defines them.
*
* @return array key definitions
*/
function keys()
{
return array_keys($this->keyTypes());
}
/**
* return key definitions for Memcached_DataObject
*
* Our caching system uses the same key definitions, but uses a different
* method to get them.
*
* @return array key definitions
*/
function keyTypes()
{
// @fixme keys
// need a sane key for reverse lookup too
return array('subscriber' => 'K', 'subscribed' => 'K');
}
function sequenceKey()
{
return array(false, false, false);
}
/**
* @param Profile $subscribed
* @param Profile $subscribed
* @return SubMirror
* @throws ServerException
*/
public static function saveMirror($subscriber, $subscribed, $style='repeat')
{
// @fixme make sure they're subscribed!
$mirror = new SubMirror();
$mirror->subscriber = $subscriber->id;
$mirror->subscribed = $subscribed->id;
$mirror->style = $style;
$mirror->created = common_sql_now();
$mirror->modified = common_sql_now();
$mirror->insert();
return $mirror;
}
/**
* @param Notice $notice
* @return mixed Notice on successful mirroring, boolean if not
*/
public function mirrorNotice($notice)
{
$profile = Profile::staticGet('id', $this->subscriber);
if (!$profile) {
common_log(LOG_ERROR, "SubMirror plugin skipping auto-repeat of notice $notice->id for missing user $profile->id");
return false;
}
if ($this->style == 'copy') {
return $this->copyNotice($profile, $notice);
} else { // default to repeat mode
return $this->repeatNotice($profile, $notice);
}
}
/**
* Mirror a notice using StatusNet's repeat functionality.
* This retains attribution within the site, and other nice things,
* but currently ends up looking like 'RT @foobar bla bla' when
* bridged out over OStatus or TwitterBridge.
*
* @param Notice $notice
* @return mixed Notice on successful repeat, true if already repeated, false on failure
*/
protected function repeatNotice($profile, $notice)
{
if($profile->hasRepeated($notice->id)) {
common_log(LOG_INFO, "SubMirror plugin skipping auto-repeat of notice $notice->id for user $profile->id; already repeated.");
return true;
} else {
common_log(LOG_INFO, "SubMirror plugin auto-repeating notice $notice->id for $profile->id");
return $notice->repeat($profile->id, 'mirror');
}
}
/**
* Mirror a notice by emitting a new notice with the same contents.
* Kind of dirty, but if pulling an external data feed into an account
* that may be what you want.
*
* @param Notice $notice
* @return mixed Notice on successful repeat, true if already repeated, false on failure
*/
protected function copyNotice($profile, $notice)
{
$options = array('is_local' => Notice::LOCAL_PUBLIC,
'url' => $notice->bestUrl(), // pass through the foreign link...
'rendered' => $notice->rendered);
$saved = Notice::saveNew($profile->id,
$notice->content,
'feed',
$options);
return $saved;
}
public /*static*/ function pkeyGet($v)
{
return parent::pkeyGet(__CLASS__, $v);
}
/**
* Get the mirroring setting for a pair of profiles, if existing.
*
* @param Profile $subscriber
* @param Profile $subscribed
* @return mixed Profile or empty
*/
public static function getMirror($subscriber, $subscribed)
{
return self::pkeyGet(array('subscriber' => $subscriber->id,
'subscribed' => $subscribed->id));
}
}

View File

@@ -0,0 +1,141 @@
<?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/>.
*
* @package StatusNet
* @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/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
class AddMirrorForm extends Form
{
/**
* Name of the form
*
* Sub-classes should overload this with the name of their form.
*
* @return void
*/
function formLegend()
{
}
/**
* Visible or invisible data elements
*
* Display the form fields that make up the data of the form.
* Sub-classes should overload this to show their data.
*
* @return void
*/
function formData()
{
$this->out->elementStart('fieldset');
$this->out->elementStart('ul');
$this->li();
$this->doInput('addmirror-feedurl',
'feedurl',
_m('Web page or feed URL:'),
$this->out->trimmed('feedurl'));
$this->unli();
$this->li();
$this->out->submit('addmirror-save', _m('Add feed'));
$this->unli();
$this->out->elementEnd('ul');
$this->out->elementEnd('fieldset');
}
private function doInput($id, $name, $label, $value=null, $instructions=null)
{
$this->out->element('label', array('for' => $id), $label);
$attrs = array('name' => $name,
'type' => 'text',
'id' => $id,
'style' => 'width: 80%');
if ($value) {
$attrs['value'] = $value;
}
$this->out->element('input', $attrs);
if ($instructions) {
$this->out->element('p', 'form_guide', $instructions);
}
}
/**
* Buttons for form actions
*
* Submit and cancel buttons (or whatever)
* Sub-classes should overload this to show their own buttons.
*
* @return void
*/
function formActions()
{
}
/**
* ID of the form
*
* Should be unique on the page. Sub-classes should overload this
* to show their own IDs.
*
* @return string ID of the form
*/
function id()
{
return 'add-mirror-form';
}
/**
* Action of the form.
*
* URL to post to. Should be overloaded by subclasses to give
* somewhere to post to.
*
* @return string URL to post to
*/
function action()
{
return common_local_url('addmirror');
}
/**
* Class of the form.
*
* @return string the form's class
*/
function formClass()
{
return 'form_settings';
}
}

View File

@@ -0,0 +1,189 @@
<?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/>.
*
* @package StatusNet
* @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/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
class EditMirrorForm extends Form
{
function __construct($action, $profile)
{
parent::__construct($action);
$this->profile = clone($profile);
$this->user = common_current_user();
$this->mirror = SubMirror::pkeyGet(array('subscriber' => $this->user->id,
'subscribed' => $this->profile->id));
}
/**
* Name of the form
*
* Sub-classes should overload this with the name of their form.
*
* @return void
*/
function formLegend()
{
}
/**
* Visible or invisible data elements
*
* Display the form fields that make up the data of the form.
* Sub-classes should overload this to show their data.
*
* @return void
*/
function formData()
{
$this->out->elementStart('fieldset');
$this->out->hidden('profile', $this->profile->id);
$this->out->elementStart('div', array('style' => 'float: left; width: 80px;'));
$img = $this->getAvatar($this->profile);
$feed = $this->getFeed($this->profile);
$this->out->elementStart('a', array('href' => $this->profile->profileurl));
$this->out->element('img', array('src' => $img, 'style' => 'float: left'));
$this->out->elementEnd('a');
$this->out->elementEnd('div');
$this->out->elementStart('div', array('style' => 'margin-left: 80px; margin-right: 20px'));
$this->out->elementStart('p');
$this->out->elementStart('div');
$this->out->element('a', array('href' => $this->profile->profileurl), $this->profile->getBestName());
$this->out->elementEnd('div');
$this->out->elementStart('div');
if ($feed) {
$this->out->text(_m('LABEL', 'Remote feed:') . ' ');
//$this->out->element('a', array('href' => $feed), $feed);
$this->out->element('input', array('value' => $feed, 'readonly' => 'readonly', 'style' => 'width: 100%'));
} else {
$this->out->text(_m('LABEL', 'Local user'));
}
$this->out->elementEnd('div');
$this->out->elementEnd('p');
$this->out->elementStart('fieldset', array('style' => 'margin-top: 20px'));
$this->out->element('legend', false, _m("Mirroring style"));
$styles = array('repeat' => _m("Repeat: reference the original user's post (sometimes shows as 'RT @blah')"),
'copy' => _m("Repost the content under my account"));
foreach ($styles as $key => $label) {
$this->out->elementStart('div');
$attribs = array('type' => 'radio',
'value' => $key,
'name' => 'style',
'id' => $this->id() . '-style');
if ($key == $this->mirror->style || ($key == 'repeat' && empty($this->mirror->style))) {
$attribs['checked'] = 'checked';
}
$this->out->element('input', $attribs);
$this->out->element('span', false, $label); // @fixme should be label, but the styles muck it up for now
$this->out->elementEnd('div');
}
$this->out->elementEnd('fieldset');
$this->out->elementStart('div');
$this->out->submit($this->id() . '-save', _m('Save'));
$this->out->element('input', array('type' => 'submit',
'value' => _m('Stop mirroring'),
'name' => 'delete',
'class' => 'submit'));
$this->out->elementEnd('div');
$this->out->elementEnd('div');
$this->out->elementEnd('fieldset');
}
private function getAvatar($profile)
{
$avatar = $this->profile->getAvatar(48);
if ($avatar) {
return $avatar->displayUrl();
} else {
return Avatar::defaultImage(48);
}
}
private function getFeed($profile)
{
// Ok this is a bit of a hack. ;)
if (class_exists('Ostatus_profile')) {
$oprofile = Ostatus_profile::staticGet('profile_id', $profile->id);
if ($oprofile) {
return $oprofile->feeduri;
}
}
var_dump('wtf');
return false;
}
/**
* ID of the form
*
* Should be unique on the page. Sub-classes should overload this
* to show their own IDs.
*
* @return string ID of the form
*/
function id()
{
return 'edit-mirror-form-' . $this->profile->id;
}
/**
* Action of the form.
*
* URL to post to. Should be overloaded by subclasses to give
* somewhere to post to.
*
* @return string URL to post to
*/
function action()
{
return common_local_url('editmirror');
}
/**
* Class of the form.
*
* @return string the form's class
*/
function formClass()
{
return 'form_settings';
}
}

View File

@@ -0,0 +1,45 @@
<?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/>.
*/
/**
* Check for subscription mirroring options on each newly seen post!
*
* @package SubMirror
* @author Brion Vibber <brion@status.net>
*/
class MirrorQueueHandler extends QueueHandler
{
function transport()
{
return 'mirror';
}
function handle($notice)
{
$mirror = new SubMirror();
$mirror->subscribed = $notice->profile_id;
if ($mirror->find()) {
while ($mirror->fetch()) {
$mirror->mirrorNotice($notice);
}
}
return true;
}
}