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 ;
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-12 13:03:30 +01:00
public class Program : IDisposable {
public const string WebSocketSource = "WebSocket" ;
public const string BotSource = "Bot" ;
private static readonly object LogLock = new ( ) ;
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-11 20:32:25 +01:00
private RefinedStorageComputer ? _rsSystem = null ;
2022-01-12 13:03:30 +01:00
private bool disposedValue ;
public RefinedStorageComputer ? RsSystem {
get = > _rsSystem ; set {
if ( _rsSystem ! = value ) {
_rsSystem = value ;
_ = 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-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-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 )
= > JsonConvert . DeserializeObject < BotConfiguration > ( File . ReadAllText ( "config.json" ) ) is BotConfiguration config
? new Program ( config ) . RunAsync ( )
: throw new InvalidProgramException ( "Configuration file missing!" ) ;
public async Task < int > RunAsync ( ) {
await _client . LoginAsync ( TokenType . Bot , _config . Token ) ;
await _client . StartAsync ( ) ;
await VerifyTextChannels ( ) ;
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 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 ) ;
} ) ;
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 ) {
if ( channel is null ) await LogWarning ( BotSource , $"Channel with id [{channelId}] does not exist!" ) ;
else await LogWarning ( BotSource , $"Channel is not a text channels and will not be used: {channel.Name} [{channel.Id}]!" ) ;
continue ;
}
if ( textChannel . Guild is RestGuild guild ) {
await guild . UpdateAsync ( ) ;
await LogInfo ( BotSource , $"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {guild.Name} [{guild.Id}]" ) ;
} else {
await LogWarning ( BotSource , $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!" ) ;
}
yield return textChannel ;
2022-01-10 16:10:32 +01:00
}
}
2022-01-12 13:03:30 +01:00
private async Task VerifyTextChannels ( ) = > _channels = await GetValidChannels ( _whitelistedChannels ) . ToArrayAsync ( ) ;
2022-01-10 16:10:32 +01:00
2022-01-12 13:03:30 +01:00
private async Task SocketReceived ( IWebSocketConnection socket , string message ) {
if ( JsonConvert . DeserializeObject < CapabilityMessage > ( message ) is not CapabilityMessage capability ) return ;
try {
var pc = capability . Role switch {
RefinedStorageComputer . Role = > new RefinedStorageComputer ( socket ) ,
string role = > throw new ArgumentException ( $"Invalid role '{role}'!" )
} ;
AddComputerSocket ( socket , pc ) ;
await LogInfo ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Presented capability as {pc.GetType().Name}" ) ;
} catch ( ArgumentException e ) {
await LogError ( WebSocketSource , e ) ;
}
2022-01-11 20:32:25 +01:00
}
2022-01-12 13:03:30 +01:00
private void AddComputerSocket ( IWebSocketConnection socket , ConnectedComputer pc ) {
if ( pc is RefinedStorageComputer rs ) RsSystem = rs ;
2022-01-11 20:32:25 +01:00
}
private void RemoveComputerSocket ( IWebSocketConnection socket ) {
2022-01-12 13:03:30 +01:00
if ( RsSystem ? . ConnectionInfo . Id = = socket . ConnectionInfo . Id ) RsSystem = 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 13:03:30 +01:00
await LogInfo ( WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client disconnected!" ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-12 13:03:30 +01:00
private static async Task SocketOpened ( IWebSocketConnection socket )
= > await LogInfo ( 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 ) ;
_ = Task . Run ( ( ) = > HandleCommand ( message , parameters , cts . Token ) ) ;
return ;
}
2022-01-12 13:03:30 +01:00
await LogInfo ( "Discord" , $"[{arg.Author.Username}] {arg.Content}" ) ;
// TODO: Relay Message to Chat Receiver
2022-01-10 16:10:32 +01:00
}
2022-01-11 20:32:25 +01:00
private Task HandleCommand ( SocketUserMessage message , string [ ] parameters , CancellationToken ct )
= > parameters is { Length : > 0 }
? parameters [ 0 ] . ToLower ( ) switch {
RefinedStorageComputer . Role = > HandleRefinedStorageCommand ( message , parameters [ 1. . ] , ct ) ,
_ = > message . ReplyAsync ( $"What the fuck do you mean by '{parameters[0]}'?" )
}
: message . ReplyAsync ( $"You really think an empty command works?" ) ;
private Task HandleRefinedStorageCommand ( SocketUserMessage message , string [ ] parameters , CancellationToken ct )
2022-01-12 13:03:30 +01:00
= > RsSystem is RefinedStorageComputer rs
? rs . HandleCommand ( message , parameters , ct )
: message . ReplyAsync ( "The Refined Storage system 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 13:03:30 +01:00
public static ConfiguredTaskAwaitable LogInfo ( string source , string message ) = > LogAsync ( new ( LogSeverity . Info , source , message ) ) . ConfigureAwait ( false ) ;
public static ConfiguredTaskAwaitable LogWarning ( string source , string message ) = > LogAsync ( new ( LogSeverity . Warning , source , message ) ) . ConfigureAwait ( false ) ;
public static ConfiguredTaskAwaitable LogError ( string source , Exception exception ) = > LogAsync ( new ( LogSeverity . Error , source , exception ? . Message , exception ) ) . ConfigureAwait ( false ) ;
private static async Task LogAsync ( LogMessage msg ) {
Log ( msg ) ;
await Task . CompletedTask ;
}
private static void Log ( LogMessage msg ) {
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
}
}