Add super basic group model/command

This commit is contained in:
Ske 2020-06-29 23:51:12 +02:00
parent 0fadc81cda
commit 253ae43c7f
15 changed files with 199 additions and 2 deletions

View File

@ -25,6 +25,7 @@ namespace PluralKit.Bot
private readonly MessageContext _messageContext;
private readonly IDataStore _data;
private readonly IDatabase _db;
private readonly PKSystem _senderSystem;
private readonly IMetrics _metrics;
@ -40,6 +41,7 @@ namespace PluralKit.Bot
_data = provider.Resolve<IDataStore>();
_senderSystem = senderSystem;
_messageContext = messageContext;
_db = provider.Resolve<IDatabase>();
_metrics = provider.Resolve<IMetrics>();
_provider = provider;
_parameters = new Parameters(message.Content.Substring(commandParseOffset));
@ -61,6 +63,7 @@ namespace PluralKit.Bot
// TODO: this is just here so the extension methods can access it; should it be public/private/?
internal IDataStore DataStore => _data;
internal IDatabase Database => _db;
public Task<DiscordMessage> Reply(string text = null, DiscordEmbed embed = null, IEnumerable<IMention> mentions = null)
{

View File

@ -97,6 +97,26 @@ namespace PluralKit.Bot
// Finally, we return the member value.
return member;
}
public static async Task<PKGroup> PeekGroup(this Context ctx)
{
var input = ctx.PeekArgument();
await using var conn = await ctx.Database.Obtain();
if (await conn.QueryGroupByName(input) is {} byName)
return byName;
if (await conn.QueryGroupByHid(input) is {} byHid)
return byHid;
return null;
}
public static async Task<PKGroup> MatchGroup(this Context ctx)
{
var group = await ctx.PeekGroup();
if (group != null) ctx.PopArgument();
return group;
}
public static string CreateMemberNotFoundError(this Context ctx, string input)
{

View File

@ -45,6 +45,8 @@ namespace PluralKit.Bot
public static Command MemberKeepProxy = new Command("member keepproxy", "member <member> keepproxy [on|off]", "Sets whether to include a member's proxy tags when proxying");
public static Command MemberRandom = new Command("random", "random", "Looks up a random member from your system");
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
public static Command GroupInfo = new Command("group", "group <name>", "Looks up information about a group");
public static Command GroupNew = new Command("group new", "group new <name>", "Creates a new group");
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "Registers a switch");
public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members");
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
@ -97,6 +99,8 @@ namespace PluralKit.Bot
return HandleSystemCommand(ctx);
if (ctx.Match("member", "m"))
return HandleMemberCommand(ctx);
if (ctx.Match("group", "g"))
return HandleGroupCommand(ctx);
if (ctx.Match("switch", "sw"))
return HandleSwitchCommand(ctx);
if (ctx.Match("ap", "autoproxy", "auto"))
@ -312,6 +316,19 @@ namespace PluralKit.Bot
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName ,MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
}
private async Task HandleGroupCommand(Context ctx)
{
// Commands with no group argument
if (ctx.Match("n", "new"))
await ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx));
if (await ctx.MatchGroup() is {} group)
{
// Commands with group argument
await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, group));
}
}
private async Task HandleSwitchCommand(Context ctx)
{
if (ctx.Match("out"))

View File

@ -0,0 +1,58 @@
using System.Threading.Tasks;
using DSharpPlus.Entities;
using PluralKit.Core;
namespace PluralKit.Bot
{
public class Groups
{
private readonly IDatabase _db;
public Groups(IDatabase db)
{
_db = db;
}
public async Task CreateGroup(Context ctx)
{
ctx.CheckSystem();
var groupName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a group name.");
if (groupName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({groupName.Length}/{Limits.MaxMemberNameLength} characters).");
await using var conn = await _db.Obtain();
var newGroup = await conn.CreateGroup(ctx.System.Id, groupName);
await ctx.Reply($"{Emojis.Success} Group \"**{groupName}**\" (`{newGroup.Hid}`) registered!\nYou can now start adding members to the group:\n- **pk;group {newGroup.Hid} add <members...>**");
}
public async Task ShowGroupCard(Context ctx, PKGroup target)
{
await using var conn = await _db.Obtain();
var system = await GetGroupSystem(ctx, target, conn);
var nameField = target.Name;
if (system.Name != null)
nameField = $"{nameField} ({system.Name})";
var eb = new DiscordEmbedBuilder()
.WithAuthor(nameField)
.WithDescription(target.Description)
.WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}");
await ctx.Reply(embed: eb.Build());
}
private static async Task<PKSystem> GetGroupSystem(Context ctx, PKGroup target, IPKConnection conn)
{
var system = ctx.System;
if (system?.Id == target.System)
return system;
return await conn.QuerySystem(target.System)!;
}
}
}

View File

@ -33,6 +33,7 @@ namespace PluralKit.Bot
builder.RegisterType<CommandTree>().AsSelf();
builder.RegisterType<Autoproxy>().AsSelf();
builder.RegisterType<Fun>().AsSelf();
builder.RegisterType<Groups>().AsSelf();
builder.RegisterType<Help>().AsSelf();
builder.RegisterType<ImportExport>().AsSelf();
builder.RegisterType<Member>().AsSelf();

View File

@ -20,7 +20,7 @@ namespace PluralKit.Core
internal class Database: IDatabase
{
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 8;
private const int TargetSchemaVersion = 9;
private readonly CoreConfig _config;
private readonly ILogger _logger;
@ -58,9 +58,11 @@ namespace PluralKit.Core
SqlMapper.AddTypeHandler(new NumericIdHandler<SystemId, int>(i => new SystemId(i)));
SqlMapper.AddTypeHandler(new NumericIdHandler<MemberId, int>(i => new MemberId(i)));
SqlMapper.AddTypeHandler(new NumericIdHandler<SwitchId, int>(i => new SwitchId(i)));
SqlMapper.AddTypeHandler(new NumericIdHandler<GroupId, int>(i => new GroupId(i)));
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SystemId, int>(i => new SystemId(i)));
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<MemberId, int>(i => new MemberId(i)));
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SwitchId, int>(i => new SwitchId(i)));
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<GroupId, int>(i => new GroupId(i)));
// Register our custom types to Npgsql
// Without these it'll still *work* but break at the first launch + probably cause other small issues

View File

@ -111,4 +111,14 @@ begin
if not exists (select 1 from members where hid = new_hid) then return new_hid; end if;
end loop;
end
$$ language plpgsql volatile;
create function find_free_group_hid() returns char(5) as $$
declare new_hid char(5);
begin
loop
new_hid := generate_hid();
if not exists (select 1 from groups where hid = new_hid) then return new_hid; end if;
end loop;
end
$$ language plpgsql volatile;

View File

@ -0,0 +1,17 @@
-- SCHEMA VERSION 9: 2020-xx-xx --
create table groups (
id int primary key generated always as identity,
hid char(5) unique not null,
system int not null references systems(id) on delete cascade,
name text not null,
description text,
created timestamp with time zone not null default (current_timestamp at time zone 'utc')
);
create table group_members (
group_id int not null references groups(id) on delete cascade,
member_id int not null references members(id) on delete cascade
);
update info set schema_version = 9;

View File

@ -10,4 +10,5 @@ drop function if exists message_context;
drop function if exists proxy_members;
drop function if exists generate_hid;
drop function if exists find_free_system_hid;
drop function if exists find_free_member_hid;
drop function if exists find_free_member_hid;
drop function if exists find_free_group_hid;

View File

@ -0,0 +1,26 @@
namespace PluralKit.Core
{
public readonly struct GroupId: INumericId<GroupId, int>
{
public int Value { get; }
public GroupId(int value)
{
Value = value;
}
public bool Equals(GroupId other) => Value == other.Value;
public override bool Equals(object obj) => obj is GroupId other && Equals(other);
public override int GetHashCode() => Value;
public static bool operator ==(GroupId left, GroupId right) => left.Equals(right);
public static bool operator !=(GroupId left, GroupId right) => !left.Equals(right);
public int CompareTo(GroupId other) => Value.CompareTo(other.Value);
public override string ToString() => $"Member #{Value}";
}
}

View File

@ -29,6 +29,12 @@ namespace PluralKit.Core
public static Task<PKMember?> QueryMemberByHid(this IPKConnection conn, string hid) =>
conn.QueryFirstOrDefaultAsync<PKMember?>("select * from members where hid = @hid", new {hid = hid.ToLowerInvariant()});
public static Task<PKGroup?> QueryGroupByName(this IPKConnection conn, string name) =>
conn.QueryFirstOrDefaultAsync<PKGroup?>("select * from groups where lower(name) = lower(@name)", new {name = name});
public static Task<PKGroup?> QueryGroupByHid(this IPKConnection conn, string hid) =>
conn.QueryFirstOrDefaultAsync<PKGroup?>("select * from groups where hid = @hid", new {hid = hid.ToLowerInvariant()});
public static Task<GuildConfig> QueryOrInsertGuildConfig(this IPKConnection conn, ulong guild) =>
conn.QueryFirstAsync<GuildConfig>("insert into servers (id) values (@guild) on conflict (id) do update set id = @guild returning *", new {guild});

View File

@ -0,0 +1,17 @@
using NodaTime;
#nullable enable
namespace PluralKit.Core
{
public class PKGroup
{
public GroupId Id { get; }
public string Hid { get; } = null!;
public SystemId System { get; }
public string Name { get; } = null!;
public string? Description { get; }
public Instant Created { get; }
}
}

View File

@ -0,0 +1,13 @@
#nullable enable
namespace PluralKit.Core
{
public class GroupPatch: PatchObject
{
public Partial<string> Name { get; set; }
public Partial<string?> Description { get; set; }
public override UpdateQueryBuilder Apply(UpdateQueryBuilder b) => b
.With("name", Name)
.With("description", Description);
}
}

View File

@ -60,5 +60,10 @@ namespace PluralKit.Core
.Build();
return conn.ExecuteAsync(query, pms);
}
public static Task<PKGroup> CreateGroup(this IPKConnection conn, SystemId system, string name) =>
conn.QueryFirstAsync<PKGroup>(
"insert into groups (hid, system, name) values (find_free_group_hid(), @System, @Name) returning *",
new {System = system, Name = name});
}
}

View File

@ -9,6 +9,7 @@ namespace PluralKit.Core {
public static readonly int MaxMembersWarnThreshold = MaxMemberCount - 50;
public static readonly int MaxDescriptionLength = 1000;
public static readonly int MaxMemberNameLength = 100; // Fair bit larger than MaxProxyNameLength for bookkeeping
public static readonly int MaxGroupNameLength = 100;
public static readonly int MaxPronounsLength = 100;
public static readonly int MaxUriLength = 256; // May need to be set higher, I know there are URLs longer than this in prod (they can rehost, I guess...)
public static readonly long AvatarFileSizeLimit = 1024 * 1024;