grammyjs / conversations

Conversational interfaces for grammY.
https://grammy.dev/plugins/conversations
MIT License
53 stars 17 forks source link

functions added to the context via middleware disappear #97

Closed devnbp closed 9 months ago

devnbp commented 9 months ago

im trying to use grammy with nestjs, here im injecting service provider into grammy logic, and i found that custom data added to the conversation context have no functions

interface IBotConfig {
  someService: SomeService;
}

export type BaseContext = Context & ConversationFlavor & { config: IBotConfig };
export type BaseConversation = Conversation<BaseContext>;

export async function someConv(
  conversation: BaseConversation,
  ctx: BaseContext,
) {
  const msgCtx = await conversation.waitFor(':text');

  await conversation.external(async () => {
    console.log(ctx.config.someService.someFunc); // undefined, someService object is available, but for some reason there are no functions any more
    console.log(msgCtx.config.someService.someFunc); // but here you can access functions with no problem
  });
}

@Injectable()
export class MyLogic implements OnModuleInit {
  private someService: SomeService;
  constructor(private readonly moduleRef: ModuleRef) {}

  onModuleInit() {
    this.someService= this.moduleRef.get(SomeService);
  }

  composer() {
    const main = new Composer<BaseContext>();

    main.use(async (ctx, next) => {
      ctx.config = {
        someService: this.someService,
      };
      await next();
    });

    main.use(session({ initial: () => ({}) }));
    main.use(conversations());
    main.use(createConversation(someConv));

    const privateMain = main.chatType('private');

    privateMain.command('test', async (ctx) => {
      await ctx.conversation.enter('someConv')
    });

    return main;
  }
}

console.log(ctx.config.someService) will be like:

{ a: 1, b: undefined }

but console.log(msgCtx.config.someService) will be:

{ a: 1, b: [Function: b] }

does it work as intended or its bug?

KnorpelSenf commented 9 months ago

It works as intended, although this clearly is one of the ugliest design decisions in the grammY ecosystem. Functions cannot be stored on the context object, so they cannot be restored after a wait call. You need to use conversation.run.

You can also try installing them on the context object directly, rather than on nested properties, as the plugin sort of tries to remap them from the latest context object. I'm not too sure that this works reliably, though.

devnbp commented 9 months ago

I have little problem with conversation.run, as I understand the usage should looks like:

// conversation file
export async function someConv(
  conversation: BaseConversation,
  ctx: BaseContext,
) {
  await conversation.run(async (ctx, next) => {
    ctx.config = {
      someService: this.someService, // how can i pass my service here?
    };
    await next();
  });

  const msgCtx = await conversation.waitFor(':text');

  await conversation.external(async () => {
    console.log(ctx.config.someService.someFunc);
    console.log(msgCtx.config.someService.someFunc);
  });
}

but the problem is nestjs module system, you cant use service providers out of module context, so i cant pass existing service object into scope of this code (or maybe idk how) but i ended up with workaround like this:

// conversation file
export function someConv(someService: SomeService) {
  const conv = async function (
    conversation: BaseConversation,
    ctx: BaseContext,
  ) {
    const msgCtx = await conversation.waitFor(':text');

    await conversation.external(async () => {
      console.log(someService.someFunc);
    });
  }

  return createConversation(conv, 'someConv');
}
import { someConv } from './conversation';

@Injectable()
export class MyLogic implements OnModuleInit {
  private someService: SomeService;
  constructor(private readonly moduleRef: ModuleRef) {}

  onModuleInit() {
    this.someService= this.moduleRef.get(SomeService);
  }

  composer() {
    const main = new Composer<BaseContext>();

    main.use(session({ initial: () => ({}) }));
    main.use(conversations());
    main.use(someConv(this.someService));

    const privateMain = main.chatType('private');

    privateMain.command('test', async (ctx) => {
      await ctx.conversation.enter('someConv')
    });

    return main;
  }
}
KnorpelSenf commented 9 months ago

the problem is nestjs module system

That is true.

You workaround works well.

A different solution would have been to make the conversation function a lambda. That way, it retains the this context from your NestJS class, so you would be able to access the service you like.

I am not saying that you need to change anything, though :)

Should we close this?

devnbp commented 9 months ago

Yeah, ty for details