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-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-10 16:10:32 +01:00
public class Program {
private const string WebSocketSource = "WebSocket" ;
private readonly object _logLock = new ( ) ;
private readonly DiscordSocketClient _client = new ( new ( ) {
LogLevel = LogSeverity . Verbose
} ) ;
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' } ;
private RefinedStorageComputer ? _rsSystem = null ;
2022-01-10 16:10:32 +01:00
public Program ( BotConfiguration config ) {
_config = config ;
_client . Log + = Log ;
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
} ;
_whitelistedChannels = config . Channels . ToHashSet ( ) ;
}
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 ( ) {
_wssv . Start ( socket = > {
socket . OnOpen = async ( ) = > await SocketOpened ( socket ) ;
socket . OnClose = async ( ) = > await SocketClosed ( socket ) ;
socket . OnMessage = async message = > await SocketReceived ( socket , message ) ;
} ) ;
await _client . LoginAsync ( TokenType . Bot , _config . Token ) ;
await _client . StartAsync ( ) ;
2022-01-11 20:32:25 +01:00
#if ! DEBUG
2022-01-10 16:10:32 +01:00
await VerifyTextChannels ( ) ;
2022-01-11 20:32:25 +01:00
#endif
2022-01-10 16:10:32 +01:00
// Block this task until the program is closed.
await Task . Delay ( - 1 ) ;
return 0 ;
}
private async Task VerifyTextChannels ( ) {
var channels = await Task . WhenAll ( _whitelistedChannels . Select ( id = > _client . GetChannelAsync ( id ) . AsTask ( ) ) . ToArray ( ) ) ;
await Task . WhenAll ( channels . Where ( i = > i is ITextChannel { Guild : RestGuild } ) . Select ( i = > ( ( RestGuild ) ( ( ITextChannel ) i ) . Guild ) . UpdateAsync ( ) ) ) ;
foreach ( var channel in channels ) {
if ( channel is ITextChannel tchannel ) Console . WriteLine ( $"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {tchannel.Guild.Name} [{tchannel.Guild.Id}]" ) ;
else throw new InvalidProgramException ( $"Cannot use this bot on non-text channel {channel.Name} [{channel.Id}]!" ) ;
}
}
2022-01-11 20:32:25 +01:00
private async Task SocketReceived ( IWebSocketConnection socket , string message ) {
var capability = JsonConvert . DeserializeObject < CapabilityMessage > ( message ) ;
2022-01-10 16:10:32 +01:00
2022-01-11 20:32:25 +01:00
if ( capability is null ) return ;
var pc = capability . Role switch {
RefinedStorageComputer . Role = > new RefinedStorageComputer ( socket ) ,
string role = > throw new ArgumentException ( $"Invalid role '{role}'!" )
} ;
AddComputerSocket ( socket , pc ) ;
await Log ( new LogMessage ( LogSeverity . Info , WebSocketSource , $"[{socket.ConnectionInfo.Id}] Presented capability as {pc.GetType().Name}" ) ) . ConfigureAwait ( false ) ;
}
private void AddComputerSocket ( IWebSocketConnection socket , RefinedStorageComputer pc ) {
_connections [ socket . ConnectionInfo . Id ] = pc ;
if ( pc is not null ) _rsSystem = pc ;
}
private void RemoveComputerSocket ( IWebSocketConnection socket ) {
2022-01-10 16:10:32 +01:00
if ( ! _connections . TryRemove ( socket . ConnectionInfo . Id , out _ ) )
throw new InvalidProgramException ( "Could not remove non-existing client!" ) ;
2022-01-11 20:32:25 +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 ) ;
await Log ( new LogMessage ( LogSeverity . Info , WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client disconnected!" ) ) . ConfigureAwait ( false ) ;
2022-01-10 16:10:32 +01:00
}
2022-01-11 20:32:25 +01:00
private async Task SocketOpened ( IWebSocketConnection socket ) = > await Log ( new LogMessage ( LogSeverity . Info , WebSocketSource , $"[{socket.ConnectionInfo.Id}] Client connected from {socket.ConnectionInfo.ClientIpAddress}:{socket.ConnectionInfo.ClientPort}!" ) ) . ConfigureAwait ( false ) ;
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 ;
var cts = new CancellationTokenSource ( timeout
#if DEBUG
* 1000
#endif
) ;
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 ;
}
await Log ( new LogMessage ( LogSeverity . Info , "Discord" , $"[{arg.Author.Username}] {arg.Content}" ) ) . ConfigureAwait ( false ) ;
2022-01-10 16:10:32 +01:00
await SendToAll ( JsonConvert . SerializeObject ( new TextMessage ( arg ) ) ) ;
}
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 )
= > _rsSystem is null
? message . ReplyAsync ( "The Refined Storage system is currently unavailable!" )
: _rsSystem . HandleCommand ( message , parameters , ct ) ;
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 ) ;
private async Task SendToAll ( string message ) {
2022-01-11 20:32:25 +01:00
async Task SendToClient ( KeyValuePair < Guid , ConnectedComputer > cp ) {
2022-01-10 16:10:32 +01:00
try {
await cp . Value . Send ( message ) ;
} catch ( Exception e ) {
await Log ( new LogMessage ( LogSeverity . Warning , WebSocketSource , $"[{cp.Key}] Sending message failed!" , e ) ) . ConfigureAwait ( false ) ;
}
}
await Task . WhenAll ( _connections . Select ( SendToClient ) . ToArray ( ) ) ;
}
private async Task Log ( LogMessage msg ) {
lock ( _logLock )
Console . WriteLine ( msg . ToString ( ) ) ;
await Task . CompletedTask ;
}
}