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));
|
||||
}
|
||||
|
||||
[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")]
|
||||
[Remarks("system timezone [timezone]")]
|
||||
[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 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 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))
|
||||
.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
|
||||
// (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});
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return await _connection.QueryAsync<PKMember>(
|
||||
@ -215,5 +221,76 @@ namespace PluralKit {
|
||||
{
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
@ -216,6 +217,17 @@ namespace PluralKit
|
||||
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 {
|
||||
@ -236,10 +248,10 @@ namespace PluralKit
|
||||
// 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>
|
||||
{
|
||||
{DurationPattern.CreateWithInvariantCulture("D'd' h'h'"), d => d.Days > 0},
|
||||
{DurationPattern.CreateWithInvariantCulture("H'h' m'm'"), d => d.Hours > 0},
|
||||
{DurationPattern.CreateWithInvariantCulture("s's'"), d => true},
|
||||
{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();
|
||||
|
||||
public static IPattern<LocalDateTime> LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
|
||||
|
Loading…
Reference in New Issue
Block a user