JetBrains Space Help

(.NET) How to Create a Chatbot

What's a chatbot? It's a Space application that communicates with a Space user in its own Chats channel. A minimum viable bot must:

  • Respond with a list of available commands when a user types / (slash) in the channel.

  • Provide at least one command: After a user sends this command to the channel, the bot must do something and then respond with a message.

What will we do

Let's create a first chatbot! Please welcome – the 'Remind me' bot!

Our bot will remind a user after a given amount of time. For example, when a user sends the remind 60 command to the bot, after 60 seconds, the bot will send a message with a reminder back. Also, our bot will have a command that provides help when requested.

Simple Space chatbot

In this tutorial, we'll go through the entire process of creating a chatbot. You can also download the resulting source code.

Starter kit for creating a chat bot

What will we need along our journey?

JetBrains Rider

JetBrains Rider

We'll write our bot in C# and .NET. There are many IDE's available for building .NET applications, so you can use your preferred IDE. In this tutorial, we'll use JetBrains Rider.

Space SDK

Space SDK

As we have learned in the Applications topic, any application must communicate with Space using the Space HTTP API. To make developing Space apps easier, we provide Space SDK for Kotlin and .NET. The SDK contains the HTTP API client that lets you easily authenticate in and communicate with Space. We'll use the Space SDK by adding a NuGet package dependency to our project.

ngrok

A tunnelling service

Such a service exposes local servers to the public internet. It will let us run our chatbot locally and access it from Space via a public URL (we'll specify it as the chatbot's endpoint). For example, you can use the ngrok tunnelling service for this purpose. To start working with ngrok, you should download the ngrok client. For our purposes, the free plan for ngrok is enough.

Step 1. Create an ASP.NET application

  1. Open JetBrains Rider.

  2. In the welcome screen, start creating a new solution with New Solution.

  3. In the list of templates, select ASP.NET Core Web Application.

  4. In Type, select Empty. This template will set up a project with the minimal required configuration.

  5. Specify a solution name and project name, for example RemindMeBot, and click Create.

    Create a solution

  6. That's it! We now have a web application where we can start building our bot.

    New web application for Space bot

Step 2. Get the Space SDK

  1. Use the context menu on the RemindMeBot project, and select Manage NuGet Packages. This will open the NuGet tool window in Rider.

  2. Search for the JetBrains.Space.AspNetCore package. This package contains helpers to create Space applications, such as the chatbot we are building.

    Install the JetBrains.Space.AspNetCore package

  3. Done! Now, we have the Space SDK in our project, and we can start building the 'Remind me' bot.

Step 3. Run the tunnelling service

Before we register our chatbot in Space, we have to get its publicly available URL. As your development environment is probably located behind NAT, the easiest way to obtain the URL is to use a tunnelling service. In our case, we'll use ngrok.

  1. Download and unzip the ngrok client.

  2. In terminal (on macOS or Linux) or in the command line (on Windows), open the ngrok directory.

  3. By default, our project is configured to run the HTTP server on the port 5001 (you can check this in the Properties/launchSettings.json file). Run tunnelling for this port:

    ./ngrok http 5001

  4. The ngrok service will start. It will look similar to:

    ngrok by @inconshreveable Session Status online Account john.doe@example.com (Plan: Free) Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://5a8dc7594bb0.ngrok.io -> http://localh Forwarding https://5a8dc7594bb0.ngrok.io -> http://local Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
    Here we are interested in the Forwarding line – it contains the public URL. ngrok redirects requests from this URL to our localhost using its tunnelling service. In the example above, the address is https://5a8dc7594bb0.ngrok.io but in your case it'll be something else as ngrok generates these random URLs dynamically.

  5. Save the public URL, e.g. by copying it to the clipboard.

  6. Great job! Now, we have a running tunnelling service and the public URL of our future chatbot.

Step 4. Register the chatbot in Space

