2019-04-19 18:48:37 +00:00
using System ;
using System.Data ;
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 ;
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 ;
static void Main ( string [ ] args ) = > new Initialize { _config = new ConfigurationBuilder ( )
. AddEnvironmentVariables ( )
. AddCommandLine ( 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-04-19 18:48:37 +00:00
// Dapper by default tries to pass ulongs to Npgsql, which rejects them since PostgreSQL technically
// doesn't support unsigned types on its own.
// Instead we add a custom mapper to encode them as signed integers instead, converting them back and forth.
SqlMapper . RemoveTypeMap ( typeof ( ulong ) ) ;
SqlMapper . AddTypeHandler < ulong > ( new UlongEncodeAsLongHandler ( ) ) ;
Dapper . DefaultTypeMap . MatchNamesWithUnderscores = true ;
using ( var services = BuildServiceProvider ( ) )
{
2019-04-20 20:25:03 +00:00
Console . WriteLine ( "- Connecting to database..." ) ;
2019-04-19 18:48:37 +00:00
var connection = services . GetRequiredService < IDbConnection > ( ) as NpgsqlConnection ;
2019-05-08 18:08:56 +00:00
connection . ConnectionString = services . GetRequiredService < CoreConfig > ( ) . Database ;
2019-04-19 18:48:37 +00:00
await connection . OpenAsync ( ) ;
2019-04-20 20:45:32 +00:00
await Schema . CreateTables ( connection ) ;
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-08 18:08:56 +00:00
. AddSingleton ( _config . GetSection ( "PluralKit" ) . Get < CoreConfig > ( ) ? ? new CoreConfig ( ) )
. AddSingleton ( _config . GetSection ( "PluralKit" ) . GetSection ( "Bot" ) . Get < BotConfig > ( ) ? ? new BotConfig ( ) )
2019-04-19 18:48:37 +00:00
. AddSingleton < IDiscordClient , DiscordSocketClient > ( )
. AddSingleton < IDbConnection , NpgsqlConnection > ( )
. AddSingleton < Bot > ( )
. AddSingleton < CommandService > ( )
2019-04-21 13:33:22 +00:00
. AddSingleton < EmbedService > ( )
2019-04-19 18:48:37 +00:00
. AddSingleton < LogChannelService > ( )
. AddSingleton < ProxyService > ( )
. AddSingleton < SystemStore > ( )
. AddSingleton < MemberStore > ( )
. AddSingleton < MessageStore > ( )
. BuildServiceProvider ( ) ;
}
class Bot
{
private IServiceProvider _services ;
private DiscordSocketClient _client ;
private CommandService _commands ;
private IDbConnection _connection ;
private ProxyService _proxy ;
2019-04-25 16:50:07 +00:00
private Timer _updateTimer ;
2019-04-19 18:48:37 +00:00
public Bot ( IServiceProvider services , IDiscordClient client , CommandService commands , IDbConnection connection , ProxyService proxy )
{
this . _services = services ;
this . _client = client as DiscordSocketClient ;
this . _commands = commands ;
this . _connection = connection ;
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-04-29 15:42:09 +00:00
_updateTimer = new Timer ( ( _ ) = > this . 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 :)" ) ;
} 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}" ) ;
} else if ( _result . Error = = CommandError . UnknownCommand | | _result . Error = = CommandError . UnmetPrecondition ) {
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-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
if ( arg . HasStringPrefix ( "pk;" , ref argPos ) | | arg . HasStringPrefix ( "pk!" , ref argPos ) | | arg . HasMentionPrefix ( _client . CurrentUser , ref argPos ) )
{
// 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`
var system = await _connection . 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 as SocketUserMessage , _connection , system ) , argPos , _services ) ;
}
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
}
}