Sesiones y almacenamiento de datos (incorporado)

Aunque siempre puedes escribir tu propio código para conectarte a un almacenamiento de datos de tu elección, grammY soporta un patrón de almacenamiento muy conveniente llamado sessions.

Salta hacia abajo si sabes cómo funcionan las sesiones.

¿Por qué debemos pensar en el almacenamiento?

A diferencia de las cuentas de usuarios regulares en Telegram, los bots tienen almacenamiento limitado en la nubeopen in new window en la nube de Telegram. Como resultado, hay algunas cosas que no puedes hacer con los bots:

  1. No puedes acceder a los mensajes antiguos que recibió tu bot.
  2. No puedes acceder a los mensajes antiguos que tu bot envió.
  3. No puedes obtener una lista de todos los chats con tu bot.
  4. Más cosas, por ejemplo, no hay resumen de medios, etc.

Básicamente, se reduce al hecho de que un bot sólo tiene acceso a la información de la actualización entrante en ese momento (por ejemplo, un mensaje), es decir, la información que está disponible en el objeto de contexto ctx.

En consecuencia, si quieres acceder a datos antiguos, tienes que almacenarlos en cuanto lleguen. Esto significa que debes tener un almacenamiento de datos, como un archivo, una base de datos o un almacenamiento en memoria.

Por supuesto, grammY te tiene cubierto aquí: no tienes que alojar esto tú mismo. Puedes utilizar el almacenamiento de sesiones de grammY, que no necesita ninguna configuración y es gratuito para siempre.

Naturalmente, hay muchos otros servicios que ofrecen almacenamiento de datos como servicio, y grammY se integra perfectamente con ellos también. Si quieres manejar tu propia base de datos, ten por seguro que grammY lo soporta igualmente bien. Desplázate hacia abajo para ver qué integraciones están actualmente disponibles.

¿Qué son las sesiones?

Es muy común que los bots almacenen algún dato por chat. Por ejemplo, digamos que queremos construir un bot que cuente el número de veces que un mensaje contiene el emoji de la pizza 🍕 en su texto. Este bot podría añadirse a un grupo, y podría decir cuánto os gusta la pizza a ti y a tus amigos.

Cuando nuestro bot de pizza recibe un mensaje, tiene que recordar cuántas veces ha visto una 🍕 en ese chat antes. Su recuento de pizzas no debería cambiar, por supuesto, cuando su hermana añada el bot de pizzas a su chat de grupo, así que lo que realmente queremos es almacenar un contador por chat.

