feat: upgrade to .NET 6, refactor everything

This commit is contained in:
spiral
2021-11-26 21:10:56 -05:00
parent d28e99ba43
commit 1918c56937
314 changed files with 27954 additions and 27966 deletions

View File

@@ -1,229 +1,252 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Humanizer;
using Myriad.Builders;
using Myriad.Types;
using NodaTime;
using PluralKit.Core;
namespace PluralKit.Bot
namespace PluralKit.Bot;
public static class ContextListExt
{
public static class ContextListExt
public static MemberListOptions ParseMemberListOptions(this Context ctx, LookupContext lookupCtx)
{
public static MemberListOptions ParseMemberListOptions(this Context ctx, LookupContext lookupCtx)
var p = new MemberListOptions();
// Short or long list? (parse this first, as it can potentially take a positional argument)
var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full");
p.Type = isFull ? ListType.Long : ListType.Short;
// Search query
if (ctx.HasNext())
p.Search = ctx.RemainderOrNull();
// Include description in search?
if (ctx.MatchFlag(
"search-description",
"filter-description",
"in-description",
"sd",
"description",
"desc"
))
p.SearchDescription = true;
// Sort property (default is by name, but adding a flag anyway, 'cause why not)
if (ctx.MatchFlag("by-name", "bn")) p.SortProperty = SortProperty.Name;
if (ctx.MatchFlag("by-display-name", "bdn")) p.SortProperty = SortProperty.DisplayName;
if (ctx.MatchFlag("by-id", "bid")) p.SortProperty = SortProperty.Hid;
if (ctx.MatchFlag("by-message-count", "bmc")) p.SortProperty = SortProperty.MessageCount;
if (ctx.MatchFlag("by-created", "bc", "bcd")) p.SortProperty = SortProperty.CreationDate;
if (ctx.MatchFlag("by-last-fronted", "by-last-front", "by-last-switch", "blf", "bls"))
p.SortProperty = SortProperty.LastSwitch;
if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage;
if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate;
if (ctx.MatchFlag("random")) p.SortProperty = SortProperty.Random;
// Sort reverse?
if (ctx.MatchFlag("r", "rev", "reverse"))
p.Reverse = true;
// Privacy filter (default is public only)
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("private-only", "private", "priv")) p.PrivacyFilter = PrivacyLevel.Private;
if (ctx.MatchFlag("public-only", "public", "pub")) p.PrivacyFilter = PrivacyLevel.Public;
// PERM CHECK: If we're trying to access non-public members of another system, error
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
// TODO: should this just return null instead of throwing or something? >.>
throw new PKError("You cannot look up private members of another system.");
// Additional fields to include in the search results
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
p.IncludeLastSwitch = true;
if (ctx.MatchFlag("with-last-message", "with-last-proxy", "wlm", "wlp"))
throw new PKError("Sorting by last message is temporarily disabled due to database issues, sorry.");
// p.IncludeLastMessage = true;
if (ctx.MatchFlag("with-message-count", "wmc"))
p.IncludeMessageCount = true;
if (ctx.MatchFlag("with-created", "wc"))
p.IncludeCreated = true;
if (ctx.MatchFlag("with-avatar", "with-image", "wa", "wi", "ia", "ii", "img"))
p.IncludeAvatar = true;
if (ctx.MatchFlag("with-pronouns", "wp"))
p.IncludePronouns = true;
// Always show the sort property, too
if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true;
if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true;
if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true;
// Done!
return p;
}
public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, IDatabase db,
SystemId system, string embedTitle, string color, MemberListOptions opts)
{
// We take an IDatabase instead of a IPKConnection so we don't keep the handle open for the entire runtime
// We wanna release it as soon as the member list is actually *fetched*, instead of potentially minutes later (paginate timeout)
var members = (await db.Execute(conn => conn.QueryMemberList(system, opts.ToQueryOptions())))
.SortByMemberListOptions(opts, lookupCtx)
.ToList();
var itemsPerPage = opts.Type == ListType.Short ? 25 : 5;
await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, color, Renderer);
// Base renderer, dispatches based on type
Task Renderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
var p = new MemberListOptions();
// Add a global footer with the filter/sort string + result count
eb.Footer(new Embed.EmbedFooter($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}."));
// Short or long list? (parse this first, as it can potentially take a positional argument)
var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full");
p.Type = isFull ? ListType.Long : ListType.Short;
// Then call the specific renderers
if (opts.Type == ListType.Short)
ShortRenderer(eb, page);
else
LongRenderer(eb, page);
// Search query
if (ctx.HasNext())
p.Search = ctx.RemainderOrNull();
// Include description in search?
if (ctx.MatchFlag("search-description", "filter-description", "in-description", "sd", "description", "desc"))
p.SearchDescription = true;
// Sort property (default is by name, but adding a flag anyway, 'cause why not)
if (ctx.MatchFlag("by-name", "bn")) p.SortProperty = SortProperty.Name;
if (ctx.MatchFlag("by-display-name", "bdn")) p.SortProperty = SortProperty.DisplayName;
if (ctx.MatchFlag("by-id", "bid")) p.SortProperty = SortProperty.Hid;
if (ctx.MatchFlag("by-message-count", "bmc")) p.SortProperty = SortProperty.MessageCount;
if (ctx.MatchFlag("by-created", "bc", "bcd")) p.SortProperty = SortProperty.CreationDate;
if (ctx.MatchFlag("by-last-fronted", "by-last-front", "by-last-switch", "blf", "bls")) p.SortProperty = SortProperty.LastSwitch;
if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage;
if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate;
if (ctx.MatchFlag("random")) p.SortProperty = SortProperty.Random;
// Sort reverse?
if (ctx.MatchFlag("r", "rev", "reverse"))
p.Reverse = true;
// Privacy filter (default is public only)
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("private-only", "private", "priv")) p.PrivacyFilter = PrivacyLevel.Private;
if (ctx.MatchFlag("public-only", "public", "pub")) p.PrivacyFilter = PrivacyLevel.Public;
// PERM CHECK: If we're trying to access non-public members of another system, error
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
// TODO: should this just return null instead of throwing or something? >.>
throw new PKError("You cannot look up private members of another system.");
// Additional fields to include in the search results
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
p.IncludeLastSwitch = true;
if (ctx.MatchFlag("with-last-message", "with-last-proxy", "wlm", "wlp"))
throw new PKError("Sorting by last message is temporarily disabled due to database issues, sorry.");
// p.IncludeLastMessage = true;
if (ctx.MatchFlag("with-message-count", "wmc"))
p.IncludeMessageCount = true;
if (ctx.MatchFlag("with-created", "wc"))
p.IncludeCreated = true;
if (ctx.MatchFlag("with-avatar", "with-image", "wa", "wi", "ia", "ii", "img"))
p.IncludeAvatar = true;
if (ctx.MatchFlag("with-pronouns", "wp"))
p.IncludePronouns = true;
// Always show the sort property, too
if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true;
if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true;
if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true;
// Done!
return p;
return Task.CompletedTask;
}
public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, IDatabase db, SystemId system, string embedTitle, string color, MemberListOptions opts)
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
// We take an IDatabase instead of a IPKConnection so we don't keep the handle open for the entire runtime
// We wanna release it as soon as the member list is actually *fetched*, instead of potentially minutes later (paginate timeout)
var members = (await db.Execute(conn => conn.QueryMemberList(system, opts.ToQueryOptions())))
.SortByMemberListOptions(opts, lookupCtx)
.ToList();
var itemsPerPage = opts.Type == ListType.Short ? 25 : 5;
await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, color, Renderer);
// Base renderer, dispatches based on type
Task Renderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
// We may end up over the description character limit
// so run it through a helper that "makes it work" :)
eb.WithSimpleLineContent(page.Select(m =>
{
// Add a global footer with the filter/sort string + result count
eb.Footer(new($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}."));
var ret = $"[`{m.Hid}`] **{m.NameFor(ctx)}** ";
// Then call the specific renderers
if (opts.Type == ListType.Short)
ShortRenderer(eb, page);
else
LongRenderer(eb, page);
return Task.CompletedTask;
}
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
// We may end up over the description character limit
// so run it through a helper that "makes it work" :)
eb.WithSimpleLineContent(page.Select(m =>
switch (opts.SortProperty)
{
var ret = $"[`{m.Hid}`] **{m.NameFor(ctx)}** ";
switch (opts.SortProperty)
{
case SortProperty.Birthdate:
case SortProperty.Birthdate:
{
var birthday = m.BirthdayFor(lookupCtx);
if (birthday != null)
ret += $"(birthday: {m.BirthdayString})";
break;
}
case SortProperty.DisplayName:
{
if (m.DisplayName != null)
ret += $"({m.DisplayName})";
break;
}
case SortProperty.MessageCount:
{
if (m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
break;
}
case SortProperty.LastSwitch:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
break;
}
// case SortProperty.LastMessage:
// {
// if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
// ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
// break;
// }
case SortProperty.CreationDate:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
break;
}
default:
{
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
{
var birthday = m.BirthdayFor(lookupCtx);
if (birthday != null)
ret += $"(birthday: {m.BirthdayString})";
break;
ret += $"({count} messages)";
}
case SortProperty.DisplayName:
else if (opts.IncludeLastSwitch &&
m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
{
if (m.DisplayName != null)
ret += $"({m.DisplayName})";
break;
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
}
case SortProperty.MessageCount:
// else if (opts.IncludeLastMessage && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
// ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
else if (opts.IncludeCreated &&
m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
{
if (m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
break;
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
}
case SortProperty.LastSwitch:
else if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatarUrl)
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
break;
ret += $"([avatar URL]({avatarUrl}))";
}
// case SortProperty.LastMessage:
// {
// if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
// ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
// break;
// }
case SortProperty.CreationDate:
else if (opts.IncludePronouns && m.PronounsFor(lookupCtx) is { } pronouns)
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
break;
ret += $"({pronouns})";
}
default:
else if (m.HasProxyTags)
{
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
else if (opts.IncludeLastSwitch && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
// else if (opts.IncludeLastMessage && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
// ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
else if (opts.IncludeCreated && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
else if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatarUrl)
ret += $"([avatar URL]({avatarUrl}))";
else if (opts.IncludePronouns && m.PronounsFor(lookupCtx) is { } pronouns)
ret += $"({pronouns})";
else if (m.HasProxyTags)
{
var proxyTagsString = m.ProxyTagsString();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
ret += $"*(*{proxyTagsString}*)*";
}
break;
var proxyTagsString = m.ProxyTagsString();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
ret += $"*(*{proxyTagsString}*)*";
}
}
return ret;
}));
}
void LongRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
var zone = ctx.System?.Zone ?? DateTimeZone.Utc;
foreach (var m in page)
{
var profile = new StringBuilder($"**ID**: {m.Hid}");
if (m.DisplayName != null && m.NamePrivacy.CanAccess(lookupCtx))
profile.Append($"\n**Display name**: {m.DisplayName}");
if (m.PronounsFor(lookupCtx) is { } pronouns)
profile.Append($"\n**Pronouns**: {pronouns}");
if (m.BirthdayFor(lookupCtx) != null)
profile.Append($"\n**Birthdate**: {m.BirthdayString}");
if (m.ProxyTags.Count > 0)
profile.Append($"\n**Proxy tags**: {m.ProxyTagsString()}");
if ((opts.IncludeMessageCount || opts.SortProperty == SortProperty.MessageCount) && m.MessageCountFor(lookupCtx) is { } count && count > 0)
profile.Append($"\n**Message count:** {count}");
// if ((opts.IncludeLastMessage || opts.SortProperty == SortProperty.LastMessage) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
// profile.Append($"\n**Last message:** {DiscordUtils.SnowflakeToInstant(lastMsg.Value).FormatZoned(zone)}");
if ((opts.IncludeLastSwitch || opts.SortProperty == SortProperty.LastSwitch) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
profile.Append($"\n**Last switched in:** {lastSw.Value.FormatZoned(zone)}");
if ((opts.IncludeCreated || opts.SortProperty == SortProperty.CreationDate) && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
profile.Append($"\n**Created on:** {created.FormatZoned(zone)}");
if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatar)
profile.Append($"\n**Avatar URL:** {avatar.TryGetCleanCdnUrl()}");
if (m.DescriptionFor(lookupCtx) is { } desc)
profile.Append($"\n\n{desc}");
if (m.MemberVisibility == PrivacyLevel.Private)
profile.Append("\n*(this member is hidden)*");
eb.Field(new(m.NameFor(ctx), profile.ToString().Truncate(1024)));
break;
}
}
return ret;
}));
}
void LongRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
var zone = ctx.System?.Zone ?? DateTimeZone.Utc;
foreach (var m in page)
{
var profile = new StringBuilder($"**ID**: {m.Hid}");
if (m.DisplayName != null && m.NamePrivacy.CanAccess(lookupCtx))
profile.Append($"\n**Display name**: {m.DisplayName}");
if (m.PronounsFor(lookupCtx) is { } pronouns)
profile.Append($"\n**Pronouns**: {pronouns}");
if (m.BirthdayFor(lookupCtx) != null)
profile.Append($"\n**Birthdate**: {m.BirthdayString}");
if (m.ProxyTags.Count > 0)
profile.Append($"\n**Proxy tags**: {m.ProxyTagsString()}");
if ((opts.IncludeMessageCount || opts.SortProperty == SortProperty.MessageCount) &&
m.MessageCountFor(lookupCtx) is { } count && count > 0)
profile.Append($"\n**Message count:** {count}");
// if ((opts.IncludeLastMessage || opts.SortProperty == SortProperty.LastMessage) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
// profile.Append($"\n**Last message:** {DiscordUtils.SnowflakeToInstant(lastMsg.Value).FormatZoned(zone)}");
if ((opts.IncludeLastSwitch || opts.SortProperty == SortProperty.LastSwitch) &&
m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
profile.Append($"\n**Last switched in:** {lastSw.Value.FormatZoned(zone)}");
if ((opts.IncludeCreated || opts.SortProperty == SortProperty.CreationDate) &&
m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
profile.Append($"\n**Created on:** {created.FormatZoned(zone)}");
if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatar)
profile.Append($"\n**Avatar URL:** {avatar.TryGetCleanCdnUrl()}");
if (m.DescriptionFor(lookupCtx) is { } desc)
profile.Append($"\n\n{desc}");
if (m.MemberVisibility == PrivacyLevel.Private)
profile.Append("\n*(this member is hidden)*");
eb.Field(new Embed.Field(m.NameFor(ctx), profile.ToString().Truncate(1024)));
}
}
}

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NodaTime;
@@ -8,128 +5,127 @@ using NodaTime;
using PluralKit.Core;
#nullable enable
namespace PluralKit.Bot
namespace PluralKit.Bot;
public class MemberListOptions
{
public class MemberListOptions
public SortProperty SortProperty { get; set; } = SortProperty.Name;
public bool Reverse { get; set; }
public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public;
public GroupId? GroupFilter { get; set; }
public string? Search { get; set; }
public bool SearchDescription { get; set; }
public ListType Type { get; set; }
public bool IncludeMessageCount { get; set; }
public bool IncludeLastSwitch { get; set; }
public bool IncludeLastMessage { get; set; }
public bool IncludeCreated { get; set; }
public bool IncludeAvatar { get; set; }
public bool IncludePronouns { get; set; }
public string CreateFilterString()
{
public SortProperty SortProperty { get; set; } = SortProperty.Name;
public bool Reverse { get; set; }
public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public;
public GroupId? GroupFilter { get; set; }
public string? Search { get; set; }
public bool SearchDescription { get; set; }
public ListType Type { get; set; }
public bool IncludeMessageCount { get; set; }
public bool IncludeLastSwitch { get; set; }
public bool IncludeLastMessage { get; set; }
public bool IncludeCreated { get; set; }
public bool IncludeAvatar { get; set; }
public bool IncludePronouns { get; set; }
public string CreateFilterString()
var str = new StringBuilder();
str.Append("Sorting ");
if (SortProperty != SortProperty.Random) str.Append("by ");
str.Append(SortProperty switch
{
var str = new StringBuilder();
str.Append("Sorting ");
if (SortProperty != SortProperty.Random) str.Append("by ");
str.Append(SortProperty switch
{
SortProperty.Name => "member name",
SortProperty.Hid => "member ID",
SortProperty.DisplayName => "display name",
SortProperty.CreationDate => "creation date",
SortProperty.LastMessage => "last message",
SortProperty.LastSwitch => "last switch",
SortProperty.MessageCount => "message count",
SortProperty.Birthdate => "birthday",
SortProperty.Random => "randomly",
_ => new ArgumentOutOfRangeException($"Couldn't find readable string for sort property {SortProperty}")
});
SortProperty.Name => "member name",
SortProperty.Hid => "member ID",
SortProperty.DisplayName => "display name",
SortProperty.CreationDate => "creation date",
SortProperty.LastMessage => "last message",
SortProperty.LastSwitch => "last switch",
SortProperty.MessageCount => "message count",
SortProperty.Birthdate => "birthday",
SortProperty.Random => "randomly",
_ => new ArgumentOutOfRangeException($"Couldn't find readable string for sort property {SortProperty}")
});
if (Search != null)
{
str.Append($", searching for \"{Search}\"");
if (SearchDescription) str.Append(" (including description)");
}
str.Append(PrivacyFilter switch
{
null => ", showing all members",
PrivacyLevel.Private => ", showing only private members",
PrivacyLevel.Public => "", // (default, no extra line needed)
_ => new ArgumentOutOfRangeException($"Couldn't find readable string for privacy filter {PrivacyFilter}")
});
return str.ToString();
if (Search != null)
{
str.Append($", searching for \"{Search}\"");
if (SearchDescription) str.Append(" (including description)");
}
public DatabaseViewsExt.MemberListQueryOptions ToQueryOptions() =>
new DatabaseViewsExt.MemberListQueryOptions
{
PrivacyFilter = PrivacyFilter,
GroupFilter = GroupFilter,
Search = Search,
SearchDescription = SearchDescription
};
}
public static class MemberListOptionsExt
{
public static IEnumerable<ListedMember> SortByMemberListOptions(this IEnumerable<ListedMember> input, MemberListOptions opts, LookupContext ctx)
str.Append(PrivacyFilter switch
{
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
opts.Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
null => ", showing all members",
PrivacyLevel.Private => ", showing only private members",
PrivacyLevel.Public => "", // (default, no extra line needed)
_ => new ArgumentOutOfRangeException(
$"Couldn't find readable string for privacy filter {PrivacyFilter}")
});
var randGen = new global::System.Random();
var culture = StringComparer.InvariantCultureIgnoreCase;
return (opts.SortProperty switch
{
// As for the OrderByDescending HasValue calls: https://www.jerriepelser.com/blog/orderby-with-null-values/
// We want nulls last no matter what, even if orders are reversed
SortProperty.Hid => input.OrderBy(m => m.Hid, ReverseMaybe(culture)),
SortProperty.Name => input.OrderBy(m => m.NameFor(ctx), ReverseMaybe(culture)),
SortProperty.CreationDate => input.OrderBy(m => m.Created, ReverseMaybe(Comparer<Instant>.Default)),
SortProperty.MessageCount => input.OrderByDescending(m => m.MessageCount, ReverseMaybe(Comparer<int>.Default)),
SortProperty.DisplayName => input
.OrderByDescending(m => m.DisplayName != null)
.ThenBy(m => m.DisplayName, ReverseMaybe(culture)),
SortProperty.Birthdate => input
.OrderByDescending(m => m.AnnualBirthday.HasValue)
.ThenBy(m => m.AnnualBirthday, ReverseMaybe(Comparer<AnnualDate?>.Default)),
SortProperty.LastMessage => throw new PKError("Sorting by last message is temporarily disabled due to database issues, sorry."),
// SortProperty.LastMessage => input
// .OrderByDescending(m => m.LastMessage.HasValue)
// .ThenByDescending(m => m.LastMessage, ReverseMaybe(Comparer<ulong?>.Default)),
SortProperty.LastSwitch => input
.OrderByDescending(m => m.LastSwitchTime.HasValue)
.ThenByDescending(m => m.LastSwitchTime, ReverseMaybe(Comparer<Instant?>.Default)),
SortProperty.Random => input
.OrderBy(m => randGen.Next()),
_ => throw new ArgumentOutOfRangeException($"Unknown sort property {opts.SortProperty}")
})
// Lastly, add a by-name fallback order for collisions (generally hits w/ lots of null values)
.ThenBy(m => m.NameFor(ctx), culture);
}
return str.ToString();
}
public enum SortProperty
public DatabaseViewsExt.MemberListQueryOptions ToQueryOptions() =>
new()
{
PrivacyFilter = PrivacyFilter,
GroupFilter = GroupFilter,
Search = Search,
SearchDescription = SearchDescription
};
}
public static class MemberListOptionsExt
{
public static IEnumerable<ListedMember> SortByMemberListOptions(this IEnumerable<ListedMember> input,
MemberListOptions opts, LookupContext ctx)
{
Name,
DisplayName,
Hid,
MessageCount,
CreationDate,
LastSwitch,
LastMessage,
Birthdate,
Random
}
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
opts.Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
public enum ListType
{
Short,
Long
var randGen = new global::System.Random();
var culture = StringComparer.InvariantCultureIgnoreCase;
return (opts.SortProperty switch
{
// As for the OrderByDescending HasValue calls: https://www.jerriepelser.com/blog/orderby-with-null-values/
// We want nulls last no matter what, even if orders are reversed
SortProperty.Hid => input.OrderBy(m => m.Hid, ReverseMaybe(culture)),
SortProperty.Name => input.OrderBy(m => m.NameFor(ctx), ReverseMaybe(culture)),
SortProperty.CreationDate => input.OrderBy(m => m.Created, ReverseMaybe(Comparer<Instant>.Default)),
SortProperty.MessageCount => input.OrderByDescending(m => m.MessageCount,
ReverseMaybe(Comparer<int>.Default)),
SortProperty.DisplayName => input
.OrderByDescending(m => m.DisplayName != null)
.ThenBy(m => m.DisplayName, ReverseMaybe(culture)),
SortProperty.Birthdate => input
.OrderByDescending(m => m.AnnualBirthday.HasValue)
.ThenBy(m => m.AnnualBirthday, ReverseMaybe(Comparer<AnnualDate?>.Default)),
SortProperty.LastMessage => throw new PKError(
"Sorting by last message is temporarily disabled due to database issues, sorry."),
// SortProperty.LastMessage => input
// .OrderByDescending(m => m.LastMessage.HasValue)
// .ThenByDescending(m => m.LastMessage, ReverseMaybe(Comparer<ulong?>.Default)),
SortProperty.LastSwitch => input
.OrderByDescending(m => m.LastSwitchTime.HasValue)
.ThenByDescending(m => m.LastSwitchTime, ReverseMaybe(Comparer<Instant?>.Default)),
SortProperty.Random => input
.OrderBy(m => randGen.Next()),
_ => throw new ArgumentOutOfRangeException($"Unknown sort property {opts.SortProperty}")
})
// Lastly, add a by-name fallback order for collisions (generally hits w/ lots of null values)
.ThenBy(m => m.NameFor(ctx), culture);
}
}
}
public enum SortProperty
{
Name,
DisplayName,
Hid,
MessageCount,
CreationDate,
LastSwitch,
LastMessage,
Birthdate,
Random
}
public enum ListType { Short, Long }