Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: combine filters in .wait() method #11

Closed
mi3lix9 opened this issue Jun 28, 2022 · 15 comments · Fixed by #122
Closed

feat: combine filters in .wait() method #11

mi3lix9 opened this issue Jun 28, 2022 · 15 comments · Fixed by #122
Labels
v2 Needs to be fixed before v2

Comments

@mi3lix9
Copy link

mi3lix9 commented Jun 28, 2022

Currently, it's not possible to combine filters in one code. Instead, every filter has its own function .waitFor(), .waitFrom(), etc...

I suggest adding params to .wait() function with filters, like this:

await conversation.wait({
  for: ":text",
  from: ctx.from?.id
});
@KnorpelSenf
Copy link
Member

This is a good suggestion to specify multiple conditions that all need to be true. How would you suggest that people can specify that one of several conditions should be true? For example, I'd like to wait either for a photo, or the /cancel command.

@mi3lix9
Copy link
Author

mi3lix9 commented Jul 20, 2022

This is a good suggestion to specify multiple conditions that all need to be true. How would you suggest that people can specify that one of several conditions should be true? For example, I'd like to wait either for a photo, or the /cancel command.

I think this may add more complexity so I suggest to filter using ctx or something like this:

ctx = await conversation.wait({
  for: [":photo", "/cancel"],
  from: ctx.from?.id
});

if (ctx.message.text === "/cancel") {
  return;
}

@mi3lix9 mi3lix9 changed the title feat: combile filters in .wait() method feat: combine filters in .wait() method Jul 20, 2022
@KnorpelSenf
Copy link
Member

ctx = await conversation.wait({
  for: [":photo", "/cancel"],
  from: ctx.from?.id
});

if (ctx.message.text === "/cancel") {
  return;
}

I'm afraid that this won't work well. It implies that the plugin should check if the strings start with / and then perform a wildly different logic than for everything else (which will be a filter query).

Most importantly, it is not a general way of combining the predicates. For example, we also have waitUntil which people can use to pass custom predicates. Your suggestion does not offer a way to work with those.

I agree that combining wait predicates is a desirable thing to do, and I'd love to have a good abstraction.

Until now, we can already use conversation.waitUntil(ctx => ctx.hasCommand("start") || ctx.hasQuery(":photo")). This can be combined in arbitrarily complex ways using AND and OR and what not. However, it requires people to abstract their own lambda, so it looks like it's not as clean as the good old middleware trees that poeple could use to combine middleware filtering (before we had conversations).

I do not consider this issue as blocking. In other words, if we find a great way of combining things, I'd be happy to implement it. However, if we don't, I'll release 1.0 anyway, but in that case I'm of course open to adding it for a future feature release if we happen to find the right abstraction after 1.0.

@mi3lix9
Copy link
Author

mi3lix9 commented Jul 21, 2022

I've just known about waitUltil(), I think if I knew it earlier I wouldn't open this issue, but after I see your examples I think we can let wait() for simple filters and waitUntil for complex ones

I suggest postponing this feature after 1.0 release, we may get better ideas :)

@KnorpelSenf
Copy link
Member

@mi3lix9 what do you think about this?

const textOrPhotoMessage = await Promise.race([
  conversation.waitFor(':text'),
  conversation.waitFor(':photo'),
])

This does not work yet, but it is feasible to implement it. It is the logical OR for any operations.

Analogously, it would be great to be able to do this:

const text = await Promise.all([
  conversation.waitFor(':text'),
  conversation.waitFrom(ctx.from?.id),
])

I am not sure if this is possible to implement, but I would not mind to try. It is the logical AND for any operations.

If we want, we could add aliases to make it easier to use, such as conversation.some and conversation.every or perhaps conversation.any and conversation.all.

@mi3lix9
Copy link
Author

mi3lix9 commented Aug 2, 2022

I like it, it is readable and easy to use,
With alias will it be like this?

const textOrPhotoMessage = await conversation.any([
  conversation.waitFor(':text'),
  conversation.waitFor(':photo'),
])
const text = await conversation.all([
  conversation.waitFor(':text'),
  conversation.waitFrom(ctx.from?.id),
])

