MembeBot: ohhh I member
¨¿Listo para implementar tu propio sistema de notificaciones de eventos en calendario sin depender de gigantes tecnológicos?"

Imagina un sistema de notificaciones de calendarios que no solo funciona, sino que te pertenece. Usar XMPP (el protocolo abierto de mensajería instantánea descentralizada) junto con Node-RED (el entorno de desarrollo basado en flujos) y CalDAV (el estándar para sincronizar calendarios) te permite crear un bot personalizado que envía alertas directamente a tu cliente de mensajería preferido, sin intermediarios. La magia aquí no es la tecnología en sí, sino la libertad que te brinda: no estás atado a plataformas cerradas ni a servicios que cambian sus políticas de privacidad cada seis meses. Es como tener un asistente personal que responde solo a tus órdenes, sin que nadie más pueda apagar la luz... bueno, salvo que vivas en el dominio de Edesur y se les cante cortarte el servicio.
El primer obstáculo suele ser la curva de aprendizaje, pero una vez superado, el proceso se vuelve tan intuitivo como conectar ladrillos plásticos en un juego para niños. Node-RED es eso para programadores: arrastra y conecta módulos sin escribir líneas de código... o al menos escribiendo la menor cantidad posible. Con un poco de paciencia, hasta los más reacios a la tecnología pueden configurar un bot que envíe recordatorios de reuniones o eventos importantes a tu cuenta de XMPP (como Quicksi, Conversations o incluso un servidor autoalojado como ejabberd, prosody u openfire). Y lo mejor: no necesitas pagar por licencias ni suscripciones, solo necesitas una maquina que pueda correr todo esto (por ejemplo un Raspberry 3b+), un poco de tiempo y ganas de explorar.
¿Por qué conformarse con soluciones que te obligan a "vender tu alma al demonio"? Las plataformas propietarias suelen ser como un castillo de naipes: bonitos al principio, pero que se derrumban cuando cambian las reglas del juego. Con un bot autoalojado basado en software libre, el que controla los datos, el que decide cómo se procesan y qué hacer con ellos, sos vos. No hay anuncios molestos, no hay límites en el número de notificaciones ni en la personalización. Podes integrar desde alertas por correo electrónico hasta mensajes en redes sociales o incluso notificaciones en tu dispositivo, todo desde un flujo que vos diseñas. Es tu sistema, son tus reglas.
El corazón de este pequeño ecosistema es CalDAV, el protocolo que permite sincronizar calendarios entre dispositivos. Ya sea que uses Nextcloud, ownCloud o alternativas mas livianas como Radicale, CalDAV te da la flexibilidad de acceder a tus eventos desde cualquier lugar, sin depender de servicios como Google Calendar o Microsoft Outlook o pagar por un M$ Exchange y todo el fierro que necesitas para correrlo. La combinación con XMPP añade una capa extra de privacidad: tus notificaciones viajan por un protocolo abierto cifrado, sin pasar por servidores de terceros que podrían usar tus datos en formas inimaginables. Es como tener un túnel privado para tus alertas, donde solo vos y tu bot saben qué hay dentro.
Y hablando de bot, tanto este bot como el que vimos en el post sobre Node-RED anterior, es solo el comienzo. Con Node-RED, podes escalar tu proyecto hasta límites que ni imaginabas: desde automatizar tareas repetitivas hasta crear sistemas de alerta complejos que interactúen con sensores IoT, bases de datos o incluso APIs de servicios externos. La comunidad detrás de estos proyectos es bastísima, y hay miles de ejemplos y tutoriales disponibles para que no tengas que reinventar la rueda. La magia está en que podes adaptarlo a casi cualquier necesidad, desde un simple recordatorio, manejar los puertos GPIO de un Raspberry o hasta un sistema middleware que funcione como bus de servicios.
Al final del día, lo que realmente importa es la autonomía. No estás limitado por las decisiones de una corporación, ni por los caprichos de un algoritmo. Con un bot autoalojado, vos decidís cómo funciona, cuándo se actualiza y qué datos se comparten. Es como tener un superpoder... la capacidad de personalizar tu experiencia digital sin rendirle culto a nadie. Así que, ¿qué estas esperando? Descargate Node-RED, configurate un servidor XMPP o date de alta en un servidor amigo, sincroniza tu CalDAV y preparate para disfrutar de notificaciones que funcionan y son fáciles de personalizar. La tecnología debería servirte, no al revés.
Vamos a hacer tres flujos, los dos primeros funcionan en salas de chat y el ultimo para chats privados, es decir, 1:1 con el bot.
Flujo planificado para correr una vez por mes y enviar eventos de los próximos 30 días
El objetivo del siguiente flujo es enviar, el primer dia de cada mes, una lista de eventos para los próximos 30 días a una sala de chat.

