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 ;
using Discord.WebSocket ;
using Fleck ;
using Newtonsoft.Json ;
2022-01-15 21:26:32 +01:00
using OneOf ;
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-15 21:26:32 +01:00
public class Program : IDisposable , ICommandHandler < ResponseType > {
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-11 20:32:25 +01:00
private readonly ConcurrentDictionary < Guid , ConnectedComputer > _connections = new ( ) ;
private static readonly char [ ] WhiteSpace = new char [ ] { '\t' , '\n' , ' ' , '\r' } ;
2022-01-12 13:03:30 +01:00
public ITextChannel [ ] _channels = Array . Empty < ITextChannel > ( ) ;
2022-01-15 21:26:32 +01:00
private ConnectedComputer ? _rsSystem = null ;
2022-01-12 13:03:30 +01:00
private bool disposedValue ;
2022-01-15 21:26:32 +01:00
public static bool OnlineNotifications = > false ;
2022-01-16 21:31:07 +01:00
public static readonly string ClientScript = GetClientScript ( ) ;
private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider ( 10 ) ;
private string GetVerifiedClientScript ( ) = > ClientScript . Replace ( "$TOKEN" , _tokenProvider . GenerateToken ( ) ) ;
private static string GetClientScript ( ) {
using var stream = Assembly . GetExecutingAssembly ( ) . GetManifestResourceStream ( "MinecraftDiscordBot.ClientScript.lua" ) ;
if ( stream is null ) throw new FileNotFoundException ( "Client script could not be loaded!" ) ;
using var sr = new StreamReader ( stream ) ;
return sr . ReadToEnd ( ) ;
}
2022-01-12 13:03:30 +01:00
2022-01-15 21:26:32 +01:00
public ConnectedComputer ? Computer {
2022-01-12 13:03:30 +01:00
get = > _rsSystem ; set {
if ( _rsSystem ! = value ) {
_rsSystem = value ;
2022-01-15 21:26:32 +01:00
if ( OnlineNotifications )
_ = Task . Run ( ( ) = > Broadcast ( i = > i . SendMessageAsync ( value is null
? $"The Refined Storage went offline. Please check the server!"
: $"The Refined Storage is back online!" ) ) ) ;
2022-01-12 13:03:30 +01:00
}
}
}
2022-01-10 16:10:32 +01:00
2022-01-12 13:03:30 +01:00
private async Task Broadcast ( Func < ITextChannel , Task < IUserMessage > > message ) = > _ = await Task . WhenAll ( _channels . Select ( message ) ) ;
2022-01-10 16:10:32 +01:00
public Program ( BotConfiguration config ) {
_config = config ;
2022-01-12 13:03:30 +01:00
_client . Log + = LogAsync ;
2022-01-11 20:32:25 +01:00
_client . MessageReceived + = ( msg ) = > DiscordMessageReceived ( msg ) ;
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-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 ( ) {
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-12 13:03:30 +01:00
StartWebSocketServer ( ) ;
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 ( ) {
if ( await GetValidChannels ( _whitelistedChannels ) . ToArrayAsync ( ) 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 ;
}
_channels = channels ;
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-12 13:03:30 +01:00
private async IAsyncEnumerable < ITextChannel > GetValidChannels ( IEnumerable < ulong > ids ) {
foreach ( var channelId in ids ) {
var channel = await _client . GetChannelAsync ( channelId ) ;
if ( channel is not ITextChannel textChannel ) {
2022-01-12 19:03:51 +01:00
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}]!" ) ;
2022-01-12 13:03:30 +01:00
continue ;
}
if ( textChannel . Guild is RestGuild guild ) {
await guild . UpdateAsync ( ) ;
2022-01-12 19:03:51 +01:00
await LogInfoAsync ( BotSource , $"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {guild.Name} [{guild.Id}]" ) ;
2022-01-12 13:03:30 +01:00
} else {
2022-01-12 19:03:51 +01:00
await LogWarningAsync ( BotSource , $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!" ) ;
2022-01-12 13:03:30 +01:00
}
yield 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. . ] ) ,
_ = > DisruptClientConnection ( socket , "Protocol violation!" )
} ) ;
}
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!" ) ;
AddComputerSocket ( socket , new ( socket ) ) ;
}
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-15 21:26:32 +01:00
private void AddComputerSocket ( IWebSocketConnection socket , ConnectedComputer pc ) = > Computer = pc ;
2022-01-11 20:32:25 +01:00
private void RemoveComputerSocket ( IWebSocketConnection socket ) {
2022-01-15 21:26:32 +01:00
if ( Computer is { ConnectionInfo . Id : Guid id } & & id = = socket . ConnectionInfo . Id ) Computer = 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-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 ) ) {
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-12 13:03:30 +01:00
// TODO: Relay Message to Chat Receiver
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 ) ,
_ = > message . ReplyAsync ( $"Whoops, someone forgot to implement '{response.GetType()}' responses?" ) ,
} ;
2022-01-11 20:32:25 +01:00
2022-01-16 15:58:35 +01:00
private readonly ConcurrentDictionary < ulong , ResponseType . IChoiceResponse > _choiceWait = new ( ) ;
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 ) {
if ( Computer is ICommandHandler < ResponseType > handler )
try {
return await handler . HandleCommand ( message , parameters , ct ) ;
} catch ( TaskCanceledException ) {
return ResponseType . AsString ( "Your request could not be processed in time!" ) ;
} catch ( ReplyException e ) {
await LogWarningAsync ( BotSource , e . Message ) ;
return ResponseType . AsString ( $"Your request failed: {e.Message}" ) ;
} 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 ) {
Log ( msg ) ;
await Task . CompletedTask ;
}
2022-01-12 19:03:51 +01:00
public static void Log ( LogMessage msg ) {
2022-01-12 13:03:30 +01:00
lock ( LogLock )
Console . WriteLine ( msg . ToString ( ) ) ;
}
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-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 ) ;
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 ;
}
}
}