Markets of Discord Part Two
This post continues to chronicle my efforts to create a prediction market bot for Discord. Read part one here.
We last left our code ready to simulate a prediction market but with nothing to run it. So lets start on the discord bot side of things. I am going to use Discord.Net to manage the connection to Discord. It is available on NuGet and has a nice Command package that makes mapping user input to actions a breeze.
Lets start with the main method, the entry method.
class Program
{
static void Main(string[] args)
{
using (var context = new MarketContext())
{
var manager = new MarketsManager(context);
var token = ConfigurationManager.AppSettings["token"];
using (var bot = new Bot("PredictiveMarket", manager))
{
bot.Start(token);
}
}
}
}
You may recognize the MarketContext
from last time, but the MarketsManager
and Bot
are new. First, however, I want to take a look at the configuration setup a bit. ConfigurationManager
is from System.Configuration
and allows us to access the information stored in the App.config
file. The token value, which is how the bot is linked to the bot user account that you can setup for your bot, is stored in the AppSettings section of the file. The trick is that you don’t want the token in source control. To that effect I created a secrets.config
file, excluded it from source control, and put the token in that file. Then the App.config
just needs to reference the secrets.config
file.
App.Config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<appSettings configSource="secrets.config"></appSettings>
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(localdb)\ProjectsV13;Initial Catalog=PredictionMarket;Integrated Security=True" providerName="System.Data.SqlClient"/>
</connectionStrings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
</entityFramework>
</configuration
secrets.config
<?xml version="1.0" encoding="utf-8" ?>
<appSettings>
<add key="token" value="faketokennotrealtokenputrealtokenhere"/>
</appSettings>
To you need to figure out how to get the token or setup a bot in discord see this video by foxbot which does a pretty good job explain the steps and as well as just general information about creating a bot with Discord.Net.
We will get to MarketsManager
later, but we should skip ahead to the class I so helpfully called Bot
. The constructor of Bot
creates the DiscordClient
and adds a few settings, including an OnLogMessage
handler which I copied from the sample Discord.Net app. Then the commands are configured. I settled on using $market
as a prefix as it seemed unlikely to cause conflicts. Additionally, AllowMentionPrefix
is set to true, which allows users to get the bot to respond by using Discords @
ing syntax. Next there are some command handlers which we will get to.
In the Start
method we have the ExecuteAndWait
method that allows an asynchronous context within a synchronous one. Inside the discord client connects to discord using the token and then, purely optionally, says it is playing the game “Discord.Net” to let people know what is running the bot.
using Discord;
using Discord.Commands;
using PredictionMarketBot.InfoModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace PredictionMarketBot
{
public class Bot : IDisposable
{
private DiscordClient Client { get; set; }
private MarketsManager Manager { get; set; }
public Bot(string appName, MarketsManager manager)
{
Manager = manager;
Client = new DiscordClient(c =>
{
c.AppName = appName;
c.MessageCacheSize = 0;
c.LogLevel = LogSeverity.Info;
c.LogHandler = OnLogMessage;
});
Client.UsingCommands(c =>
{
c.CustomPrefixHandler = (msg) =>
{
if (msg.User.IsBot)
return -1;
var isMatch = Regex.IsMatch(msg.Text, @"^\$market");
if (isMatch)
return 7;
return -1;
};
c.AllowMentionPrefix = true;
c.HelpMode = HelpMode.Public;
c.ExecuteHandler = OnCommandExecuted;
c.ErrorHandler = OnCommandError;
});
CreateCommands();
}
public void Start(string token)
{
Client.ExecuteAndWait(async () =>
{
while (true)
{
try
{
await Client.Connect(token);
Client.SetGame("Discord.Net");
break;
}
catch (Exception ex)
{
Client.Log.Error($"Login Failed", ex);
await Task.Delay(Client.Config.FailedReconnectDelay);
}
}
});
/* Commands and Command handlers */
/* IDisposable stuff */
}
}
Rather than go through all the command code right here (I am going to post the code on github within a day or so EDIT: Here), but I do want to explain the basic structure.
var service = Client.GetService<CommandService>();
service.CreateCommand("predict")
.Description("predicts the outcome of an event")
.Do(async (e) =>
{
var simulator = GetSimulator(e);
if (simulator == null)
return;
var market = simulator.GetMarketInfo();
var result = simulator.Predict();
var msg = $"**{market.Description}** {result.Name} ({result.CurrentProbability:P})";
await Client.Reply(e, msg);
});
That is a simple command that returns the markets current prediction. You can see how the fluent api of the Discord.Net Commands package makes it really easy to work with. Just use Client.GetService<CommandService>()
to retrieve the service we defined in the constructor and then add commands to it. CreateCommand
takes the keyword for the command. You can add aliases with the Alias
method , parameters with the Parameter
method and a description displayed by the help command with the Description
method. The Do
method is just as simple, it contains the action of the command. The lambda inside takes a CommandEventArg
which contains all the information from discord.
The Reply
method is a simple extension method that calls SendMessage
on the channel the user sent the message in.
public static Task Reply(this DiscordClient client, CommandEventArgs e, string text)
=> Reply(client, e.User, e.Channel, text);
public async static Task Reply(this DiscordClient client, User user, Channel channel, string text)
{
if (text != null)
{
await channel.SendMessage(text);
}
}
The final piece of the puzzel is the MarketsManager
class and GetSimulator
method. Basically, they link the server the command is being sent from to a market. I ended up modifying the code from part one a bit, and one of the pieces was adding in links to the discord server on markets and links to the discord user on the player.
I plan on posing the code on github in a few days with a good readme and I will add a link here once I do. EDIT: Here is the github link.
Comments
Leave a comment