reclaim social media

felix schwenzel,    

ich werde in den nächsten zwei tagen wahrscheinlich nicht dazu kommen, mehr über das reclaim-social-media projekt von sascha und mir zu schreiben. ein paar absätze und ein paar downloads habe ich heute mittag zusammengeschrieben. leider ist die einrichtung noch nicht ganz trivial. dazu dann mehr nach der republica.

* * *

links:

heldinnen meiner fernseh-kindheit im dschungel

felix schwenzel,    

kürzlich bin ix im netz darüber gestolpert, dass inger nilsson, die in den 60er jahren pipi langstrumpf spielte, 2009 in der schwedischen version von „Ich bin ein Star – Holt mich hier raus!“ mitgespielt hat („Kändisdjungeln“).

dann eben stefanie powers in der glaserei mit einer katze gesehen und kurz darauf gesehen, dass stefanie powers 2011 in der britischen version des australischen dschungels eine folge lang mitgespielt hat.

ich bin sicher mit einem nachmittag recherche fände ich mindestens 20 fernsehhelden der 80er, die zwischenzeitlich in den dschungel gegangen sind.

quote.fm-RSS mit den empfehlungen aller gefolgten

felix schwenzel,    

seitdem quote.fm RSS-feeds einzelner nutzer anbietet (hier die ode von martin weigert auf die quote.fm-RSS-feeds) habe ich ein paar quote.fm-nutzer in meinem RSS-dings (früher google reader, jetzt auf meiner eigenen fever installation) aboniert. und lieben gerlernt. das liegt zum einen natürlich an den jeweiligen nutzern. abonniert hatte ich bisher:

aber ich habe mich schon immer gefragt, warum bietet mir quote.fm nicht alle leute denen ich auf quote.fm folge als RSS-feed an? warum sollte ich alle 140 leute denen ich folge einzeln abonnieren? kürzlich fiel mir dann beim duschen ein, dass das ja nicht so schwer sein könnte sowas zu scripten.

die quote.fm-API abfragen nach denen denen ich folge (quote.fm/api/user/listFollowings?username=ix), aus dieser liste feed-URLs konstruieren und diese feed-liste SimplePie zum frass vorwerfen und neu rausschreiben. hier ist das ergebnis:

* * *

eben via martin weigert gelesen, dass quote.fm das geld ausgegangen ist und quote.fm an eine agentur vertickt haben:

That means: Philipp, Marcel, Flo and I are saying our good-byes and sometime in the (quite near) future elbdudler will take over and hopefully build this thing here into something that you guys will appreciate.

hoffentlich wird das nicht so schlimm wie es sich anhört, aber noch funktioniert dieses quote.fm-dings ja noch.

* * *

das script hat als abhängigkeit lediglich SimplePie (download) und einen cache-ordner auf der gleichen ebene in der das script liegt:

das script ist nicht elegant gescriptet, funktioniert aber. fehler schliesse ich wie immer ausdrücklich nicht aus.

<?php
$input = '
<form method="get" name="schick it ab" action="http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME'].'">
	<fieldset>
		<label for="1">quote.fm benutzername</label>
		<input id ="1" type="text" name="user" />
		<input type="submit" name="senden" />
	</fieldset>
</form>';
	$user_id = $_GET["user"];
	if ($user_id == "") {	
	$user_id = "ix"; 
	echo $input;
	exit;
	}
	$count = 200;
	$apiurl= 'https://quote.fm/api/user/listFollowings?username='.$user_id;
    $timeout = 15;

	$feeds = array();
	$i=0;
while ( $i <= 20 ) // mehr als 20 seiten? eher nicht.
	{
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $apiurl.'&pageSize=100&page='.$i);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $timeout );
   	curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
	$output = curl_exec($ch);
	curl_close($ch);
	$rawData = trim($output);

	$response = json_decode($rawData, true);
//
//	echo $response;
	if ( ($response['code'] == "404") AND ($i == 0) ) { 

	echo "<p>fehler. <i>".$user_id."</i> kein gültiger quote.fm benutzername.</p>"; 
	echo $input;
?>	

<?php
	exit; }	

	if ( ($response['totalCount'] - (($i+1) * 100)) <= 0 ) { $i=20; }
	foreach($response['entities'] as $entry){
		$feeds[] = 'http://quote.fm/'.$entry['username'].'/feed';
		//echo $entry['username'];
	}
	$i++;
}

//print_r($feeds);

// Include the SimplePie library
// For 1.3+:
require_once('SimplePie/autoloader.php');

// Create a new SimplePie object
$feed = new SimplePie();

// Instead of only passing in one feed url, we'll pass in an array of three
$feed->set_feed_url($feeds);

// We'll use favicon caching here (Optional)
//$feed->set_favicon_handler('handler_image.php');

