Initial commit
This commit is contained in:
14
Cocotte/Cocotte.csproj
Normal file
14
Cocotte/Cocotte.csproj
Normal 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>
|
||||
19
Cocotte/Modules/PingModule.cs
Normal file
19
Cocotte/Modules/PingModule.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
Cocotte/Options/DiscordOptions.cs
Normal file
9
Cocotte/Options/DiscordOptions.cs
Normal 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
40
Cocotte/Program.cs
Normal 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();
|
||||
11
Cocotte/Properties/launchSettings.json
Normal file
11
Cocotte/Properties/launchSettings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Cocotter": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
Cocotte/Services/CocotteService.cs
Normal file
122
Cocotte/Services/CocotteService.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Cocotte/Services/DiscordLoggingService.cs
Normal file
56
Cocotte/Services/DiscordLoggingService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
6
Cocotte/Services/SharedCounter.cs
Normal file
6
Cocotte/Services/SharedCounter.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Cocotte.Services;
|
||||
|
||||
public class SharedCounter
|
||||
{
|
||||
public int Count { get; set; } = 0;
|
||||
}
|
||||
29
Cocotte/Utils/LogExtensions.cs
Normal file
29
Cocotte/Utils/LogExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Cocotte/appsettings.Development.json
Normal file
8
Cocotte/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Cocotte/appsettings.json
Normal file
8
Cocotte/appsettings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user