2019-04-19 18:48:37 +00:00
using System ;
2019-07-11 20:34:38 +00:00
using System.Collections.Generic ;
2019-04-19 18:48:37 +00:00
using System.Data ;
2019-07-11 19:25:23 +00:00
using System.Data.Common ;
2019-06-13 14:53:04 +00:00
using System.Diagnostics ;
2019-06-30 22:55:41 +00:00
using System.IO ;
2019-04-19 18:48:37 +00:00
using System.Linq ;
using System.Reflection ;
2019-04-25 16:50:07 +00:00
using System.Threading ;
2019-04-19 18:48:37 +00:00
using System.Threading.Tasks ;
using Dapper ;
using Discord ;
using Discord.Commands ;
using Discord.WebSocket ;
2019-05-08 18:08:56 +00:00
using Microsoft.Extensions.Configuration ;
2019-04-19 18:48:37 +00:00
using Microsoft.Extensions.DependencyInjection ;
2019-05-13 20:44:49 +00:00
using NodaTime ;
2019-04-19 18:48:37 +00:00
using Npgsql ;
2019-04-21 13:33:22 +00:00
namespace PluralKit.Bot
2019-04-19 18:48:37 +00:00
{
class Initialize
{
2019-05-08 18:08:56 +00:00
private IConfiguration _config ;
2019-07-09 18:39:29 +00:00
static void Main ( string [ ] args ) = > new Initialize { _config = InitUtils . BuildConfiguration ( args ) . Build ( ) } . MainAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
2019-04-19 18:48:37 +00:00
private async Task MainAsync ( )
{
2019-04-20 20:25:03 +00:00
Console . WriteLine ( "Starting PluralKit..." ) ;
2019-05-19 20:03:28 +00:00
2019-07-09 18:39:29 +00:00
InitUtils . Init ( ) ;
2019-05-13 20:44:49 +00:00
2019-04-19 18:48:37 +00:00
using ( var services = BuildServiceProvider ( ) )
{
2019-04-20 20:25:03 +00:00
Console . WriteLine ( "- Connecting to database..." ) ;
2019-07-14 03:23:27 +00:00
using ( var conn = await services . GetRequiredService < DbConnectionFactory > ( ) . Obtain ( ) )
2019-07-11 19:25:23 +00:00
await Schema . CreateTables ( conn ) ;
2019-04-19 18:48:37 +00:00
2019-04-20 20:25:03 +00:00
Console . WriteLine ( "- Connecting to Discord..." ) ;
2019-04-19 18:48:37 +00:00
var client = services . GetRequiredService < IDiscordClient > ( ) as DiscordSocketClient ;
2019-05-08 18:08:56 +00:00
await client . LoginAsync ( TokenType . Bot , services . GetRequiredService < BotConfig > ( ) . Token ) ;
2019-04-19 18:48:37 +00:00
await client . StartAsync ( ) ;
2019-04-20 20:25:03 +00:00
Console . WriteLine ( "- Initializing bot..." ) ;
2019-04-19 18:48:37 +00:00
await services . GetRequiredService < Bot > ( ) . Init ( ) ;
2019-04-20 20:25:03 +00:00
2019-04-19 18:48:37 +00:00
await Task . Delay ( - 1 ) ;
}
}
public ServiceProvider BuildServiceProvider ( ) = > new ServiceCollection ( )
2019-05-16 23:23:09 +00:00
. AddTransient ( _ = > _config . GetSection ( "PluralKit" ) . Get < CoreConfig > ( ) ? ? new CoreConfig ( ) )
. AddTransient ( _ = > _config . GetSection ( "PluralKit" ) . GetSection ( "Bot" ) . Get < BotConfig > ( ) ? ? new BotConfig ( ) )
2019-07-11 19:25:23 +00:00
. AddTransient ( svc = > new DbConnectionFactory ( svc . GetRequiredService < CoreConfig > ( ) . Database ) )
2019-05-08 18:08:56 +00:00
2019-04-19 18:48:37 +00:00
. AddSingleton < IDiscordClient , DiscordSocketClient > ( )
. AddSingleton < Bot > ( )
2019-07-11 20:34:38 +00:00
. AddTransient < CommandService > ( _ = > new CommandService ( new CommandServiceConfig
{
CaseSensitiveCommands = false ,
QuotationMarkAliasMap = new Dictionary < char , char >
{
{ '"' , '"' } ,
{ '\'' , '\'' } ,
{ '‘ ' , '’ ' } ,
{ '“' , '”' } ,
{ '„' , '‟' } ,
} ,
DefaultRunMode = RunMode . Async
} ) )
2019-05-16 23:23:09 +00:00
. AddTransient < EmbedService > ( )
. AddTransient < ProxyService > ( )
. AddTransient < LogChannelService > ( )
2019-06-14 20:48:19 +00:00
. AddTransient < DataFileService > ( )
2019-05-16 23:23:09 +00:00
. AddSingleton < WebhookCacheService > ( )
2019-04-19 18:48:37 +00:00
2019-05-16 23:23:09 +00:00
. AddTransient < SystemStore > ( )
. AddTransient < MemberStore > ( )
. AddTransient < MessageStore > ( )
2019-06-13 14:53:04 +00:00
. AddTransient < SwitchStore > ( )
2019-04-19 18:48:37 +00:00
. BuildServiceProvider ( ) ;
}
class Bot
{
private IServiceProvider _services ;
private DiscordSocketClient _client ;
private CommandService _commands ;
private ProxyService _proxy ;
2019-04-25 16:50:07 +00:00
private Timer _updateTimer ;
2019-04-19 18:48:37 +00:00
2019-06-27 08:38:45 +00:00
public Bot ( IServiceProvider services , IDiscordClient client , CommandService commands , ProxyService proxy )
2019-04-19 18:48:37 +00:00
{
this . _services = services ;
this . _client = client as DiscordSocketClient ;
this . _commands = commands ;
this . _proxy = proxy ;
}
public async Task Init ( )
{
_commands . AddTypeReader < PKSystem > ( new PKSystemTypeReader ( ) ) ;
_commands . AddTypeReader < PKMember > ( new PKMemberTypeReader ( ) ) ;
_commands . CommandExecuted + = CommandExecuted ;
await _commands . AddModulesAsync ( Assembly . GetEntryAssembly ( ) , _services ) ;
2019-04-20 20:25:03 +00:00
_client . Ready + = Ready ;
2019-04-26 16:15:25 +00:00
// Deliberately wrapping in an async function *without* awaiting, we don't want to "block" since this'd hold up the main loop
// These handlers return Task so we gotta be careful not to return the Task itself (which would then be awaited) - kinda weird design but eh
2019-04-29 15:42:09 +00:00
_client . MessageReceived + = async ( msg ) = > MessageReceived ( msg ) . CatchException ( HandleRuntimeError ) ;
_client . ReactionAdded + = async ( message , channel , reaction ) = > _proxy . HandleReactionAddedAsync ( message , channel , reaction ) . CatchException ( HandleRuntimeError ) ;
_client . MessageDeleted + = async ( message , channel ) = > _proxy . HandleMessageDeletedAsync ( message , channel ) . CatchException ( HandleRuntimeError ) ;
2019-04-19 18:48:37 +00:00
}
2019-04-25 16:50:07 +00:00
private async Task UpdatePeriodic ( )
2019-04-20 20:25:03 +00:00
{
2019-04-25 16:50:07 +00:00
// Method called every 60 seconds
await _client . SetGameAsync ( $"pk;help | in {_client.Guilds.Count} servers" ) ;
}
private async Task Ready ( )
{
2019-06-27 08:38:45 +00:00
_updateTimer = new Timer ( ( _ ) = > UpdatePeriodic ( ) , null , 0 , 60 * 1000 ) ;
2019-04-25 16:50:07 +00:00
2019-04-20 20:25:03 +00:00
Console . WriteLine ( $"Shard #{_client.ShardId} connected to {_client.Guilds.Sum(g => g.Channels.Count)} channels in {_client.Guilds.Count} guilds." ) ;
Console . WriteLine ( $"PluralKit started as {_client.CurrentUser.Username}#{_client.CurrentUser.Discriminator} ({_client.CurrentUser.Id})." ) ;
}
2019-04-19 18:48:37 +00:00
private async Task CommandExecuted ( Optional < CommandInfo > cmd , ICommandContext ctx , IResult _result )
{
2019-04-27 14:30:34 +00:00
// TODO: refactor this entire block, it's fugly.
2019-04-19 18:48:37 +00:00
if ( ! _result . IsSuccess ) {
2019-04-27 14:30:34 +00:00
if ( _result . Error = = CommandError . Unsuccessful | | _result . Error = = CommandError . Exception ) {
// If this is a PKError (ie. thrown deliberately), show user facing message
// If not, log as error
var exception = ( _result as ExecuteResult ? ) ? . Exception ;
if ( exception is PKError ) {
await ctx . Message . Channel . SendMessageAsync ( $"{Emojis.Error} {exception.Message}" ) ;
} else if ( exception is TimeoutException ) {
await ctx . Message . Channel . SendMessageAsync ( $"{Emojis.Error} Operation timed out. Try being faster next time :)" ) ;
2019-05-21 21:40:26 +00:00
} else if ( _result is PreconditionResult )
{
await ctx . Message . Channel . SendMessageAsync ( $"{Emojis.Error} {_result.ErrorReason}" ) ;
2019-04-27 14:30:34 +00:00
} else {
2019-04-29 15:42:09 +00:00
HandleRuntimeError ( ( _result as ExecuteResult ? ) ? . Exception ) ;
2019-04-27 14:30:34 +00:00
}
} else if ( ( _result . Error = = CommandError . BadArgCount | | _result . Error = = CommandError . MultipleMatches ) & & cmd . IsSpecified ) {
await ctx . Message . Channel . SendMessageAsync ( $"{Emojis.Error} {_result.ErrorReason}\n**Usage: **pk;{cmd.Value.Remarks}" ) ;
2019-06-13 14:53:04 +00:00
} else if ( _result . Error = = CommandError . UnknownCommand | | _result . Error = = CommandError . UnmetPrecondition | | _result . Error = = CommandError . ObjectNotFound ) {
2019-04-27 14:30:34 +00:00
await ctx . Message . Channel . SendMessageAsync ( $"{Emojis.Error} {_result.ErrorReason}" ) ;
2019-04-26 15:14:20 +00:00
}
2019-04-19 18:48:37 +00:00
}
}
private async Task MessageReceived ( SocketMessage _arg )
{
2019-05-16 23:23:09 +00:00
var serviceScope = _services . CreateScope ( ) ;
2019-04-29 15:42:09 +00:00
// Ignore system messages (member joined, message pinned, etc)
var arg = _arg as SocketUserMessage ;
if ( arg = = null ) return ;
// Ignore bot messages
if ( arg . Author . IsBot | | arg . Author . IsWebhook ) return ;
int argPos = 0 ;
// Check if message starts with the command prefix
2019-07-10 08:01:06 +00:00
if ( arg . HasStringPrefix ( "pk;" , ref argPos , StringComparison . OrdinalIgnoreCase ) | | arg . HasStringPrefix ( "pk!" , ref argPos , StringComparison . OrdinalIgnoreCase ) | | arg . HasMentionPrefix ( _client . CurrentUser , ref argPos ) )
2019-04-29 15:42:09 +00:00
{
2019-07-10 08:01:06 +00:00
// Essentially move the argPos pointer by however much whitespace is at the start of the post-argPos string
var trimStartLengthDiff = arg . Content . Substring ( argPos ) . Length - arg . Content . Substring ( argPos ) . TrimStart ( ) . Length ;
argPos + = trimStartLengthDiff ;
2019-04-29 15:42:09 +00:00
// If it does, fetch the sender's system (because most commands need that) into the context,
// and start command execution
// Note system may be null if user has no system, hence `OrDefault`
2019-07-11 19:25:23 +00:00
PKSystem system ;
2019-07-14 03:23:27 +00:00
using ( var conn = await serviceScope . ServiceProvider . GetService < DbConnectionFactory > ( ) . Obtain ( ) )
2019-07-11 19:25:23 +00:00
system = await conn . QueryFirstOrDefaultAsync < PKSystem > ( "select systems.* from systems, accounts where accounts.uid = @Id and systems.id = accounts.system" , new { Id = arg . Author . Id } ) ;
await _commands . ExecuteAsync ( new PKCommandContext ( _client , arg , system ) , argPos , serviceScope . ServiceProvider ) ;
2019-04-29 15:42:09 +00:00
}
else
{
// If not, try proxying anyway
await _proxy . HandleMessageAsync ( arg ) ;
2019-04-19 18:48:37 +00:00
}
}
2019-04-20 20:36:54 +00:00
2019-04-29 15:42:09 +00:00
private void HandleRuntimeError ( Exception e )
2019-04-20 20:36:54 +00:00
{
Console . Error . WriteLine ( e ) ;
}
2019-04-19 18:48:37 +00:00
}
}