RSS2Queet

Das letzte, was mir für den Abschluss meines Umzugs¹ vom Zwitscherchen zu Quitter (eine GNUsocial-Instanz) noch fehlte, war ein Ersatz für das bequeme automatische Wegzwitschern aktualisierter Blogbeiträge.

Zum Glück ist die API relativ einfach nutzbar, solange man kein OAuth benötigt. Es ist zum Beispiel möglich, mit einem Shellskript einen Queet abzusetzen – wobei ich hier natürlich die Zugangsdaten unkenntlich gemacht habe:

#!/bin/sh
api='https://quitter.se/api/statuses/update.xml'
username='xxxxxxxx' # Quitter-Benutzername hierher
password='xxxxxxxx' # Quitter-Passwort hierher
curl -u "$username:$password" "$api" -d status="$*" >/dev/null

Für eine wirklich komfortable Lösung des Problems brauchte ich freilich etwas mehr Aufwand.

  1. Eine beliebige Menge RSS-Feeds soll überwacht werden
  2. Alle Artikel, die seit dem letzten Abholen des Feeds hinzugekommen sind, sollen in Queets verwandelt werden.
  3. Der Queet soll den Titel des Postings und einen möglichst kurzen Link enthalten. (Auf URL-Kürzer habe ich vorerst verzichtet.)
  4. Es soll mit Leichtigkeit möglich sein, den Aufruf in die crontab zu schreiben.

Das Ergebnis ist folgendes, verblüffend kompliziert aussehendes Python-Skript, das aber nur so kompliziert aussieht, weil ich es ganz schnell geschrieben habe. Passend zum Quick-and-Dirty-Stil habe ich auf jede halbwegs verantwortungsvolle Fehlerbehandlung verzichtet…

#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################
#
# rss2queet.py
# $Id: rss2queet.py,v 1.9 2014/09/16 22:32:54 elias Exp $
#
# Das muss ich leider in Python 2 coden, weil ich auf dem Serverchen
# kein anderes Python habe. (Ich halte die Installation dort so klein
# wie möglich).
#
# Eine Portierung nach Python 3 ist relativ aufwändig.
#
# Und ja, ich weiß, wie dieser Code saugt. Ich habe ihn sehr schnell
# geschrieben, weil ich ihn haben wollte, um schneller von Twitter
# wegzukommen. Er gewinnt keine Ästhetikpreise und hat mich trotz des
# allgegenwärtigen Quick-and-Dirty-Stils länger aufgehalten, als ich
# mir gewünscht habe. Fehlerbehandlung fehlt völlig.
#
# Dieses Skript ist unter den Bedingungen der Piratenlizenz lizenziert
# http://www.tamagothi.de/impressum/lizenz/
#
########################################################################

import ConfigParser
import os.path
import xml.dom.minidom
import re
import datetime
import time
import urllib
import httplib


########################################################################
#
# Konfiguration
# -------------
#
# Hier werden einfach alle URLs der RSS-Feeds in ein Array eingetragen.
# Der Rest verläuft "automagisch".
#
# Hier sind einfach die RSS-Feeds meiner Blögchen eingetragen.
#
feeds = [ 'http://schwerdtfegr.wordpress.com/feed/',
          'http://spam.tamagothi.de/feed/',
          'http://www.tamagothi.de/author/elias/feed/',
          'http://alarmknopf.wordpress.com/feed/',
          'http://gagada.wordpress.com/feed/',
          'http://fraktalwelten.wordpress.com/feed/',
          'http://tamagothi.wordpress.com/feed/',
          'http://proll.wordpress.com/feed/',
          'http://wwwut.wordpress.com/feed/',
        ]
#
# API-Entrypoint für den Statusupdate
# Hier kann natürlich auch ein anderer Einstiegspunkt stehen.
#
api_update = 'https://quitter.se/api/statuses/update.xml'
#
# Username
#
username = '----'
#
# Passwort
#
password = '----'
#
# Da diese Datei ein Passwort im Klartext enthält, sollte sie vielleicht
# nicht mit chmod 666 abgelegt werden. ;)
#
# In den meisten Fällen braucht ab hier nicht mehr angepasst zu werden.
#
########################################################################
#
config_path = os.path.expanduser('~/.rss2queet')
#
# Ab hier gibts nichts mehr, was angepasst werden muss.
# Vielleicht erschlägt mal jemand die Fehler, die ich gemacht habe... :D
#
########################################################################

