grammyjs / storages

Storage adapters for grammY sessions.
48 stars 23 forks source link

(redis, question | docs) How to work with external storage to initialize the initial data of the user's session? #226

Closed koddr closed 2 weeks ago

koddr commented 2 weeks ago

It is not very clear how to work with data from external storage to initialize the initial data of the user's session.

For example, when working with Redis adapter and lazySession:

import { Bot, Context, LazySessionFlavor, Enhance, lazySession, enhanceStorage } from 'grammy'
import { RedisAdapter } from '@grammyjs/storage-redis'
import { Redis } from 'ioredis'

// ...

interface IUserSessionData {
  counter: number
}

type TUserContext = Context & LazySessionFlavor<IUserSessionData>

const bot = new Bot<TUserContext>(process.env.TELEGRAM_BOT_TOKEN)

const redis = new Redis({
  host: process.env.REDIS_HOST,
  password: process.env.REDIS_PASSWORD,
  username: process.env.REDIS_USERNAME ? process.env.REDIS_USERNAME : 'default',
  port: process.env.REDIS_PORT ? Number(process.env.REDIS_PORT) : 6379,
  lazyConnect: true,
})

const enhanced = new RedisAdapter<Enhance<IUserSessionData>>({ instance: redis })

const storage = enhanceStorage({
  storage: enhanced,
  millisecondsToLive: 30 * 60 * 1000,
})

bot.use(
  lazySession({
    initial: () => ({ /* ??? */ }),
    getSessionKey: (ctx: Omit<TUserContext, 'session'>) => ctx.from?.id.toString(),
    storage,
  }),
)

// ...

What do I need to do so that when creating middleware with session settings, I can pick up the current data? Now, every time the bot is rebooted, the session is reset to those specified in initial block in lazySession.

Since the session key in Redis is equal to the user ID in Telegram (according to the snippet above), in order to programmatically insert all data from Redis into the ctx.session, I need to make a separate function, which should be at the beginning of each bot.*(...), etc.?

How would I improve this?

It would be convenient if the session initialization method could retrieve data from the remote storage itself. And if there is no data (or the storage is unavailable), then take the specified fallback values.

Or let the developer configure the function himself, which will take data from Redis and put it in the ctx.session of the current bot user:

bot.use(
  lazySession({
    initial: () => ({ counter: 0 }), // <-- fallback values here
    getInitialSessionData: (ctx) => myFunc(ctx), // <-- send context with current Telegram user ID to get Redis record by key
    getSessionKey: (ctx: Omit<TUserContext, 'session'>) => ctx.from?.id.toString(),
    storage,
  }),
)

I think, such an improvement would be useful for everyone (especially for beginners) and would make the bot code much easier.

KnorpelSenf commented 2 weeks ago

initial only needs to provide data that should be used for new chats, i.e. those without existing session data.

The logic inside the session plugin can be simplified to this:

const key = getSessionKey(ctx)
const sessionData = await storage.read(key) ?? initial()
await next() // call downstream handlers
await storage.write(key, sessionData)

So I think it already works exactly like you expect? Or perhaps I'm misunderstanding what you're trying to do, then I need you to elaborate.

koddr commented 2 weeks ago

@KnorpelSenf thanks for reply.

Oh, wait… It looks like there was some mistake on my side.

When connecting to Redis, an error occurred that I didn't catch, so I always returned null to the session request by key.

In any case, it would be cool to add this explanation of the work to the description of the sessions with Redis in the grammY documentation so that the process is as transparent as possible 😊

KnorpelSenf commented 1 week ago

@koddr do you mean that the redis session adapter returns null on error?

koddr commented 1 week ago

@KnorpelSenf I have conducted several clean tests and have not encountered a repeat of this bug. So it wasn't about him, and the adapter for Redis is working correctly.

KnorpelSenf commented 1 week ago

Awesome!