$feed->set_cache_duration ( 3600 );
$feed->set_timeout ( 10 );
$feed->set_stupidly_fast( true );
//$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
$feed->force_feed(true);


// Initialize the feed object
$feed->init();

// This will work if all of the feeds accept the same settings.
$feed->handle_content_type();

// Begin our XHTML markup
header("Content-Type: application/rss+xml");
echo '<?xml version="1.0" encoding="UTF-8"?>';
?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">

<channel>
<title>Recent recommendations of all people <?php echo $user_id; ?> is following</title>
<link>https://quote.fm/<?php echo $user_id; ?></link>
<description>Recent recommendations of all people <?php echo $user_id; ?> is following</description>
<lastBuildDate><?php echo date(DATE_ATOM , time()); ?></lastBuildDate>
<language>en</language>
<atom:link href="<?php echo 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME'].'?user='.$user_id; ?>" rel="self" type="application/rss+xml" />
<generator>http://quote.fm</generator>

<?php if ($feed->error): ?>
<?php //echo $feed->error; ?>
<?php print_r( $feed->error); ?>
<?php endif; ?>

<?php $j = 0; ?>
<?php foreach ($feed->get_items() as $item): ?>

<item>
<title><![CDATA[<?php echo $item->get_title(); ?>]]></title>
<link><?php echo $item->get_permalink(); ?></link>
<pubDate><?php echo $item->get_date('D, j M Y H:i:s O'); ?></pubDate>
<dc:creator><?php $author = $item->get_author(0); if ($author != "") {echo $author->get_name(); } ?></dc:creator>
<category><![CDATA[<?php $category = $item->get_category(0); if ($category != "") { echo $category->get_label();} ?>]]></category>
<guid isPermaLink="true"><?php echo $item->get_permalink(); ?></guid>
<description><![CDATA[<p><?php $feed = $item->get_feed(); echo $feed->get_title(); ?></p>
<?php echo $item->get_content(); ?>]]></description>

<content:encoded><![CDATA[<p><?php echo $feed->get_title(); ?></p>
<?php echo $item->get_content(); ?>]]></content:encoded>
</item>
<?php $j++; if ($j >= $count) { break; }?>
<?php endforeach; ?>
</channel>
</rss>

bulgurpilaw nach ottolenghi

felix schwenzel,    

bulgurpilaw nach ottolenghi

eigentlich sollen es 3 kleine zwiebeln sein, ich habe aber 3 mittelgrosse zwiebeln in halbe ringe geschnitten und zusammen mit (leider nur) 2 kleinen in ringe geschnittenen spitzpaprika in 90 millilitern olivenöl 10 minuten an- und weichgebraten.

danach habe ich 2 esslöffel tomatenmark, 2 EL koriandersamen, ein teelöffel sechuan pfeffer (ottolenghi schlägt rosa pfeffer vor), etwas zucker, salz und pfeffer und 100 gramm korinthen nochmal 2 minuten mitgebraten.

400 gramm mittelgroben bulgur habe ich wie beim risotto auch nochmal ein bisschen glasiggebraten und dann mit einem halben liter wasser abgelöscht und aufgekocht. das ganze dann 20 minuten ohne hitze quellen lassen, petersilie (statt schnittlauch), fertig. mit einem klecks jogurt schmeckts besser als ohne.

selbstgehostetes twitter-RSS mit der twitter API 1.1

felix schwenzel,    

vor ein paar tagen veröffentlichte gabe weatherhead einen artikel mit dem titel „TweetFeeder Script: From Twitter to RSS“. gabe weatherhead hat sich ein script gebastelt das ein paar twitter accounts sporadisch nach links die keine bilder sind überprüft und diese links dann in eines seiner pinboard-accounts schreibt. der artikel hätte also genauer „From Twitter to Pinboard“ heissen müssen.

gestern bookmarkte sascha lobo den (neuen?) dienst twitter-rss.com, der genau das tut was er im domainnamen ankündigt: er macht beliebige twitter-konten per RSS abonnierbar.

in den letzten monaten habe ich mich auch immer wieder mit dem thmea twitter und RSS beschäftigt, was daran liegt, dass twitter sich zu meinem bedauern mehr und mehr einigelt und abschottet. im september wurden die möglichkeiten per ifttt daten aus twitter rauszuholen von twitter empfindlich eingeschränkt und seit diesem frühjahr arbeitet twitter daran die relativ offene API der version 1.0 mit der version 1.1 zu ersetzen, die für jede kleinigkeit authentifizierung benötigt und die eh noch versteckt vorhandenen twitter-RSS-feeds beseitigt.

RSS ist und bleibt mein favorisierter weg daten zu verarbeiten, sei es meine rückseite zu bestücken, meine monatlichen twitter-favoriten-listen automatisch zu erzeugen oder per RSS-reader den überblick zu behalten. deshalb habe ich mir meinen eigenen php-basierten twitter-zu-RSS-übersetzer, bzw. proxy gebaut.