Las sesiones son una forma elegante de almacenar datos por chat. Utilizarías el identificador del chat como clave en tu base de datos, y un contador como valor. En este caso, llamaríamos al identificador del chat la clave de la sesión. (Puedes leer más sobre las claves de sesión aquí abajo. Efectivamente, tu bot almacenará un mapa desde un identificador de chat a unos datos de sesión personalizados, es decir, algo así:

{
  "424242": { "pizzaCount": 24 },
  "987654": { "pizzaCount": 1729 }
}

Cuando decimos base de datos, en realidad nos referimos a cualquier solución de almacenamiento de datos. Esto incluye archivos, almacenamiento en la nube o cualquier otra cosa. Bien, pero ¿qué son las sesiones ahora?

Podemos instalar un middleware en el bot que proporcionará los datos de la sesión del chat en ctx.session para cada actualización. El plugin instalado hará algo antes y después de que nuestros manejadores sean llamados:

  1. Antes de nuestro middleware. El plugin de sesión carga los datos de sesión del chat actual desde la base de datos. Almacena los datos en el objeto de contexto bajo ctx.session.
  2. Nuestro middleware se ejecuta. Podemos leer ctx.session para inspeccionar qué valor estaba en la base de datos. Por ejemplo, si se envía un mensaje al chat con el identificador 424242, sería ctx.session = { pizzaCount: 24 } mientras se ejecuta nuestro middleware (al menos con el estado de la base de datos de ejemplo anterior). También podemos modificar ctx.session arbitrariamente, por lo que podemos añadir, eliminar y cambiar campos a nuestro gusto.
  3. Después de nuestro middleware. El middleware de sesión se asegura de que los datos se escriban de nuevo en la base de datos. Cualquiera que sea el valor de ctx.session después de que el middleware termine de ejecutarse, se guardará en la base de datos.

Como resultado, ya no tenemos que preocuparnos de comunicarnos con el almacenamiento de datos. Simplemente modificamos los datos en ctx.session, y el plugin se encargará del resto.

Cuándo usar las sesiones

Sáltate el paso si ya sabes que quieres usar sesiones. Puedes pensar, esto es genial, ¡nunca más tendré que preocuparme por las bases de datos! Y tienes razón, las sesiones son una solución ideal, pero sólo para algunos tipos de datos.

Según nuestra experiencia, hay casos de uso en los que las sesiones realmente brillan. Por otro lado, hay casos en los que una base de datos tradicional puede ser más adecuada.

Esta comparación puede ayudarte a decidir si utilizar las sesiones o no.

SesionesBase de datos
Accesoun almacenamiento aislado por chataccede a los mismos datos desde múltiples chats
Compartirlos datos son sólo utilizados por el botlos datos son utilizados por otros sistemas (por ejemplo, un servidor web conectado)
Formatocualquier objeto JavaScript: cadenas, números, matrices, etc.cualquier dato (binario, archivos, estructurado, etc)
Tamaño por chatpreferiblemente menos de ~3 MB por chatcualquier tamaño
Característica exclusivaRequerida por algunos plugins de grammY.Soporta transacciones de base de datos.

Esto no significa que las cosas no puedan funcionar si eliges sesiones/bases de datos por encima de las otras.

Por ejemplo, por supuesto que puedes almacenar grandes datos binarios en tu sesión. Sin embargo, tu bot no funcionaría tan bien como podría hacerlo de otro modo, por lo que recomendamos usar sesiones sólo cuando tengan sentido.

Cómo usar las sesiones

Puedes añadir soporte de sesión a grammY utilizando el middleware de sesión incorporado.

Ejemplo de uso

Aquí hay un ejemplo de bot que cuenta los mensajes que contienen un emoji de pizza 🍕:

import { Bot, Context, session, SessionFlavor } from "grammy";

// Definir la forma de nuestra sesión.
interface SessionData {
  pizzaCount: number;
}

// Tipo de flavor context para incluir las sesiones.
type MyContext = Context & SessionFlavor<SessionData>;

const bot = new Bot<MyContext>("");

// Instalar el middleware de sesión, y definir el valor inicial de la sesión.
function initial(): SessionData {
  return { pizzaCount: 0 };
}
bot.use(session({ initial }));

bot.command("hunger", async (ctx) => {
  const count = ctx.session.pizzaCount;
  await ctx.reply(`¡Tu nivel de hambre es ${count}!`);
});

bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++);

bot.start();
const { Bot, session } = require("grammy");

const bot = new Bot("");

// Instalar el middleware de sesión, y definir el valor inicial de la sesión.
function initial() {
  return { pizzaCount: 0 };
}
bot.use(session({ initial }));

bot.command("hunger", async (ctx) => {
  const count = ctx.session.pizzaCount;
  await ctx.reply(`¡Tu nivel de hambre es ${count}!`);
});

bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++);

bot.start();
import {
  Bot,
  Context,
  session,
  SessionFlavor,
} from "https://deno.land/x/grammy@v1.11.2/mod.ts";

// Definir la forma de nuestra sesión.
interface SessionData {
  pizzaCount: number;
}

// Tipo de flavor context para incluir las sesiones.
type MyContext = Context & SessionFlavor<SessionData>;

const bot = new Bot<MyContext>("");

// Instalar el middleware de sesión, y definir el valor inicial de la sesión.
function initial(): SessionData {
  return { pizzaCount: 0 };
}
bot.use(session({ initial }));

bot.command("hunger", async (ctx) => {
  const count = ctx.session.pizzaCount;
  await ctx.reply(`¡Tu nivel de hambre es ${count}!`);
});

bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++);

bot.start();

Nótese que también tenemos que ajustar el tipo de contexto para que la sesión esté disponible en él. El context flavor se llama SessionFlavor.

Datos de la sesión inicial

