2022-02-23 22:31:54 +00:00
using BirthdayBot.BackgroundServices ;
2020-10-05 04:40:38 +00:00
using BirthdayBot.Data ;
2022-02-23 22:31:54 +00:00
using Discord.Interactions ;
2020-10-05 04:40:38 +00:00
using Discord.Net ;
2022-02-23 22:31:54 +00:00
using Microsoft.Extensions.DependencyInjection ;
using System.Reflection ;
2022-01-31 03:53:02 +00:00
using static BirthdayBot . TextCommands . CommandsCommon ;
2020-10-05 04:40:38 +00:00
2021-11-21 21:21:42 +00:00
namespace BirthdayBot ;
/// <summary>
/// Single shard instance for Birthday Bot. This shard independently handles all input and output to Discord.
/// </summary>
2022-02-23 22:31:54 +00:00
public class ShardInstance : IDisposable {
2021-11-21 21:21:42 +00:00
private readonly ShardManager _manager ;
private readonly ShardBackgroundWorker _background ;
2022-01-31 06:26:33 +00:00
private readonly Dictionary < string , CommandHandler > _textDispatch ;
2022-02-23 22:31:54 +00:00
private readonly InteractionService _interactionService ;
private readonly IServiceProvider _services ;
2021-11-21 21:21:42 +00:00
2022-02-23 22:31:54 +00:00
internal DiscordSocketClient DiscordClient { get ; }
2021-11-21 21:21:42 +00:00
public int ShardId = > DiscordClient . ShardId ;
2020-10-05 04:40:38 +00:00
/// <summary>
2021-11-21 21:21:42 +00:00
/// Returns a value showing the time in which the last background run successfully completed.
2020-10-05 04:40:38 +00:00
/// </summary>
2022-02-23 22:31:54 +00:00
internal DateTimeOffset LastBackgroundRun = > _background . LastBackgroundRun ;
2021-11-21 21:21:42 +00:00
/// <summary>
/// Returns the name of the background service currently in execution.
/// </summary>
2022-02-23 22:31:54 +00:00
internal string? CurrentExecutingService = > _background . CurrentExecutingService ;
internal Configuration Config = > _manager . Config ;
2020-10-05 04:40:38 +00:00
2022-01-31 06:26:33 +00:00
public const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner." ;
2022-02-04 05:49:19 +00:00
public const string UnknownCommandError = "Oops, that command isn't supposed to be there... Please try something else." ;
2022-01-31 06:26:33 +00:00
2021-11-21 21:21:42 +00:00
/// <summary>
/// Prepares and configures the shard instances, but does not yet start its connection.
/// </summary>
2022-02-23 22:31:54 +00:00
internal ShardInstance ( ShardManager manager , IServiceProvider services , Dictionary < string , CommandHandler > textCmds ) {
2021-11-21 21:21:42 +00:00
_manager = manager ;
2022-02-23 22:31:54 +00:00
_services = services ;
2022-01-31 06:26:33 +00:00
_textDispatch = textCmds ;
2021-11-21 21:21:42 +00:00
2022-02-23 22:31:54 +00:00
DiscordClient = _services . GetRequiredService < DiscordSocketClient > ( ) ;
2021-11-21 21:21:42 +00:00
DiscordClient . Log + = Client_Log ;
DiscordClient . Ready + = Client_Ready ;
DiscordClient . MessageReceived + = Client_MessageReceived ;
2022-02-23 22:31:54 +00:00
_interactionService = _services . GetRequiredService < InteractionService > ( ) ;
_interactionService . AddModulesAsync ( Assembly . GetExecutingAssembly ( ) , null ) ;
DiscordClient . InteractionCreated + = DiscordClient_InteractionCreated ;
_interactionService . SlashCommandExecuted + = InteractionService_SlashCommandExecuted ;
2020-10-05 23:10:02 +00:00
2021-11-21 21:21:42 +00:00
// Background task constructor begins background processing immediately.
_background = new ShardBackgroundWorker ( this ) ;
}
/// <summary>
/// Starts up this shard's connection to Discord and background task handling associated with it.
/// </summary>
public async Task StartAsync ( ) {
await DiscordClient . LoginAsync ( TokenType . Bot , Config . BotToken ) . ConfigureAwait ( false ) ;
await DiscordClient . StartAsync ( ) . ConfigureAwait ( false ) ;
}
/// <summary>
2021-12-06 02:58:10 +00:00
/// Does all necessary steps to stop this shard, including canceling background tasks and disconnecting.
2021-11-21 21:21:42 +00:00
/// </summary>
public void Dispose ( ) {
2022-02-23 22:31:54 +00:00
// TODO are these necessary?
_interactionService . SlashCommandExecuted - = InteractionService_SlashCommandExecuted ;
DiscordClient . InteractionCreated - = DiscordClient_InteractionCreated ;
2021-11-21 21:21:42 +00:00
DiscordClient . Log - = Client_Log ;
DiscordClient . Ready - = Client_Ready ;
DiscordClient . MessageReceived - = Client_MessageReceived ;
2022-02-23 22:31:54 +00:00
_interactionService . Dispose ( ) ;
2021-11-21 21:21:42 +00:00
_background . Dispose ( ) ;
2021-12-06 02:58:10 +00:00
DiscordClient . LogoutAsync ( ) . Wait ( 5000 ) ;
DiscordClient . StopAsync ( ) . Wait ( 5000 ) ;
DiscordClient . Dispose ( ) ;
Log ( nameof ( ShardInstance ) , "Shard instance disposed." ) ;
2021-11-21 21:21:42 +00:00
}
2020-10-05 04:40:38 +00:00
2021-11-21 21:21:42 +00:00
public void Log ( string source , string message ) = > Program . Log ( $"Shard {ShardId:00}] [{source}" , message ) ;
2021-02-02 06:31:24 +00:00
2021-11-21 21:21:42 +00:00
private Task Client_Log ( LogMessage arg ) {
// Suppress certain messages
if ( arg . Message ! = null ) {
2022-01-04 23:06:25 +00:00
// These warnings appear often as of Discord.Net v3...
2021-11-22 19:49:55 +00:00
if ( arg . Message . StartsWith ( "Unknown Dispatch " ) | | arg . Message . StartsWith ( "Unknown Channel" ) ) return Task . CompletedTask ;
2021-11-21 21:21:42 +00:00
switch ( arg . Message ) // Connection status messages replaced by ShardManager's output
2020-10-05 04:40:38 +00:00
{
2021-11-21 21:21:42 +00:00
case "Connecting" :
case "Connected" :
case "Ready" :
2022-01-04 23:06:25 +00:00
case "Disconnecting" :
case "Disconnected" :
case "Resumed previous session" :
2021-11-21 21:21:42 +00:00
case "Failed to resume previous session" :
2021-12-06 02:58:10 +00:00
case "Discord.WebSocket.GatewayReconnectException: Server requested a reconnect" :
2020-10-05 04:40:38 +00:00
return Task . CompletedTask ;
}
2021-11-21 21:21:42 +00:00
Log ( "Discord.Net" , $"{arg.Severity}: {arg.Message}" ) ;
2020-10-05 04:40:38 +00:00
}
2021-12-06 02:58:10 +00:00
if ( arg . Exception ! = null ) Log ( "Discord.Net exception" , arg . Exception . ToString ( ) ) ;
2021-11-21 21:21:42 +00:00
return Task . CompletedTask ;
}
/// <summary>
2022-01-31 06:26:33 +00:00
/// Registers all available slash commands.
/// Additionally, sets the shard's status to display the help command.
2021-11-21 21:21:42 +00:00
/// </summary>
2022-01-31 06:26:33 +00:00
private async Task Client_Ready ( ) {
await DiscordClient . SetGameAsync ( CommandPrefix + "help" ) ;
#if ! DEBUG
2022-02-23 22:31:54 +00:00
// Update slash/interaction commands
await _interactionService . RegisterCommandsGloballyAsync ( true ) . ConfigureAwait ( false ) ;
2022-01-31 06:26:33 +00:00
#else
// Debug: Register our commands locally instead, in each guild we're in
foreach ( var g in DiscordClient . Guilds ) {
2022-02-23 22:31:54 +00:00
await _interactionService . RegisterCommandsToGuildAsync ( g . Id , true ) . ConfigureAwait ( false ) ;
// TODO log?
2022-01-31 06:26:33 +00:00
}
#endif
}
2021-11-21 21:21:42 +00:00
/// <summary>
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
/// </summary>
private async Task Client_MessageReceived ( SocketMessage msg ) {
if ( msg . Channel is not SocketTextChannel channel ) return ;
if ( msg . Author . IsBot | | msg . Author . IsWebhook ) return ;
if ( ( ( IMessage ) msg ) . Type ! = MessageType . Default ) return ;
var author = ( SocketGuildUser ) msg . Author ;
// Limit 3:
// For all cases: base command, 2 parameters.
// Except this case: "bb.config", subcommand name, subcommand parameters in a single string
var csplit = msg . Content . Split ( " " , 3 , StringSplitOptions . RemoveEmptyEntries ) ;
if ( csplit . Length > 0 & & csplit [ 0 ] . StartsWith ( CommandPrefix , StringComparison . OrdinalIgnoreCase ) ) {
// Determine if it's something we're listening for.
2022-01-31 06:26:33 +00:00
if ( ! _textDispatch . TryGetValue ( csplit [ 0 ] [ CommandPrefix . Length . . ] , out CommandHandler ? command ) ) return ;
2021-11-21 21:21:42 +00:00
// Load guild information here
var gconf = await GuildConfiguration . LoadAsync ( channel . Guild . Id , false ) ;
// Ban check
if ( ! gconf ! . IsBotModerator ( author ) ) // skip check if user is a moderator
2020-10-05 04:40:38 +00:00
{
2021-11-21 21:21:42 +00:00
if ( await gconf . IsUserBlockedAsync ( author . Id ) ) return ; // silently ignore
}
// Execute the command
try {
Log ( "Command" , $"{channel.Guild.Name}/{author.Username}#{author.Discriminator}: {msg.Content}" ) ;
await command ( this , gconf , csplit , channel , author ) ;
} catch ( Exception ex ) {
if ( ex is HttpException ) return ;
Log ( "Command" , ex . ToString ( ) ) ;
try {
2022-01-31 06:26:33 +00:00
channel . SendMessageAsync ( InternalError ) . Wait ( ) ;
2021-11-21 21:21:42 +00:00
} catch ( HttpException ) { } // Fail silently
2020-10-05 04:40:38 +00:00
}
}
}
2022-01-31 06:26:33 +00:00
2022-02-23 22:31:54 +00:00
private async Task InteractionService_SlashCommandExecuted ( SlashCommandInfo arg1 , IInteractionContext arg2 , IResult arg3 ) {
if ( arg3 . IsSuccess ) return ;
Log ( "Interaction error" , Enum . GetName ( typeof ( InteractionCommandError ) , arg3 . Error ) + " " + arg3 . ErrorReason ) ;
// TODO finish this up
}
private async Task DiscordClient_InteractionCreated ( SocketInteraction arg ) {
// TODO this is straight from the example - look it over
2022-01-31 06:26:33 +00:00
try {
2022-02-23 22:31:54 +00:00
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules
var context = new SocketInteractionContext ( DiscordClient , arg ) ;
await _interactionService . ExecuteCommandAsync ( context , _services ) ;
} catch ( Exception ex ) {
Console . WriteLine ( ex ) ;
// If a 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 ( arg . Type = = InteractionType . ApplicationCommand )
await arg . GetOriginalResponseAsync ( ) . ContinueWith ( async ( msg ) = > await msg . Result . DeleteAsync ( ) ) ;
2022-01-31 06:26:33 +00:00
}
}
2020-10-05 04:40:38 +00:00
}