ein eigener, selbstgescripteter und selbstgehosteter übersetzer hat ein paar vorteile gegenüber lösungen wie twitter-rss.com:

  • ich mache mich nicht von einem weiteren drittanbieter abhängig
  • ich kann bestimmen wie die ausgabe aussieht oder formatiert ist
  • ich kann alles selbst steuern und erweitern

der beste teil ist natürlich: wenn viele dieses oder andere scripte nutzen um twitter-ströme auszulesen, kann twitter nicht einfach die schotten dicht machen, wie bei ifttt. ifttt ein paar berechtigungen zu entziehen ist einfach, den selbstgehosteten scripten oder webapps von hunderten oder tausenden nutzern rechte zu entziehen ist schon sehr viel schwerer.

* * *

mit meinem script lese ich den twitter-strom meines eigenen accounts aus. technisch passiert nichts aufregendes:

  • ich authentifiziere mich mit meiner eigens definierten twitter-app, bzw. deren schlüsseln
  • twitter liefert mir json-codiert die letzten 20 meiner tweets, exklusive retweets
  • aus der json-antwort baue ich mir meinen RSS-feed zusammen und gebe ihn aus

wenn ich die anfrage etwas anpasse kann ich auf diese art und weise beispielsweise auch meine twitter-favoriten auslesen und als RSS ausgeben:

// favoriten
	$apiurl = 'http://api.twitter.com/1.1/favorites/list.json';
// timeline
	$apiurl = 'http://api.twitter.com/1.1/statuses/user_timeline.json';

mit einer twitter-such-anfrage sollte das ähnlich klappen, das habe ich aber noch nicht ausprobiert.

als grössten vorteil sehe ich, dass ich die RSS-ausgabe selbst steuern und formatieren kann. das script wandelt bespielsweise die verkackten t.co-links die die twitter-api zurückliefert in klartext-adressen um, hashtags in suchlinks und twitter-namen in profillinks. der titel eines RSS-items liefert den rohen volltext eines tweets, die description des RSS-items liefert hingegen das offizielle embed-format eines tweets zurück, also nach diesem schema:

rss ausgabe

das ausgabeformat lässt sich bei bedarf natürlich leicht anpassen. in einem tweet eingebettete bilder bette ich per HTML in die tweet-description ein. das ist nicht besonders schön, aber effektiv (demo):

rss ausgabe mit eingebettetem bild

* * *

für die twitter-kommunikation und die RSS-generierung nutze ich zwei php-klassen. einerseits die OAuth 1.0A library von @themattharris und den Universal Feed Generator von anis uddin ahmad. das ganze paket lässt sich hier runterladen, hier eine demoausgabe meiner tweets.

<?php
/*
// generiert einen rss-feed eines nutzers ohne seine (nativen) retweets
//
*/
	header('Content-Type: text/html; charset=utf-8');
	require 'tmhOAuth/tmhOAuth.php';
	require 'tmhOAuth/tmhUtilities.php';
	include('feed_generator/FeedWriter.php'); // include the feed generator feedwriter file

// app: hier wirres.net
// eigene app keys gibts hier: https://dev.twitter.com/apps/new
	$tmhOAuth = new tmhOAuth(array(
	  'consumer_key'    => 'xxx',
	  'consumer_secret' => 'xxx',
	  'user_token'      => 'xxx',
	  'user_secret'     => 'xxx',
	));

	$lang = 'de';
	$count = 20;
	$user = 'diplix';
	$user_name = 'Felix Schwenzel';
	$userid = ''; //?
//	$apiurl = 'http://api.twitter.com/1.1/favorites/list.json';
	$apiurl = 'http://api.twitter.com/1.1/statuses/user_timeline.json';
	$embedcode = true;

	$tmhOAuth->request(
    'GET',
    $apiurl,
		array(
			'lang'				=> $lang,
			'count'				=> $count,
			'screen_name'		=> $user,
			'include_rts' 		=> 'false',
			'include_entities' 	=> 'true'
		),
	  	true
  	);

//echo $tmhOAuth->response['response'];

if ($tmhOAuth->response['code'] == 200) {
	$data = json_decode($tmhOAuth->response['response'],1);
	$feed = new FeedWriter(RSS2);

//	$feed->setTitle('@'.$user.'s Twitter Favs'); // set your title
	$feed->setTitle('@'.$user.'s Twitter Timeline'); // set your title
	$feed->setLink('http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME']); // set the url to the feed page you're generating

//	$feed->setChannelElement('updated', date(DATE_ATOM , time()));
//	$feed->setChannelElement('author', $user_name); // set the author name
	$feed->setChannelElement('description', $user_name.'s tweets'); // set the description