Estarás diciendo "Despacio cerebrito!" o "De que estas hablando Willis?" buem ... descifremos nuestros conjuros nodo a nodo. Primero lo mas importante, necesitamos una forma de consultar eventos en una agenda, en Cyberdelia contamos con un servidor caldav/cardav para gestionar nuestros contactos y calendarios. Estamos usando Radicale, un servidor simple, que levantamos con el siguiente docker compose:
services:
radicale:
image: tomsquest/docker-radicale
container_name: radicale
env_file:
- stack.env
ports:
- 5232:5232
init: true
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- SETUID
- SETGID
- CHOWN
- KILL
deploy:
resources:
limits:
memory: 256M
pids: 50
healthcheck:
test: curl -f http://127.0.0.1:5232 || exit 1
interval: 30s
retries: 3
restart: unless-stopped
volumes:
- radicale_data:/data
- radicale_config:/config
volumes:
radicale_data:
radicale_config:
Y ya que estamos, Node-RED tampoco tiene mucho misterio, un simple compose y la magia esta hecha:
services:
app:
image: nodered/node-red:latest
container_name: node-red
environment:
NODE_RED_CREDENTIAL_SECRET: ${NODE_RED_CREDENTIAL_SECRET}
TZ: ${TZ}
volumes:
- data:/data
ports:
- 1880:1880
restart: unless-stopped
volumes:
data:
Dicho esto solo queda agregar que con cualquier cuenta XMPP en un servidor de confianza contamos con todo lo necesario para seguir con nuestros conjuros.
Pasemos al nodo que nos permite interactuar con CALDAV... primero, necesitamos buscar dentro de la paleta de nodos para instalar el nodo node-red-contrib-ical-events.

El nodo node-red-contrib-ical-events es una extensión que permite integrar y gestionar eventos de calendarios directamente en nuestros conjuros. Este módulo facilita la conexión con fuentes de eventos como CalDAV o cualquier URL de archivo ICS (iCalendar) entre otros proveedores privativos. Su principal ventaja es la capacidad de extraer, procesar y actuar sobre eventos de calendario en tiempo real, ideal para sistemas de domótica, recordatorios automatizados o sincronización de tareas.
Es compatible con múltiples protocolos, pero los mas relevantes son:
iCal/ICS: Lee eventos desde URLs públicas o privadas de archivos ICS.CalDAV: Accede a calendarios de servidores compatibles como Nextcloud o Radicale (o servicios on-premise de software privativo, pero no lo recomendamos).
En términos prácticos vamos a estar usando alguno de estos tres nodos:

ical-trigger:
El calendario se revisa cuando ocurren eventos de entrada, por ejemplo un con un nodo trigger, entre otros, o usando reglas cron para controlar el cronjob. Se genera un cronjob separado para eventos futuros del calendario dentro de la ventana temporal configurable. Lo destacable es que este nodo no genera una salida cuando recibe un evento, sino que genera una salida cuando un evento del calendario inicia.

ical-sensor:
Filtra eventos actuales cuando recibe un evento o usando reglas cron.

ical-upcoming:
Filtra eventos futuros, ideal para recordatorios y se ejecuta cuando recibe un evento o usando reglas cron.

Mas el nodo ical-config que configura la conexión inicial (URL del calendario, autenticación, etc.).