Cuando un usuario contacta por primera vez con tu bot, no hay datos de sesión disponibles para él. Por lo tanto, es importante que especifiques la opción initial para el middleware de sesión. Pasa una función que genere un nuevo objeto con datos de sesión iniciales para los nuevos chats.

// Crea un nuevo objeto que se utilizará como datos iniciales de la sesión.
function createInitialSessionData() {
  return {
    pizzaCount: 0,
    // más datos aquí
  };
}
bot.use(session({ initial: createInitialSessionData }));

Lo mismo pero mucho más corto:

bot.use(session({ initial: () => ({ pizzaCount: 0 }) }));

Compartir objetos

Asegúrate de crear siempre un objeto nuevo. No haga esto:

// PELIGRO, MAL, INCORRECTO, PARAR
const initialData = { pizzaCount: 0 }; // NO
bot.use(session({ initial: { pizzaCount: 0 } })); // EL MAL

Si se hiciera esto, varios chats podrían compartir el mismo objeto de sesión en la memoria. Por lo tanto, cambiar los datos de la sesión en un chat puede afectar accidentalmente a los datos de la sesión en el otro chat.

También puede omitir la opción initial por completo, aunque se aconseja no hacerlo. Si no la especifica, la lectura de ctx.session arrojará un error para los nuevos usuarios.

Claves de sesión

Esta sección describe una característica avanzada de la que la mayoría de la gente no tiene que preocuparse. Es posible que desee continuar con la sección sobre almacenamiento de sus datos.

Puedes especificar qué clave de sesión usar pasando una función llamada getSessionKey a las opcionesopen in new window. De esta manera, puedes cambiar fundamentalmente el funcionamiento del plugin de sesión. Por defecto, los datos se almacenan por chat. El uso de getSessionKey le permite almacenar los datos por usuario, o por combinación de usuario-chat, o como usted quiera. Aquí hay tres ejemplos:

// Almacena los datos por chat (por defecto).
function getSessionKey(ctx: Context): string | undefined {
  // Permite que todos los usuarios de un chat grupal compartan la misma sesión,
  // pero dar una privada independiente a cada usuario en los chats privados
  return ctx.chat?.id.toString();
}

// Almacena los datos por usuario.
function getSessionKey(ctx: Context): string | undefined {
  // Da a cada usuario su almacenamiento de sesión personal
  // (se compartirá en los grupos y en su chat privado)
  return ctx.from?.id.toString();
}

// Almacena los datos por combinación usuario-chat.
function getSessionKey(ctx: Context): string | undefined {
  // Dar a cada usuario su almacenamiento de una sesión personal por chat con el bot
  // (una sesión independiente para cada grupo y su chat privado)
  return ctx.from === undefined || ctx.chat === undefined
    ? undefined
    : `${ctx.from.id}/${ctx.chat.id}`;
}

bot.use(session({ getSessionKey }));
// Almacena los datos por chat (por defecto).
function getSessionKey(ctx) {
  // Permite que todos los usuarios de un chat grupal compartan la misma sesión,
  // pero dar una privada independiente a cada usuario en los chats privados
  return ctx.chat?.id.toString();
}

// Almacena los datos por usuario.
function getSessionKey(ctx) {
  // Da a cada usuario su almacenamiento de sesión personal
  // (se compartirá en los grupos y en su chat privado)
  return ctx.from?.id.toString();
}

// Almacena los datos por combinación usuario-chat.
function getSessionKey(ctx) {
  // Dar a cada usuario su almacenamiento de una sesión personal por chat con el bot
  // (una sesión independiente para cada grupo y su chat privado)
  return ctx.from === undefined || ctx.chat === undefined
    ? undefined
    : `${ctx.from.id}/${ctx.chat.id}`;
}

bot.use(session({ getSessionKey }));

Siempre que getSessionKey devuelva undefined, ctx.session estará undefined. Por ejemplo, el resolvedor de claves de sesión por defecto no funcionará para las actualizaciones de poll/poll_answer o inline_query porque no pertenecen a un chat (ctx.chat está undefined).

Claves de sesión y Webhooks

Cuando estés ejecutando tu bot con webhooks, deberías evitar usar la opción getSessionKey. Telegram envía los webhooks secuencialmente por chat, por lo que la resolución de la clave de sesión por defecto es la única implementación que garantiza no causar pérdida de datos.

