Ten tu propia pasarela RSS -> Twitter

Cuando Hispasec decidió publicar los anuncios del boletín Una al Día a través de Twitter (además de la lista de correo de toda la vida y de la propia página web de la empresa. Es magnífica. Si no estáis suscritos, ya estáis tardando) tuvimos un pequeño gran debate interno sobre cómo hacerlo exactamente. La cuestión giró, fundamentalmente, sobre si deberíamos tener un servicio de publicación propio o bien depender de proveedores externos.

Mi argumento es que una empresa de seguridad no debería depender de un proveedor externo para algo tan simple y que ello abre riesgos: si empleas un proveedor externo, le estás dando permiso para publicar cualquier cosa en tu timeline. Si ese proveedor sufre un problema de seguridad o una intrusión, el atacante tiene un amplio control sobre tu cuenta de Twitter. El problema es similar a publicar banners hospedados en servicios externos, algo a lo que Hispasec se negó siempre (por insistencia justificada de Bernardo Quintero, y el tiempo le ha dado la razón muchas veces).

La discusión tuvo su miga, pero al final me salí con la mía.

Lamentablemente todo el código que encontré por ahí en aquel momento se autenticaba mediante usuario y clave, algo que Twitter había anunciado que estaba en extinción y que todo el mundo debía migrar a una cosa extraña y novedosa llamada OAuth. No había otra que esperar o currárselo uno mismo.

Me lo curré yo mismo.

El programa se ejecutaba por cron cada 20 minutos, leía el feed RSS de la web y publicaba los titulares nuevos en Twitter. Dado que los tweets están limitados a 140 caracteres, se utilizaba bitly como acortador de URLs lo que, además, nos proporcionaba estadísticas detalladas de clicks y nos permitía separar fácilmente la gente que entraba por Twitter de la que entraba pinchando en la URL en otros medios.

Cuando me fui de Hispasec migraron a un proveedor externo (crucemos los dedos), y el código de la pasarela quedó por ahí, abandonado y cogiendo polvo.

Cuando empecé este blog tuve la misma disyuntiva, pero esta vez la decisión era fácil. Desempolvé el código viejo, lo migré a Python 3, y lo actualicé con las nuevas APIs de Twitter, especialmente en lo relativo al acortamiento de URLs.

Esto último es interesante. Hace unos años Twitter desplegó el servicio t.co y procedió a acortar automáticamente todas las URLs publicadas, incluso cuando la URL original es más corta que el "acortamiento". Seguir utilizando algo como bitly supondría un doble indireccionamiento, lo que aumenta la latencia de la resolución y los puntos de fallo posibles, aunque mantendrías las estadísticas pormenorizadas. Pero esa doble indirección hace que la pasarela Twitter -> Facebook que uso muestre el enlace bitly, y no el enlace final. Y las estadísticas pormenorizadas las puedo obtener igualmente analizando los logs de mi servidor web y no soy un obseso del tema, así que eliminé la dependencia de bitly.

Aquí el código en todo su esplendor:

Este código se libera como dominio público. Haced con él lo que queráis, aunque me gustaría que me contéis qué cosas interesantes sacáis.

     #!/usr/local/bin/python3

     # (c) 20xx-2014 jcea@jcea.es - http://www.jcea.es/
     # Código liberado como Dominio Público.
     # Haz con él lo que quieras.

     import feedparser
     import pickle
     import os
     import socket
     import fcntl

     os.chdir("/home/blog-twitter/data/")
     os.setgroups([60001])  # Nobody
     os.setgid(60001)
     os.setuid(60001)


     persist = 'ya_publicados.pickle'

     # Si no puede, falla inmediatamente
     lock = open(persist+'.lock', 'a')
     fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)

     socket.setdefaulttimeout(60)


     ya_publicado = set()
     if os.path.exists(persist) :
         ya_publicado = pickle.load(open(persist, 'rb'))

     feed = feedparser.parse('http://blog.jcea.es/rss.xml')
     assert feed.status == 200
     assert feed.encoding == 'utf-8'

     limpieza = set([i.id for i in feed.entries])
     ya_publicado.intersection_update(limpieza)  # Limpieza de post antiguos
     a_publicar = [i for i in feed.entries if i.id not in ya_publicado]

     for entrada in a_publicar :
         import twitter  # Solo importa si lo vamos a necesitar

         # Autenticacion OAUTH para TWITTER, segun los detalles de
         # http://jmillerinc.com/2010/05/31/twitter-from-the-command-line-in-python-using-oauth/
         # http://github.com/sixohsix/twitter/blob/master/twitter/ircbot.py
         consumer_key = "XXXXXX"
         consumer_secret = "XXXXXX"

         # Podemos crear un nuevo cliente en twitter y pedir que nos
         # genere unos token de acceso directamente, que podemos usar ya sin mas.
         if not os.path.exists('oauth_token_file') :
             from twitter.oauth_dance import oauth_dance
             oauth_dance('FEED RSS de http://blog.jcea.es/', consumer_key, consumer_secret,
                     "oauth_token_file")

         oauth_token, oauth_secret = \
             twitter.oauth.read_token_file('oauth_token_file')

         twitter_API = twitter.Twitter(auth=twitter.OAuth(oauth_token,
             oauth_secret, consumer_key, consumer_secret))

         # El 1 es por el espacio entre titulo y url
         # El 22 es por el acortamiento de t.co
         # el 4 es por el ' #fb'
         l = 140-1-22-4
         titulo = entrada.title
         if len(titulo) > l :
             titulo = titulo[:l-3]+'...'

         # El acortamiento a 't.co' es automático y transparente, al
         # publicar en twitter.
         titulo = titulo+' '+entrada.link+' #fb'

         try :
             twitter_API.statuses.update(status=titulo)
         except twitter.api.TwitterHTTPError as e:
             import json
             e = json.loads(e.response_data.decode('utf-8'))
             if e['errors'][0]['message'] != 'Status is a duplicate.' :
                 raise

         print(titulo)

         ya_publicado.add(entrada.id)
         f = open(persist+'.new', 'wb')
         pickle.dump(ya_publicado, f)
         f.flush()
         os.fsync(f)
         f.close()
         os.rename(persist+'.new', persist)

     # El lock se libera automáticamente al terminar
     # pero así no dejamos basurilla en el directorio

     lock.close()
     os.remove(persist+".lock")

Ahora mismo ejecuto este código "a mano" cada vez que publico algo en este blog. Es interesante que este programa se pueda ejecutar en nuestro portátil, no hace falta que resida en un servidor o que se ejecute siempre. En ese caso puede ser conveniente borrar la parte de cambio de grupo/usuario.

La única configuración que hay que hacer son los detalles OAuth, pero está todo muy bien explicado en los enlaces incluídos en los comentarios. También, claro, el nombre y la URL del feed RSS.

Un detalle a tener en cuenta es que mi código añade un sufijo "#fb" al final. Esto es una marca empleada por otra pasarela que uso para pasar de Twitter a Facebook. Mira por donde ésto es una dependencia externa que tengo que limpiar un día de estos... Podéis quitar eso en vuestra versión y ganar cuatro caracteres en los títulos.

ACTUALIZACIÓN 20150626: Hay una actualización de este código en mi artículo Pasarela RSS -> Twitter que también sube imágenes asociadas.