// iterate through the response to add items to the feed
	if ( is_array( $data ) ) {
		foreach($data as $entry){
			$post_date_gmt = strtotime( $entry['created_at'] );
			$post_date_gmt = gmdate( 'Y-m-d H:i:s', $post_date_gmt );
			$post_date     = gmdate( 'd.m.Y H:i', strtotime( $entry['created_at'] ));
			$tweetlink = 'http://twitter.com/'.$user.'/status/'.$entry['id_str'];

			$post_content = $entry['text'];
//			$post_content = ( html_entity_decode( trim( $post_content ) ) );
			$post_content = ( html_entity_decode( $post_content ) ); // ohne trim?

//links einsetzen/auflösen
			if ( count( $entry['entities']['urls'] ) ) {
				foreach ( $entry['entities']['urls'] as $url ) {
					$post_content = str_replace( $url['url'], '<a href="'.$url['expanded_url'].'">'.$url['display_url'].'</a>', $post_content );
				}
			}

// bild attached?			
			$image_url = '';
			$image_html = '';
			if ( count( $entry['entities']['media'] ) ) {
				foreach ( $entry['entities']['media'] as $media ) {
					$post_content = str_replace( $media['url'], '<a href="'.$media['expanded_url'].'">'.$media['display_url'].'</a>', $post_content );
					if ($media['type']=='photo') {
						$image_url = $media['media_url'];
						$image_html = '<div class="twitter-image"><a href="'.$media['expanded_url'].'"><img src="'.$image_url.'" alt=""></a></div>';
					}
				}
			}

		    $post_content = preg_replace( '/\s((http|ftp)+(s)?:\/\/[^<>\s]+)/i', ' <a href=\"\\0\" target=\"_blank\">\\0</a>',$post_content);
		    $post_content = preg_replace('/[@]+([A-Za-z0-9-_]+)/', '<a href="http://twitter.com/\\1" target="_blank">\\0</a>', $post_content );  
		    $post_content = preg_replace('/[#]+([A-Za-z0-9-_]+)/', '<a href="http://twitter.com/search?q=%23\\1" target="_blank">\\0</a>', $post_content );  
			$post_content_original = $post_content;

// embedcode konstruieren
			if ($embedcode) {
				$post_content = '<blockquote class="twitter-tweet imported"><p>'.$post_content.'</p>'.$image_html.'— '.$entry['user']['name'].' (<a href="https://twitter.com/'.$entry['user']['screen_name'].'/">@'.$entry['user']['screen_name'].'</a>) <a href="'.$tweetlink.'">'.$post_date.'</a></blockquote><script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>';
			}

// tags auflisten
			$tags = array();
			if ( preg_match_all( '/(^|[(\[\s])#(\w+)/', $entry['text'], $tag ) ) {
				$tags = $tag[2];
				echo $tags;
			}

			$in_reply_to_user_id     = !empty( $entry['in_reply_to_user_id_str'] ) ? $entry['in_reply_to_user_id_str'] : '';
			$in_reply_to_screen_name = !empty( $entry['in_reply_to_screen_name'] ) ? $entry['in_reply_to_screen_name'] : '';
			$in_reply_to_status_id   = !empty( $entry['in_reply_to_status_id_str'] ) ? $entry['in_reply_to_status_id_str'] : '';

			$item = $feed->createNewItem();

//            $item->setTitle($post_date);
            $item->setTitle(strip_tags($post_content_original));
            $item->setDate($entry['created_at']);
            $item->setLink($tweetlink);
            $item->setDescription($post_content);
			$item->addElement('guid', $tweetlink, array('isPermaLink'=>'true'));

			//metagedoens
//  			$item->addElement('in_reply_to_user_id', $in_reply_to_user_id);
//  			$item->addElement('in_reply_to_screen_name', $in_reply_to_screen_name);
//  			$item->addElement('in_reply_to_status_id', $in_reply_to_status_id);

// hashtags zu kategorien
//	  			$item->addElement('category', $tags);
				foreach ( $tags as $tag ) {
		  			$item->addElement('category', $tag);
				}

            $feed->addItem($item);
        }

// that's it... don't echo anything else, just call this method
	$feed->genarateFeed();
  	} else {
//    return NULL;
  }
}

* * *

das alles ist natürlich lange nicht perfekt. der universal feed generator generiert derzeit beispielsweise keine mehrfachen RSS-kategorien, so dass bei mehreren hashtags eines tweets immer nur der letzte als RSS-kategorie im RSS-feed landet. ich hoffe das dieser feed-generator das künftig besser erledigt. caching wäre irgendwann auch keine schlechte idee, um die twitter API nicht über gebühr zu strapazieren. eine zentrale konfiguration und eine flexibilisierung, so dass ich mir alle möglichen feeds mit dem script erzeugen kann, nicht nur twitter-favoriten und die eigenen tweets.