Building an Event Handler - GlennPrince/DNetBotTemplate GitHub Wiki
The Discord Bot is event driven, in that when an event happens the bot will respond. To write your own event handler should be resonably easy. There are two ways an event can flow, outbound events from Discord that are handled by Azure Functions or inbound events to Discord that the bot proxy needs to handle. Lets look at how you add both by using the current !ping command.
To handle an outbound event, such as processing a message, you need to have an Azure Function listening to EventGrid. There are two basic ways to do this:
- Create a new individual fucntion for each thing you want to do listening to a Discord Event. For instance, you might create a function for each command you want to support all listening to the DNetBot.Message.NewMessage Event Type.
- Create a single function for each processing event. For instance you might create a single command handler that listents to the DNetBot.Message.NewMessage Event Type and calls the relevant routine when any command is found.
Both ways have their merits, but for this example we have a Function that listens to the DNetBot.Message.NewMessage Event Type and returns a corresponding message with the !pong text. Create a new Function in the DNetBotFunctions solution that triggers from Event Grid or copy an existing function. The trimmed sample function is included below:
public static class NewMessage
{
private static EventGridClient eventGridClient = new EventGridClient(new TopicCredentials(System.Environment.GetEnvironmentVariable("EventGridKey")));
[FunctionName("NewMessage")]
public static void Run([EventGridTrigger]EventGridEvent eventGridEvent, ILogger log)
{
DiscordMessage message = new DiscordMessage(eventGridEvent.Data.ToString());
if (message.Content.StartsWith("!ping"))
{
var returnMessage = new DiscordMessage();
returnMessage.ChannelId = message.ChannelId;
returnMessage.Content = "pong!";
var myEvent = new EventGridEvent(eventGridEvent.Id, "ReturnMessage", returnMessage, "DNetBot.Message.ReturnMessage", DateTime.Now, "1.0", "returnmessage");
string eventGridHostname = new Uri(System.Environment.GetEnvironmentVariable("EventGridDomain")).Host;
try
{
eventGridClient.PublishEventsAsync(eventGridHostname, new List<EventGridEvent>() { myEvent }).Wait();
}
catch(Exception ex)
{
log.LogError(ex.ToString());
}
}
}
}
Because this function is sending a return message, we have to make sure that an EventGridClient exists. Next we deserialized the DiscordMessage from the event payload. We then perform our logic of testing to see if the message starts with the !ping command. Finally we create a new event we a payload to send back to the Event Grid pipeline.
When you have your event coded up and is working we need to change our deployment.yml
file to add the event listener when we deploy our solution. At the bottom of this file there is the following script:
# Function Subscribe to the New Message Topic
- name: Subscribe New Message Function to Topic
uses: azure/CLI@v1
env:
SUBSCRIPTION_NAME: ${{ env.DNETBOT_NAME }}-function-messages-new
EVENTGRID_DOMAIN: ${{ env.DNETBOT_NAME }}-eventgrid-domain
ENDPOINT_ADDRESS: /subscriptions/${{ secrets.SUBSCRIPTION_ID }}/resourceGroups/${{ env.AZURE_RG }}/providers/Microsoft.Web/sites/${{ env.DNETBOT_NAME }}-function/functions/NewMessage
TOPIC_RESOURCE: /subscriptions/${{ secrets.SUBSCRIPTION_ID }}/resourceGroups/${{ env.AZURE_RG }}/providers/Microsoft.EventGrid/domains/${{ env.DNETBOT_NAME }}-eventgrid-domain/topics/messages
with:
azcliversion: 2.14.2
inlineScript: |
echo ::add-mask::$SUBSCRIPTION_NAME
echo ::add-mask::$ENDPOINT_ADDRESS
az eventgrid event-subscription create --name $SUBSCRIPTION_NAME --source-resource-id $TOPIC_RESOURCE --endpoint $ENDPOINT_ADDRESS --endpoint-type azurefunction --output none
Copy this code block of each new function you create and modify the following:
- name: - Change the task name to something
- SUBSCRIPTION_NAME: - Change
function-messages-new
to something unique related to your Function - ENDPOINT_ADDRESS: - Change
NewMessage
to the value you entered in[FunctionName]
in your Function code - TOPIC_RESOURCE: - If you are listening to a different topic (See Event Types), update
messages
to reflect the new topic
Redeploy your code and your event should be wired up and ready to go.
To handle an inbound event we have to update our DnetBot core application with a new webhook. We use a similar method to our functions by modifying our EventGridController. Take a copy of the ReturnMessage method and create a new method to handle the response. The code for this is below:
// POST: /api/EventGrid/ReturnMessage
[HttpPost("returnmessage")]
public IActionResult ReturnMessage([FromBody] EventGridEvent[] events)
{
foreach (var eventGridEvent in events)
{
_logger.Log(LogLevel.Information, "Event Grid Event Received. Type: " + eventGridEvent.EventType.ToString());
// 1. If there is no EventType through a bad request
if (eventGridEvent == null) return BadRequest();
// 2. If the EventType is the Event Grid handshake event, respond with a SubscriptionValidationResponse.
else if (eventGridEvent.EventType == EventTypes.EventGridSubscriptionValidationEvent)
return Ok(ValidateWebHook(eventGridEvent.Data));
// 3. If the EventType is a return message, send a message to Discord
else if (eventGridEvent.EventType == "DNetBot.Message.ReturnMessage")
{
var message = JsonConvert.DeserializeObject<DiscordMessage>(eventGridEvent.Data.ToString());
_discordSocketService.SendMessage(message).Wait();
return Ok();
}
else
return BadRequest();
}
return Ok();
}
For this class, modify the class name and the HttpPost attribute to reflect the new method. The section of code after number 3. is the logic for this method that needs to be updated as required. When you have your event coded up and is working we need to change our deployment.yml
file to add the event listener when we deploy our solution. At the bottom of this file there is the following script:
# Proxy Subscribe to the Return Topic
- name: Subscribe Proxy to Topics
uses: azure/CLI@v1
env:
SUBSCRIPTION_NAME: ${{ env.DNETBOT_NAME }}-proxy-returnmessage
EVENTGRID_DOMAIN: ${{ env.DNETBOT_NAME }}-eventgrid-domain
ENDPOINT_ADDRESS: https://${{ env.DNETBOT_NAME }}-proxy.azurewebsites.net/api/EventGrid/ReturnMessage
TOPIC_RESOURCE: /subscriptions/${{ secrets.SUBSCRIPTION_ID }}/resourceGroups/${{ env.AZURE_RG }}/providers/Microsoft.EventGrid/domains/${{ env.DNETBOT_NAME }}-eventgrid-domain/topics/returnmessage
with:
azcliversion: 2.14.2
inlineScript: |
echo ::add-mask::$SUBSCRIPTION_NAME
echo ::add-mask::$ENDPOINT_ADDRESS
az eventgrid event-subscription create --name $SUBSCRIPTION_NAME --source-resource-id $TOPIC_RESOURCE --endpoint $ENDPOINT_ADDRESS --output none
Copy this code block of each new event handler you create and modify the following:
- name: - Change the task name to something
- SUBSCRIPTION_NAME: - Change
proxy-returnmessage
to something unique related to your new method - ENDPOINT_ADDRESS: - Change
ReturnMessage
to the value you entered in[FunctionName]
in your HttpPost attribute - TOPIC_RESOURCE: - If you are listening to a different topic, update
returnmessage
to reflect the new topic