Si debes usar la opción (que por supuesto sigue siendo posible), debes saber lo que estás haciendo. Asegúrese de entender las consecuencias de esta configuración leyendo el artículo este y especialmente este.

Almacenamiento de sus datos

En todos los ejemplos anteriores, los datos de la sesión se almacenan en su memoria RAM, por lo que tan pronto como su bot se detiene, todos los datos se pierden. Esto es conveniente cuando desarrollas tu bot o si ejecutas pruebas automáticas (no se necesita configurar la base de datos), sin embargo, es muy probable que no se desee en producción. En producción, querrás persistir tus datos, por ejemplo en un archivo, una base de datos, o algún otro almacenamiento.

Deberías utilizar la opción storage del middleware de sesión para conectarlo a tu almacén de datos. Puede que ya haya un adaptador de almacenamiento escrito para grammY que puedas utilizar (ver abajo, pero si no, normalmente sólo se necesitan 5 líneas de código para implementar uno tú mismo.

Lazy Sessions

Esta sección describe una optimización del rendimiento de la que la mayoría de la gente no tiene que preocuparse. Es posible que desee continuar con la sección sobre adaptadores de almacenamiento conocidos.

Las lazy sessions son una implementación alternativa de las sesiones que puede reducir significativamente el tráfico de la base de datos de tu bot al omitir operaciones de lectura y escritura superfluas.

Supongamos que tu bot está en un chat de grupo en el que no responde a los mensajes de texto normales, sino sólo a los comandos. Sin sesiones, esto sucedería:

  1. Se envía una actualización con un nuevo mensaje de texto a tu bot
  2. No se invoca ningún manejador, por lo que no se realiza ninguna acción
  3. El middleware se completa inmediatamente

En cuanto se instalan sesiones (por defecto, estrictas), que proporcionan directamente los datos de la sesión en el objeto de contexto, sucede lo siguiente

  1. La actualización con el nuevo mensaje de texto se envía a su bot
  2. Los datos de la sesión se cargan desde el almacenamiento de la sesión (por ejemplo, la base de datos)
  3. No se invoca ningún manejador, por lo que no se realiza ninguna acción
  4. Los datos idénticos de la sesión se escriben de nuevo en el almacenamiento de la sesión
  5. El middleware se completa, y ha realizado una lectura y una escritura en el almacenamiento de datos

Dependiendo de la naturaleza de tu bot, esto puede llevar a un montón de lecturas y escrituras superfluas. Las sesiones perezosas te permiten saltarte los pasos 2. y 4. si resulta que ningún manejador invocado necesita datos de sesión. En ese caso, no se leerá ningún dato del almacén de datos, ni se escribirá en él.

Esto se consigue interceptando el acceso a ctx.session. Si no se invoca ningún gestor, nunca se accederá a ctx.session. Las sesiones perezosas utilizan esto como un indicador para evitar la comunicación con la base de datos.

En la práctica, en lugar de tener los datos de la sesión disponibles en ctx.session, ahora tendrá una promesa de los datos de la sesión disponible en ctx.session.

// Sesiones por defecto (sesiones estrictas)
bot.command("settings", (ctx) => {
  // `session` es el dato de la sesión
  const session = ctx.session;
});

// Lazy sessions
bot.command("settings", async (ctx) => {
  // `promise` es una Promise de los datos de la sesión, y
  const promise = ctx.session;
  // `session` es el dato de la sesión
  const session = await ctx.session;
});

Si nunca accedes a ctx.session, no se realizará ninguna operación, pero en cuanto accedas a la propiedad session del objeto contexto, se lanzará la operación de lectura. Si nunca se lanza la lectura (o se asigna directamente un nuevo valor a ctx.session), sabemos que tampoco necesitaremos escribir ningún dato de vuelta, porque no hay forma de que haya sido alterado. En consecuencia, nos saltamos también la operación de escritura. Como resultado, conseguimos un mínimo de operaciones de lectura y escritura, pero puedes usar la sesión casi idéntica a la anterior, sólo con unas pocas palabras clave async y await mezcladas en tu código.

Entonces, ¿qué es necesario para utilizar sesiones perezosas en lugar de las sesiones por defecto (estrictas)? Principalmente tienes que hacer tres cosas:

  1. Flavor tu contexto con LazySessionFlavor en lugar de SessionFlavor. Funcionan de la misma manera, sólo que ctx.session se envuelve dentro de una promesa para la variante perezosa.
  2. Usa lazySession en lugar de session para registrar tu middleware de sesión.
  3. Pon siempre una promesa en línea await ctx.session en lugar de ctx.session en todas las partes de tu middleware, tanto para lecturas como para escrituras. No te preocupes: puedes await la promesa con tus datos de sesión tantas veces como quieras, pero siempre te referirás al mismo valor, así que nunca habrá lecturas duplicadas para una actualización.

Ten en cuenta que con las sesiones lazy, puedes asignar tanto objetos como promesas de objetos a ctx.session. Si estableces que ctx.session sea una promesa, se esperará antes de escribir los datos de vuelta al almacenamiento de datos. Esto permitiría el siguiente código:

bot.command("reset", (ctx) => {
  // Mucho más corto que tener que `esperar ctx.session` primero:
  ctx.session = ctx.session.then((stats) => {
    stats.counter = 0;
  });
});

Se puede argumentar bien que usar explícitamente await es preferible a asignar una promesa a ctx.session, la cuestión es que podrías hacer esto si te gusta más ese estilo por alguna razón.

Plugins que necesitan sesiones

Los desarrolladores de plugins que hacen uso de ctx.session siempre deben permitir a los usuarios pasar SessionFlavor | LazySessionFlavor y por lo tanto soportar ambos modos. En el código del plugin, simplemente espere ctx.session todo el tiempo: si se pasa un objeto no prometido, éste simplemente se evaluará a sí mismo, por lo que efectivamente sólo se escribe código para sesiones perezosas y así se soportan sesiones estrictas automáticamente.

Adaptadores de almacenamiento conocidos

Por defecto, las sesiones serán almacenadas en su memoria por el adaptador de almacenamiento incorporado. También puedes utilizar las sesiones persistentes que grammY ofrece gratuitamente, o conectarte a almacenamientos externos.

Así es como puedes instalar uno de los adaptadores de almacenamiento desde abajo.

const storageAdapter = ... // depende de la configuración

bot.use(session({
  initial: ...
  storage: storageAdapter,
}));

RAM (por defecto)

Por defecto, todos los datos se almacenan en la memoria RAM. Esto significa que todas las sesiones se pierden tan pronto como tu bot se detenga.

Puedes usar la clase MemorySessionStorage (API Referenceopen in new window) del paquete central de grammY si quieres configurar más cosas sobre el almacenamiento de datos en la RAM.

bot.use(session({
  initial: ...
  storage: new MemorySessionStorage() // también el valor por defecto
}));

Free Storage

El almacenamiento gratuito está pensado para ser utilizado en proyectos de aficionados. Las aplicaciones a escala de producción deberían alojar su propia base de datos. La lista de integraciones soportadas de soluciones de almacenamiento externo está aquí abajo.

Un beneficio de usar grammY es que obtienes acceso a almacenamiento gratuito en la nube. No requiere ninguna configuración - toda la autenticación se hace usando tu token de bot. ¡Echa un vistazo a el repositorioopen in new window!

Es muy fácil de usar:

import { freeStorage } from "@grammyjs/storage-free";

bot.use(session({
  initial: ...
  storage: freeStorage<SessionData>(bot.token),
}));
const { freeStorage } = require("@grammyjs/storage-free");

bot.use(session({
  initial: ...
  storage: freeStorage(bot.token),
}));
import { freeStorage } from "https://deno.land/x/grammy_storage/free/mod.ts";

bot.use(session({
  initial: ...
  storage: freeStorage<SessionData>(bot.token),
}));

Ya está. Tu bot ahora utilizará un almacenamiento de datos persistente.

Aquí hay un ejemplo de bot completo que puedes copiar para probarlo.

import { Bot, Context, session, SessionFlavor } from "grammy";
import { freeStorage } from "@grammyjs/storage-free";

// Definir la estructura de la sesión.
interface SessionData {
  count: number;
}
type MyContext = Context & SessionFlavor<SessionData>;

// Crear el bot y registrar el middleware de sesión.
const bot = new Bot<MyContext>(""); // <-- pon tu token de bot entre los ""

bot.use(session({
  initial: () => ({ count: 0 }),
  storage: freeStorage<SessionData>(bot.token),
}));

// Utilizar datos de sesión persistentes en los manejadores de actualización.
bot.on("message", async (ctx) => {
  ctx.session.count++;
  await ctx.reply(`Message count: ${ctx.session.count}`);
});

bot.catch((err) => console.error(err));
bot.start();
const { Bot, session } = require("grammy");
const { freeStorage } = require("@grammyjs/storage-free");

// Crear el bot y registrar el middleware de sesión.
const bot = new Bot(""); // <-- pon tu token de bot entre los ""

bot.use(session({
  initial: () => ({ count: 0 }),
  storage: freeStorage(bot.token),
}));

// Utilizar datos de sesión persistentes en los manejadores de actualización.
bot.on("message", async (ctx) => {
  ctx.session.count++;
  await ctx.reply(`Message count: ${ctx.session.count}`);
});

bot.catch((err) => console.error(err));
bot.start();
import {
  Bot,
  Context,
  session,
  SessionFlavor,
} from "https://deno.land/x/grammy@v1.11.2/mod.ts";
import { freeStorage } from "https://deno.land/x/grammy_storage/free/mod.ts";

// Definir la estructura de la sesión.
interface SessionData {
  count: number;
}
type MyContext = Context & SessionFlavor<SessionData>;

// Crear el bot y registrar el middleware de sesión.
const bot = new Bot<MyContext>(""); // <-- pon tu token de bot entre los ""

bot.use(session({
  initial: () => ({ count: 0 }),
  storage: freeStorage<SessionData>(bot.token),
}));

// Utilizar datos de sesión persistentes en los manejadores de actualización.
bot.on("message", async (ctx) => {
  ctx.session.count++;
  await ctx.reply(`Message count: ${ctx.session.count}`);
});

bot.catch((err) => console.error(err));
bot.start();

Soluciones de almacenamiento externo

Mantenemos una lista de adaptadores de almacenamiento oficiales que le permiten almacenar sus datos de sesión en diferentes lugares. Cada uno de ellos requerirá que te registres en un proveedor de alojamiento, o que alojes tu propia solución de almacenamiento.

¿Su almacenamiento no es compatible? No hay problema.

Crear un adaptador de almacenamiento personalizado es extremadamente sencillo. La opción storage funciona con cualquier objeto que se adhiera a esta interfazopen in new window, por lo que puedes conectarte a tu almacenamiento con sólo unas líneas de código.

Si has publicado tu propio adaptador de almacenamiento, no dudes en editar esta página y enlazarla aquí, para que otras personas puedan utilizarla.

Todos los adaptadores de almacenamiento pueden instalarse de la misma manera. En primer lugar, debes buscar el nombre del paquete del adaptador que hayas elegido. Por ejemplo, el adaptador de almacenamiento para Supabase se llama supabase.

En Node.js, puedes instalar los adaptadores a través de npm i @grammyjs/storage-<nombre>. Por ejemplo, el adaptador de almacenamiento para Supabase puede instalarse mediante npm i @grammyjs/storage-supabase.

En Deno, todos los adaptadores de almacenamiento se publican en el mismo módulo de Deno. A continuación, puede importar el adaptador que necesite desde su sub-ruta en https://deno.land/x/grammy_storages/<adaptador>/src/mod.ts. Por ejemplo, el adaptador de almacenamiento para Supabase puede importarse desde https://deno.land/x/grammy_storages/supabase/src/mod.ts.

Consulta los repositorios respectivos sobre cada configuración individual. Contienen información sobre cómo conectarlos a tu solución de almacenamiento.

Resumen del plugin

Este plugin está incorporado en el núcleo de grammY. No necesitas instalar nada para usarlo. Simplemente importa todo desde el propio grammY.

Además, tanto la documentación como la referencia de la API de este plugin están unificadas con el paquete del núcleo.