2019-04-19 20:48:37 +02:00
using System ;
using System.Data ;
using System.Linq ;
using System.Reflection ;
2019-04-25 18:50:07 +02:00
using System.Threading ;
2019-04-19 20:48:37 +02:00
using System.Threading.Tasks ;
using Dapper ;
using Discord ;
using Discord.Commands ;
using Discord.WebSocket ;
using Microsoft.Extensions.DependencyInjection ;
using Npgsql ;
using Npgsql.BackendMessages ;
using Npgsql.PostgresTypes ;
using Npgsql.TypeHandling ;
using Npgsql.TypeMapping ;
using NpgsqlTypes ;
2019-04-21 15:33:22 +02:00
namespace PluralKit.Bot
2019-04-19 20:48:37 +02:00
{
class Initialize
{
static void Main ( ) = > new Initialize ( ) . MainAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
private async Task MainAsync ( )
{
2019-04-20 22:25:03 +02:00
Console . WriteLine ( "Starting PluralKit..." ) ;
2019-04-19 20:48:37 +02: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 22:25:03 +02:00
Console . WriteLine ( "- Connecting to database..." ) ;
2019-04-19 20:48:37 +02:00
var connection = services . GetRequiredService < IDbConnection > ( ) as NpgsqlConnection ;
connection . ConnectionString = Environment . GetEnvironmentVariable ( "PK_DATABASE_URI" ) ;
await connection . OpenAsync ( ) ;
2019-04-20 22:45:32 +02:00
await Schema . CreateTables ( connection ) ;
2019-04-19 20:48:37 +02:00
2019-04-20 22:25:03 +02:00
Console . WriteLine ( "- Connecting to Discord..." ) ;
2019-04-19 20:48:37 +02:00
var client = services . GetRequiredService < IDiscordClient > ( ) as DiscordSocketClient ;
await client . LoginAsync ( TokenType . Bot , Environment . GetEnvironmentVariable ( "PK_TOKEN" ) ) ;
await client . StartAsync ( ) ;
2019-04-20 22:25:03 +02:00
Console . WriteLine ( "- Initializing bot..." ) ;
2019-04-19 20:48:37 +02:00
await services . GetRequiredService < Bot > ( ) . Init ( ) ;
2019-04-20 22:25:03 +02:00
2019-04-19 20:48:37 +02:00
await Task . Delay ( - 1 ) ;
}
}
public ServiceProvider BuildServiceProvider ( ) = > new ServiceCollection ( )
. AddSingleton < IDiscordClient , DiscordSocketClient > ( )
. AddSingleton < IDbConnection , NpgsqlConnection > ( )
. AddSingleton < Bot > ( )
. AddSingleton < CommandService > ( )
2019-04-21 15:33:22 +02:00
. AddSingleton < EmbedService > ( )
2019-04-19 20:48:37 +02: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 18:50:07 +02:00
private Timer _updateTimer ;
2019-04-19 20:48:37 +02: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 22:25:03 +02:00
_client . Ready + = Ready ;
2019-04-19 20:48:37 +02:00
_client . MessageReceived + = MessageReceived ;
_client . ReactionAdded + = _proxy . HandleReactionAddedAsync ;
_client . MessageDeleted + = _proxy . HandleMessageDeletedAsync ;
}
2019-04-25 18:50:07 +02:00
private async Task UpdatePeriodic ( )
2019-04-20 22:25:03 +02:00
{
2019-04-25 18:50:07 +02:00
// Method called every 60 seconds
await _client . SetGameAsync ( $"pk;help | in {_client.Guilds.Count} servers" ) ;
}
private async Task Ready ( )
{
_updateTimer = new Timer ( ( _ ) = > Task . Run ( this . UpdatePeriodic ) , null , 0 , 60 * 1000 ) ;
2019-04-20 22:25:03 +02: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 20:48:37 +02:00
private async Task CommandExecuted ( Optional < CommandInfo > cmd , ICommandContext ctx , IResult _result )
{
if ( ! _result . IsSuccess ) {
2019-04-26 17:14:20 +02:00
// If this is a PKError (ie. thrown deliberately), show user facing message
// If not, log as error
var pkError = ( _result as ExecuteResult ? ) ? . Exception as PKError ;
if ( pkError ! = null ) {
await ctx . Message . Channel . SendMessageAsync ( "\u274C " + pkError . Message ) ;
} else {
HandleRuntimeError ( ctx . Message as SocketMessage , ( _result as ExecuteResult ? ) ? . Exception ) ;
}
2019-04-19 20:48:37 +02:00
}
}
private async Task MessageReceived ( SocketMessage _arg )
{
2019-04-20 22:36:54 +02:00
try {
// 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
2019-04-20 22:45:32 +02:00
// 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 ) ;
2019-04-20 22:36:54 +02:00
}
else
{
// If not, try proxying anyway
await _proxy . HandleMessageAsync ( arg ) ;
}
} catch ( Exception e ) {
// Generic exception handler
HandleRuntimeError ( _arg , e ) ;
2019-04-19 20:48:37 +02:00
}
}
2019-04-20 22:36:54 +02:00
private void HandleRuntimeError ( SocketMessage arg , Exception e )
{
Console . Error . WriteLine ( e ) ;
}
2019-04-19 20:48:37 +02:00
}
}