Before we can start developing, we must get Space authentication credentials for our bot. Moreover, we also need a special verification token that will allow the bot to verify our Space instance. To get all of these, we must register our bot in Space.

  1. Open your Space instance.

  2. On the navigation bar, click administration.png Administration and choose Applications.

  3. Click New application.

  4. Give the application a unique name, say, remind-me-bot and click Create.

  5. Open the Requested rights tab. We will not change anything on this tab – as our bot is very simple, it doesn't get any data from Space.

    We're on this tab just for you to note how important it is – if your app is supposed to access various Space modules, you should provide it corresponding permissions. To check what rights are required for a certain HTTP API call, go to API Playground.

    Requested rights
  6. Open the Authentication tab. Our chatbot will authenticate on behalf of itself using the Client Credentials Flow. This means that we should select the Client Credentials Flow checkbox on this page. Also we need to save application's Client ID and Client secret. Our chatbot will use them to obtain a Space access token via the flow.

    Authentications
  7. Open the Endpoint tab and then click the Generate verification token button. This will generate the token that will allow our bot to verify Space. Save it somewhere.

    In the Endpoint URI, specify the public URL generated by ngrok for our bot. Let's make this endpoint less generic and add an additional path to the URL, for example api/myapp. So, the final endpoint would be https://{random_string_from_ngrok}.ngrok.io/api/myapp

    Endpoint
  8. Great job! Now, our bot is registered in Space, we have all required authentication data and we're ready to start developing our bot.

Step 5. Register a web hook handler

