selbstgehostetes twitter-RSS mit der twitter API 1.1

felix schwenzel, , in wirres.net    

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.