Building an Event Handler - GlennPrince/DNetBotTemplate GitHub Wiki

Building an Event Handler

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.

Outbound Events

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:

  1. name: - Change the task name to something
  2. SUBSCRIPTION_NAME: - Change function-messages-new to something unique related to your Function
  3. ENDPOINT_ADDRESS: - Change NewMessage to the value you entered in [FunctionName] in your Function code
  4. 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.

Inbound Events

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:

  1. name: - Change the task name to something
  2. SUBSCRIPTION_NAME: - Change proxy-returnmessage to something unique related to your new method
  3. ENDPOINT_ADDRESS: - Change ReturnMessage to the value you entered in [FunctionName] in your HttpPost attribute
  4. TOPIC_RESOURCE: - If you are listening to a different topic, update returnmessage to reflect the new topic
⚠️ **GitHub.com Fallback** ⚠️