using System.Text; using Humanizer; using NodaTime; using PluralKit.Core; #nullable enable namespace PluralKit.Bot; public class ListOptions { private SortProperty? _sortProperty { get; set; } public SortProperty SortProperty { get => _sortProperty ?? SortProperty.Name; set { if (_sortProperty != null) throw new PKError("Cannot sort in multiple ways at the same time. Please choose only one sorting method."); _sortProperty = value; } } 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 bool IncludeDisplayName { get; set; } public string CreateFilterString() { var str = new StringBuilder(); str.Append("Sorting "); if (SortProperty != SortProperty.Random) str.Append("by "); str.Append(SortProperty switch { SortProperty.Name => "name", SortProperty.Hid => "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.Truncate(100)}\""); if (SearchDescription) str.Append(" (including description)"); } str.Append(PrivacyFilter switch { null => ", showing all items", PrivacyLevel.Private => ", showing only private items", PrivacyLevel.Public => "", // (default, no extra line needed) _ => new ArgumentOutOfRangeException( $"Couldn't find readable string for privacy filter {PrivacyFilter}") }); return str.ToString(); } public DatabaseViewsExt.ListQueryOptions ToQueryOptions() => new() { PrivacyFilter = PrivacyFilter, GroupFilter = GroupFilter, Search = Search, SearchDescription = SearchDescription }; } public static class ListOptionsExt { public static IEnumerable SortByMemberListOptions(this IEnumerable input, ListOptions opts, LookupContext ctx) { IComparer ReverseMaybe(IComparer c) => opts.Reverse ? Comparer.Create((a, b) => c.Compare(b, a)) : c; 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 .OrderByDescending(m => m.MetadataPrivacy.CanAccess(ctx)) .ThenBy(m => m.MetadataPrivacy.Get(ctx, m.Created, default), ReverseMaybe(Comparer.Default)), SortProperty.MessageCount => input .OrderByDescending(m => m.MessageCount != 0 && m.MetadataPrivacy.CanAccess(ctx)) .ThenByDescending(m => m.MetadataPrivacy.Get(ctx, m.MessageCount, 0), ReverseMaybe(Comparer.Default)), SortProperty.DisplayName => input .OrderByDescending(m => m.DisplayName != null && m.NamePrivacy.CanAccess(ctx)) .ThenBy(m => m.NamePrivacy.Get(ctx, m.DisplayName), ReverseMaybe(culture)), SortProperty.Birthdate => input .OrderByDescending(m => m.AnnualBirthday.HasValue && m.BirthdayPrivacy.CanAccess(ctx)) .ThenBy(m => m.BirthdayPrivacy.Get(ctx, m.AnnualBirthday), ReverseMaybe(Comparer.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.Default)), SortProperty.LastSwitch => input .OrderByDescending(m => m.LastSwitchTime.HasValue && m.MetadataPrivacy.CanAccess(ctx)) .ThenByDescending(m => m.MetadataPrivacy.Get(ctx, m.LastSwitchTime), ReverseMaybe(Comparer.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 static IEnumerable SortByGroupListOptions(this IEnumerable input, ListOptions opts, LookupContext ctx) { IComparer ReverseMaybe(IComparer c) => opts.Reverse ? Comparer.Create((a, b) => c.Compare(b, a)) : c; 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(g => g.Hid, ReverseMaybe(culture)), SortProperty.Name => input.OrderBy(g => g.NameFor(ctx), ReverseMaybe(culture)), SortProperty.CreationDate => input .OrderByDescending(g => g.MetadataPrivacy.CanAccess(ctx)) .ThenBy(g => g.MetadataPrivacy.Get(ctx, g.Created, default), ReverseMaybe(Comparer.Default)), SortProperty.DisplayName => input .OrderByDescending(g => g.DisplayName != null && g.NamePrivacy.CanAccess(ctx)) .ThenBy(g => g.NamePrivacy.Get(ctx, g.DisplayName), ReverseMaybe(culture)), SortProperty.Random => input .OrderBy(g => 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(g => g.NameFor(ctx), culture); } public static void AssertIsValid(this ListOptions opts) { if (opts.Type == ListType.Short) { var hasMultipleIncluded = new[] { opts.IncludeMessageCount, opts.IncludeLastSwitch, opts.IncludeLastMessage, opts.IncludeCreated, opts.IncludeAvatar, opts.IncludePronouns, opts.IncludeDisplayName, }.Sum(x => Convert.ToInt32(x)) > 1; if (hasMultipleIncluded) throw new PKError("The short list does not support showing items from multiple flags. Try using the full list instead."); } // the check for multiple *sorting* property flags is done in SortProperty setter } } public enum SortProperty { Name, DisplayName, Hid, MessageCount, CreationDate, LastSwitch, LastMessage, Birthdate, Random } public enum ListType { Short, Long }