2019-04-19 18:48:37 +00:00
using System ;
2019-04-26 15:14:20 +00:00
using System.Linq ;
using System.Runtime.Serialization ;
2019-06-13 18:33:17 +00:00
using System.Text.RegularExpressions ;
2019-04-19 18:48:37 +00:00
using System.Threading.Tasks ;
using Dapper ;
using Discord.Commands ;
2019-06-13 18:33:17 +00:00
using NodaTime ;
using NodaTime.Extensions ;
using NodaTime.Text ;
using NodaTime.TimeZones ;
2019-07-09 22:19:18 +00:00
using PluralKit.Core ;
2019-04-19 18:48:37 +00:00
2019-04-21 13:33:22 +00:00
namespace PluralKit.Bot.Commands
2019-04-19 18:48:37 +00:00
{
[Group("system")]
2019-07-10 07:35:37 +00:00
[Alias("s")]
2019-04-19 18:48:37 +00:00
public class SystemCommands : ContextParameterModuleBase < PKSystem >
{
public override string Prefix = > "system" ;
2019-04-27 14:30:34 +00:00
public override string ContextNoun = > "system" ;
2019-04-19 18:48:37 +00:00
public SystemStore Systems { get ; set ; }
public MemberStore Members { get ; set ; }
2019-06-15 10:19:44 +00:00
public SwitchStore Switches { get ; set ; }
2019-04-21 13:33:22 +00:00
public EmbedService EmbedService { get ; set ; }
2019-06-15 10:19:44 +00:00
2019-04-19 18:48:37 +00:00
2019-04-21 13:33:22 +00:00
[Command]
2019-07-10 10:03:41 +00:00
[Remarks("system <name>")]
2019-04-26 15:14:20 +00:00
public async Task Query ( PKSystem system = null ) {
2019-04-21 13:33:22 +00:00
if ( system = = null ) system = Context . SenderSystem ;
2019-05-21 21:40:26 +00:00
if ( system = = null ) throw Errors . NoSystemError ;
2019-04-21 13:33:22 +00:00
2019-04-25 16:50:07 +00:00
await Context . Channel . SendMessageAsync ( embed : await EmbedService . CreateSystemEmbed ( system ) ) ;
2019-04-21 13:33:22 +00:00
}
2019-04-19 18:48:37 +00:00
[Command("new")]
2019-07-10 11:55:48 +00:00
[Alias("register", "create", "init", "add", "make")]
2019-04-29 15:44:20 +00:00
[Remarks("system new <name>")]
2019-04-26 15:14:20 +00:00
public async Task New ( [ Remainder ] string systemName = null )
2019-04-19 18:48:37 +00:00
{
2019-04-29 17:43:09 +00:00
if ( ContextEntity ! = null ) throw Errors . NotOwnSystemError ;
2019-05-11 21:56:56 +00:00
if ( Context . SenderSystem ! = null ) throw Errors . ExistingSystemError ;
2019-04-19 18:48:37 +00:00
var system = await Systems . Create ( systemName ) ;
2019-04-21 13:33:22 +00:00
await Systems . Link ( system , Context . User . Id ) ;
2019-04-27 14:30:34 +00:00
await Context . Channel . SendMessageAsync ( $"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now." ) ;
2019-04-19 18:48:37 +00:00
}
[Command("name")]
2019-07-10 11:55:48 +00:00
[Alias("rename", "changename")]
2019-04-29 15:44:20 +00:00
[Remarks("system name <name>")]
2019-04-29 17:43:09 +00:00
[MustHaveSystem]
2019-04-26 15:14:20 +00:00
public async Task Name ( [ Remainder ] string newSystemName = null ) {
2019-04-29 18:28:53 +00:00
if ( newSystemName ! = null & & newSystemName . Length > Limits . MaxSystemNameLength ) throw Errors . SystemNameTooLongError ( newSystemName . Length ) ;
2019-04-19 18:48:37 +00:00
Context . SenderSystem . Name = newSystemName ;
await Systems . Save ( Context . SenderSystem ) ;
2019-04-26 15:14:20 +00:00
await Context . Channel . SendMessageAsync ( $"{Emojis.Success} System name {(newSystemName != null ? " changed " : " cleared ")}." ) ;
2019-04-19 18:48:37 +00:00
}
[Command("description")]
2019-07-10 11:55:48 +00:00
[Alias("desc")]
2019-04-29 15:44:20 +00:00
[Remarks("system description <description>")]
2019-04-29 17:43:09 +00:00
[MustHaveSystem]
2019-04-26 15:14:20 +00:00
public async Task Description ( [ Remainder ] string newDescription = null ) {
2019-04-29 18:28:53 +00:00
if ( newDescription ! = null & & newDescription . Length > Limits . MaxDescriptionLength ) throw Errors . DescriptionTooLongError ( newDescription . Length ) ;
2019-04-19 18:48:37 +00:00
Context . SenderSystem . Description = newDescription ;
await Systems . Save ( Context . SenderSystem ) ;
2019-04-26 15:14:20 +00:00
await Context . Channel . SendMessageAsync ( $"{Emojis.Success} System description {(newDescription != null ? " changed " : " cleared ")}." ) ;
2019-04-19 18:48:37 +00:00
}
[Command("tag")]
2019-04-29 15:44:20 +00:00
[Remarks("system tag <tag>")]
2019-04-29 17:43:09 +00:00
[MustHaveSystem]
2019-04-26 15:14:20 +00:00
public async Task Tag ( [ Remainder ] string newTag = null ) {
2019-04-19 18:48:37 +00:00
Context . SenderSystem . Tag = newTag ;
2019-07-10 11:44:03 +00:00
if ( newTag ! = null )
{
if ( newTag . Length > Limits . MaxSystemTagLength ) throw Errors . SystemNameTooLongError ( newTag . Length ) ;
// Check unproxyable messages *after* changing the tag (so it's seen in the method) but *before* we save to DB (so we can cancel)
var unproxyableMembers = await Members . GetUnproxyableMembers ( Context . SenderSystem ) ;
if ( unproxyableMembers . Count > 0 )
{
var msg = await Context . Channel . SendMessageAsync (
$"{Emojis.Warn} Changing your system tag to '{newTag}' will result in the following members being unproxyable, since the tag would bring their name over 32 characters:\n**{string.Join(" , ", unproxyableMembers.Select((m) => m.Name))}**\nDo you want to continue anyway?" ) ;
if ( ! await Context . PromptYesNo ( msg ) ) throw new PKError ( "Tag change cancelled." ) ;
}
2019-04-26 15:14:20 +00:00
}
2019-04-19 18:48:37 +00:00
await Systems . Save ( Context . SenderSystem ) ;
2019-04-26 15:14:20 +00:00
await Context . Channel . SendMessageAsync ( $"{Emojis.Success} System tag {(newTag != null ? " changed " : " cleared ")}." ) ;
2019-04-19 18:48:37 +00:00
}
2019-04-26 16:15:25 +00:00
[Command("delete")]
2019-07-10 11:55:48 +00:00
[Alias("remove", "destroy", "erase", "yeet")]
2019-04-29 15:44:20 +00:00
[Remarks("system delete")]
2019-04-29 17:43:09 +00:00
[MustHaveSystem]
2019-04-26 16:15:25 +00:00
public async Task Delete ( ) {
var msg = await Context . Channel . SendMessageAsync ( $"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{Context.SenderSystem.Hid}`).\n**Note: this action is permanent.**" ) ;
var reply = await Context . AwaitMessage ( Context . Channel , Context . User , timeout : TimeSpan . FromMinutes ( 1 ) ) ;
if ( reply . Content ! = Context . SenderSystem . Hid ) throw new PKError ( $"System deletion cancelled. Note that you must reply with your system ID (`{Context.SenderSystem.Hid}`) *verbatim*." ) ;
await Systems . Delete ( Context . SenderSystem ) ;
await Context . Channel . SendMessageAsync ( $"{Emojis.Success} System deleted." ) ;
}
2019-04-29 15:42:09 +00:00
[Group("list")]
2019-07-10 11:55:48 +00:00
[Alias("l", "members")]
2019-04-29 15:42:09 +00:00
public class SystemListCommands : ModuleBase < PKCommandContext > {
public MemberStore Members { get ; set ; }
[Command]
2019-04-29 15:44:20 +00:00
[Remarks("system [system] list ")]
2019-04-29 15:42:09 +00:00
public async Task MemberShortList ( ) {
var system = Context . GetContextEntity < PKSystem > ( ) ? ? Context . SenderSystem ;
2019-04-29 17:43:09 +00:00
if ( system = = null ) throw Errors . NoSystemError ;
2019-04-29 15:42:09 +00:00
var members = await Members . GetBySystem ( system ) ;
var embedTitle = system . Name ! = null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`" ;
await Context . Paginate < PKMember > (
2019-04-29 17:44:17 +00:00
members . OrderBy ( m = > m . Name ) . ToList ( ) ,
2019-04-29 15:42:09 +00:00
25 ,
embedTitle ,
2019-04-29 17:52:07 +00:00
( eb , ms ) = > eb . Description = string . Join ( "\n" , ms . Select ( ( m ) = > {
if ( m . HasProxyTags ) return $"[`{m.Hid}`] **{m.Name}** *({m.ProxyString})*" ;
return $"[`{m.Hid}`] **{m.Name}**" ;
} ) )
2019-04-29 15:42:09 +00:00
) ;
}
[Command("full")]
[Alias("big", "details", "long")]
2019-04-29 15:44:20 +00:00
[Remarks("system [system] list full ")]
2019-04-29 15:42:09 +00:00
public async Task MemberLongList ( ) {
var system = Context . GetContextEntity < PKSystem > ( ) ? ? Context . SenderSystem ;
2019-04-29 17:43:09 +00:00
if ( system = = null ) throw Errors . NoSystemError ;
2019-04-29 15:42:09 +00:00
var members = await Members . GetBySystem ( system ) ;
var embedTitle = system . Name ! = null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`" ;
await Context . Paginate < PKMember > (
2019-04-29 17:44:17 +00:00
members . OrderBy ( m = > m . Name ) . ToList ( ) ,
2019-04-29 15:42:09 +00:00
10 ,
embedTitle ,
( eb , ms ) = > {
2019-04-29 17:52:07 +00:00
foreach ( var m in ms ) {
var profile = $"**ID**: {m.Hid}" ;
if ( m . Pronouns ! = null ) profile + = $"\n**Pronouns**: {m.Pronouns}" ;
if ( m . Birthday ! = null ) profile + = $"\n**Birthdate**: {m.BirthdayString}" ;
if ( m . Prefix ! = null | | m . Suffix ! = null ) profile + = $"\n**Proxy tags**: {m.ProxyString}" ;
if ( m . Description ! = null ) profile + = $"\n\n{m.Description}" ;
eb . AddField ( m . Name , profile ) ;
2019-04-29 15:42:09 +00:00
}
}
) ;
}
}
2019-06-15 10:19:44 +00:00
[Command("fronter")]
2019-07-10 11:55:48 +00:00
[Alias("f", "front", "fronters")]
2019-07-10 10:03:41 +00:00
[Remarks("system [system] fronter ")]
2019-06-15 10:19:44 +00:00
public async Task SystemFronter ( )
{
var system = ContextEntity ? ? Context . SenderSystem ;
if ( system = = null ) throw Errors . NoSystemError ;
var sw = await Switches . GetLatestSwitch ( system ) ;
if ( sw = = null ) throw Errors . NoRegisteredSwitches ;
2019-06-15 10:43:35 +00:00
await Context . Channel . SendMessageAsync ( embed : await EmbedService . CreateFronterEmbed ( sw , system . Zone ) ) ;
}
[Command("fronthistory")]
2019-07-10 11:55:48 +00:00
[Alias("fh", "history", "switches")]
2019-07-10 10:03:41 +00:00
[Remarks("system [system] fronthistory ")]
2019-06-15 10:43:35 +00:00
public async Task SystemFrontHistory ( )
{
var system = ContextEntity ? ? Context . SenderSystem ;
if ( system = = null ) throw Errors . NoSystemError ;
var sws = ( await Switches . GetSwitches ( system , 10 ) ) . ToList ( ) ;
if ( sws . Count = = 0 ) throw Errors . NoRegisteredSwitches ;
await Context . Channel . SendMessageAsync ( embed : await EmbedService . CreateFrontHistoryEmbed ( sws , system . Zone ) ) ;
2019-06-15 10:19:44 +00:00
}
2019-06-30 21:41:01 +00:00
[Command("frontpercent")]
2019-07-10 11:55:48 +00:00
[Alias("frontbreakdown", "frontpercent", "front%", "fp")]
2019-07-10 10:03:41 +00:00
[Remarks("system [system] frontpercent [ duration ] ")]
2019-06-30 21:41:01 +00:00
public async Task SystemFrontPercent ( string durationStr = "30d" )
{
var system = ContextEntity ? ? Context . SenderSystem ;
if ( system = = null ) throw Errors . NoSystemError ;
var duration = PluralKit . Utils . ParsePeriod ( durationStr ) ;
if ( duration = = null ) throw Errors . InvalidDateTime ( durationStr ) ;
var rangeEnd = SystemClock . Instance . GetCurrentInstant ( ) ;
var rangeStart = rangeEnd - duration . Value ;
var frontpercent = await Switches . GetPerMemberSwitchDuration ( system , rangeEnd - duration . Value , rangeEnd ) ;
await Context . Channel . SendMessageAsync ( embed : await EmbedService . CreateFrontPercentEmbed ( frontpercent , rangeStart . InZone ( system . Zone ) ) ) ;
}
2019-06-13 18:33:17 +00:00
[Command("timezone")]
2019-07-10 11:55:48 +00:00
[Alias("tz")]
2019-06-13 18:33:17 +00:00
[Remarks("system timezone [timezone] ")]
2019-06-15 10:19:44 +00:00
[MustHaveSystem]
2019-06-13 18:33:17 +00:00
public async Task SystemTimezone ( [ Remainder ] string zoneStr = null )
{
if ( zoneStr = = null )
{
Context . SenderSystem . UiTz = "UTC" ;
await Systems . Save ( Context . SenderSystem ) ;
await Context . Channel . SendMessageAsync ( $"{Emojis.Success} System time zone cleared." ) ;
return ;
}
2019-06-13 21:42:39 +00:00
var zone = await FindTimeZone ( zoneStr ) ;
2019-06-13 18:33:17 +00:00
if ( zone = = null ) throw Errors . InvalidTimeZone ( zoneStr ) ;
var currentTime = SystemClock . Instance . GetCurrentInstant ( ) . InZone ( zone ) ;
var msg = await Context . Channel . SendMessageAsync (
2019-06-15 10:33:24 +00:00
$"This will change the system time zone to {zone.Id}. The current time is {Formats.ZonedDateTimeFormat.Format(currentTime)}. Is this correct?" ) ;
2019-06-13 18:33:17 +00:00
if ( ! await Context . PromptYesNo ( msg ) ) throw Errors . TimezoneChangeCancelled ;
Context . SenderSystem . UiTz = zone . Id ;
await Systems . Save ( Context . SenderSystem ) ;
await Context . Channel . SendMessageAsync ( $"System time zone changed to {zone.Id}." ) ;
}
2019-06-13 21:42:39 +00:00
public async Task < DateTimeZone > FindTimeZone ( string zoneStr ) {
// First, if we're given a flag emoji, we extract the flag emoji code from it.
zoneStr = PluralKit . Utils . ExtractCountryFlag ( zoneStr ) ? ? zoneStr ;
// Then, we find all *locations* matching either the given country code or the country name.
var locations = TzdbDateTimeZoneSource . Default . Zone1970Locations ;
var matchingLocations = locations . Where ( l = > l . Countries . Any ( c = >
string . Equals ( c . Code , zoneStr , StringComparison . InvariantCultureIgnoreCase ) | |
string . Equals ( c . Name , zoneStr , StringComparison . InvariantCultureIgnoreCase ) ) ) ;
// Then, we find all (unique) time zone IDs that match.
var matchingZones = matchingLocations . Select ( l = > DateTimeZoneProviders . Tzdb . GetZoneOrNull ( l . ZoneId ) )
. Distinct ( ) . ToList ( ) ;
// If the set of matching zones is empty (ie. we didn't find anything), we try a few other things.
if ( matchingZones . Count = = 0 )
{
// First, we try to just find the time zone given directly and return that.
var givenZone = DateTimeZoneProviders . Tzdb . GetZoneOrNull ( zoneStr ) ;
if ( givenZone ! = null ) return givenZone ;
// If we didn't find anything there either, we try parsing the string as an offset, then
// find all possible zones that match that offset. For an offset like UTC+2, this doesn't *quite*
// work, since there are 57(!) matching zones (as of 2019-06-13) - but for less populated time zones
// this could work nicely.
var inputWithoutUtc = zoneStr . Replace ( "UTC" , "" ) . Replace ( "GMT" , "" ) ;
var res = OffsetPattern . CreateWithInvariantCulture ( "+H" ) . Parse ( inputWithoutUtc ) ;
if ( ! res . Success ) res = OffsetPattern . CreateWithInvariantCulture ( "+H:mm" ) . Parse ( inputWithoutUtc ) ;
// If *this* didn't parse correctly, fuck it, bail.
if ( ! res . Success ) return null ;
var offset = res . Value ;
// To try to reduce the count, we go by locations from the 1970+ database instead of just the full database
// This elides regions that have been identical since 1970, omitting small distinctions due to Ancient History(tm).
var allZones = TzdbDateTimeZoneSource . Default . Zone1970Locations . Select ( l = > l . ZoneId ) . Distinct ( ) ;
matchingZones = allZones . Select ( z = > DateTimeZoneProviders . Tzdb . GetZoneOrNull ( z ) )
. Where ( z = > z . GetUtcOffset ( SystemClock . Instance . GetCurrentInstant ( ) ) = = offset ) . ToList ( ) ;
}
// If we have a list of viable time zones, we ask the user which is correct.
// If we only have one, return that one.
if ( matchingZones . Count = = 1 )
return matchingZones . First ( ) ;
// Otherwise, prompt and return!
return await Context . Choose ( "There were multiple matches for your time zone query. Please select the region that matches you the closest:" , matchingZones ,
z = >
{
if ( TzdbDateTimeZoneSource . Default . Aliases . Contains ( z . Id ) )
return $"**{z.Id}**, {string.Join(" , ", TzdbDateTimeZoneSource.Default.Aliases[z.Id])}" ;
return $"**{z.Id}**" ;
} ) ;
}
2019-04-19 18:48:37 +00:00
public override async Task < PKSystem > ReadContextParameterAsync ( string value )
{
var res = await new PKSystemTypeReader ( ) . ReadAsync ( Context , value , _services ) ;
return res . IsSuccess ? res . BestMatch as PKSystem : null ;
}
}
}