2022-01-15 21:26:32 +01:00
using CommandLine ;
2022-01-10 16:10:32 +01:00
using Discord ;
2022-01-11 20:32:25 +01:00
using Discord.Commands ;
2022-01-10 16:10:32 +01:00
using Discord.Rest ;
2022-01-18 10:10:49 +01:00
using Discord.Webhook ;
2022-01-10 16:10:32 +01:00
using Discord.WebSocket ;
using Fleck ;
2022-01-16 22:29:50 +01:00
using MinecraftDiscordBot.Commands ;
2022-01-18 10:10:49 +01:00
using MinecraftDiscordBot.Models ;
2022-01-16 22:29:50 +01:00
using MinecraftDiscordBot.Services ;
2022-01-10 16:10:32 +01:00
using System.Collections.Concurrent ;
2022-01-11 20:32:25 +01:00
using System.Reflection ;
2022-01-12 13:03:30 +01:00
using System.Runtime.CompilerServices ;
2022-01-10 11:33:18 +01:00
2022-01-10 16:10:32 +01:00
namespace MinecraftDiscordBot ;
2022-01-10 11:33:18 +01:00
2022-01-17 15:24:04 +01:00
public class Program : IDisposable , ICommandHandler < ResponseType > , IUserRoleManager {
2022-01-12 13:03:30 +01:00
public const string WebSocketSource = "WebSocket" ;
public const string BotSource = "Bot" ;
private static readonly object LogLock = new ( ) ;
2022-01-16 15:58:35 +01:00
public const int ChoiceTimeout = 20 * 1000 ;
2022-01-10 16:10:32 +01:00
private readonly DiscordSocketClient _client = new ( new ( ) {
2022-01-12 13:03:30 +01:00
LogLevel = LogSeverity . Verbose ,
GatewayIntents = GatewayIntents . AllUnprivileged & ~ ( GatewayIntents . GuildScheduledEvents | GatewayIntents . GuildInvites )
2022-01-10 16:10:32 +01:00
} ) ;
private readonly WebSocketServer _wssv ;
private readonly BotConfiguration _config ;
private readonly HashSet < ulong > _whitelistedChannels ;
2022-01-16 22:29:50 +01:00
private readonly ConcurrentDictionary < Guid , RootCommandService > _connections = new ( ) ;
2022-01-11 20:32:25 +01:00
private static readonly char [ ] WhiteSpace = new char [ ] { '\t' , '\n' , ' ' , '\r' } ;
2022-01-18 10:10:49 +01:00
public IEnumerable < ActiveChannel > Channels = > _channels ? ? throw new InvalidProgramException ( "Channels used before verification!" ) ;
public ActiveChannel [ ] ? _channels ;
2022-01-12 13:03:30 +01:00
private bool disposedValue ;
2022-01-18 13:05:34 +01:00
private static ITextChannel ? LogChannel ;
2022-01-17 19:10:14 +01:00
private readonly RootCommandService _computer ;
2022-01-17 15:24:04 +01:00
public static bool OnlineNotifications = > true ;
2022-01-18 13:05:34 +01:00
public const LogSeverity DiscordLogSeverity = LogSeverity . Warning ;
2022-01-16 22:29:50 +01:00
private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua" ;
2022-01-18 10:10:49 +01:00
private const string WebhookName = "minecraftbot" ;
2022-01-16 22:29:50 +01:00
public readonly string ClientScript ;
2022-01-16 21:51:37 +01:00
private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider ( InstanceId , 10 ) ;
private static readonly int InstanceId = new Random ( ) . Next ( ) ;
2022-01-16 21:31:07 +01:00
2022-01-16 22:29:50 +01:00
private string GetVerifiedClientScript ( ) = > ClientScript
. Replace ( "$TOKEN" , _tokenProvider . GenerateToken ( ) ) ;
2022-01-16 21:31:07 +01:00
2022-01-17 15:24:04 +01:00
private static string GetClientScript ( BotConfiguration config ) {
2022-01-16 22:29:50 +01:00
using var stream = Assembly . GetExecutingAssembly ( ) . GetManifestResourceStream ( ClientScriptName ) ;
2022-01-16 21:31:07 +01:00
if ( stream is null ) throw new FileNotFoundException ( "Client script could not be loaded!" ) ;
using var sr = new StreamReader ( stream ) ;
2022-01-16 22:29:50 +01:00
return sr . ReadToEnd ( )
. Replace ( "$HOST" , $"ws://{config.SocketHost}:{config.Port}" ) ;
2022-01-16 21:31:07 +01:00
}
2022-01-12 13:03:30 +01:00
2022-01-18 10:10:49 +01:00
private Task Broadcast ( Func < ITextChannel , Task < IUserMessage > > message )
= > Task . WhenAll ( Channels . Select ( i = > message ( i . Channel ) ) ) ;
2022-01-10 16:10:32 +01:00
public Program ( BotConfiguration config ) {
_config = config ;
2022-01-17 19:10:14 +01:00
_computer = new ( this ) ;
2022-01-18 10:10:49 +01:00
_computer . ChatMessageReceived + = MinecraftMessageReceived ;
2022-01-18 11:46:23 +01:00
_computer . SocketChanged + = ComputerConnectedChanged ;
2022-01-17 15:24:04 +01:00
_administrators = config . Administrators . ToHashSet ( ) ;
2022-01-16 22:29:50 +01:00
ClientScript = GetClientScript ( config ) ;
2022-01-12 13:03:30 +01:00
_client . Log + = LogAsync ;
2022-01-18 11:46:23 +01:00
_client . MessageReceived + = ( msg ) = > DiscordMessageReceived ( msg , 20 * 1000 ) ;
2022-01-16 15:58:35 +01:00
_client . ReactionAdded + = DiscordReactionAdded ;
2022-01-10 16:10:32 +01:00
_wssv = new WebSocketServer ( $"ws://0.0.0.0:{config.Port}" ) {
RestartAfterListenError = true
} ;
2022-01-12 13:03:30 +01:00
FleckLog . LogAction = LogWebSocket ;
2022-01-10 16:10:32 +01:00
_whitelistedChannels = config . Channels . ToHashSet ( ) ;
}
2022-01-18 13:05:34 +01:00
private void ComputerConnectedChanged ( object? sender , IWebSocketConnection ? e )
= > _ = Task . Run ( ( ) = > Broadcast ( i = > i . SendMessageAsync ( e is not null
? "The Minecraft client is now available!"
: "The Minecraft client disconnected!" ) ) ) ;
2022-01-18 10:10:49 +01:00
private void MinecraftMessageReceived ( object? sender , ChatEvent e )
= > Task . Run ( ( ) = > WebhookBroadcast ( i = > i . SendMessageAsync ( e . Message , username : e . Username , avatarUrl : $"https://crafatar.com/renders/head/{e.UUID}" ) ) ) ;
private Task < T [ ] > WebhookBroadcast < T > ( Func < DiscordWebhookClient , Task < T > > apply ) = > Task . WhenAll ( Channels . Select ( i = > apply ( new DiscordWebhookClient ( i . Webhook ) ) ) ) ;
2022-01-12 13:03:30 +01:00
private void LogWebSocket ( LogLevel level , string message , Exception exception ) = > Log ( new ( level switch {
LogLevel . Debug = > LogSeverity . Debug ,
LogLevel . Info = > LogSeverity . Info ,
LogLevel . Warn = > LogSeverity . Warning ,
LogLevel . Error = > LogSeverity . Error ,
_ = > LogSeverity . Critical // Unknown logging states should behave critical
} , WebSocketSource , message , exception ) ) ;
2022-01-10 16:10:32 +01:00
public static Task < int > Main ( string [ ] args )
2022-01-12 14:32:25 +01:00
= > Parser . Default . ParseArguments < BotConfiguration , ConfigFile > ( args )
. MapResult < BotConfiguration , ConfigFile , Task < int > > (
RunWithConfig ,
RunWithConfig ,
errs = > Task . FromResult ( 1 ) ) ;
private static Task < int > RunWithConfig ( IBotConfigurator arg ) = > new Program ( arg . Config ) . RunAsync ( ) ;
2022-01-10 16:10:32 +01:00
public async Task < int > RunAsync ( ) {
2022-01-16 21:51:37 +01:00
StartWebSocketServer ( ) ;
2022-01-10 16:10:32 +01:00
await _client . LoginAsync ( TokenType . Bot , _config . Token ) ;
await _client . StartAsync ( ) ;
2022-01-12 14:32:25 +01:00
if ( ! await HasValidChannels ( ) )
return 1 ;
2022-01-10 16:10:32 +01:00
// Block this task until the program is closed.
await Task . Delay ( - 1 ) ;
return 0 ;
}
2022-01-12 14:32:25 +01:00
private async Task < bool > HasValidChannels ( ) {
2022-01-18 13:05:34 +01:00
if ( _config . LogChannel is ulong logChannelId ) {
LogChannel = await IsValidChannel ( logChannelId ) ;
if ( LogChannel is null )
await LogWarningAsync ( BotSource , $"The given log channel ID is not valid '{logChannelId}'!" ) ;
}
if ( await GetValidChannels ( _whitelistedChannels ) is not { Length : > 0 } channels ) {
2022-01-12 19:03:51 +01:00
await LogErrorAsync ( BotSource , new InvalidOperationException ( "No valid textchannel was whitelisted!" ) ) ;
2022-01-12 14:32:25 +01:00
return false ;
}
2022-01-18 10:10:49 +01:00
_channels = await Task . WhenAll ( channels . Select ( async i = > new ActiveChannel ( i , await GetOrCreateWebhook ( i ) ) ) ) ;
static async Task < IWebhook > GetOrCreateWebhook ( ITextChannel i ) {
var hooks = ( await i . GetWebhooksAsync ( ) ) . Where ( i = > i . Name = = WebhookName ) . FirstOrDefault ( ) ;
return hooks ? ? await i . CreateWebhookAsync ( WebhookName ) ;
}
2022-01-12 14:32:25 +01:00
return true ;
}
2022-01-12 13:03:30 +01:00
private void StartWebSocketServer ( ) = > _wssv . Start ( socket = > {
socket . OnOpen = async ( ) = > await SocketOpened ( socket ) ;
socket . OnClose = async ( ) = > await SocketClosed ( socket ) ;
socket . OnMessage = async message = > await SocketReceived ( socket , message ) ;
} ) ;
2022-01-12 14:32:25 +01:00
2022-01-18 13:05:34 +01:00
private async Task < ITextChannel [ ] > GetValidChannels ( IEnumerable < ulong > ids )
= > ( await Task . WhenAll ( ids . Select ( i = > IsValidChannel ( i ) ) ) ) . OfType < ITextChannel > ( ) . ToArray ( ) ;
private async Task < ITextChannel ? > IsValidChannel ( ulong channelId ) {
var channel = await _client . GetChannelAsync ( channelId ) ;
if ( channel is not ITextChannel textChannel ) {
if ( channel is null ) await LogWarningAsync ( BotSource , $"Channel with id [{channelId}] does not exist!" ) ;
else await LogWarningAsync ( BotSource , $"Channel is not a text channels and will not be used: {channel.Name} [{channel.Id}]!" ) ;
return null ;
}
2022-01-12 13:03:30 +01:00
2022-01-18 13:05:34 +01:00
if ( textChannel . Guild is RestGuild guild ) {
await guild . UpdateAsync ( ) ;
await LogInfoAsync ( BotSource , $"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {guild.Name} [{guild.Id}]" ) ;
} else {
await LogWarningAsync ( BotSource , $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!" ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-18 13:05:34 +01:00
return textChannel ;
2022-01-10 16:10:32 +01:00
}
2022-01-16 21:31:07 +01:00
private async Task SocketReceived ( IWebSocketConnection socket , string message ) {
await LogInfoAsync ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Received: {message}" ) ;
await ( message switch {
"getcode" = > SendClientCode ( socket ) ,
string s when s . StartsWith ( "login=" ) = > ClientComputerConnected ( socket , s [ 6. . ] ) ,
2022-01-17 19:10:14 +01:00
string s when s . StartsWith ( "error=" ) = > ClientComputerError ( socket , s [ 6. . ] ) ,
2022-01-16 21:31:07 +01:00
_ = > DisruptClientConnection ( socket , "Protocol violation!" )
} ) ;
}
2022-01-17 19:10:14 +01:00
private static async Task ClientComputerError ( IWebSocketConnection socket , string message )
= > await LogWarningAsync ( "Client" , $"Computer failed to run the script: {message}" ) ;
2022-01-16 21:31:07 +01:00
private async Task ClientComputerConnected ( IWebSocketConnection socket , string token ) {
if ( ! _tokenProvider . VerifyToken ( token ) ) {
await DisruptClientConnection ( socket , "outdated" ) ;
return ;
}
await LogInfoAsync ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client logged in with valid script!" ) ;
2022-01-17 19:10:14 +01:00
AddComputerSocket ( socket ) ;
2022-01-16 21:31:07 +01:00
}
private static async Task DisruptClientConnection ( IWebSocketConnection socket , string reason ) {
await socket . Send ( reason ) ;
await LogWarningAsync ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client will be terminated, reason: {reason}" ) ;
socket . Close ( ) ;
}
private async Task SendClientCode ( IWebSocketConnection socket ) {
await socket . Send ( GetVerifiedClientScript ( ) ) ;
await LogInfoAsync ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Script sent to client!" ) ;
}
2022-01-12 13:03:30 +01:00
2022-01-17 19:10:14 +01:00
private void AddComputerSocket ( IWebSocketConnection socket ) = > _computer . Socket = socket ;
2022-01-11 20:32:25 +01:00
private void RemoveComputerSocket ( IWebSocketConnection socket ) {
2022-01-17 19:10:14 +01:00
if ( _computer . Socket is { ConnectionInfo . Id : Guid id } & & id = = socket . ConnectionInfo . Id ) _computer . Socket = null ;
2022-01-10 16:10:32 +01:00
}
2022-01-11 20:32:25 +01:00
private async Task SocketClosed ( IWebSocketConnection socket ) {
RemoveComputerSocket ( socket ) ;
2022-01-12 19:03:51 +01:00
await LogInfoAsync ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client disconnected!" ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-16 21:31:07 +01:00
private static async Task SocketOpened ( IWebSocketConnection socket ) = > await LogInfoAsync ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client connected from {socket.ConnectionInfo.ClientIpAddress}:{socket.ConnectionInfo.ClientPort}!" ) ;
2022-01-11 20:32:25 +01:00
private async Task DiscordMessageReceived ( SocketMessage arg , int timeout = 10000 ) {
if ( arg is not SocketUserMessage message ) return ;
if ( message . Author . IsBot ) return ;
if ( ! IsChannelWhitelisted ( arg . Channel ) ) return ;
2022-01-17 15:24:04 +01:00
if ( arg . Type is not MessageType . Default ) return ;
2022-01-11 20:32:25 +01:00
2022-01-12 13:03:30 +01:00
var cts = new CancellationTokenSource ( timeout ) ;
2022-01-11 20:32:25 +01:00
if ( IsCommand ( message , out var argPos ) ) {
2022-01-17 15:24:04 +01:00
await arg . Channel . TriggerTypingAsync ( ) ;
2022-01-11 20:32:25 +01:00
var parameters = message . Content [ argPos . . ] . Split ( WhiteSpace , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
2022-01-15 21:26:32 +01:00
_ = Task . Run ( async ( ) = > {
var response = await HandleCommand ( message , parameters , cts . Token ) ;
await SendResponse ( message , response ) ;
} ) ;
2022-01-11 20:32:25 +01:00
return ;
}
2022-01-12 19:03:51 +01:00
await LogInfoAsync ( "Discord" , $"[{arg.Author.Username}] {arg.Content}" ) ;
2022-01-18 10:10:49 +01:00
_ = Task . Run ( ( ) = > _computer . Chat . SendMessageAsync ( arg . Content , arg . Author . Username , cts . Token ) ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-16 15:58:35 +01:00
private Task SendResponse ( SocketUserMessage message , ResponseType response ) = > response switch {
2022-01-15 21:26:32 +01:00
ResponseType . IChoiceResponse res = > HandleChoice ( message , res ) ,
ResponseType . StringResponse res = > message . ReplyAsync ( res . Message ) ,
2022-01-18 11:46:23 +01:00
ResponseType . FileResponse res = > message . Channel . SendFileAsync ( res . Path , text : res . Message ) ,
_ = > message . ReplyAsync ( $"Whoops, someone forgot to implement '{response.GetType().Name}' responses?" ) ,
2022-01-15 21:26:32 +01:00
} ;
2022-01-11 20:32:25 +01:00
2022-01-16 15:58:35 +01:00
private readonly ConcurrentDictionary < ulong , ResponseType . IChoiceResponse > _choiceWait = new ( ) ;
2022-01-17 15:24:04 +01:00
private readonly HashSet < ulong > _administrators ;
2022-01-16 15:58:35 +01:00
private async Task DiscordReactionAdded ( Cacheable < IUserMessage , ulong > message , Cacheable < IMessageChannel , ulong > channel , SocketReaction reaction ) {
var msgObject = await message . GetOrDownloadAsync ( ) ;
if ( reaction . UserId = = _client . CurrentUser . Id ) return ;
if ( ! _choiceWait . TryRemove ( message . Id , out var choice ) ) { await LogInfoAsync ( BotSource , "Reaction was added to message without choice object!" ) ; return ; }
await msgObject . DeleteAsync ( ) ;
await LogInfoAsync ( BotSource , $"Reaction {reaction.Emote.Name} was added to the choice by {reaction.UserId}!" ) ;
}
private async Task HandleChoice ( SocketUserMessage message , ResponseType . IChoiceResponse res ) {
2022-01-15 21:26:32 +01:00
var reply = await message . ReplyAsync ( $"{res.Query}\n{string.Join(" \ n ", res.Options)}" ) ;
2022-01-16 15:58:35 +01:00
_choiceWait [ reply . Id ] = res ;
var reactions = new Emoji [ ] { new ( "0️ ⃣" ) /*, new("1️ ⃣"), new("2️ ⃣"), new("3️ ⃣"), new("4️ ⃣"), new("5️ ⃣"), new("6️ ⃣"), new("7️ ⃣"), new("8️ ⃣"), new("9️ ⃣")*/ } ;
2022-01-15 21:26:32 +01:00
await reply . AddReactionsAsync ( reactions ) ;
2022-01-16 15:58:35 +01:00
_ = Task . Run ( async ( ) = > {
await Task . Delay ( ChoiceTimeout ) ;
_ = _choiceWait . TryRemove ( message . Id , out _ ) ;
await reply . ModifyAsync ( i = > i . Content = "You did not choose in time!" ) ;
await reply . RemoveAllReactionsAsync ( ) ;
} ) ;
2022-01-15 21:26:32 +01:00
}
public async Task < ResponseType > HandleCommand ( SocketUserMessage message , string [ ] parameters , CancellationToken ct ) {
2022-01-17 19:10:14 +01:00
if ( _computer is ICommandHandler < ResponseType > handler )
2022-01-15 21:26:32 +01:00
try {
return await handler . HandleCommand ( message , parameters , ct ) ;
} catch ( TaskCanceledException ) {
return ResponseType . AsString ( "Your request could not be processed in time!" ) ;
} catch ( ReplyException e ) {
2022-01-16 22:15:11 +01:00
await LogInfoAsync ( BotSource , e . Message ) ;
return ResponseType . AsString ( e . Message ) ;
2022-01-15 21:26:32 +01:00
} catch ( Exception e ) {
await LogErrorAsync ( BotSource , e ) ;
return ResponseType . AsString ( $"Oopsie doopsie, this should not have happened!" ) ;
}
else return ResponseType . AsString ( "The Minecraft server is currently unavailable!" ) ;
}
2022-01-11 20:32:25 +01:00
private bool IsCommand ( SocketUserMessage message , out int argPos ) {
argPos = 0 ;
return message . HasStringPrefix ( _config . Prefix , ref argPos ) ;
}
2022-01-10 16:10:32 +01:00
private bool IsChannelWhitelisted ( ISocketMessageChannel channel )
= > _whitelistedChannels . Contains ( channel . Id ) ;
2022-01-12 19:03:51 +01:00
public static ConfiguredTaskAwaitable LogInfoAsync ( string source , string message ) = > LogAsync ( new ( LogSeverity . Info , source , message ) ) . ConfigureAwait ( false ) ;
public static ConfiguredTaskAwaitable LogWarningAsync ( string source , string message ) = > LogAsync ( new ( LogSeverity . Warning , source , message ) ) . ConfigureAwait ( false ) ;
public static ConfiguredTaskAwaitable LogErrorAsync ( string source , Exception exception ) = > LogAsync ( new ( LogSeverity . Error , source , exception ? . Message , exception ) ) . ConfigureAwait ( false ) ;
public static void LogInfo ( string source , string message ) = > Log ( new ( LogSeverity . Info , source , message ) ) ;
public static void LogWarning ( string source , string message ) = > Log ( new ( LogSeverity . Warning , source , message ) ) ;
public static void LogError ( string source , Exception exception ) = > Log ( new ( LogSeverity . Error , source , exception ? . Message , exception ) ) ;
2022-01-12 13:03:30 +01:00
private static async Task LogAsync ( LogMessage msg ) {
2022-01-18 10:43:15 +01:00
lock ( LogLock ) {
var oldColor = Console . ForegroundColor ;
try {
Console . ForegroundColor = msg . Severity switch {
LogSeverity . Critical = > ConsoleColor . Magenta ,
LogSeverity . Error = > ConsoleColor . Red ,
LogSeverity . Warning = > ConsoleColor . Yellow ,
LogSeverity . Info = > ConsoleColor . White ,
LogSeverity . Verbose = > ConsoleColor . Blue ,
LogSeverity . Debug = > ConsoleColor . DarkBlue ,
_ = > ConsoleColor . Cyan ,
} ;
2022-01-18 10:10:49 +01:00
Console . WriteLine ( msg . ToString ( ) ) ;
2022-01-18 10:43:15 +01:00
} finally {
Console . ForegroundColor = oldColor ;
}
2022-01-18 10:10:49 +01:00
}
2022-01-18 13:05:34 +01:00
if ( msg . Severity < = DiscordLogSeverity & & LogChannel is ITextChannel log ) {
await log . SendMessageAsync ( $"{msg.Severity}: {msg}" ) ;
}
2022-01-12 13:03:30 +01:00
}
2022-01-18 13:05:34 +01:00
public static void Log ( LogMessage msg ) = > _ = Task . Run ( ( ) = > LogAsync ( msg ) ) ;
2022-01-12 13:03:30 +01:00
protected virtual void Dispose ( bool disposing ) {
if ( ! disposedValue ) {
if ( disposing ) {
// TODO: dispose managed state (managed objects)
_wssv . Dispose ( ) ;
_client . Dispose ( ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-12 13:03:30 +01:00
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
disposedValue = true ;
2022-01-10 16:10:32 +01:00
}
}
2022-01-12 13:03:30 +01:00
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~Program()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose ( ) {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose ( disposing : true ) ;
GC . SuppressFinalize ( this ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-17 15:24:04 +01:00
public void RequireAdministrator ( ulong user , string? message = null ) {
if ( ! _administrators . Contains ( user ) )
throw new ReplyException ( message ? ? "User is not authorized to access this command!" ) ;
}
2022-01-10 16:10:32 +01:00
}
2022-01-15 21:26:32 +01:00
2022-01-18 10:10:49 +01:00
public class ActiveChannel {
public ActiveChannel ( ITextChannel channel , IWebhook webhook ) {
Channel = channel ;
Webhook = webhook ;
}
public IWebhook Webhook { get ; }
public ITextChannel Channel { get ; }
}
2022-01-15 21:26:32 +01:00
public abstract class ResponseType {
private static string DefaultDisplay < T > ( T obj ) = > obj ? . ToString ( ) ? ? throw new InvalidProgramException ( "ToString did not yield anything!" ) ;
public static ResponseType AsString ( string message ) = > new StringResponse ( message ) ;
public static ResponseType FromChoice < T > ( string query , IEnumerable < T > choice , Func < T , Task > resultHandler , Func < T , string > ? display = null ) = > new ChoiceResponse < T > ( query , choice , resultHandler , display ? ? DefaultDisplay ) ;
2022-01-18 11:46:23 +01:00
internal static ResponseType File ( string path , string message ) = > new FileResponse ( path , message ) ;
2022-01-15 21:26:32 +01:00
public class StringResponse : ResponseType {
public StringResponse ( string message ) = > Message = message ;
public string Message { get ; }
}
public interface IChoiceResponse {
IEnumerable < string > Options { get ; }
string Query { get ; }
Task HandleResult ( int index ) ;
}
public class ChoiceResponse < T > : ResponseType , IChoiceResponse {
private readonly Func < T , Task > _resultHandler ;
private readonly T [ ] _options ;
private readonly Func < T , string > _displayer ;
public IEnumerable < string > Options = > _options . Select ( _displayer ) ;
public string Query { get ; }
public Task HandleResult ( int index ) = > _resultHandler ( _options [ index ] ) ;
public ChoiceResponse ( string query , IEnumerable < T > choice , Func < T , Task > resultHandler , Func < T , string > display ) {
Query = query ;
_resultHandler = resultHandler ;
_options = choice . ToArray ( ) ;
_displayer = display ;
}
}
2022-01-18 11:46:23 +01:00
public class FileResponse : ResponseType {
public FileResponse ( string path , string message ) {
Path = path ;
Message = message ;
}
public string Path { get ; }
public string Message { get ; }
}
2022-01-15 21:26:32 +01:00
}