Ready, set, code! We'll start by updating the configuration file in our application. Then, we'll create and register a RemindMeBotHandler class that can respond to incoming messages from Space, and send responses back.

  1. Edit the appsettings.json file in your project.

  2. Add a Space element to it. The file should look like this:

    { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Space": { "ServerUrl": "https://organization.jetbrains.space", "ClientId": "value-of-client-id", "ClientSecret": "value-of-client-secret", "VerifySigningKey": { "IsEnabled": false, "EndpointSigningKey": "value-of-endpoint-signing-key" }, "VerifyHttpBearerToken": { "IsEnabled": false, "BearerToken": "value-of-bearer-token}" }, "VerifyHttpBasicAuthentication": { "IsEnabled": false, "Username": "value-of-username", "Password": "value-of-password" }, "VerifyVerificationToken": { "IsEnabled": false, "EndpointVerificationToken": "value-of-endpoint-verification-token" } } }
    You will have to make some updates to the values of elements:

    • ServerUrl is the URL of your Space instance.

    • ClientId, ClientSecret are the ID and secret we obtained during application registration.

    • Set the IsEnabled option to true for the selected authentication method during application registration. Make sure to update other options as well, for example set the EndpointSigningKey or EndpointVerificationToken when needed.

  3. Create the RemindMeBotHandler class in your project, and add the code:

    using JetBrains.Space.AspNetCore.Experimental.WebHooks; namespace RemindMeBot { public class RemindMeBotHandler : SpaceWebHookHandler { } }
    Note that the RemindMeBotHandler class inherits from the Space SDK's SpaceWebHookHandler class. It handles a number of things for us:

    • It verifies the Space instance from which our bot receives a request, by taking the message payload and comparing the incoming token with the EndpointVerificationToken configuration value.

    • It ensures the message payload has not been tampered with, by using the EndpointSigningKey, and calculating the message signature.

  4. Now, let's wire things up. We will update the Startup class (in Startup.cs), and register the application helpers for Space.

    First, we will need to make sure we can access the configuration file we just created. Add the following constructor and property to the Startup class:

    public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; }

    Now, in Startup class, add the following code to the ConfigureServices method:

    public void ConfigureServices(IServiceCollection services) { // Register HTTP client services.AddHttpClient(); // Space client API services.AddSingleton<Connection>(provider => new ClientCredentialsConnection( new Uri(Configuration["Space:ServerUrl"]), Configuration["Space:ClientId"], Configuration["Space:ClientSecret"], provider.GetService<IHttpClientFactory>().CreateClient())); services.AddSpaceClientApi(); // Space webhook handler services.AddSpaceWebHookHandler<RemindMeBotHandler>(options => Configuration.Bind("Space", options)); }

    Some things to note:

    • AddHttpClient() registers the .NET HttpClient that is used by the Space SDK and makes it available as a service.

    • AddSingleton<Connection>(...) registers a connection to work with Space, using the configuration we created earlier.

    • AddSpaceClientApi() registers the Space API client and makes it available as a service. Later in this tutorial, we can make use of it to send a chat message to our bot's users.

    • AddSpaceWebHookHandler<RemindMeBotHandler>(...) registers a RemindMeBotHandler class as a web hook handler. This class does not exist yet: it will contain the logic of the 'Remind me' bot, which we still have to implement.

    Further in the Startup class, find the Configure method. Update the code block that starts with app.UseEndpoints to the following:

    app.UseEndpoints(endpoints => { // Space webhook receiver endpoint endpoints.MapSpaceWebHookHandler<RemindMeBotHandler>("/api/myapp"); endpoints.Map("/", async context => { context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync("Space app is running."); }); });

    Things to note:

    • endpoints.MapSpaceWebHookHandler<RemindMeBotHandler> makes the 'Remind me' bot available on the web server, on the /api/myapp path. This has to match the path that was entered during application registration.

    • endpoints.Map("/", ... registers a default response that the server will return when someone accesses the 'Remind me' bot from their browser. It is good practice to add a default response like this to your Space bot.

  5. Done! We have created a RemindMeBotHandler class where we can start adding a first command, and registered it using the Space SDK.

Step 6. Create your first command

Let's start with something simple – the help command that shows hints on how to use our chatbot.

  1. Update the RemindMeBotHandler class, and overwrite its code with the following code:

    using System.Threading.Tasks; using JetBrains.Space.AspNetCore.Experimental.WebHooks; using JetBrains.Space.Client; namespace RemindMeBot { public class RemindMeBotHandler : SpaceWebHookHandler { private readonly ChatClient _chatClient; public RemindMeBotHandler(ChatClient chatClient) { _chatClient = chatClient; } public override async Task HandleMessageAsync(MessagePayload payload) { var messageText = payload.Message.Body as ChatMessageText; if (string.IsNullOrEmpty(messageText?.Text)) return; await HandleHelpAsync(payload); } private async Task HandleHelpAsync(MessagePayload payload) { await _chatClient.Messages.SendMessageAsync( MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), ChatMessage.Text("Soon the help will be shown here!")); } } }

    Notes:

    • When the ASP.NET runtime creates the RemindMeBotHandler class, it injects a ChatClient in its constructor. The ChatClient is stored in a private field, so we can use it later to send chat messages to our users.

    • The HandleMessageAsync(MessagePayload payload) method is inherited from the Space SDK, and is called when our bot receives a message from Space. Later on, we will use this method to handle different commands. For now, the HandleHelpAsync method is invoked whenever a message is received.

    • In the HandleHelpAsync(MessagePayload payload) method, we make use of the previously injected ChatClient through the _chatClient field.

      Using the SendMessageAsync method, you can send a message to any recipient in Space. Recipients can be chat channels, issue comments, direct messages, and more.

      We're using the MessageRecipient.Member() method to address a user using direct message, and then build the ProfileIdentifier using the ID of the user who invoked the help command.

      The text of our chat message is created using the ChatMessage.Text() method. Messages can be more than just text. They can include complex formatting and even UI elements, like buttons. For now, text is sufficient.

  2. Nice! Now, we have a command that a user can actually try in action.

Step 7. Run the bot

  1. Start the application by pressing Ctrl+F5 or using the Run | Run 'RemindMeBot' menu.

    Run application
  2. Open your Space instance and find the bot: press Ctrl+K and type its name.

    Find the bot
  3. We don't anyhow analyze the commands sent by the user. On any type of request, we respond with the message generated in HandleHelpAsync. So, to test our bot, type anything in the chat. You should receive the help message:

    Send command to bot
  4. It's working! Now, let's add the rest of the bot features.

Step 8. Add support for slash commands

Let's make our bot fully functional:

  • In order to be considered a chatbot, an application must be able to respond with a list of available commands when a user types / (slash) in the chat. In this case, the bot receives the ListCommandsPayload type of payload, which we can handle by overriding the HandleListCommandsAsync method in our bot class.

  • As our bot is called the 'Remind me' bot, it needs a remind command that will start the timer and send a notification to the user once the timer completes.

  1. Let's start with listing the available commands. In the RemindMeBotHandler class, add the following code:

    public override async Task<Commands> HandleListCommandsAsync(ListCommandsPayload payload) { return new Commands(new List<CommandDetail> { new CommandDetail("help", "Show this help"), new CommandDetail("remind", "Remind me in N seconds, e.g., to remind in 10 seconds, send 'remind 10'") }); }

    When a bot receives ListCommandsPayload, it can respond with Commands that returns a list of CommandDetail. Each CommandDetail has a command name, and a description that will be displayed in the help menu.

    Space sends ListCommandsPayload every time a user presses a key. If it's the / (slash), Space will show the full list of commands. If it's some other key, Space will find and show only commands containing the key.

    Slash commands
  2. Now that we can list commands, let's update our HandleHelpAsync method and respond with proper help information. Replace the code of the HandleHelpAsync method:

    private async Task HandleHelpAsync(MessagePayload payload) { var commands = await HandleListCommandsAsync( new ListCommandsPayload { UserId = payload.UserId }); await _chatClient.Messages.SendMessageAsync( MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), ChatMessage.Block( outline: new MessageOutline("Remind me bot help", new ApiIcon("smile")), sections: new List<MessageSectionElement> { MessageSectionElement.MessageSection( header: "List of available commands", elements: new List<MessageElement> { MessageElement.MessageFields( commands.CommandsItems .Select(it => MessageFieldElement.MessageField(it.Name, it.Description)) .ToList<MessageFieldElement>()) }) })); }

    Here:

    • We're calling HandleListCommandsAsync to get the list of commands. For good measure, we're passing along the user ID as well, so that if we need it in that method, we can use it.

    • The ChatMessage.Text() has been replaced with ChatMessage.Block(), so we can return a formatted help message.

  3. We can now check the incoming MessagePayload, and determine the intended command. If the text starts with "remind", we'll invoke the HandleRemindAsync method.

    Update the HandleMessageAsync method:

    public override async Task HandleMessageAsync(MessagePayload payload) { var messageText = payload.Message.Body as ChatMessageText; if (string.IsNullOrEmpty(messageText?.Text)) return; if (messageText.Text.Trim().StartsWith("remind")) { await HandleRemindAsync(payload, messageText); return; } await HandleHelpAsync(payload); }

    Next, add a new HandleRemindAsync method:

    private async Task HandleRemindAsync(MessagePayload payload, ChatMessageText messageText) { var arguments = messageText.Text.Split(' ', StringSplitOptions.TrimEntries); if (arguments.Length != 2 || !int.TryParse(arguments[1], out var delayInSeconds)) { // We're expecting 2 elements: "remind", "X" // If that's not the case, return help. await HandleHelpAsync(payload); return; } await _chatClient.Messages.SendMessageAsync( MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), ChatMessage.Block( outline: new MessageOutline($"I will remind you in {delayInSeconds} seconds", new ApiIcon("smile")), sections: new List<MessageSectionElement>())); Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(delayInSeconds)); await _chatClient.Messages.SendMessageAsync( MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), ChatMessage.Block( outline: new MessageOutline($"Hey! {delayInSeconds} seconds are over!", new ApiIcon("smile")), sections: new List<MessageSectionElement>())); } catch (Exception) { // Since we're using Task.Run to run code outside of the // request context, we want to catch any Exception here // to prevent the server from crashing. } }); }

    Here:

    • HandleRemindAsync validates the command, and invokes HandleHelpAsync when no valid command arguments are provided. The code checks if there are two elements in the string, and that the second element is a valid integer. When a user types remind 10, the second argument has to be an integer 10.

    • A confirmation message is sent to the user.

    • Task.Run(...) schedules a delay for the amount of time that was requested, and sends a message to the user when the delay is over.

  4. Let's run the bot one more time and try all bot features:

    • Typing slash:

      Slash command
    • The help command:

      Help command
    • The remind command:

      Remind command

Great job! We have finished creating our simple bot!

Last modified: 29 September 2021