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 send a reminder to a user after a given amount of time. For example, if a user sends the remind 60 take a nap command to the bot, after 60 seconds, the bot will respond with the take a nap message. 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 Develop 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 tunneling service

Before we register our chatbot in Space, we have to get a publicly available URL for it. As your development environment is probably located behind NAT, the easiest way to get the URL is to use a tunneling 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:

    Session Status online Account user@example.com (Plan: Free) Version 3.0.6 Region United States (us) Latency - Web Interface http://127.0.0.1:4040 Forwarding https://98af-94-158-242-146.ngrok.io -> http://localhost:8080 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://98af-94-158-242-146.ngrok.io but in your case it will be something else as ngrok generates these random URLs dynamically.

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

Step 4. Register the chatbot in Space

In order Space and chatbot could communicate with each other, we must register the bot in Space.

When developing an application, you must decide on two important things:

  • Application distribution:

    • Single-org applications are intended only for a single Space organization. A Space user registers and configures a single-org application manually in the Space UI.

    • Multi-org applications are intended for multiple Space organizations. A multi-org application registers and configures itself in a particular Space instance using API calls.

    Since we are just practicing, there is no point in being distracted by the complexities of configuring a multi-org application. Instead, we will register and configure our application using the Space UI. So, we are going to create a single-org application.

  • Authorization subject: decide how your application should act in Space – on behalf of itself, on behalf of a particular Space user, or both. This determines which authorization flows the application will use.

    In our case, a chatbot will send notifications in its own chat channel on befalf of itself. As an OAuth 2.0 flow, we will use the Client Credentials flow. It lets the application authorize in Space using a client ID and a client secret.

To summarize, our chatbot is a single-org application that uses the Client Credentials authorization flow. Now, let's register our bot in Space.

  1. Open your Space instance.

  2. On the main menu, click Extensions Extensions and choose Installed.

  3. Click New application.

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

  5. On the Overview tab, select Chat bot. This will enable a chat channel for our application.

  6. Open the Authorization tab. We will not change anything on this tab – as our bot is simple and 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. Learn more about requesting permissions.

    Requested permissions
  7. Open the Authentication tab. Note that the Client Credentials Flow is enabled for all applications by default. We need to get the application's Client ID and Client secret. Our chatbot will use them to get a Space access token.

    Authentications
  8. When a user types anything in the chatbot channel, Space sends the user input to the application. So, our next step is to specify the URL of our application endpoint and choose how we will verify requests from Space.

    Open the Endpoint tab.

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

    By default, Space recommends using the Public key verification method. Let's leave the default and click Save.

    Endpoint
  9. 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 code in Program.cs, and register the application helpers for Space.

    Right after the line that reads var builder = WebApplication.CreateBuilder(args);, add the following code:

    // Space client API builder.Services.AddSingleton<Connection>(provider => new ClientCredentialsConnection( new Uri(builder.Configuration["Space:ServerUrl"]), builder.Configuration["Space:ClientId"], builder.Configuration["Space:ClientSecret"], provider.GetService<IHttpClientFactory>().CreateClient())); builder.Services.AddSpaceClientApi(); // Space webhook handler builder.Services.AddSpaceWebHookHandler<RemindMeBotHandler>(options => builder.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 Program.cs file, right after the line that reads var app = builder.Build();, add the following code:

    app.MapSpaceWebHookHandler<RemindMeBotHandler>("/api/myapp"); app.MapGet("/", () => "Space app is running.");

    Things to note:

    • app.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.

    • app.MapGet("/", ... 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( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: 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( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: 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( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: 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( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: 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: 20 April 2023