Bien, en nuestro caso estamos usando el nodo ical-upcoming solamente, porque queremos que nos notifique sobre los próximos eventos a partir de un momento determinado por una regla cron. Por ejemplo para el caso de querer una notificación diaria con los eventos del día, la configuración del nodo debería tener una regla cron 0 8 * * *. Es decir que todos los dias a las 08:00 hrs. nos enviara una notificación. En cambio, si queremos que todos los primero del mes nos retorne una lista de eventos para los próximos 30 días, la configuración del nodo sería así:

Pero obviamente acá no termina todo ... veamos que datos escupe el nodo
ical-upcoming dentro del payload:
[
{
"date": "2/16/26, 9:00 – 9:25 AM",
"eventStart": "2026-02-16T12:00:00.000Z",
"eventEnd": "2026-02-16T12:25:00.000Z",
"summary": "Test 1 (Summary)",
"duration": "PT25M",
"durationSeconds": 1500,
"location": "Test 1 (Localtion)",
"rruleText": null,
"uid": {
"uid": "576fc577d893280ac5eb5c9315d757da95a6a23f",
"date": "1771243200000"
},
"isRecurring": false,
"datetype": "date",
"allDay": false,
"calendarName": "Development Calendar",
"originalEvent": {
"component": [
"vevent",
[
[
"uid",
{},
"text",
"576fc577d893280ac5eb5c9315d757da95a6a23f"
],
[
"dtstart",
{
"tzid": "America/Argentina/Buenos_Aires"
},
"date-time",
"2026-02-16T09:00:00"
],
[
"dtend",
{
"tzid": "America/Argentina/Buenos_Aires"
},
"date-time",
"2026-02-16T09:25:00"
],
[
"class",
{},
"text",
"PUBLIC"
],
[
"created",
{},
"date-time",
"2026-02-15T05:54:35Z"
],
[
"dtstamp",
{},
"date-time",
"2026-02-15T05:53:03Z"
],
[
"last-modified",
{},
"date-time",
"2026-02-15T05:54:35Z"
],
[
"location",
{},
"text",
"Test 1 (Localtion)"
],
[
"sequence",
{},
"integer",
2
],
[
"summary",
{},
"text",
"Test 1 (Summary)"
],
[
"transp",
{},
"text",
"OPAQUE"
]
],
[]
],
"_rangeExceptionCache": {},
"exceptions": {},
"rangeExceptions": []
},
"countdown": {
"days": 1,
"hours": 6,
"minutes": 3,
"seconds": 44
},
"on": false
},
{
"date": "2/17/26, 11:00 AM – 5:55 PM",
"eventStart": "2026-02-17T14:00:00.000Z",
"eventEnd": "2026-02-17T20:55:00.000Z",
"summary": "Test 2 (Summary)",
"duration": "PT6H55M",
"durationSeconds": 24900,
"location": "Test 2 (Location)",
"rruleText": null,
"uid": {
"uid": "2ee9bafb9979197a8af13e4cb8ec2f71246a751e",
"date": "1771336800000"
},
"isRecurring": false,
"datetype": "date",
"allDay": false,
"calendarName": "Development Calendar",
"originalEvent": {
"component": [
"vevent",
[
[
"uid",
{},
"text",
"2ee9bafb9979197a8af13e4cb8ec2f71246a751e"
],
[
"dtstart",
{
"tzid": "America/Argentina/Buenos_Aires"
},
"date-time",
"2026-02-17T11:00:00"
],
[
"dtend",
{
"tzid": "America/Argentina/Buenos_Aires"
},
"date-time",
"2026-02-17T17:55:00"
],
[
"class",
{},
"text",
"PUBLIC"
],
[
"created",
{},
"date-time",
"2026-02-15T05:55:40Z"
],
[
"dtstamp",
{},
"date-time",
"2026-02-15T05:53:03Z"
],
[
"last-modified",
{},
"date-time",
"2026-02-15T05:55:40Z"
],
[
"location",
{},
"text",
"Test 2 (Location)"
],
[
"sequence",
{},
"integer",
2
],
[
"summary",
{},
"text",
"Test 2 (Summary)"
],
[
"transp",
{},
"text",
"OPAQUE"
]
],
[
[
"valarm",
[
[
"action",
{},
"text",
"DISPLAY"
],
[
"description",
{},
"text",
"Test 2 (Summary)"
],
[
"trigger",
{
"related": "START"
},
"duration",
"-PT1H"
],
[
"x-evolution-alarm-uid",
{},
"unknown",
"11694d9ee6b58c1a852b1d052f8786ad461f62db"
]
],
[]
]
]
],
"_rangeExceptionCache": {},
"exceptions": {},
"rangeExceptions": []
},
"countdown": {
"days": 2,
"hours": 8,
"minutes": 3,
"seconds": 44
},
"on": false
}
]
Vayamos por partes dijo Jack... primero limpiemos el json de datos asi podemos ver la estructura sin tanto ruido y poder analizarla mejor. La principal razon es porque necesitamos entender bien la estructura para luego usar JSONataa dentro del nodo filter.
Segun la documentación del nodo, cuando retorna valores dentro de msg.payload cada uno de los elementos del vector de eventos contenido tiene los siguientes datos:
- date
- event
- rule
- summary
- id
- location
- eventStart
- eventEnd
- description
- allDay
- attendee
- isRecurring
- calendarName
- organizer
- categories
- duration
Veamos entonces un solo evento:
{
"date": "2/16/26, 9:00 – 9:25 AM",
"eventStart": "2026-02-16T12:00:00.000Z",
"eventEnd": "2026-02-16T12:25:00.000Z",
"summary": "Test 1 (Summary)",
"duration": "PT25M",
"durationSeconds": 1500,
"location": "Test 1 (Localtion)",
"rruleText": null,
"uid": {
"uid": "576fc577d893280ac5eb5c9315d757da95a6a23f",
"date": "1771243200000"
},
"isRecurring": false,
"datetype": "date",
"allDay": false,
"calendarName": "Development Calendar",
"originalEvent": {
"component": [
"vevent",
[
[
"uid",
{},
"text",
"576fc577d893280ac5eb5c9315d757da95a6a23f"
],
[
"dtstart",
{
"tzid": "America/Argentina/Buenos_Aires"
},
"date-time",
"2026-02-16T09:00:00"
],
[
"dtend",
{
"tzid": "America/Argentina/Buenos_Aires"
},
"date-time",
"2026-02-16T09:25:00"
],
[
"class",
{},
"text",
"PUBLIC"
],
[
"created",
{},
"date-time",
"2026-02-15T05:54:35Z"
],
[
"dtstamp",
{},
"date-time",
"2026-02-15T05:53:03Z"
],
[
"last-modified",
{},
"date-time",
"2026-02-15T05:54:35Z"
],
[
"location",
{},
"text",
"Test 1 (Localtion)"
],
[
"sequence",
{},
"integer",
2
],
[
"summary",
{},
"text",
"Test 1 (Summary)"
],
[
"transp",
{},
"text",
"OPAQUE"
]
],
[]
],
"_rangeExceptionCache": {},
"exceptions": {},
"rangeExceptions": []
},
"countdown": {
"days": 1,
"hours": 6,
"minutes": 3,
"seconds": 44
},
"on": false
}
Bueno, tenemos mas datos de los documentados... el originalEvent, por ejemplo, no es mas que los mismos datos del evento pero representados de otra forma.
Con lo que ahora tenemos algo mas chico para analizar:
{
"date": "2/16/26, 9:00 – 9:25 AM",
"eventStart": "2026-02-16T12:00:00.000Z",
"eventEnd": "2026-02-16T12:25:00.000Z",
"summary": "Test 1 (Summary)",
"duration": "PT25M",
"durationSeconds": 1500,
"location": "Test 1 (Localtion)",
"rruleText": null,
"uid": {
"uid": "576fc577d893280ac5eb5c9315d757da95a6a23f",
"date": "1771243200000"
},
"isRecurring": false,
"datetype": "date",
"allDay": false,
"calendarName": "Development Calendar",
"countdown": {
"days": 1,
"hours": 6,
"minutes": 3,
"seconds": 44
},
"on": false
}
Bien, continuemos ... necesitamos tener en cuenta esta estructura porque nuestro próximo nodo es quien filtrara todos estos datos para sacar solo lo que nos interesa.
En particular los datos que necesitamos para mostrar los próximos eventos son date y summary y dado que dentro de Node-RED manejamos un mensaje de punta a punta de un flujo, nuestro JSONata dentro del nodo Filter queda de la siguiente forma msg.payload.(date & ', ' & summary).
En la configuración, por lo tanto quedara como sigue:

