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

Safe deferred responding #546

Merged
merged 13 commits into from
Mar 5, 2022
Merged

Conversation

lukellmann
Copy link
Member

@lukellmann lukellmann commented Mar 1, 2022

The problem

Right now it is possible to break Kord's type system when using deferred interaction responses, here is an example for this:

val response: EphemeralMessageInteractionResponseBehavior = interaction.deferEphemeralMessage()

// response.edit { /* ... */ }  <- using this before followup would make it safe

val followup: PublicFollowupMessage = response.followUp { /* ... */ }

followup.delete() // this line will throw an exception, it tries to delete the ephemeral original interaction response

The reason for this is that when a followup message is sent after an inital interaction response with type DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE (deferEphemeralMessage()) but before using Edit Original Interaction Response to stop the loading animation (edit()), it takes the place of the original interaction response and ignores the visibility of the followup completely.
This way you end up with an FollowupMessage object (with a possibly wrong visibility) when you should actually have an MessageInteractionResponseBehavior.

The solution

To avoid these unclear followup semantics for deferred interaction responses, this PR adds an extra step when working with them. It will now be always required to call the Edit Original Interaction Response endpoint before being able to use followups.

The example from above will now look something like this:

// the `deferred` handle has no support for followups
val deferred: DeferredEphemeralMessageInteractionResponseBehavior = interaction.deferEphemeralResponse()

// sending the original response will return a handle that supports followups
val response: EphemeralMessageInteractionResponse = deferred.respond { /* ... */ }

val followup: PublicFollowupMessage = response.followUpPublic { /* ... */ }

followup.delete() // this now works

As you can see this is very similar to the first example but now the user is forced to call the equivalent for response.edit { /* ... */ } to be able to send followups.

Changes made to make it safe

  • deprecate defer[Public|Ephemeral]Message() and replace them with defer[Public|Ephemeral]Response() that return the new Deferred[Public|Ephemeral]MessageInteractionResponseBehavior type
  • add Deferred[Public|Ephemeral]MessageInteractionResponseBehavior.respond() that essentially does the same as MessageInteractionResponse.edit() (also takes the same builder type) but has a more accurate name, this 'edit' actually stops the loading animation and responds with the initial response message
  • the public variation of DeferredMessageInteractionResponseBehavior also has a delete() method that just stops the loading animation without responding, this is not available for ephemerals
  • the new DeferredMessageInteractionResponseBehavior does not support followup messages but extends InteractionResponseBehavior (the type the followup functions were defined on), deprecate those and move them into the new type FollowupableInteractionResponseBehavior instead, this is now the supertype of every InteractionResponseBehavior that still supports followups

Other changes

  • add MessageInteractionResponse (and public/ephemeral variation) that extends MessageInteractionResponseBehavior and also holds a Message (analogous to FollowupMessage and FollowupMessageBehavior)
  • MessageInteractionResponseBehavior.edit() now returns the new MessageInteractionResponse instead of Message (the Message is still available via the message property)
  • change the return type of PublicMessageInteractionResponseBehavior.delete() from Unit to FollowupableInteractionResponseBehavior, it can still be used to send followups, operations on the original message are no longer possible
  • documentation

@lukellmann lukellmann self-assigned this Mar 1, 2022
@lukellmann
Copy link
Member Author

Two things I'm not sure about:

The naming for FollowupableInteractionResponseBehavior: it does not really sound nice but it describes what it does. I would love to hear other suggestions.

Should PublicMessageInteractionResponseBehavior.delete() and DeferredPublicMessageInteractionResponseBehavior.delete() actually return a handle for followup messages? Or is this a bad idea?

@lukellmann lukellmann requested a review from HopeBaron March 1, 2022 23:30
Copy link
Member

@HopeBaron HopeBaron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems good overall; however you would like to rename FollowUpX to followup to remain consistent with the class name.

@lukellmann
Copy link
Member Author

