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 nube en la nube de Telegram. Como resultado, hay algunas cosas que no puedes hacer con los bots:
- No puedes acceder a los mensajes antiguos que recibió tu bot.
- No puedes acceder a los mensajes antiguos que tu bot envió.
- No puedes obtener una lista de todos los chats con tu bot.
- 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
para cada actualización. El plugin instalado hará algo antes y después de que nuestros manejadores sean llamados:
- 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 - Nuestro middleware se ejecuta. Podemos leer
ctx
para inspeccionar qué valor estaba en la base de datos. Por ejemplo, si se envía un mensaje al chat con el identificador.session 424242
, seríactx
mientras se ejecuta nuestro middleware (al menos con el estado de la base de datos de ejemplo anterior). También podemos modificar.session = { pizza Count: 24 } ctx
arbitrariamente, por lo que podemos añadir, eliminar y cambiar campos a nuestro gusto..session - 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
después de que el middleware termine de ejecutarse, se guardará en la base de datos..session
Como resultado, ya no tenemos que preocuparnos de comunicarnos con el almacenamiento de datos. Simplemente modificamos los datos en ctx
, 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.
Sesiones | Base de datos | |
---|---|---|
Acceso | un almacenamiento aislado por chat | accede a los mismos datos desde múltiples chats |
Compartir | los datos son sólo utilizados por el bot | los datos son utilizados por otros sistemas (por ejemplo, un servidor web conectado) |
Formato | cualquier objeto JavaScript: cadenas, números, matrices, etc. | cualquier dato (binario, archivos, estructurado, etc) |
Tamaño por chat | preferiblemente menos de ~3 MB por chat | cualquier tamaño |
Característica exclusiva | Requerida 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 Session
.
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
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 get
a las opciones. De esta manera, puedes cambiar fundamentalmente el funcionamiento del plugin de sesión. Por defecto, los datos se almacenan por chat. El uso de get
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 get
devuelva undefined
, ctx
estará undefined
. Por ejemplo, el resolvedor de claves de sesión por defecto no funcionará para las actualizaciones de poll
/poll
o inline
porque no pertenecen a un chat (ctx
está undefined
).
Claves de sesión y Webhooks
Cuando estés ejecutando tu bot con webhooks, deberías evitar usar la opción get
. 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:
- Se envía una actualización con un nuevo mensaje de texto a tu bot
- No se invoca ningún manejador, por lo que no se realiza ninguna acción
- 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
- La actualización con el nuevo mensaje de texto se envía a su bot
- Los datos de la sesión se cargan desde el almacenamiento de la sesión (por ejemplo, la base de datos)
- No se invoca ningún manejador, por lo que no se realiza ninguna acción
- Los datos idénticos de la sesión se escriben de nuevo en el almacenamiento de la sesión
- 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
. Si no se invoca ningún gestor, nunca se accederá a ctx
. 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
, ahora tendrá una promesa de los datos de la sesión disponible en ctx
.
// 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
, 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
), 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:
- Flavor tu contexto con
Lazy
en lugar deSession Flavor Session
. Funcionan de la misma manera, sólo queFlavor ctx
se envuelve dentro de una promesa para la variante perezosa..session - Usa
lazy
en lugar deSession session
para registrar tu middleware de sesión. - Pon siempre una promesa en línea
await ctx
en lugar de.session ctx
en todas las partes de tu middleware, tanto para lecturas como para escrituras. No te preocupes: puedes.session 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
. Si estableces que ctx
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
, 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
siempre deben permitir a los usuarios pasar Session
y por lo tanto soportar ambos modos. En el código del plugin, simplemente espere ctx
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 Memory
(API Reference) 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 repositorio!
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.
- Supabase: https://
github .com /grammyjs /storage -supabase - Deta.sh Base: https://
github .com /grammyjs /storage -deta - Google Firestore (Node.js-only): https://
github .com /grammyjs /storage -firestore - Files: https://
github .com /grammyjs /storages /tree /main /packages /file - MongoDB: https://
github .com /grammyjs /storages /tree /main /packages /mongodb - Redis: https://
github .com /grammyjs /storages /tree /main /packages /redis - PostgreSQL: https://
github .com /grammyjs /storages /tree /main /packages /psql - TypeORM (solo para Node.js): https://
github .com /grammyjs /storages /tree /main /packages /typeorm - DenoDB (solo para Deno): https://github.com/grammyjs/storages/tree/main/packages/denodb
- Prisma (solo para Node.js): https://github.com/grammyjs/storages/tree/main/packages/prisma
¿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 interfaz, 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
. Por ejemplo, el adaptador de almacenamiento para Supabase puede instalarse mediante npm i @grammyjs
.
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://
. Por ejemplo, el adaptador de almacenamiento para Supabase puede importarse desde https://
.
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.