Esto nos retornara otro vector en el payload, el cual procesaremos en el nodo BuildMessage con el siguiente código JavaScript:
var msgrt = RED.util.cloneMessage(msg);
msgrt.payload = "Upcoming Cyberdelic Events for the next 30 days:\n"
msg.payload.forEach(function (obj) { msgrt.payload = msgrt.payload.concat(obj,"\n"); });
return msgrt;
Construido el mensaje, ya estamos listos para enviarlo por XMPP. Por ejemplo:

Flujo para respuestas automaticas en salas de chat
Que nuestro bot este sentado en alguna sala sin hacer nada hasta que le toque hacer su magia una vez por mes no tiene mucha gracia, asi que vamos a agregarle algo de interacción respodiendo cuando es mencionado.

Este flujo retorna una respuesta simple Membe? ohhh!!! I member si el mensaje entrante viene desde un canal y esta dirigido al Nick del bot, pero no el que menciona el nick del bot, no es el bot. Esto aparentemente poco importante es algo que tenemos que tener muy en cuenta, ya que en este caso podríamos iniciar un bucle infinito. Esa magia la hacemos en estos nodos:

El nodo Switch permite que los mensajes se dirijan a diferentes ramas de un flujo evaluando un conjunto de reglas contra cada mensaje, básicamente funciona como el "switch" que conocemos todos y es común a muchos lenguajes de programación.
Puede aplicarse cuatro tipos de reglas:
- Las reglas por valor se evalúan contra la propiedad configurada
- Las reglas de secuencia se pueden utilizar en secuencias de mensajes, como las generadas por el nodo
Split - Se puede proporcionar una expresión JSONata que se evaluará contra todo el mensaje y coincidirá si la expresión devuelve un valor verdadero.
- Una regla
Otherwisese puede utilizar para igualar si ninguna de las reglas anteriores han coincidido.
El nodo va a enrutar un mensaje a todas las salidas correspondientes a las reglas que el mensaje satisface. También se puede configurar para dejar de evaluar reglas cuando encuentra una que coincide.
| is it from me? | is it for me? |
|---|---|
![]() |
![]() |
En el nodo is if from me? podemos ver que en realidad no estamos buscando mensajes que contengan en el topic el nombre o nick del bot, sino que estamos eligiendo continuar con el flujo cuando la regla otherwise se satisface.

Luego creamos el mensaje en el nodo function y lo enviamos a la sala, mensaje que se mostraría de la siguiente forma:

Flujo para respuestas a mensajes privados con informacion de los proximos eventos

Este tercer flujo se activa cuando el "bot", por llamarlo de alguna manera, recibe un mensaje directo de otro usuario. Al igual que en el primer caso, consulta el calendario y retorna una lista de eventos para los próximos 30 días.
Aquí lo importante es que, si buen el nodo ical-upcoming esta programada para dispararse todos los primero de mes a las 10:00 AM el flujo finalmente no tiene a quien enviarle el mensaje. Por lo tanto solo responde con los proximos eventos si el flujo fue iniciado por un mensaje privado al bot. Esto se debe a que el campo To: del nodo para enviar mensajes es opcional y si no se establece utiliza la propiedad msg.topic del mensaje, dato que mantenemos durante todo el flujo.
Podes ver a este bot funcionando en la vida real, visitanos en Lost in Cyberspace.