lukellmann commented Mar 2, 2022

seems good overall; however you would like to rename FollowUpX to followup to remain consistent with the class name.

The Discord documentation uses the spelling "Followup Messages" (see here) as a noun. It does not use it as a verb anywhere but I would say that the spelling would be "to follow up" for a verb, which would be followUp in camel case.
So I'm not sure about that.

@lukellmann
Copy link
Member Author

Two things I'm not sure about:

The naming for FollowupableInteractionResponseBehavior: it does not really sound nice but it describes what it does. I would love to hear other suggestions.

Should PublicMessageInteractionResponseBehavior.delete() and DeferredPublicMessageInteractionResponseBehavior.delete() actually return a handle for followup messages? Or is this a bad idea?

What are your thoughts on this?

@HopeBaron
Copy link
Member

  1. We should either pick followup or FollowUp for the class and the function for discoverability; go with the first as per the docs
  2. I can't think of a better name as I can't think of better terms.
  3. I don't really get what you mean.

@lukellmann
Copy link
Member Author

  1. We should either pick followup or FollowUp for the class and the function for discoverability; go with the first as per the docs
  2. I can't think of a better name as I can't think of better terms.
  3. I don't really get what you mean.
  1. Alright, you convinced me, I'll update this
  2. naming is hard...
  3. I changed the return values of these delete methods to FollowupableInteractionResponseBehavior, the returned object can still be used for followups but not to interact with the original response and I'm wondering if this is a good idea.

@HopeBaron
Copy link
Member

No, there is no need to do that since the user is expected to keep the og object for interactions just leave it as Unit iirc

@lukellmann
Copy link
Member Author

No, there is no need to do that since the user is expected to keep the og object for interactions just leave it as Unit iirc

Now I remember why I introduced it in the first place:
If you defer a response you can't directly followup (that's the whole point of this PR). But if you defer a public response and then delete it (this will just stop the loading) you can still send followups but you need another type for this, DeferredMessageInteractionResponseBehavior does not support followups:

val deferred = interaction.deferPublicResponse()
val followupable = deferred.delete()
followupable.createEphemeralFollowup { /* ... */ }

I then changed it for PublicMessageInteractionResponseBehavior for consistency, but it's not necessary there.

So there is actually a need to do this but tbh this is kind of an edge case.

@lukellmann
Copy link
Member Author

No, there is no need to do that since the user is expected to keep the og object for interactions just leave it as Unit iirc

Now I remember why I introduced it in the first place: If you defer a response you can't directly followup (that's the whole point of this PR). But if you defer a public response and then delete it (this will just stop the loading) you can still send followups but you need another type for this, DeferredMessageInteractionResponseBehavior does not support followups:

val deferred = interaction.deferPublicResponse()
val followupable = deferred.delete()
followupable.createEphemeralFollowup { /* ... */ }

I then changed it for PublicMessageInteractionResponseBehavior for consistency, but it's not necessary there.

So there is actually a need to do this but tbh this is kind of an edge case.

I reverted it for PublicMessageInteractionResponseBehavior but kept it for DeferredMessageInteractionResponseBehavior for now.

@lukellmann lukellmann requested a review from HopeBaron March 3, 2022 22:53
@HopeBaron
Copy link
Member

👍

@HopeBaron
Copy link
Member

I think we can use FollowupPermittedInteractionResponse would be a better name? Re: Followupable naming.

@lukellmann
Copy link
Member Author

I think we can use FollowupPermittedInteractionResponse would be a better name? Re: Followupable naming.

Sounds good 👍

@lukellmann
Copy link
Member Author

I went with FollowupPermittingInteractionResponseBehavior

@HopeBaron HopeBaron merged commit 21e329f into kordlib:0.8.x Mar 5, 2022
@lukellmann lukellmann deleted the changes/safe-responding branch March 5, 2022 21:37
@lukellmann lukellmann removed the request for review from HopeBaron December 20, 2023 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants