Barebones, untested sort/filtering

This commit is contained in:
Ske 2020-06-04 13:21:47 +02:00
parent d5c6e2baac
commit 7c85dc360b
7 changed files with 315 additions and 104 deletions

View File

@ -100,7 +100,7 @@ namespace PluralKit.Bot
if (ctx.Match("list", "l", "members")) if (ctx.Match("list", "l", "members"))
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System)); return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
if (ctx.Match("f", "find", "search", "query", "fd")) if (ctx.Match("f", "find", "search", "query", "fd"))
return ctx.Execute<SystemList>(SystemFind, m => m.MemberFind(ctx, ctx.System)); return ctx.Execute<SystemList>(SystemFind, m => m.MemberList(ctx, ctx.System));
if (ctx.Match("link")) if (ctx.Match("link"))
return ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx)); return ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx));
if (ctx.Match("unlink")) if (ctx.Match("unlink"))
@ -188,7 +188,7 @@ namespace PluralKit.Bot
else if (ctx.Match("list", "l", "members")) else if (ctx.Match("list", "l", "members"))
await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System)); await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
else if (ctx.Match("find", "search", "query", "fd", "s")) else if (ctx.Match("find", "search", "query", "fd", "s"))
await ctx.Execute<SystemList>(SystemFind, m => m.MemberFind(ctx, ctx.System)); await ctx.Execute<SystemList>(SystemFind, m => m.MemberList(ctx, ctx.System));
else if (ctx.Match("f", "front", "fronter", "fronters")) else if (ctx.Match("f", "front", "fronter", "fronters"))
{ {
if (ctx.Match("h", "history")) if (ctx.Match("h", "history"))
@ -225,7 +225,7 @@ namespace PluralKit.Bot
else if (ctx.Match("list", "l", "members")) else if (ctx.Match("list", "l", "members"))
await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, target)); await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, target));
else if (ctx.Match("find", "search", "query", "fd", "s")) else if (ctx.Match("find", "search", "query", "fd", "s"))
await ctx.Execute<SystemList>(SystemFind, m => m.MemberFind(ctx, target)); await ctx.Execute<SystemList>(SystemFind, m => m.MemberList(ctx, target));
else if (ctx.Match("f", "front", "fronter", "fronters")) else if (ctx.Match("f", "front", "fronter", "fronters"))
{ {
if (ctx.Match("h", "history")) if (ctx.Match("h", "history"))

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus.Entities; using Dapper;
using Humanizer;
using PluralKit.Core; using PluralKit.Core;
@ -13,124 +11,70 @@ namespace PluralKit.Bot
{ {
public class SystemList public class SystemList
{ {
private IDataStore _data; private readonly DbConnectionFactory _db;
public SystemList(IDataStore data) public SystemList(DbConnectionFactory db)
{ {
_data = data; _db = db;
} }
private async Task RenderMemberList(Context ctx, PKSystem system, bool canShowPrivate, int membersPerPage, string embedTitle, Func<PKMember, bool> filter, public async Task MemberList(Context ctx, PKSystem target)
Func<DiscordEmbedBuilder, IEnumerable<PKMember>, Task>
renderer)
{ {
var authCtx = ctx.LookupContextFor(system); if (target == null) throw Errors.NoSystemError;
var shouldShowPrivate = authCtx == LookupContext.ByOwner && canShowPrivate; ctx.CheckSystemPrivacy(target, target.MemberListPrivacy);
var membersToShow = await _data.GetSystemMembers(system)
.Where(filter)
.OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase)
.ToListAsync();
var membersToShowWithPrivacy = membersToShow
.Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate)
.ToList();
var anyMembersHidden = !shouldShowPrivate && membersToShowWithPrivacy.Count != membersToShow.Count;
// GetRendererFor must be called before GetOptions as it consumes a potential positional full argument that'd otherwise land in the filter
var renderer = GetRendererFor(ctx);
var opts = GetOptions(ctx, target);
var members = await GetMemberList(target, opts);
await ctx.Paginate( await ctx.Paginate(
membersToShowWithPrivacy.ToAsyncEnumerable(), members.ToAsyncEnumerable(),
membersToShowWithPrivacy.Count, members.Count,
membersPerPage, renderer.MembersPerPage,
embedTitle, GetEmbedTitle(target, opts),
(eb, ms) => (eb, ms) =>
{ {
var footer = $"{membersToShowWithPrivacy.Count} total."; eb.WithFooter($"{members.Count} total.");
if (anyMembersHidden && authCtx == LookupContext.ByOwner) renderer.RenderPage(eb, ms);
footer += " Private members have been hidden. Add \"all\" to the command to include them."; return Task.CompletedTask;
eb.WithFooter(footer);
return renderer(eb, ms);
}); });
} }
private Task ShortRenderer(DiscordEmbedBuilder eb, IEnumerable<PKMember> members) private async Task<IReadOnlyList<PKListMember>> GetMemberList(PKSystem target, SortFilterOptions opts)
{ {
eb.Description = string.Join("\n", members.Select((m) => using var conn = await _db.Obtain();
{ var args = new {System = target.Id, opts.Filter};
if (m.HasProxyTags) return (await conn.QueryAsync<PKListMember>(opts.BuildQuery(), args)).ToList();
{
var proxyTagsString = m.ProxyTagsString().SanitizeMentions();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({proxyTagsString})*";
}
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**";
}));
return Task.CompletedTask;
} }
private Task LongRenderer(DiscordEmbedBuilder eb, IEnumerable<PKMember> members) private string GetEmbedTitle(PKSystem target, SortFilterOptions opts)
{ {
foreach (var m in members) var title = new StringBuilder("Members of ");
{
var profile = $"**ID**: {m.Hid}"; if (target.Name != null) title.Append($"{target.Name.SanitizeMentions()} (`{target.Hid}`)");
if (m.DisplayName != null) profile += $"\n**Display name**: {m.DisplayName}"; else title.Append($"`{target.Hid}`");
if (m.Pronouns != null) profile += $"\n**Pronouns**: {m.Pronouns}";
if (m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}"; if (opts.Filter != null) title.Append($"matching **{opts.Filter.SanitizeMentions()}**");
if (m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
if (m.Description != null) profile += $"\n\n{m.Description}"; return title.ToString();
if (m.MemberPrivacy == PrivacyLevel.Private)
profile += "\n*(this member is private)*";
eb.AddField(m.Name, profile.Truncate(1024));
}
return Task.CompletedTask;
} }
public async Task MemberList(Context ctx, PKSystem system) private SortFilterOptions GetOptions(Context ctx, PKSystem target)
{ {
if (system == null) throw Errors.NoSystemError; var opts = SortFilterOptions.FromFlags(ctx);
ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); opts.Filter = ctx.RemainderOrNull();
// If we're *explicitly* trying to access non-public members of another system, error
var embedTitle = system.Name != null if (opts.PrivacyFilter != PrivacyFilter.PublicOnly && ctx.LookupContextFor(target) != LookupContext.ByOwner)
? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" throw new PKError("You cannot look up private members of another system.");
: $"Members of `{system.Hid}`"; return opts;
var shouldShowLongList = ctx.Match("f", "full", "big", "details", "long");
var canShowPrivate = ctx.Match("a", "all", "everyone", "private");
if (shouldShowLongList)
await RenderMemberList(ctx, system, canShowPrivate, 5, embedTitle, _ => true, LongRenderer);
else
await RenderMemberList(ctx, system, canShowPrivate, 25, embedTitle, _ => true, ShortRenderer);
} }
public async Task MemberFind(Context ctx, PKSystem system) private IListRenderer GetRendererFor(Context ctx)
{ {
if (system == null) throw Errors.NoSystemError; var longList = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full");
ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); if (longList)
return new LongRenderer(LongRenderer.MemberFields.FromFlags(ctx));
var shouldShowLongList = ctx.Match("full", "big", "details", "long") || ctx.MatchFlag("f", "full"); return new ShortRenderer();
var canShowPrivate = ctx.Match("all", "everyone", "private") || ctx.MatchFlag("a", "all");
var searchTerm = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must specify a search term.");
var embedTitle = system.Name != null
? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`) matching **{searchTerm.SanitizeMentions()}**"
: $"Members of `{system.Hid}` matching **{searchTerm.SanitizeMentions()}**";
bool Filter(PKMember member) =>
member.Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) ||
(member.DisplayName?.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false);
if (shouldShowLongList)
await RenderMemberList(ctx, system, canShowPrivate, 5, embedTitle, Filter, LongRenderer);
else
await RenderMemberList(ctx, system, canShowPrivate, 25, embedTitle, Filter, ShortRenderer);
} }
} }
} }

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using DSharpPlus.Entities;
namespace PluralKit.Bot
{
public interface IListRenderer
{
int MembersPerPage { get; }
void RenderPage(DiscordEmbedBuilder eb, IEnumerable<PKListMember> members);
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using DSharpPlus.Entities;
using Humanizer;
using PluralKit.Core;
namespace PluralKit.Bot
{
public class LongRenderer: IListRenderer
{
public int MembersPerPage => 5;
private readonly MemberFields _fields;
public LongRenderer(MemberFields fields)
{
_fields = fields;
}
public void RenderPage(DiscordEmbedBuilder eb, IEnumerable<PKListMember> members)
{
foreach (var m in members)
{
var profile = $"**ID**: {m.Hid}";
if (_fields.ShowDisplayName && m.DisplayName != null) profile += $"\n**Display name**: {m.DisplayName}";
if (_fields.ShowPronouns && m.Pronouns != null) profile += $"\n**Pronouns**: {m.Pronouns}";
if (_fields.ShowBirthday && m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
if (_fields.ShowPronouns && m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
if (_fields.ShowMessageCount && m.MessageCount > 0) profile += $"\n**Message count:** {m.MessageCount}";
if (_fields.ShowDescription && m.Description != null) profile += $"\n\n{m.Description}";
if (_fields.ShowPrivacy && m.MemberPrivacy == PrivacyLevel.Private)
profile += "\n*(this member is private)*";
eb.AddField(m.Name, profile.Truncate(1024));
}
}
public class MemberFields
{
public bool ShowDisplayName = true;
public bool ShowCreated = true;
public bool ShowMessageCount = true;
public bool ShowPronouns = true;
public bool ShowBirthday = true;
public bool ShowProxyTags = true;
public bool ShowDescription = true;
public bool ShowPrivacy = true;
public static MemberFields FromFlags(Context ctx)
{
// TODO
return new MemberFields
{
ShowMessageCount = false,
ShowCreated = false
};
}
}
}
}

View File

@ -0,0 +1,9 @@
using PluralKit.Core;
namespace PluralKit.Bot
{
public class PKListMember: PKMember
{
public int MessageCount { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DSharpPlus.Entities;
namespace PluralKit.Bot
{
public class ShortRenderer: IListRenderer
{
public int MembersPerPage => 25;
public void RenderPage(DiscordEmbedBuilder eb, IEnumerable<PKListMember> members)
{
eb.Description = string.Join("\n", members.Select(m =>
{
if (m.HasProxyTags)
{
var proxyTagsString = m.ProxyTagsString().SanitizeMentions();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({proxyTagsString})*";
}
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**";
}));
}
}
}

View File

@ -0,0 +1,154 @@
using System.Text;
using PluralKit.Core;
namespace PluralKit.Bot
{
public class SortFilterOptions
{
public SortProperty SortProperty = SortProperty.Name;
public SortDirection Direction = SortDirection.Ascending;
public PrivacyFilter PrivacyFilter = PrivacyFilter.PublicOnly;
public string Filter = null;
public bool SearchInDescription = false;
public string CreateFilterString()
{
// TODO
return "uwu";
}
public string BuildQuery()
{
// Select clause
StringBuilder query = new StringBuilder();
query.Append(SortProperty switch
{
SortProperty.MessageCount =>
"select members.*, count(messages.*) as message_count, max(messages.mid) as last_message from members",
SortProperty.LastMessage =>
"select members.*, count(messages.*) as message_count, max(messages.mid) as last_message from members",
SortProperty.LastSwitch => "select members.*, max(switches.timestamp) as last_switch_time from members",
_ => "select members.* from members"
});
// Join clauses
query.Append(SortProperty switch
{
SortProperty.MessageCount => " left join messages on messages.member = members.id",
SortProperty.LastMessage => " left join messages on messages.member = members.id",
SortProperty.LastSwitch =>
" left join switch_members on switch_members.member = members.id left join switches on switch_members.switch = switches.id",
_ => ""
});
// Where clauses
query.Append(" where members.system = @System");
// Privacy filter
query.Append(PrivacyFilter switch
{
PrivacyFilter.PrivateOnly => $" and members.member_privacy = {(int) PrivacyLevel.Private}",
PrivacyFilter.PublicOnly => $" and members.member_privacy = {(int) PrivacyLevel.Public}",
_ => ""
});
// String filter
if (Filter != null)
{
// Use position rather than ilike to not bother with escaping and such
query.Append(" and (");
query.Append(
"position(lower(@Filter) in lower(members.name)) > 0 or position(lower(@Filter) in lower(coalesce(members.display_name, ''))) > 0");
if (SearchInDescription)
query.Append(" or position(lower(@Filter) in lower(coalesce(members.description, ''))) > 0");
query.Append(")");
}
// Group clause
query.Append(SortProperty switch
{
SortProperty.MessageCount => " group by members.id",
SortProperty.LastMessage => " group by members.id",
SortProperty.LastSwitch => " group by members.id",
_ => ""
});
// Order clause
query.Append(SortProperty switch
{
SortProperty.Hid => " order by members.hid",
SortProperty.CreationDate => " order by members.created",
SortProperty.Birthdate =>
" order by extract(month from members.birthday), extract(day from members.birthday)",
SortProperty.MessageCount => " order by count(messages.mid)",
SortProperty.LastMessage => " order by max(messages.mid)",
SortProperty.LastSwitch => " order by max(switches.timestamp)",
_ => " order by members.name"
});
// Order direction
if (Direction == SortDirection.Descending)
query.Append(" desc");
return query.ToString();
}
public static SortFilterOptions FromFlags(Context ctx)
{
var p = new SortFilterOptions();
// Direction
if (ctx.MatchFlag("r", "rev", "reverse", "desc", "descending"))
p.Direction = SortDirection.Descending;
// Sort
if (ctx.MatchFlag("by-id", "bi"))
p.SortProperty = SortProperty.Hid;
else if (ctx.MatchFlag("by-msgcount", "by-mc", "by-msgs", "bm"))
p.SortProperty = SortProperty.MessageCount;
else if (ctx.MatchFlag("by-date", "by-time", "by-created", "bc"))
p.SortProperty = SortProperty.CreationDate;
else if (ctx.MatchFlag("by-switch"))
p.SortProperty = SortProperty.LastSwitch;
else if (ctx.MatchFlag("by-last-message"))
p.SortProperty = SortProperty.LastMessage;
// Description
if (ctx.MatchFlag("desc"))
p.SearchInDescription = true;
// Privacy
if (ctx.MatchFlag("a", "all"))
p.PrivacyFilter = PrivacyFilter.All;
else if (ctx.MatchFlag("po", "private", "private-only", "only-private", "priv"))
p.PrivacyFilter = PrivacyFilter.PrivateOnly;
return p;
}
}
public enum SortProperty
{
Name,
Hid,
MessageCount,
CreationDate,
LastSwitch,
LastMessage,
Birthdate
}
public enum SortDirection
{
Ascending,
Descending
}
public enum PrivacyFilter
{
All,
PublicOnly,
PrivateOnly
}
}