It's easy also to be nested too

const textOrPhotoFromUser = await conversation.all([
  conversation.waitFrom(ctx.from?.id),
  conversation.any([
    conversation.waitFor(':text'),
    conversation.waitFor(':photo'),
])

@KnorpelSenf
Copy link
Member

Correct, that's exactly what I meant.

@mi3lix9
Copy link
Author

mi3lix9 commented Aug 2, 2022

It would be cool if we can use forms also
I don't know if it's possible but maybe something like this:

const text = await conversation.all([
  conversation.form.text(),
  conversation.waitFrom(ctx.from?.id),
])

@KnorpelSenf
Copy link
Member

Yes, this will automatically be possible if we implement the first suggestions. Forms are just syntactic sugar for wait calls with filters, so anything that works with wait calls will also work with forms without any changes.

@mi3lix9
Copy link
Author

mi3lix9 commented Aug 6, 2022

I tried this piece of code

const text = await Promise.all([
  conversation.waitFor(':text'),
  conversation.waitFrom(ctx.from?.id),
])

And it doesn't work

It behaves like we haven't added Promise.add at all

it needs two updates to work

@KnorpelSenf
Copy link
Member

Exactly. That's what I meant when I said:

This does not work yet, but it is feasible to implement it.

:)

@mi3lix9
Copy link
Author

mi3lix9 commented Sep 5, 2022

I compared my idea with yours, and still see your way is great and easy to read.
I like conversation.any([]) & conversation.all([])

@KnorpelSenf KnorpelSenf removed the help wanted Extra attention is needed label Sep 25, 2022
@WcaleNieWolny
Copy link

for anyone interested: I wrote a very simple work around to combine the following

conversation.waitFrom() and await conversation.waitFor('message:text')

function waitFromMsg(
  ctx: Conversation<SessionContext>,
  user: number,
): Promise<Filter<SessionContext, 'message:text'> & { from: User }> {
  const predicate = (ctx: SessionContext): ctx is Filter<SessionContext, 'message:text'> & { from: User } =>
    ctx.from?.id === user && grammyContext.has.filterQuery('message:text')(ctx)
  return ctx.waitUntil(predicate)
}

@KnorpelSenf
Copy link
Member

Context.has.filterQuery("message:text")(ctx) can be simplified to ctx.has("message:text"). Alternatively, you can pull it out into a variable hasText and only call hasText(ctx) which saves a few CPU cycles.

@KnorpelSenf
Copy link
Member

KnorpelSenf commented Sep 4, 2024

With the unreleased version 2.0, this suggestion is already possible.

const textOrPhotoMessage = await Promise.race([
  conversation.waitFor(':text'),
  conversation.waitFor(':photo'),
])

However, if you send a photo, the waitFor(":text") call will be left hanging. As soon as the next text message arrives, it will resolve. Due to the way race works, it will have no effect, thus effectively capturing and suppressing the next text message. That is undesired.

We can consider making wait calls cancellable via a cancellable promise. This would enable us to implement conversation.any which takes care of cancelling all other wait calls as soon as one of them resolves.

With v2, every update is only returned from a single wait call. There is no parallel resolving of wait calls. This means that the following suggestion becomes impossible (or rather, it means something else now).

const text = await Promise.all([
  conversation.waitFor(':text'),
  conversation.waitFrom(ctx.from?.id),
])

Instead, we can consider returning a promise with extra methods that lets people narrow down the update further. Something based on chaining like conversation.waitFor(":text").andFor(":forward_origin") could be used to wait for forwarded text messages. However, I am not in favour of adding more methods to promises, so I would ideally like to see a different proposal. Adding a .cancel to the promises is already something I'm not super comfortable with.

@KnorpelSenf KnorpelSenf added the v2 Needs to be fixed before v2 label Sep 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v2 Needs to be fixed before v2
Projects
No open projects
Status: To do
Development

Successfully merging a pull request may close this issue.

3 participants