# Globale Variablen
# -----------------

config = ConfigParser.ConfigParser()
time_now = int(datetime.datetime.utcnow().strftime('%s'))
timestamp_re = re.compile(r' *\+\d*$')


# Programm
# --------
#
# Sorry, auf hilfreiche Kommentare wurde weitgehend verzichtet.

class AuthURLOpener(urllib.FancyURLopener):
    version = 'RSS2Queet/0.2beta'
    
    def get_user_passwd(self, host, real, clear_cache=0):
        return (username, password)

urllib._urlopener = AuthURLOpener()


def init():
    config.read(config_path)


def cleanup():
    config.set('DEFAULT', 'lastrun', time_now)
    f = open(config_path, 'wt')
    config.write(f)
    f.close()
    

def make_unix_timestamp(time_string):
    # ARRGH!
    # No better way than strptime and ignoring timezone in Python 2.
    cleaned_up = timestamp_re.sub('', time_string)
    return datetime.datetime.strptime(cleaned_up, '%a, %d %b %Y %H:%M:%S')


def queet_item(text, link):
    message = u"%s %s" % (text, link)
    message = message.encode('ascii', 'xmlcharrefreplace')
    data = urllib.urlencode({u'status': message})
    r = urllib.urlopen(api_update, data)
    

def get_text_easy(nodelist):
    accu = []
    for node in nodelist:
        if node.nodeType == node.TEXT_NODE:
            accu.append(node.data)
    return u''.join(accu)


def get_node_text(node):
    return get_text_easy(node.childNodes)


def get_item_pubdate(item):
    pubdate_str = get_node_text(item.getElementsByTagName('pubDate')[0])
    return make_unix_timestamp(pubdate_str)


def handle_item(item, lastrun):
    item_timestamp = get_item_pubdate(item)
    last_timestamp = datetime.datetime.fromtimestamp(lastrun)
    if item_timestamp <= last_timestamp:
        return
    item_title = get_node_text(item.getElementsByTagName('title')[0])
    item_uri = get_node_text(item.getElementsByTagName('guid')[0])
    queet_item(item_title, item_uri)
    

def handle_item_list(item_list, lastrun):
    for item in item_list:
        handle_item(item, lastrun)


def handle_channel(channel_doc, lastrun):
    handle_item_list(channel_doc.getElementsByTagName('item'), lastrun)

    
def get_document_for_feed(feed_url):
    return xml.dom.minidom.parse(urllib.urlopen(feed_url))


def handle_feed(feed_url):
    if not config.has_section(feed_url):
        config.add_section(feed_url)
        config.set(feed_url, 'lastrun', time_now)
    else:
        lastrun = config.getfloat(feed_url, 'lastrun')
        config.set(feed_url, 'lastrun', time_now)
        handle_channel(get_document_for_feed(feed_url), lastrun)


def main():
    init()
    for feed_url in feeds:
        handle_feed(feed_url)
    cleanup()


if __name__ == '__main__':
    main()

In der Hoffnung, dass es anderen Menschen ein kleines bisschen Arbeit abnimmt, steht das Skriptchen hier zum freien Download zur Verfügung. Es kann unter den Bedingungen der Piratenlizenz benutzt, bearbeitet, verbreitet oder verschrottet werden. Viel Spaß damit!

Kleine Anmerkung: Wenn das Skript zum ersten Mal für einen neuen Feed gestartet wird, holt es diesen Feed noch nicht ab, sondern setzt nur die aktuelle Zeit als Zeitpunkt der letzten Abholung. Auf diese Weise wollte ich vermeiden, dass die Statusmeldungen bei Quitter mit großen Mengen alter Texte vollgespammt werden.

¹Warum? Weil das Zwitscherchen nun für mich entscheiden will, welche Beiträge wichtig sind und welche nicht und weil ich das scheiße finde. Von den jüngsten Geschäftsideen eines börsennotierten Unternehmens ohne seriöses Geschäftsmodell einmal ganz abgesehen.

Dieser Beitrag wurde unter Technisches abgelegt und mit , , , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.