dreeves / omnibot

Discord/Slack bot to play a dictionary guessing game and other things
1 stars 0 forks source link

Reply to original bid command when printing final results #120

Open jakecoble opened 1 year ago

jakecoble commented 1 year ago

[currently paused while we hash out #165]

dreeves commented 1 year ago

Maybe there's a nice generalization that the business logic could use? Like every message it receives includes a message ID as metadata. The business logic can then remember any message ID it may want to (in this case the one that kicked off the auction) and specify as part of its response like "make this be a reply to message ID X".

I think that might be pretty nice for this and potentially other use cases.

And maybe it's a reasonable way to implement this use case, if we think it's worth it?

jakecoble commented 1 year ago

Slack is going to be the platform to thwart us here. We can't get an ID for the message which instigates the command, and the webhook we're given for the replies expires in 30 minutes.

We could go ahead and implement this and just accept that it'll stop working if the auction goes on longer than 30 minutes, but, otherwise, I've not found a way to do this on Slack.

dreeves commented 1 year ago

That makes sense, actually, since Slack just doesn't really have this nice feature that Discord has where any message can be a reply to any previous message. So could this message ID metadata just be null if the platform is Slack and the business logic can just deal?

jakecoble commented 1 year ago

To be clear, Slack does support replies. It's just that there isn't a way, so far as I know, to get the id of the message that issued the command while we're processing said command. There may be a hacky solution we discover down the line.

jakecoble commented 1 year ago

Looks like Discord.js does support replying to the original command with interaction.followUp(), but that has a 15 minute time limit. Shall I check whether we're within the time limit and follow up if we're still within the 15 minutes and otherwise send a regular message? or does that limitation make replying to the original message not worth it?

dreeves commented 1 year ago

Looks like Discord.js does support replying to the original command with

Wait, Discord.js? I thought we were talking about Slack...

Assuming Slack, I'm pretty confused about what this 15-minute followup thing could mean. From using Slack as a human it doesn't seem that Slack really supports replies at all. You can create a thread, but that's usually not what you want to do. Discord does replies really nicely, and has a more heavyweight threading feature that's separate from replies. Slack seems to just not have normal replies.

But maybe I'm really misunderstanding things.

But assuming I'm not profoundly confused, I tentatively stand by the opinion that the business logic should have access to a message ID of the thing the bot is replying to but we just set that ID to null if platform=slack.

jakecoble commented 1 year ago

Long Story Short: Nevermind it's fine

It's fine. I'll get this working on Discord and we'll pass null to the command when it's on Slack. The rest of this is just clarification.

Replying vs Threading

I realize now what you meant by "where any message can be a reply to any previous message". You don't consider threads to be replies. The library for Slack, Bolt.js, has a function called message.reply() which starts a thread under message, so I was using the term reply to mean the threading feature.

Ok, so if threading doesn't satisfy the "Reply to original bid" requirement, then you were right and Slack can't give us what we want.

Neither Platform Gives Us an ID (But that's ok on Discord)

Discord supports replies without threading. But it doesn't give us the message ID of the instigating slash command message. It's not even clear that such a message exists: Discord slash commands don't let the invoking message get posted to the channel anyway, so there's no message to refer to. The thing we're "replying to" isn't a message, but an interaction, which is its own thing.

However, we're having the bot post the invoking message itself, which does create a message with an ID we have access to, so I can pass that into the slash command. On Discord.

You don't have to worry about the thing about following up and time limits. That's confusing and not relevant anymore. Replying to the interaction had limits, but, if we're replying to a message, that's different.

jakecoble commented 1 year ago

flow

After some more tinkering, I've discovered a problem with the current flow that makes this impossible. When you issue a slash command, there is no message to reply to. That's why we have to echo the slash command ourselves.

I believed we could just save the id of the message we send to reply to, but we send it only after the business logic has been executed. If we want this to work, we may have to change how slash-command business logic is implemented.

dreeves commented 1 year ago

Ha. Well. (Impressive exposition there, btw!)

This is now a fun design challenge! I like the current architecture where the business logic gets a {platform, channel, sender, msg} hash and returns a {voxmode, reply} hash. I'd also like to generalize beyond slash commands.

If the bot is replying to a non-slash-command, we can pass in the message ID no problem, right?

And, to review, the tricky thing is that for a slash command, there literally is no message until the bot sends its response to the slash command. So suppose we let the business logic pick its own message ID for the message it is currently sending? The Omnibot layer could keep a dictionary mapping the chosen ID to the platform's actual ID.

Walking through that to make sure it makes sense...

First, we augment the input and output hashes like so:

{platform, channel, sender, msg, msgID} → {voxmode, reply, replyToID, selfID}

If the input was a slash command then msgID will be null. And replyToID may be null if the bot's reply is not going to be a reply to anything. The final key, selfID, is also optional.

Here's a sketch of what actually happens in the case of the /bid command:

  1. Alice does her /bid which Omnibot turns into {platform, channel, sender: "@alice", msg: "/bid start an auction with @bob", msgID: null}.
  2. The business logic constructs its reply, generating a UUID that we'll call abc123: {holla, "blah blah auction started!", replyToID: null, selfID: "abc123"}. It remembers that selfID as part of the current auction.
  3. The Omnibot layer sends that reply and gets a message ID for it from the platform. Let's say the platform assigns Omnibot's "blah blah auction started" message a message ID of 789. So Omnibot adds "abc123"=>789 to the Message ID Dictionary.
  4. People enter their bids and at some point the last bid comes in: {platform, channel, sender, msg: "/bid a million dollars", msgID: null}
  5. The business logic now does a voxmode=blurt message that's a reply to the "blah blah auction started" message: {blurt, "auction results as follows...", "abc123", null}.
  6. Omnibot sends that out, looking up the selfID of abc123 in the dictionary to know that it corresponds to actual platform message ID 789.
  7. The end users see the auction results as a proper reply to the message that initiated the auction.

Finally, to avoid special cases when using the Message ID Dictionary (call it mdict), whenever Omnibot gets a message ID, M, from the platform, it adds an M=>M entry. That way, whenever it gets a message to send out with a replyToID in it, it can set the reply-to field to mdict[replyToID] because that lookup will necessarily return a message ID the platform understands. (But if that's dumb, we could instead have the mdict[x] lookup fall back to just using x itself when x isn't found in the dictionary.)

jakecoble commented 1 year ago

That could work. It may be less complex and indirect to pass in the reply functions as we'd done before voxmode. holla and blurt could accept a replyTo parameter and return promises that resolve with the posted message's ID.

{
    execute: ({platform, channel, sender, msg, whisp, holla, blurt}) => {
        // business logic stuff
        holla(response)
            .then(id => auctionCommandID = id);
        // later...
        blurt(response, auctionCommandID)
    }
}

Something like that. That said, this would involve promises, and not everybody groks those.

dreeves commented 1 year ago

That sounds smart. Maybe we could do something conceptually similar that doesn't need promises? Or maybe promises is the best implementation. But let me repeat the walk-through of the /bid example to make sure I understand.

(I now believe that this has #165 as a prereq. I just added a sketch of the input/output there.)

The key is that the input that the business logic gets includes a reply function that the business logic invokes to send its reply.

  1. Alice does her /bid which Omnibot turns into {..., msg: "/bid start an auction with @bob", msgID: null, replyFunc} which has a null message ID because she sent her message as a slash command, aka she whispered it to Omnibot.
  2. The business logic calls replyFunc which returns a message ID for Omnibot's reply (unless Omnibot whispers it back, in which case it's null because there's no actual message in the channel) which the business logic remembers. Say it's "abc123".
  3. People enter their bids and at some point the last bid comes in: {..., msg: "/bid a million dollars", msgID: null, replyFunc}
  4. The business logic now calls replyFunc, specifying "abc123" as the message ID to reply to, and announces the auction results.
  5. The end users see the auction results as a proper reply to the message that initiated the auction.
Rougher/older notes to clean up

Conceptually, the bizlogic wants to ingest incoming messages with IDs attached that it can remember if it wants to. Also when it sends a message it wants a message ID for what it sent. That way it has flexibility to do replies (or emoji-react?) or otherwise interact with any message that it has seen. A monkey wrench is that a slash command is sent to the bizlogic as an incoming message but it's not a message and has no message ID. If voxmode=holla then Omnibot'll echo it back and _that_ will have a message ID. The bizlogic just has to cope with that. An incoming message may have msgID==null and the bizlogic understands that that means this message was essentially whispered to Omnibot. There's no public message that can later be referred to. Which is fine. Webhooks are like that too. Conceptually that's also like whispering something to Omnibot. It can then blurt something in a channel if it decides to. (DIGRESSION moved to #165) For message IDs, let's focus on the question of how the bizlogic can learn the message ID of what it's sending. Options: 1. The thing I sketched above where it makes up a UUID and the Omnibot layer maintains a mapping to the actual message ID for the platform. 2. The promise thing? 6. The bizlogic can make use of a send() function to send a message and that function returns the message ID of what was sent?

dreeves commented 1 year ago

Questions from @jakecoble (note we've been abbreviating "business logic" as "bizlogic" and the complement of that as "partylogic"):

When the partylogic wants to reply to the original command, we could use interaction.reply(). That'd post a message as a reply to the command as you'd expect. However, if we do that, we can't reply with phem anymore.

To work around that, the current bizlogic creates a new message with the text of the command, then sends that message to Discord so the partylogic has something to reply to.

So we have this:

  1. A user calls /ping
  2. partylogic calls sendmesg({... mrid: msid, mesg: "pong!" })
  3. bizlogic sends a message containing the text /ping to Discord.
  4. bizlogic replies to the message sent in step 3 with a message containing the text "pong!"

If we want the partylogic to reply to the "original command" when the auction is done, we could do one of two things, as I see it:

  1. Use interaction.reply() and interaction.followUp() as normal and document the fact that replying to the original command means the loss of phem.
  2. In the bizlogic, save the id of the message from step 3. When the partylogic replies to the original command, we reply to that message instead if it already exists.

PS: oops, except bizlogic and partylogic may be swapped above! let's go with bizlogic vs infralogic from now on -- should be less confusing!