Initial commit

This commit is contained in:
2022-11-03 15:15:25 +01:00
commit 9a1a8ecd1c
13 changed files with 1026 additions and 0 deletions

14
Cocotte/Cocotte.csproj Normal file
View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-Cocotter-70E782F3-B3C1-4BA0-965D-D21E31F2F052</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.8.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
using Discord.Interactions;
namespace Cocotte.Modules;
public class PingModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<PingModule> _logger;
public PingModule(ILogger<PingModule> logger)
{
_logger = logger;
}
[SlashCommand("ping", "Check if Coco is alive")]
public async Task Ping()
{
await RespondAsync($":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true);
}
}

View File

@@ -0,0 +1,9 @@
namespace Cocotte.Options;
public class DiscordOptions
{
public const string SectionName = "DiscordOptions";
public string? Token { get; init; }
public ulong? DevGuildId { get; init; }
}

40
Cocotte/Program.cs Normal file
View File

@@ -0,0 +1,40 @@
using Cocotte;
using Cocotte.Options;
using Cocotte.Services;
using Discord;
using Discord.Commands;
using Discord.Interactions;
using Discord.WebSocket;
DiscordSocketConfig discordSocketConfig = new()
{
LogLevel = LogSeverity.Debug,
MessageCacheSize = 200
};
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((_, configuration) =>
{
configuration.AddJsonFile("discord.json", false, false);
})
.ConfigureServices((context, services) =>
{
// Options
services.Configure<DiscordOptions>(context.Configuration.GetSection(DiscordOptions.SectionName));
// Discord.Net
services.AddHostedService<DiscordLoggingService>();
services.AddSingleton<CommandService>();
services.AddSingleton(discordSocketConfig);
services.AddSingleton<DiscordSocketClient>();
services.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()));
services.AddHostedService<CocotteService>();
// Custom
services.AddSingleton<SharedCounter>();
})
.Build();
await host.RunAsync();

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"Cocotter": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,122 @@
using System.Reflection;
using Cocotte.Options;
using Cocotte.Utils;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Options;
namespace Cocotte.Services;
public class CocotteService : BackgroundService
{
private readonly ILogger<CocotteService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IHostEnvironment _hostEnvironment;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly DiscordSocketClient _client;
private readonly DiscordOptions _options;
private readonly InteractionService _interactionService;
public CocotteService(ILogger<CocotteService> logger, IServiceProvider serviceProvider,
IHostEnvironment hostEnvironment,
IHostApplicationLifetime hostApplicationLifetime, DiscordSocketClient client,
IOptions<DiscordOptions> options, InteractionService interactionService)
{
_logger = logger;
_serviceProvider = serviceProvider;
_hostApplicationLifetime = hostApplicationLifetime;
_client = client;
_options = options.Value;
_interactionService = interactionService;
_hostEnvironment = hostEnvironment;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Check token first
if (_options.Token is null)
{
_logger.LogError("Couldn't find any discord bot token, exiting...");
_hostApplicationLifetime.StopApplication();
return;
}
// Initialize modules and commands
await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _serviceProvider);
_client.Ready += ClientOnReady;
_client.InteractionCreated += HandleInteraction;
// Start bot
await _client.LoginAsync(TokenType.Bot, _options.Token);
await _client.StartAsync();
await Task.Delay(Timeout.Infinite, stoppingToken);
}
private async Task ClientOnReady()
{
// Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state.
// Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands.
if (_hostEnvironment.IsDevelopment())
{
// Check that a dev guild is set
if (!_options.DevGuildId.HasValue)
{
_logger.LogError("Couldn't find any dev guild while application is run in dev mode, exiting...");
_hostApplicationLifetime.StopApplication();
return;
}
await _interactionService.RegisterCommandsToGuildAsync(_options.DevGuildId.Value, true);
}
else
{
await _interactionService.RegisterCommandsGloballyAsync();
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
await _client.StopAsync();
}
private async Task HandleInteraction(SocketInteraction interaction)
{
_logger.LogTrace("[Interaction/Trace] Received interaction: by {user} in #{channel} of type {type}", interaction.User, interaction.Channel, interaction.Type);
try
{
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules.
var context = new SocketInteractionContext(_client, interaction);
// Execute the incoming command.
var result = await _interactionService.ExecuteCommandAsync(context, _serviceProvider);
if (!result.IsSuccess)
{
_logger.LogDebug("[Interaction/Trace] Error while executing interaction: {interaction} in {channel}", interaction.Token, interaction.Channel);
switch (result.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
}
}
}
catch
{
// If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if (interaction.Type is InteractionType.ApplicationCommand)
{
await interaction.GetOriginalResponseAsync().ContinueWith(async msg => await msg.Result.DeleteAsync());
}
}
}
}

View File

@@ -0,0 +1,56 @@
using Cocotte.Utils;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
namespace Cocotte.Services;
public class DiscordLoggingService : IHostedService
{
private readonly ILogger<DiscordLoggingService> _logger;
private readonly DiscordSocketClient _client;
private readonly CommandService _command;
public DiscordLoggingService(ILogger<DiscordLoggingService> logger, DiscordSocketClient client,
CommandService command)
{
_logger = logger;
_client = client;
_command = command;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_client.Log += LogAsync;
_command.Log += LogAsync;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_client.Log -= LogAsync;
_command.Log -= LogAsync;
return Task.CompletedTask;
}
private Task LogAsync(LogMessage message)
{
if (message.Exception is CommandException cmdException)
{
_logger.Log(message.Severity.ToLogLevel(), cmdException,
"[Command/{severity}] ({source}) {commandName} failed to execute in {channel}.",
message.Source,
message.Severity,
cmdException.Command.Aliases.First(),
cmdException.Context.Channel);
}
else
{
_logger.Log(message.Severity.ToLogLevel(), message.Exception, "[General/{severity}] ({source}) {message}", message.Severity, message.Source, message.Message);
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,6 @@
namespace Cocotte.Services;
public class SharedCounter
{
public int Count { get; set; } = 0;
}

View File

@@ -0,0 +1,29 @@
using Discord;
namespace Cocotte.Utils;
public static class LogExtensions
{
public static LogLevel ToLogLevel(this LogSeverity severity) => severity switch
{
LogSeverity.Critical => LogLevel.Critical,
LogSeverity.Debug => LogLevel.Debug,
LogSeverity.Error => LogLevel.Error,
LogSeverity.Info => LogLevel.Information,
LogSeverity.Verbose => LogLevel.Trace,
LogSeverity.Warning => LogLevel.Warning,
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null)
};
public static void WriteToLogger<TLogger>(this LogMessage message, ILogger<TLogger> logger)
{
if (message.Severity == LogSeverity.Critical)
{
logger.LogCritical(message.Exception, "Discord.Net log from {source}: {message}", message.Source, message.Message);
}
else
{
logger.Log(message.Severity.ToLogLevel(), "Discord.Net log from {source}: {message}", message.Source, message.Message);
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

8
Cocotte/appsettings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}