2022-12-22 00:42:43 +00:00
using Discord ;
using Discord.Commands ;
using Discord.Net ;
using Discord.WebSocket ;
using Newtonsoft.Json.Linq ;
2022-06-13 00:48:24 +00:00
using Shared ;
namespace Server ;
2022-12-22 00:42:43 +00:00
public class DiscordBot
{
private readonly Logger logger = new Logger ( "Discord" ) ;
private Settings . DiscordTable localSettings = Settings . Instance . Discord ;
private DiscordSocketClient ? client = null ;
private SocketTextChannel ? logChannel = null ;
private bool firstInitTriggered = false ;
//check how this works with neither, one or the other, or both channels set.
public DiscordBot ( )
{
CommandHandler . RegisterCommand ( "dscrestart" , _ = >
{
2022-12-24 17:50:19 +00:00
//Task.Run is to fix deadlock (dispose can only be finalized if all discord callbacks are returned,
//and since this delegate is called directly from a callback, it would cause a deadlock).
Task . Run ( ( ) = >
{
Stop ( ) ;
2022-12-22 00:42:43 +00:00
#pragma warning disable CS4014
2022-12-24 17:50:19 +00:00
Init ( ) ;
2022-12-22 00:42:43 +00:00
#pragma warning restore CS4014
2022-12-24 17:50:19 +00:00
} ) ;
2022-12-22 00:42:43 +00:00
return "Restarting Discord bot..." ;
2022-06-20 18:55:01 +00:00
} ) ;
2022-12-22 00:42:43 +00:00
logger . Info ( "Starting discord bot (ctor)" ) ;
Settings . LoadHandler + = OnLoadSettings ;
Logger . AddLogHandler ( LogToDiscordLogChannel ) ;
2022-06-13 00:48:24 +00:00
}
2023-07-11 11:04:54 +00:00
//this nonsense is to prevent race conditions from starting multiple bots.
//this would be a great thing to instead simply have an "await Init()" put
//in the ctor (but awaits can't be there), and Task.Wait shouldn't be used that way.
2022-12-22 00:42:43 +00:00
private object firstInitLockObj = new object ( ) ;
public async Task FirstInit ( )
{
lock ( firstInitLockObj )
{
if ( firstInitTriggered )
return ;
firstInitTriggered = true ;
}
await Init ( ) ;
2022-06-20 18:55:01 +00:00
}
2022-12-22 00:42:43 +00:00
private async Task Init ( )
{
if ( client ! = null )
{
return ; //this is bad if the client ever crashes and isn't reassigned to null, but we don't want multiple instances of the bot running at the same time.
2022-09-07 14:23:55 +00:00
}
2022-12-24 18:00:30 +00:00
if ( localSettings . Token = = null | | ( localSettings . AdminChannel = = null & & localSettings . CommandChannel = = null ) )
2022-12-22 00:42:43 +00:00
{
//no point trying to run anything if there's no discord token and/or no channel for a user to interact with the bot through.
logger . Error ( "Tried to run the discord bot, but the Token and/or communication channels are not specified in the settings." ) ;
2022-09-07 14:23:55 +00:00
return ;
}
2022-12-22 00:42:43 +00:00
client = new DiscordSocketClient (
new DiscordSocketConfig ( )
{
LogLevel = LogSeverity . Warning
} ) ;
2022-12-24 12:02:21 +00:00
client . Log + = async ( a ) = > await Task . Run ( ( ) = >
{
//as time goes on, we may encounter logged info that we literally don't care about. Fill out an if statement to properly
//filter it out to avoid logging it to discord.
2022-12-25 03:29:19 +00:00
if ( localSettings . FilterOutNonIssueWarnings )
2022-12-25 03:15:34 +00:00
{
2023-07-03 03:18:52 +00:00
string [ ] disinterestedMessages =
{ //these messages happen sometimes, and are of no concern.
"Server requested a reconnect" ,
"The remote party closed the WebSocket connection without completing the close handshake" ,
"without listening to any events related to that intent, consider removing the intent from"
} ;
foreach ( string dis in disinterestedMessages )
2022-12-25 03:29:19 +00:00
{
2023-07-03 03:18:52 +00:00
if ( ( a . Exception ? . ToString ( ) . Contains ( dis ) ? ? false ) | |
( a . Message ? . Contains ( dis ) ? ? false ) )
{
return ;
}
2023-06-28 00:32:04 +00:00
}
2022-12-25 03:15:34 +00:00
}
2023-07-03 03:18:52 +00:00
string message = a . Message ? ? string . Empty + ( a . Exception ! = null ? "Exception: " + a . Exception . ToString ( ) : "" ) ; //TODO: this crashes
2022-12-24 12:02:21 +00:00
ConsoleColor col ;
switch ( a . Severity )
{
default :
case LogSeverity . Info :
case LogSeverity . Debug :
col = ConsoleColor . White ;
break ;
case LogSeverity . Critical :
case LogSeverity . Error :
col = ConsoleColor . Red ;
break ;
case LogSeverity . Warning :
col = ConsoleColor . Yellow ;
break ;
}
LogToDiscordLogChannel ( $"Discord: {a.Source}" , a . Severity . ToString ( ) , message , col ) ;
} ) ;
2022-12-22 00:42:43 +00:00
try
{
await client . LoginAsync ( Discord . TokenType . Bot , localSettings . Token ) ;
await client . StartAsync ( ) ;
SemaphoreSlim wait = new SemaphoreSlim ( 0 ) ;
#pragma warning disable CS1998
client . Ready + = async ( ) = >
#pragma warning restore CS1998
{
wait . Release ( ) ;
} ;
await wait . WaitAsync ( ) ;
//we need to wait for the ready event before we can do any of this nonsense.
2022-12-24 18:00:30 +00:00
logChannel = ( ulong . TryParse ( localSettings . AdminChannel , out ulong lcid ) ? ( client ! = null ? await client . GetChannelAsync ( lcid ) : null ) : null ) as SocketTextChannel ;
2022-12-22 00:42:43 +00:00
client ! . MessageReceived + = ( m ) = > HandleCommandAsync ( m ) ;
logger . Info ( "Discord bot has been initialized." ) ;
}
catch ( Exception e )
{
logger . Error ( e ) ;
}
}
2022-09-07 14:23:55 +00:00
2022-12-22 00:42:43 +00:00
private void Stop ( )
{
try
{
if ( client ! = null )
2023-07-11 10:58:27 +00:00
{
if ( ! client . StopAsync ( ) . Wait ( 60000 ) )
logger . Warn ( "Tried to stop the discord bot, but attempt took >60 seconds, so it failed!" ) ;
}
2022-12-22 00:42:43 +00:00
client ? . Dispose ( ) ;
}
catch { /*lol (lmao)*/ }
client = null ;
logChannel = null ;
localSettings = Settings . Instance . Discord ;
}
private async void LogToDiscordLogChannel ( string source , string level , string text , ConsoleColor color )
{
2022-12-24 18:00:30 +00:00
logChannel = ( ulong . TryParse ( localSettings . AdminChannel , out ulong lcid ) ? ( client ! = null ? await client . GetChannelAsync ( lcid ) : null ) : null ) as SocketTextChannel ;
2022-12-22 00:42:43 +00:00
if ( logChannel ! = null )
{
try
{
switch ( color )
{
//I looked into other hacky methods of doing more colors, the rest seemed unreliable.
default :
foreach ( string mesg in SplitMessage ( Logger . PrefixNewLines ( text , $"{level} [{source}]" ) , 1994 ) ) //room for 6 '`'
await logChannel . SendMessageAsync ( $"```{mesg}```" ) ;
break ;
2023-07-03 03:18:52 +00:00
case ConsoleColor . Yellow : //this is actually light blue now (discord changed it awhile ago).
2022-12-22 00:42:43 +00:00
foreach ( string mesg in SplitMessage ( Logger . PrefixNewLines ( text , $"{level} [{source}]" ) , 1990 ) ) //room for 6 '`', "fix" and "\n"
await logChannel . SendMessageAsync ( $"```fix\n{mesg}```" ) ;
break ;
case ConsoleColor . Red :
foreach ( string mesg in SplitMessage ( Logger . PrefixNewLines ( text , $"-{level} [{source}]" ) , 1989 ) ) //room for 6 '`', "diff" and "\n"
await logChannel . SendMessageAsync ( $"```diff\n{mesg}```" ) ;
break ;
case ConsoleColor . Green :
foreach ( string mesg in SplitMessage ( Logger . PrefixNewLines ( text , $"+{level} [{source}]" ) , 1989 ) ) //room for 6 '`', "diff" and "\n"
await logChannel . SendMessageAsync ( $"```diff\n{mesg}```" ) ;
break ;
}
}
catch ( Exception e )
{
// don't log again, it'll just stack overflow the server!
await Console . Error . WriteLineAsync ( "Exception in discord logger" ) ;
await Console . Error . WriteLineAsync ( e . ToString ( ) ) ;
2022-09-07 14:23:55 +00:00
}
}
2022-12-22 00:42:43 +00:00
}
2022-09-07 14:23:55 +00:00
2022-12-22 00:42:43 +00:00
private async Task HandleCommandAsync ( SocketMessage arg )
{
if ( arg is not SocketUserMessage )
return ; //idk what to do in this circumstance.
2022-12-24 18:00:30 +00:00
if ( ( arg . Channel . Id . ToString ( ) = = localSettings . CommandChannel | | arg . Channel . Id . ToString ( ) = = localSettings . AdminChannel ) & & ! arg . Author . IsBot )
2022-12-22 00:42:43 +00:00
{
string message = ( await arg . Channel . GetMessageAsync ( arg . Id ) ) . Content ;
//run command
try
{
2022-12-24 12:54:10 +00:00
string? args = null ;
2022-12-22 00:42:43 +00:00
if ( string . IsNullOrEmpty ( localSettings . Prefix ) )
{
2022-12-24 12:54:10 +00:00
args = message ;
2022-12-22 00:42:43 +00:00
}
else if ( message . StartsWith ( localSettings . Prefix ) )
{
2022-12-24 12:54:10 +00:00
args = message [ localSettings . Prefix . Length . . ] ;
2022-12-22 00:42:43 +00:00
}
else if ( message . StartsWith ( $"<@{client!.CurrentUser.Id}>" ) )
{
2022-12-24 12:54:10 +00:00
args = message [ client ! . CurrentUser . Mention . Length . . ] . TrimStart ( ) ;
2022-12-22 00:42:43 +00:00
}
2022-12-24 12:54:10 +00:00
if ( args ! = null )
2022-12-22 00:42:43 +00:00
{
2022-12-24 12:54:10 +00:00
await arg . Channel . TriggerTypingAsync ( ) ;
string resp = string . Join ( '\n' , CommandHandler . GetResult ( args ) . ReturnStrings ) ;
2022-12-24 08:04:23 +00:00
if ( localSettings . LogCommands )
{
logger . Info ( $"\" { arg . Author . Username } \ " ran the command: \"{message}\" via discord" ) ;
}
2022-12-22 00:42:43 +00:00
foreach ( string mesg in SplitMessage ( resp ) )
await ( arg as SocketUserMessage ) . ReplyAsync ( mesg ) ;
}
2022-09-07 14:23:55 +00:00
}
2022-12-22 00:42:43 +00:00
catch ( Exception e )
{
logger . Error ( e ) ;
}
}
else
{
//don't respond to commands not in these channels, and no bots
//probably don't print out error message because DDOS
}
}
private void OnLoadSettings ( )
{
Settings . DiscordTable oldSettings = localSettings ;
localSettings = Settings . Instance . Discord ;
if ( localSettings . CommandChannel = = null )
logger . Warn ( "You probably should set your CommandChannel in settings.json" ) ;
2022-12-24 18:00:30 +00:00
if ( localSettings . AdminChannel = = null )
2023-07-11 10:58:27 +00:00
logger . Warn ( "You probably should set your AdminChannel in settings.json" ) ;
2022-12-22 00:42:43 +00:00
2022-12-24 18:00:30 +00:00
if ( oldSettings . Token ! = localSettings . Token | | oldSettings . AdminChannel ! = localSettings . AdminChannel | | oldSettings . CommandChannel ! = localSettings . CommandChannel )
2022-12-22 00:42:43 +00:00
{
//start over fresh (there might be a more intelligent way to do this without restarting the bot if only the log/command channel changed, but I'm lazy.
Stop ( ) ;
#pragma warning disable CS4014
Init ( ) ;
#pragma warning restore CS4014
2022-06-13 00:48:24 +00:00
}
}
2022-07-29 01:16:24 +00:00
private static List < string > SplitMessage ( string message , int maxSizePerElem = 2000 )
{
List < string > result = new List < string > ( ) ;
2022-12-22 00:42:43 +00:00
for ( int i = 0 ; i < message . Length ; i + = maxSizePerElem )
2022-07-29 01:16:24 +00:00
{
result . Add ( message . Substring ( i , message . Length - i < maxSizePerElem ? message . Length - i : maxSizePerElem ) ) ;
}
return result ;
}
2022-12-22 00:42:43 +00:00
~ DiscordBot ( )
{
Stop ( ) ;
2022-06-13 00:48:24 +00:00
}
2022-09-07 14:23:55 +00:00
}