current behaviour is to silently ignore the incompatible ones, but this will make it more obvious as to why what they're trying to do is not working
200 lines
8.3 KiB
C#
200 lines
8.3 KiB
C#
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<ListedMember> SortByMemberListOptions(this IEnumerable<ListedMember> input,
|
|
ListOptions opts, LookupContext ctx)
|
|
{
|
|
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
|
|
opts.Reverse ? Comparer<T>.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<Instant>.Default)),
|
|
SortProperty.MessageCount => input
|
|
.OrderByDescending(m => m.MessageCount != 0 && m.MetadataPrivacy.CanAccess(ctx))
|
|
.ThenByDescending(m => m.MetadataPrivacy.Get(ctx, m.MessageCount, 0), ReverseMaybe(Comparer<int>.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<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 && m.MetadataPrivacy.CanAccess(ctx))
|
|
.ThenByDescending(m => m.MetadataPrivacy.Get(ctx, 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 static IEnumerable<ListedGroup> SortByGroupListOptions(this IEnumerable<ListedGroup> input,
|
|
ListOptions opts, LookupContext ctx)
|
|
{
|
|
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
|
|
opts.Reverse ? Comparer<T>.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<Instant>.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)) > 0;
|
|
|
|
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 } |