Add front percent command
This commit is contained in:
parent
7eeaea39fe
commit
42147fd9cc
@ -172,6 +172,22 @@ namespace PluralKit.Bot.Commands
|
|||||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFrontHistoryEmbed(sws, system.Zone));
|
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFrontHistoryEmbed(sws, system.Zone));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command("frontpercent")]
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
[Command("timezone")]
|
[Command("timezone")]
|
||||||
[Remarks("system timezone [timezone]")]
|
[Remarks("system timezone [timezone]")]
|
||||||
[MustHaveSystem]
|
[MustHaveSystem]
|
||||||
|
@ -65,5 +65,7 @@ namespace PluralKit.Bot {
|
|||||||
public static PKError InvalidImportFile => new PKError("Imported data file invalid. Make sure this is a .json file directly exported from PluralKit or Tupperbox.");
|
public static PKError InvalidImportFile => new PKError("Imported data file invalid. Make sure this is a .json file directly exported from PluralKit or Tupperbox.");
|
||||||
public static PKError ImportCancelled => new PKError("Import cancelled.");
|
public static PKError ImportCancelled => new PKError("Import cancelled.");
|
||||||
public static PKError MessageNotFound(ulong id) => new PKError($"Message with ID '{id}' not found. Are you sure it's a message proxied by PluralKit?");
|
public static PKError MessageNotFound(ulong id) => new PKError($"Message with ID '{id}' not found. Are you sure it's a message proxied by PluralKit?");
|
||||||
|
|
||||||
|
public static PKError DurationParseError(string durationStr) => new PKError($"Could not parse '{durationStr}' as a valid duration. Try a format such as `30d`, `1d3h` or `20m30s`.");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -136,5 +136,32 @@ namespace PluralKit.Bot {
|
|||||||
.WithTimestamp(SnowflakeUtils.FromSnowflake(msg.Message.Mid))
|
.WithTimestamp(SnowflakeUtils.FromSnowflake(msg.Message.Mid))
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Embed> CreateFrontPercentEmbed(IDictionary<PKMember, Duration> frontpercent, ZonedDateTime startingFrom)
|
||||||
|
{
|
||||||
|
var totalDuration = SystemClock.Instance.GetCurrentInstant() - startingFrom.ToInstant();
|
||||||
|
|
||||||
|
var eb = new EmbedBuilder()
|
||||||
|
.WithColor(Color.Blue)
|
||||||
|
.WithFooter($"Since {Formats.ZonedDateTimeFormat.Format(startingFrom)} ({Formats.DurationFormat.Format(totalDuration)} ago)");
|
||||||
|
|
||||||
|
var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others"
|
||||||
|
|
||||||
|
var membersOrdered = frontpercent.OrderBy(pair => pair.Value).Take(maxEntriesToDisplay).ToList();
|
||||||
|
foreach (var pair in membersOrdered)
|
||||||
|
{
|
||||||
|
var frac = pair.Value / totalDuration;
|
||||||
|
eb.AddField(pair.Key.Name, $"{frac*100:F0}% ({Formats.DurationFormat.Format(pair.Value)})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membersOrdered.Count > maxEntriesToDisplay)
|
||||||
|
{
|
||||||
|
eb.AddField("(others)",
|
||||||
|
Formats.DurationFormat.Format(membersOrdered.Skip(maxEntriesToDisplay)
|
||||||
|
.Aggregate(Duration.Zero, (prod, next) => prod + next.Value)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eb.Build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -189,13 +189,19 @@ namespace PluralKit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<PKSwitch>> GetSwitches(PKSystem system, int count)
|
public async Task<IEnumerable<PKSwitch>> GetSwitches(PKSystem system, int count = 9999999)
|
||||||
{
|
{
|
||||||
// TODO: refactor the PKSwitch data structure to somehow include a hydrated member list
|
// TODO: refactor the PKSwitch data structure to somehow include a hydrated member list
|
||||||
// (maybe when we get caching in?)
|
// (maybe when we get caching in?)
|
||||||
return await _connection.QueryAsync<PKSwitch>("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count});
|
return await _connection.QueryAsync<PKSwitch>("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<int>> GetSwitchMemberIds(PKSwitch sw)
|
||||||
|
{
|
||||||
|
return await _connection.QueryAsync<int>("select member from switch_members where switch = @Switch",
|
||||||
|
new {Switch = sw.Id});
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<PKMember>> GetSwitchMembers(PKSwitch sw)
|
public async Task<IEnumerable<PKMember>> GetSwitchMembers(PKSwitch sw)
|
||||||
{
|
{
|
||||||
return await _connection.QueryAsync<PKMember>(
|
return await _connection.QueryAsync<PKMember>(
|
||||||
@ -215,5 +221,76 @@ namespace PluralKit {
|
|||||||
{
|
{
|
||||||
await _connection.ExecuteAsync("delete from switches where id = @Id", new {Id = sw.Id});
|
await _connection.ExecuteAsync("delete from switches where id = @Id", new {Id = sw.Id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct SwitchListEntry
|
||||||
|
{
|
||||||
|
public ICollection<PKMember> Members;
|
||||||
|
public Duration TimespanWithinRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SwitchListEntry>> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd)
|
||||||
|
{
|
||||||
|
// TODO: only fetch the necessary switches here
|
||||||
|
// todo: this is in general not very efficient LOL
|
||||||
|
// returns switches in chronological (newest first) order
|
||||||
|
var switches = await GetSwitches(system);
|
||||||
|
|
||||||
|
// we skip all switches that happened later than the range end, and taking all the ones that happened after the range start
|
||||||
|
// *BUT ALSO INCLUDING* the last switch *before* the range (that partially overlaps the range period)
|
||||||
|
var switchesInRange = switches.SkipWhile(sw => sw.Timestamp >= periodEnd).TakeWhileIncluding(sw => sw.Timestamp > periodStart).ToList();
|
||||||
|
|
||||||
|
// query DB for all members involved in any of the switches above and collect into a dictionary for future use
|
||||||
|
// this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary
|
||||||
|
// key used in GetPerMemberSwitchDuration below
|
||||||
|
var memberObjects = (await _connection.QueryAsync<PKMember>(
|
||||||
|
"select distinct members.* from members, switch_members where switch_members.switch = any(@Switches) and switch_members.member = members.id", // lol postgres specific `= any()` syntax
|
||||||
|
new {Switches = switchesInRange.Select(sw => sw.Id).ToList()}))
|
||||||
|
.ToDictionary(m => m.Id);
|
||||||
|
|
||||||
|
|
||||||
|
// we create the entry objects
|
||||||
|
var outList = new List<SwitchListEntry>();
|
||||||
|
|
||||||
|
// loop through every switch that *occurred* in-range and add it to the list
|
||||||
|
// end time is the switch *after*'s timestamp - we cheat and start it out at the range end so the first switch in-range "ends" there instead of the one after's start point
|
||||||
|
var endTime = periodEnd;
|
||||||
|
foreach (var switchInRange in switchesInRange)
|
||||||
|
{
|
||||||
|
// find the start time of the switch, but clamp it to the range (only applicable to the Last Switch Before Range we include in the TakeWhileIncluding call above)
|
||||||
|
var switchStartClamped = switchInRange.Timestamp;
|
||||||
|
if (switchStartClamped < periodStart) switchStartClamped = periodStart;
|
||||||
|
|
||||||
|
var span = endTime - switchStartClamped;
|
||||||
|
outList.Add(new SwitchListEntry
|
||||||
|
{
|
||||||
|
Members = (await GetSwitchMemberIds(switchInRange)).Select(id => memberObjects[id]).ToList(),
|
||||||
|
TimespanWithinRange = span
|
||||||
|
});
|
||||||
|
|
||||||
|
// next switch's end is this switch's start
|
||||||
|
endTime = switchInRange.Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return outList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IDictionary<PKMember, Duration>> GetPerMemberSwitchDuration(PKSystem system, Instant periodStart,
|
||||||
|
Instant periodEnd)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<PKMember, Duration>();
|
||||||
|
|
||||||
|
// Sum up all switch durations for each member
|
||||||
|
// switches with multiple members will result in the duration to add up to more than the actual period range
|
||||||
|
foreach (var sw in await GetTruncatedSwitchList(system, periodStart, periodEnd))
|
||||||
|
{
|
||||||
|
foreach (var member in sw.Members)
|
||||||
|
{
|
||||||
|
if (!dict.ContainsKey(member)) dict.Add(member, sw.TimespanWithinRange);
|
||||||
|
else dict[member] += sw.TimespanWithinRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@ -216,6 +217,17 @@ namespace PluralKit
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<T> TakeWhileIncluding<T>(this IEnumerable<T> list, Func<T, bool> predicate)
|
||||||
|
{
|
||||||
|
// modified from https://stackoverflow.com/a/6817553
|
||||||
|
foreach(var el in list)
|
||||||
|
{
|
||||||
|
yield return el;
|
||||||
|
if (!predicate(el))
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Emojis {
|
public static class Emojis {
|
||||||
@ -236,10 +248,10 @@ namespace PluralKit
|
|||||||
// a smaller duration we may only bother with showing <x>h <x>m or <x>m <x>s
|
// a smaller duration we may only bother with showing <x>h <x>m or <x>m <x>s
|
||||||
public static IPattern<Duration> DurationFormat = new CompositePatternBuilder<Duration>
|
public static IPattern<Duration> DurationFormat = new CompositePatternBuilder<Duration>
|
||||||
{
|
{
|
||||||
{DurationPattern.CreateWithInvariantCulture("D'd' h'h'"), d => d.Days > 0},
|
{DurationPattern.CreateWithInvariantCulture("s's'"), d => true},
|
||||||
{DurationPattern.CreateWithInvariantCulture("H'h' m'm'"), d => d.Hours > 0},
|
|
||||||
{DurationPattern.CreateWithInvariantCulture("m'm' s's'"), d => d.Minutes > 0},
|
{DurationPattern.CreateWithInvariantCulture("m'm' s's'"), d => d.Minutes > 0},
|
||||||
{DurationPattern.CreateWithInvariantCulture("s's'"), d => true}
|
{DurationPattern.CreateWithInvariantCulture("H'h' m'm'"), d => d.Hours > 0},
|
||||||
|
{DurationPattern.CreateWithInvariantCulture("D'd' h'h'"), d => d.Days > 0}
|
||||||
}.Build();
|
}.Build();
|
||||||
|
|
||||||
public static IPattern<LocalDateTime> LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
|
public static IPattern<LocalDateTime> LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
|
||||||
|
Loading…
Reference in New Issue
Block a user