Tooteando cámaras de Madrid

Spoiler : este script usa Google App Script. Si Google te da asco seguramente no quieras seguir leyendo. Avisado estás

OpenData

Dentro del catálogo de datos abiertos del ayuntamiento de Madrid hay uno que publica un xml con los datos de todas las cáramas que controlan el tráfico (y a nosotros por ende) de la ciudad

Cada elemento indica la posición, nombre etc y una URL hacia la última imagen tomada (creo que se refrescan cada 5 minutos aprox)

En este post voy a explicar cómo uso este OpenData para elegir una cámara aleatoria y subirla a mi cuenta de "Mastodon" (en realidad GotoSocial) junto con un texto. Tal vez te pueda servir de ejemplo/inspiración para crear tus scripts.

El script se ejecuta en Google Script pero podría tenerlo en una raspberry, en mi ordenador …​ Y por otra parte está hecho en Javascript pero podrías hacerlo hasta con puro Bash con un curl

Crear fedi-aplicación

Lo primero a hacer es crearnos una aplicación en nuestra cuenta del Fediverso. Cada sistema (mastodon, gotosocial, pleroma) lo hacen de forma diferente pero muy parecida.

Básicamente en settings crearás una "application", le das permisos de read-write y una vez que la autorizas te devuelve un token.

Este token hay que guardarlo aunque en cualquier momento puedes borrar la application y volver a recrearlo

INFO

El nombre de la aplicación que creas aparecerá como metadato y se podrá "ver" en tu timeline.

Crear Script

Si tienes cuenta en Google (gmail por ejemplo) puedes crear un proyecto en https://script.google.com y configurar "disparadores" que lo ejecuten

En nuestro caso vamos a usar un disparador de tiempo que ejecutará el script todos los días entre las 8 y las 9 de la mañana

En el editor que nos abre Google copiaremos este código (el cual explico a continuación)

const url = "https://datos.madrid.es/egob/catalogo/202088-0-trafico-camaras.kml"
const instancia = 'social.jagedn.dev';
const bootToken = 'ZTMYYTQ4-----recuerda no compartir tu token ni en un post';

function myFunction() {

  let xml = UrlFetchApp.fetch(url).getContentText('ISO-8859-1');
  let document = XmlService.parse(xml);
  let root = document.getRootElement();
  let ids = new Array()
  root.getChildren().forEach(document=>{
    document.getChildren().forEach(item=>{
      if(item.getName()=="Placemark"){
        item.getChildren().forEach(extendedData=>{
          if( extendedData.getName()=='ExtendedData'){
            extendedData.getChildren().forEach(data=>{
              if(data.getAttribute("name").getValue()=="Numero"){
                data.getChildren().forEach(value=>{
                    ids.push(value.getText())
                })
              }
            })
          }
        })
      }
    })
  })
  let id = ids[Math.floor(Math.random() * ids.length)];
  let image = UrlFetchApp.fetch("https://informo.madrid.es/cameras/Camara"+id+".jpg?v="+new Date().getMilliseconds()).getBlob()
  sendMedia(image, "Una #random webcam de #Madrid al día\n(Enviada con un script, podriamos decir que es un bot o no, qué se yo)")
}

function sendMedia(blob, toot) {

  var urlMedia = `https://${instancia}/api/v1/media`;
  var url = `https://${instancia}/api/v1/statuses`;

  var boundary = "xxxxxxxxxx";
  var data = "";

  data += "--" + boundary + "\r\n";
  data += "Content-Disposition: form-data; name=\"file\"; filename=\"chart.png\"\r\n";
  data += "Content-Type:image/png\r\n\r\n";

  var payloadImage = Utilities.newBlob(data).getBytes()
    .concat(blob.getBytes())
    .concat(Utilities.newBlob("\r\n--" + boundary + "--").getBytes());

   var optionsImage = {
    method : "post",
    contentType : "multipart/form-data; boundary=" + boundary,
    payload : payloadImage,
    headers:{
      'Authorization': `Bearer ${bootToken}`
    },
    muteHttpExceptions: true,
  };
  var respImage = UrlFetchApp.fetch(urlMedia, optionsImage).getContentText();
  Logger.log(respImage)
  var jsonImage = JSON.parse(respImage);

  var payload = {
    status: toot,
    media_ids: [jsonImage.id]
  }
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload),
    'headers': {
      'Authorization': `Bearer ${bootToken}`
    },
    'muteHttpExceptions':true
  };
  Logger.log(payload)

  const resp = UrlFetchApp.fetch(url, options);
  Logger.log(resp)
}

myFunction

este es el punto de entrada a nuestro proyecto. Simplemente se descarga la última versión del XML y lo parsea, extrayendo los ids de todas las cámaras

Como el XLM es un KML (xml con etiquetas de posicionar elementos geográficos) es un poco raruno pero se lee fácil.

Básicamente elegimos un id al azar y nos descargamos la imagen (la url es la misma para todas las cámaras cambiando el id). Le añado además un parámetro en la url con la fecha actual para evitar temas de cacheo

Una vez tenemos el "blog", o sea el binario remoto, llamamos a la funcion que lo enviará al fediverso de nuestra instancia

Si usas otro lenguaje (tipo PHP o Java) o incluso con Javascript usando axios, puedes usar sus funciones para subir un fichero mediante multipart/form-data (formato requerido por las instancias)

Mi ejemplo es una implementación "a pelo" de cómo subir un multipart usando "boundary"

Una vez enviada la imagen a la url /api/v1/media el servidor nos devolverá un json con la info del recurso creado por lo que nos guardamos su id

A continuación creamos un toot llamando a la url /api/v1/statuses pasando un json con el payload requerido. Simplemente proporcionamos un texto (en este caso fijo, en otros podría ser generado al vuelo) y añadimos el media_ids con el id que nos devolvió de la imagen

Y eso es todo, si todo va bien habremos publicado un toot con una imagen callejera

Otras ideas

Tomando una hoja de GoogleSheet como fuente de datos puedes hacer un script que tootee un chiste, o que genere un gráfico de barras según los datos del excel y lo subas al fediverso

O pasar de Google y tenerlo autoalojado en una raspberry…​

La imaginación es el límite

Este texto ha sido escrito por un humano

This post has been written by a human

2019 - 2026 | Mixed with Bootstrap | Baked with JBake v2.6.7 | Terminos Terminos y Privacidad