2019-10-05 07:41:00 +02:00
using System ;
using System.Threading.Tasks ;
2019-12-21 21:51:41 +01:00
using App.Metrics ;
2020-01-26 01:27:45 +01:00
using Autofac ;
2021-11-29 21:35:21 -05:00
using NodaTime ;
2020-12-22 16:55:13 +01:00
using Myriad.Cache ;
2020-12-22 13:15:26 +01:00
using Myriad.Extensions ;
using Myriad.Gateway ;
2021-01-15 11:29:43 +01:00
using Myriad.Rest ;
2020-12-24 14:52:44 +01:00
using Myriad.Rest.Types ;
2020-12-22 16:55:13 +01:00
using Myriad.Rest.Types.Requests ;
2020-12-22 13:15:26 +01:00
using Myriad.Types ;
2020-02-12 15:16:19 +01:00
using PluralKit.Core ;
2019-10-05 07:41:00 +02:00
2021-11-26 21:10:56 -05:00
namespace PluralKit.Bot ;
public class Context
2019-10-05 07:41:00 +02:00
{
2021-11-26 21:10:56 -05:00
private readonly ILifetimeScope _provider ;
private readonly IMetrics _metrics ;
private readonly CommandMessageService _commandMessageService ;
private Command ? _currentCommand ;
2025-12-21 01:19:02 -05:00
private BotConfig _botConfig ;
2022-06-15 19:28:34 -04:00
public Context ( ILifetimeScope provider , int shardId , Guild ? guild , Channel channel , MessageCreateEvent message ,
2024-11-10 15:46:36 +13:00
int commandParseOffset , PKSystem senderSystem , SystemConfig config ,
2024-12-31 08:09:18 -07:00
GuildConfig ? guildConfig , string [ ] prefixes )
2019-10-05 07:41:00 +02:00
{
2021-11-26 21:10:56 -05:00
Message = ( Message ) message ;
2022-01-14 18:39:03 -05:00
ShardId = shardId ;
2021-11-26 21:10:56 -05:00
Guild = guild ;
2024-11-10 15:46:36 +13:00
GuildConfig = guildConfig ;
2021-11-26 21:10:56 -05:00
Channel = channel ;
System = senderSystem ;
2021-11-29 21:35:21 -05:00
Config = config ;
2021-11-26 21:10:56 -05:00
Cache = provider . Resolve < IDiscordCache > ( ) ;
Database = provider . Resolve < IDatabase > ( ) ;
Repository = provider . Resolve < ModelRepository > ( ) ;
2022-11-24 06:32:55 +00:00
Redis = provider . Resolve < RedisService > ( ) ;
2021-11-26 21:10:56 -05:00
_metrics = provider . Resolve < IMetrics > ( ) ;
_provider = provider ;
_commandMessageService = provider . Resolve < CommandMessageService > ( ) ;
2025-12-21 01:19:02 -05:00
_botConfig = provider . Resolve < BotConfig > ( ) ;
2022-05-02 14:53:08 -04:00
CommandPrefix = message . Content ? . Substring ( 0 , commandParseOffset ) ;
2024-12-31 08:09:18 -07:00
DefaultPrefix = prefixes [ 0 ] ;
2021-11-27 10:50:01 -05:00
Parameters = new Parameters ( message . Content ? . Substring ( commandParseOffset ) ) ;
2021-11-26 21:10:56 -05:00
Rest = provider . Resolve < DiscordApiClient > ( ) ;
Cluster = provider . Resolve < Cluster > ( ) ;
}
public readonly IDiscordCache Cache ;
public readonly DiscordApiClient Rest ;
2019-10-05 07:41:00 +02:00
2021-11-26 21:10:56 -05:00
public readonly Channel Channel ;
public User Author = > Message . Author ;
public GuildMemberPartial Member = > ( ( MessageCreateEvent ) Message ) . Member ;
2020-12-22 16:55:13 +01:00
2021-11-26 21:10:56 -05:00
public readonly Message Message ;
public readonly Guild Guild ;
2024-11-10 15:46:36 +13:00
public readonly GuildConfig ? GuildConfig ;
2022-01-14 18:39:03 -05:00
public readonly int ShardId ;
2021-11-26 21:10:56 -05:00
public readonly Cluster Cluster ;
2021-01-15 11:29:43 +01:00
2024-09-14 12:19:47 +09:00
public Task < PermissionSet > BotPermissions = > Cache . BotPermissionsIn ( Guild ? . Id ? ? 0 , Channel . Id ) ;
2024-08-07 18:54:25 +09:00
public Task < PermissionSet > UserPermissions = > Cache . PermissionsForMCE ( ( MessageCreateEvent ) Message ) ;
2020-04-24 15:50:28 -04:00
2020-12-22 13:15:26 +01:00
2021-11-26 21:10:56 -05:00
public readonly PKSystem System ;
2021-11-29 21:35:21 -05:00
public readonly SystemConfig Config ;
public DateTimeZone Zone = > Config ? . Zone ? ? DateTimeZone . Utc ;
2020-04-24 15:50:28 -04:00
2025-12-21 01:19:02 -05:00
public bool Premium
{
get
{
if ( Config ? . PremiumLifetime ? ? false ) return true ;
// generate _this_ current instant _before_ the check, otherwise it will always be true...
var premiumUntil = Config ? . PremiumUntil ? ? SystemClock . Instance . GetCurrentInstant ( ) ;
return SystemClock . Instance . GetCurrentInstant ( ) < premiumUntil ;
}
}
public string PremiumEmoji = > ( Config ? . PremiumLifetime ? ? false )
? ( $"<:lifetime_premium:{_botConfig.PremiumLifetimeEmoji}>" ? ? "\u2729" )
: Premium
? ( $"<:premium_subscriber:{_botConfig.PremiumSubscriberEmoji}>" ? ? "\u2729" )
: "" ;
2022-05-02 14:53:08 -04:00
public readonly string CommandPrefix ;
2024-12-31 08:09:18 -07:00
public readonly string DefaultPrefix ;
2021-11-26 21:10:56 -05:00
public readonly Parameters Parameters ;
2021-08-27 11:03:47 -04:00
2021-11-26 21:10:56 -05:00
internal readonly IDatabase Database ;
internal readonly ModelRepository Repository ;
2022-11-24 06:32:55 +00:00
internal readonly RedisService Redis ;
2021-11-26 21:10:56 -05:00
2025-07-16 11:48:48 -04:00
public async Task < Message > Reply ( string text = null , Embed embed = null , AllowedMentions ? mentions = null , MultipartFile [ ] ? files = null )
2021-11-26 21:10:56 -05:00
{
var botPerms = await BotPermissions ;
2019-10-05 07:41:00 +02:00
2021-11-26 21:10:56 -05:00
if ( ! botPerms . HasFlag ( PermissionSet . SendMessages ) )
// Will be "swallowed" during the error handler anyway, this message is never shown.
throw new PKError ( "PluralKit does not have permission to send messages in this channel." ) ;
2019-10-05 07:41:00 +02:00
2021-11-26 21:10:56 -05:00
if ( embed ! = null & & ! botPerms . HasFlag ( PermissionSet . EmbedLinks ) )
throw new PKError ( "PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled." ) ;
2025-07-16 12:01:01 -04:00
2025-07-16 11:48:48 -04:00
if ( files ! = null & & ! botPerms . HasFlag ( PermissionSet . AttachFiles ) )
throw new PKError ( "PluralKit does not have permission to attach files in this channel. Please ensure I have the **Attach Files** permission enabled." ) ;
2021-11-26 21:10:56 -05:00
var msg = await Rest . CreateMessage ( Channel . Id , new MessageRequest
2020-12-22 16:55:13 +01:00
{
2021-11-26 21:10:56 -05:00
Content = text ,
2022-02-26 16:28:20 -05:00
Embeds = embed ! = null ? new [ ] { embed } : null ,
2021-11-26 21:10:56 -05:00
// Default to an empty allowed mentions object instead of null (which means no mentions allowed)
2025-09-07 10:16:50 +12:00
AllowedMentions = mentions ? ? new AllowedMentions ( )
} , files : files ) ;
// store log of sent message, so it can be queried or deleted later
// skip DMs as DM messages can always be deleted
if ( Guild ! = null )
await Repository . AddCommandMessage ( new Core . CommandMessage
{
Mid = msg . Id ,
Guild = Guild ! . Id ,
Channel = Channel . Id ,
Sender = Author . Id ,
OriginalMid = Message . Id ,
} ) ;
return msg ;
}
public async Task < Message > Reply ( MessageComponent [ ] components = null , AllowedMentions ? mentions = null , MultipartFile [ ] ? files = null )
{
var botPerms = await BotPermissions ;
if ( ! botPerms . HasFlag ( PermissionSet . SendMessages ) )
// Will be "swallowed" during the error handler anyway, this message is never shown.
throw new PKError ( "PluralKit does not have permission to send messages in this channel." ) ;
if ( files ! = null & & ! botPerms . HasFlag ( PermissionSet . AttachFiles ) )
throw new PKError ( "PluralKit does not have permission to attach files in this channel. Please ensure I have the **Attach Files** permission enabled." ) ;
var msg = await Rest . CreateMessage ( Channel . Id , new MessageRequest
{
Components = components ,
Flags = Message . MessageFlags . IsComponentsV2 ,
// Default to an empty allowed mentions object instead of null (which means no mentions allowed)
2021-11-26 21:10:56 -05:00
AllowedMentions = mentions ? ? new AllowedMentions ( )
2025-07-16 11:48:48 -04:00
} , files : files ) ;
2021-08-27 11:03:47 -04:00
2025-06-08 19:52:37 +00:00
// store log of sent message, so it can be queried or deleted later
// skip DMs as DM messages can always be deleted
if ( Guild ! = null )
await Repository . AddCommandMessage ( new Core . CommandMessage
{
Mid = msg . Id ,
Guild = Guild ! . Id ,
Channel = Channel . Id ,
Sender = Author . Id ,
OriginalMid = Message . Id ,
} ) ;
2019-10-05 07:41:00 +02:00
2021-11-26 21:10:56 -05:00
return msg ;
}
2021-08-27 11:03:47 -04:00
2021-11-29 21:35:21 -05:00
public async Task Execute < T > ( Command ? commandDef , Func < T , Task > handler , bool deprecated = false )
2021-11-26 21:10:56 -05:00
{
_currentCommand = commandDef ;
2020-06-18 17:08:36 +02:00
2022-06-14 15:55:08 -04:00
if ( deprecated & & commandDef ! = null )
{
2024-12-31 08:09:18 -07:00
await Reply ( $"{Emojis.Warn} Server configuration has moved to `{DefaultPrefix}serverconfig`. The command you are trying to run is now `{DefaultPrefix}{commandDef.Key}`." ) ;
2022-06-14 15:55:08 -04:00
}
2021-11-26 21:10:56 -05:00
try
{
using ( _metrics . Measure . Timer . Time ( BotMetrics . CommandTime , new MetricTags ( "Command" , commandDef ? . Key ? ? "null" ) ) )
await handler ( _provider . Resolve < T > ( ) ) ;
2021-08-27 11:03:47 -04:00
2021-11-26 21:10:56 -05:00
_metrics . Measure . Meter . Mark ( BotMetrics . CommandsRun ) ;
}
catch ( PKSyntaxError e )
{
2024-12-31 08:09:18 -07:00
await Reply ( $"{Emojis.Error} {e.Message}\n**Command usage:**\n> {DefaultPrefix}{commandDef?.Usage}" ) ;
2021-11-26 21:10:56 -05:00
}
catch ( PKError e )
{
await Reply ( $"{Emojis.Error} {e.Message}" ) ;
}
catch ( TimeoutException )
{
// Got a complaint the old error was a bit too patronizing. Hopefully this is better?
await Reply ( $"{Emojis.Error} Operation timed out, sorry. Try again, perhaps?" ) ;
}
2024-12-31 05:37:57 -07:00
catch ( TaskCanceledException )
{
// HTTP timeouts...
await Reply ( $"{Emojis.Error} Operation timed out, please try again later." ) ;
}
2019-10-05 07:41:00 +02:00
}
2021-11-26 21:10:56 -05:00
2021-12-07 01:32:29 -05:00
/// <summary>
/// Same as LookupContextFor, but skips flags / config checks.
/// </summary>
public LookupContext DirectLookupContextFor ( SystemId systemId )
= > System ? . Id = = systemId ? LookupContext . ByOwner : LookupContext . ByNonOwner ;
2021-12-06 00:32:54 -05:00
public LookupContext LookupContextFor ( SystemId systemId )
{
var hasPrivateOverride = this . MatchFlag ( "private" , "priv" ) ;
var hasPublicOverride = this . MatchFlag ( "public" , "pub" ) ;
if ( hasPrivateOverride & & hasPublicOverride )
throw new PKError ( "Cannot match both public and private flags at the same time." ) ;
2021-12-06 04:01:42 -05:00
if ( System ? . Id ! = systemId )
2021-12-06 00:32:54 -05:00
{
if ( hasPrivateOverride )
throw Errors . NotOwnInfo ;
return LookupContext . ByNonOwner ;
}
2021-11-26 21:10:56 -05:00
2021-12-06 00:32:54 -05:00
if ( hasPrivateOverride )
return LookupContext . ByOwner ;
if ( hasPublicOverride )
return LookupContext . ByNonOwner ;
2021-11-26 21:10:56 -05:00
2021-12-06 04:01:42 -05:00
return Config . ShowPrivateInfo
? LookupContext . ByOwner
: LookupContext . ByNonOwner ;
2021-12-06 00:32:54 -05:00
}
2021-11-26 21:10:56 -05:00
public IComponentContext Services = > _provider ;
2019-10-05 07:41:00 +02:00
}