Refactor data stores, merging the Store classes

This commit is contained in:
Ske 2019-10-26 19:45:30 +02:00
parent 1ab84b54dd
commit 6a73b3bdd6
21 changed files with 540 additions and 338 deletions

@ -8,17 +8,17 @@ namespace PluralKit.API.Controllers
[Route("v1/a")]
public class AccountController: ControllerBase
{
private SystemStore _systems;
private IDataStore _data;
public AccountController(SystemStore systems)
public AccountController(IDataStore data)
{
_systems = systems;
_data = data;
}
[HttpGet("{aid}")]
public async Task<ActionResult<PKSystem>> GetSystemByAccount(ulong aid)
{
var system = await _systems.GetByAccount(aid);
var system = await _data.GetSystemByAccount(aid);
if (system == null) return NotFound("Account not found.");
return Ok(system);

@ -9,13 +9,13 @@ namespace PluralKit.API.Controllers
[Route("v1/m")]
public class MemberController: ControllerBase
{
private MemberStore _members;
private IDataStore _data;
private DbConnectionFactory _conn;
private TokenAuthService _auth;
public MemberController(MemberStore members, DbConnectionFactory conn, TokenAuthService auth)
public MemberController(IDataStore data, DbConnectionFactory conn, TokenAuthService auth)
{
_members = members;
_data = data;
_conn = conn;
_auth = auth;
}
@ -23,7 +23,7 @@ namespace PluralKit.API.Controllers
[HttpGet("{hid}")]
public async Task<ActionResult<PKMember>> GetMember(string hid)
{
var member = await _members.GetByHid(hid);
var member = await _data.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
return Ok(member);
@ -39,7 +39,7 @@ namespace PluralKit.API.Controllers
return BadRequest("Member name cannot be null.");
// Enforce per-system member limit
var memberCount = await _members.MemberCount(system);
var memberCount = await _data.GetSystemMemberCount(system);
if (memberCount >= Limits.MaxMemberCount)
return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount}).");
@ -61,7 +61,7 @@ namespace PluralKit.API.Controllers
if (newMember.Suffix != null && newMember.Suffix.Length > 1000)
return BadRequest();
var member = await _members.Create(system, newMember.Name);
var member = await _data.CreateMember(system, newMember.Name);
member.Name = newMember.Name;
member.DisplayName = newMember.DisplayName;
@ -72,7 +72,7 @@ namespace PluralKit.API.Controllers
member.Description = newMember.Description;
member.Prefix = newMember.Prefix;
member.Suffix = newMember.Suffix;
await _members.Save(member);
await _data.SaveMember(member);
return Ok(member);
}
@ -81,7 +81,7 @@ namespace PluralKit.API.Controllers
[RequiresSystem]
public async Task<ActionResult<PKMember>> PatchMember(string hid, [FromBody] PKMember newMember)
{
var member = await _members.GetByHid(hid);
var member = await _data.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
if (member.System != _auth.CurrentSystem.Id) return Unauthorized($"Member '{hid}' is not part of your system.");
@ -116,7 +116,7 @@ namespace PluralKit.API.Controllers
member.Description = newMember.Description;
member.Prefix = newMember.Prefix;
member.Suffix = newMember.Suffix;
await _members.Save(member);
await _data.SaveMember(member);
return Ok(member);
}
@ -125,12 +125,12 @@ namespace PluralKit.API.Controllers
[RequiresSystem]
public async Task<ActionResult<PKMember>> DeleteMember(string hid)
{
var member = await _members.GetByHid(hid);
var member = await _data.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
if (member.System != _auth.CurrentSystem.Id) return Unauthorized($"Member '{hid}' is not part of your system.");
await _members.Delete(member);
await _data.DeleteMember(member);
return Ok();
}

@ -21,17 +21,17 @@ namespace PluralKit.API.Controllers
[Route("msg")]
public class MessageController: ControllerBase
{
private MessageStore _messages;
private IDataStore _data;
public MessageController(MessageStore messages)
public MessageController(IDataStore _data)
{
_messages = messages;
this._data = _data;
}
[HttpGet("{mid}")]
public async Task<ActionResult<MessageReturn>> GetMessage(ulong mid)
{
var msg = await _messages.Get(mid);
var msg = await _data.GetMessage(mid);
if (msg == null) return NotFound("Message not found.");
return new MessageReturn

@ -31,17 +31,13 @@ namespace PluralKit.API.Controllers
[Route("v1/s")]
public class SystemController : ControllerBase
{
private SystemStore _systems;
private MemberStore _members;
private SwitchStore _switches;
private IDataStore _data;
private DbConnectionFactory _conn;
private TokenAuthService _auth;
public SystemController(SystemStore systems, MemberStore members, SwitchStore switches, DbConnectionFactory conn, TokenAuthService auth)
public SystemController(IDataStore data, DbConnectionFactory conn, TokenAuthService auth)
{
_systems = systems;
_members = members;
_switches = switches;
_data = data;
_conn = conn;
_auth = auth;
}
@ -56,7 +52,7 @@ namespace PluralKit.API.Controllers
[HttpGet("{hid}")]
public async Task<ActionResult<PKSystem>> GetSystem(string hid)
{
var system = await _systems.GetByHid(hid);
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
return Ok(system);
}
@ -64,10 +60,10 @@ namespace PluralKit.API.Controllers
[HttpGet("{hid}/members")]
public async Task<ActionResult<IEnumerable<PKMember>>> GetMembers(string hid)
{
var system = await _systems.GetByHid(hid);
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
var members = await _members.GetBySystem(system);
var members = await _data.GetSystemMembers(system);
return Ok(members);
}
@ -76,7 +72,7 @@ namespace PluralKit.API.Controllers
{
if (before == null) before = SystemClock.Instance.GetCurrentInstant();
var system = await _systems.GetByHid(hid);
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
using (var conn = await _conn.Obtain())
@ -96,13 +92,13 @@ namespace PluralKit.API.Controllers
[HttpGet("{hid}/fronters")]
public async Task<ActionResult<FrontersReturn>> GetFronters(string hid)
{
var system = await _systems.GetByHid(hid);
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
var sw = await _switches.GetLatestSwitch(system);
var sw = await _data.GetLatestSwitch(system);
if (sw == null) return NotFound("System has no registered switches.");
var members = await _switches.GetSwitchMembers(sw);
var members = await _data.GetSwitchMembers(sw);
return Ok(new FrontersReturn
{
Timestamp = sw.Timestamp,
@ -130,7 +126,7 @@ namespace PluralKit.API.Controllers
system.AvatarUrl = newSystem.AvatarUrl;
system.UiTz = newSystem.UiTz ?? "UTC";
await _systems.Save(system);
await _data.SaveSystem(system);
return Ok(system);
}
@ -142,10 +138,10 @@ namespace PluralKit.API.Controllers
return BadRequest("Duplicate members in member list.");
// We get the current switch, if it exists
var latestSwitch = await _switches.GetLatestSwitch(_auth.CurrentSystem);
var latestSwitch = await _data.GetLatestSwitch(_auth.CurrentSystem);
if (latestSwitch != null)
{
var latestSwitchMembers = await _switches.GetSwitchMembers(latestSwitch);
var latestSwitchMembers = await _data.GetSwitchMembers(latestSwitch);
// Bail if this switch is identical to the latest one
if (latestSwitchMembers.Select(m => m.Hid).SequenceEqual(param.Members))
@ -174,7 +170,7 @@ namespace PluralKit.API.Controllers
}
// Finally, log the switch (yay!)
await _switches.RegisterSwitch(_auth.CurrentSystem, membersInOrder);
await _data.AddSwitch(_auth.CurrentSystem, membersInOrder);
return NoContent();
}
}

@ -25,11 +25,8 @@ namespace PluralKit.API
.AddJsonOptions(opts => { opts.SerializerSettings.BuildSerializerSettings(); });
services
.AddTransient<SystemStore>()
.AddTransient<MemberStore>()
.AddTransient<SwitchStore>()
.AddTransient<MessageStore>()
.AddTransient<IDataStore, PostgresDataStore>()
.AddSingleton(svc => InitUtils.InitMetrics(svc.GetRequiredService<CoreConfig>(), "API"))
.AddScoped<TokenAuthService>()

@ -8,11 +8,11 @@ namespace PluralKit.API
{
public PKSystem CurrentSystem { get; set; }
private SystemStore _systems;
private IDataStore _data;
public TokenAuthService(SystemStore systems)
public TokenAuthService(IDataStore data)
{
_systems = systems;
_data = data;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
@ -20,7 +20,7 @@ namespace PluralKit.API
var token = context.Request.Headers["Authorization"].FirstOrDefault();
if (token != null)
{
CurrentSystem = await _systems.GetByToken(token);
CurrentSystem = await _data.GetSystemByToken(token);
}
await next.Invoke(context);

@ -110,10 +110,7 @@ namespace PluralKit.Bot
.AddTransient<ProxyCacheService>()
.AddSingleton<WebhookCacheService>()
.AddTransient<SystemStore>()
.AddTransient<MemberStore>()
.AddTransient<MessageStore>()
.AddTransient<SwitchStore>()
.AddTransient<IDataStore, PostgresDataStore>()
.AddSingleton(svc => InitUtils.InitMetrics(svc.GetRequiredService<CoreConfig>()))
.AddSingleton<PeriodicStatCollector>()

@ -17,8 +17,7 @@ namespace PluralKit.Bot.CommandSystem
private readonly SocketUserMessage _message;
private readonly Parameters _parameters;
private readonly SystemStore _systems;
private readonly MemberStore _members;
private readonly IDataStore _data;
private readonly PKSystem _senderSystem;
private Command _currentCommand;
@ -28,8 +27,7 @@ namespace PluralKit.Bot.CommandSystem
{
_client = provider.GetRequiredService<IDiscordClient>() as DiscordShardedClient;
_message = message;
_systems = provider.GetRequiredService<SystemStore>();
_members = provider.GetRequiredService<MemberStore>();
_data = provider.GetRequiredService<IDataStore>();
_senderSystem = senderSystem;
_provider = provider;
_parameters = new Parameters(message.Content.Substring(commandParseOffset));
@ -86,7 +84,7 @@ namespace PluralKit.Bot.CommandSystem
{
await Reply($"{Emojis.Error} {e.Message}");
}
catch (TimeoutException e)
catch (TimeoutException)
{
// Got a complaint the old error was a bit too patronizing. Hopefully this is better?
await Reply($"{Emojis.Error} Operation timed out, sorry. Try again, perhaps?");
@ -121,10 +119,10 @@ namespace PluralKit.Bot.CommandSystem
// Direct IDs and mentions are both handled by the below method:
if (input.TryParseMention(out var id))
return await _systems.GetByAccount(id);
return await _data.GetSystemByAccount(id);
// Finally, try HID parsing
var system = await _systems.GetByHid(input);
var system = await _data.GetSystemByHid(input);
return system;
}
@ -138,11 +136,11 @@ namespace PluralKit.Bot.CommandSystem
// - A textual name of a member *in your own system*
// First, try member HID parsing:
if (await _members.GetByHid(input) is PKMember memberByHid)
if (await _data.GetMemberByHid(input) is PKMember memberByHid)
return memberByHid;
// Then, if we have a system, try finding by member name in system
if (_senderSystem != null && await _members.GetByName(_senderSystem, input) is PKMember memberByName)
if (_senderSystem != null && await _data.GetMemberByName(_senderSystem, input) is PKMember memberByName)
return memberByName;
// We didn't find anything, so we return null.

@ -7,10 +7,10 @@ namespace PluralKit.Bot.Commands
{
public class APICommands
{
private SystemStore _systems;
public APICommands(SystemStore systems)
private IDataStore _data;
public APICommands(IDataStore data)
{
_systems = systems;
_data = data;
}
public async Task GetToken(Context ctx)
@ -34,7 +34,7 @@ namespace PluralKit.Bot.Commands
private async Task<string> MakeAndSetNewToken(PKSystem system)
{
system.Token = PluralKit.Utils.GenerateToken();
await _systems.Save(system);
await _data.SaveSystem(system);
return system.Token;
}

@ -8,11 +8,11 @@ namespace PluralKit.Bot.Commands
{
public class LinkCommands
{
private SystemStore _systems;
private IDataStore _data;
public LinkCommands(SystemStore systems)
public LinkCommands(IDataStore data)
{
_systems = systems;
_data = data;
}
public async Task LinkSystem(Context ctx)
@ -20,15 +20,15 @@ namespace PluralKit.Bot.Commands
ctx.CheckSystem();
var account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
var accountIds = await _systems.GetLinkedAccountIds(ctx.System);
var accountIds = await _data.GetSystemAccounts(ctx.System);
if (accountIds.Contains(account.Id)) throw Errors.AccountAlreadyLinked;
var existingAccount = await _systems.GetByAccount(account.Id);
var existingAccount = await _data.GetSystemByAccount(account.Id);
if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount);
var msg = await ctx.Reply($"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.");
if (!await ctx.PromptYesNo(msg, user: account)) throw Errors.MemberLinkCancelled;
await _systems.Link(ctx.System, account.Id);
await _data.AddAccount(ctx.System, account.Id);
await ctx.Reply($"{Emojis.Success} Account linked to system.");
}
@ -42,7 +42,7 @@ namespace PluralKit.Bot.Commands
else
account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
var accountIds = (await _systems.GetLinkedAccountIds(ctx.System)).ToList();
var accountIds = (await _data.GetSystemAccounts(ctx.System)).ToList();
if (!accountIds.Contains(account.Id)) throw Errors.AccountNotLinked;
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount;
@ -50,7 +50,7 @@ namespace PluralKit.Bot.Commands
$"Are you sure you want to unlink {account.Mention} from your system?");
if (!await ctx.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled;
await _systems.Unlink(ctx.System, account.Id);
await _data.RemoveAccount(ctx.System, account.Id);
await ctx.Reply($"{Emojis.Success} Account unlinked.");
}
}

@ -11,16 +11,14 @@ namespace PluralKit.Bot.Commands
{
public class MemberCommands
{
private SystemStore _systems;
private MemberStore _members;
private IDataStore _data;
private EmbedService _embeds;
private ProxyCacheService _proxyCache;
public MemberCommands(SystemStore systems, MemberStore members, EmbedService embeds, ProxyCacheService proxyCache)
public MemberCommands(IDataStore data, EmbedService embeds, ProxyCacheService proxyCache)
{
_systems = systems;
_members = members;
_data = data;
_embeds = embeds;
_proxyCache = proxyCache;
}
@ -39,19 +37,19 @@ namespace PluralKit.Bot.Commands
}
// Warn if there's already a member by this name
var existingMember = await _members.GetByName(ctx.System, memberName);
var existingMember = await _data.GetMemberByName(ctx.System, memberName);
if (existingMember != null) {
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.SanitizeMentions()}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?");
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
}
// Enforce per-system member limit
var memberCount = await _members.MemberCount(ctx.System);
var memberCount = await _data.GetSystemMemberCount(ctx.System);
if (memberCount >= Limits.MaxMemberCount)
throw Errors.MemberLimitReachedError;
// Create the member
var member = await _members.Create(ctx.System, memberName);
var member = await _data.CreateMember(ctx.System, memberName);
memberCount++;
// Send confirmation and space hint
@ -83,7 +81,7 @@ namespace PluralKit.Bot.Commands
}
// Warn if there's already a member by this name
var existingMember = await _members.GetByName(ctx.System, newName);
var existingMember = await _data.GetMemberByName(ctx.System, newName);
if (existingMember != null) {
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.SanitizeMentions()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
@ -91,7 +89,7 @@ namespace PluralKit.Bot.Commands
// Rename the member
target.Name = newName;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member renamed.");
if (newName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it.");
@ -108,7 +106,7 @@ namespace PluralKit.Bot.Commands
if (description.IsLongerThan(Limits.MaxDescriptionLength)) throw Errors.DescriptionTooLongError(description.Length);
target.Description = description;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member description {(description == null ? "cleared" : "changed")}.");
}
@ -121,7 +119,7 @@ namespace PluralKit.Bot.Commands
if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) throw Errors.MemberPronounsTooLongError(pronouns.Length);
target.Pronouns = pronouns;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}.");
}
@ -139,7 +137,7 @@ namespace PluralKit.Bot.Commands
}
target.Color = color;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}.");
}
@ -158,7 +156,7 @@ namespace PluralKit.Bot.Commands
}
target.Birthday = date;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {target.BirthdayString}")}.");
}
@ -175,7 +173,7 @@ namespace PluralKit.Bot.Commands
// Just reset and send OK message
target.Prefix = null;
target.Suffix = null;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member proxy tags cleared.");
return;
}
@ -188,7 +186,7 @@ namespace PluralKit.Bot.Commands
// If the prefix/suffix is empty, use "null" instead (for DB)
target.Prefix = prefixAndSuffix[0].Length > 0 ? prefixAndSuffix[0] : null;
target.Suffix = prefixAndSuffix[1].Length > 0 ? prefixAndSuffix[1] : null;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member proxy tags changed to `{target.ProxyString.SanitizeMentions()}`. Try proxying now!");
await _proxyCache.InvalidateResultsForSystem(ctx.System);
@ -201,7 +199,7 @@ namespace PluralKit.Bot.Commands
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.Name.SanitizeMentions()}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
await _members.Delete(target);
await _data.DeleteMember(target);
await ctx.Reply($"{Emojis.Success} Member deleted.");
await _proxyCache.InvalidateResultsForSystem(ctx.System);
@ -217,7 +215,7 @@ namespace PluralKit.Bot.Commands
if (user.AvatarId == null) throw Errors.UserHasNoAvatar;
target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256);
await _members.Save(target);
await _data.SaveMember(target);
var embed = new EmbedBuilder().WithImageUrl(target.AvatarUrl).Build();
await ctx.Reply(
@ -228,7 +226,7 @@ namespace PluralKit.Bot.Commands
{
await Utils.VerifyAvatarOrThrow(url);
target.AvatarUrl = url;
await _members.Save(target);
await _data.SaveMember(target);
var embed = new EmbedBuilder().WithImageUrl(url).Build();
await ctx.Reply($"{Emojis.Success} Member avatar changed.", embed: embed);
@ -237,14 +235,14 @@ namespace PluralKit.Bot.Commands
{
await Utils.VerifyAvatarOrThrow(attachment.Url);
target.AvatarUrl = attachment.Url;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working.");
}
else
{
target.AvatarUrl = null;
await _members.Save(target);
await _data.SaveMember(target);
await ctx.Reply($"{Emojis.Success} Member avatar cleared.");
}
@ -262,7 +260,7 @@ namespace PluralKit.Bot.Commands
throw Errors.DisplayNameTooLong(newDisplayName, ctx.System.MaxMemberNameLength);
target.DisplayName = newDisplayName;
await _members.Save(target);
await _data.SaveMember(target);
var successStr = $"{Emojis.Success} ";
if (newDisplayName != null)
@ -288,7 +286,7 @@ namespace PluralKit.Bot.Commands
public async Task ViewMember(Context ctx, PKMember target)
{
var system = await _systems.GetById(target.System);
var system = await _data.GetSystemById(target.System);
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target));
}
}

@ -9,14 +9,14 @@ namespace PluralKit.Bot.Commands
public class ModCommands
{
private LogChannelService _logChannels;
private MessageStore _messages;
private IDataStore _data;
private EmbedService _embeds;
public ModCommands(LogChannelService logChannels, MessageStore messages, EmbedService embeds)
public ModCommands(LogChannelService logChannels, IDataStore data, EmbedService embeds)
{
_logChannels = logChannels;
_messages = messages;
_data = data;
_embeds = embeds;
}
@ -47,7 +47,7 @@ namespace PluralKit.Bot.Commands
messageId = ulong.Parse(match.Groups[1].Value);
else throw new PKSyntaxError($"Could not parse `{word}` as a message ID or link.");
var message = await _messages.Get(messageId);
var message = await _data.GetMessage(messageId);
if (message == null) throw Errors.MessageNotFound(messageId);
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message));

@ -12,11 +12,11 @@ namespace PluralKit.Bot.Commands
{
public class SwitchCommands
{
private SwitchStore _switches;
private IDataStore _data;
public SwitchCommands(SwitchStore switches)
public SwitchCommands(IDataStore data)
{
_switches = switches;
_data = data;
}
public async Task Switch(Context ctx)
@ -55,16 +55,16 @@ namespace PluralKit.Bot.Commands
if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers;
// Find the last switch and its members if applicable
var lastSwitch = await _switches.GetLatestSwitch(ctx.System);
var lastSwitch = await _data.GetLatestSwitch(ctx.System);
if (lastSwitch != null)
{
var lastSwitchMembers = await _switches.GetSwitchMembers(lastSwitch);
var lastSwitchMembers = await _data.GetSwitchMembers(lastSwitch);
// Make sure the requested switch isn't identical to the last one
if (lastSwitchMembers.Select(m => m.Id).SequenceEqual(members.Select(m => m.Id)))
throw Errors.SameSwitch(members);
}
await _switches.RegisterSwitch(ctx.System, members);
await _data.AddSwitch(ctx.System, members);
if (members.Count == 0)
await ctx.Reply($"{Emojis.Success} Switch-out registered.");
@ -86,7 +86,7 @@ namespace PluralKit.Bot.Commands
if (time.ToInstant() > SystemClock.Instance.GetCurrentInstant()) throw Errors.SwitchTimeInFuture;
// Fetch the last two switches for the system to do bounds checking on
var lastTwoSwitches = (await _switches.GetSwitches(ctx.System, 2)).ToArray();
var lastTwoSwitches = (await _data.GetSwitches(ctx.System, 2)).ToArray();
// If we don't have a switch to move, don't bother
if (lastTwoSwitches.Length == 0) throw Errors.NoRegisteredSwitches;
@ -100,7 +100,7 @@ namespace PluralKit.Bot.Commands
// Now we can actually do the move, yay!
// But, we do a prompt to confirm.
var lastSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[0]);
var lastSwitchMembers = await _data.GetSwitchMembers(lastTwoSwitches[0]);
var lastSwitchMemberStr = string.Join(", ", lastSwitchMembers.Select(m => m.Name));
var lastSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(ctx.System.Zone));
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
@ -112,7 +112,7 @@ namespace PluralKit.Bot.Commands
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchMoveCancelled;
// aaaand *now* we do the move
await _switches.MoveSwitch(lastTwoSwitches[0], time.ToInstant());
await _data.MoveSwitch(lastTwoSwitches[0], time.ToInstant());
await ctx.Reply($"{Emojis.Success} Switch moved.");
}
@ -121,10 +121,10 @@ namespace PluralKit.Bot.Commands
ctx.CheckSystem();
// Fetch the last two switches for the system to do bounds checking on
var lastTwoSwitches = (await _switches.GetSwitches(ctx.System, 2)).ToArray();
var lastTwoSwitches = (await _data.GetSwitches(ctx.System, 2)).ToArray();
if (lastTwoSwitches.Length == 0) throw Errors.NoRegisteredSwitches;
var lastSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[0]);
var lastSwitchMembers = await _data.GetSwitchMembers(lastTwoSwitches[0]);
var lastSwitchMemberStr = string.Join(", ", lastSwitchMembers.Select(m => m.Name));
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
@ -136,7 +136,7 @@ namespace PluralKit.Bot.Commands
}
else
{
var secondSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[1]);
var secondSwitchMembers = await _data.GetSwitchMembers(lastTwoSwitches[1]);
var secondSwitchMemberStr = string.Join(", ", secondSwitchMembers.Select(m => m.Name));
var secondSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[1].Timestamp);
msg = await ctx.Reply(
@ -144,7 +144,7 @@ namespace PluralKit.Bot.Commands
}
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchDeleteCancelled;
await _switches.DeleteSwitch(lastTwoSwitches[0]);
await _data.DeleteSwitch(lastTwoSwitches[0]);
await ctx.Reply($"{Emojis.Success} Switch deleted.");
}

@ -14,21 +14,16 @@ namespace PluralKit.Bot.Commands
{
public class SystemCommands
{
private SystemStore _systems;
private MemberStore _members;
private SwitchStore _switches;
private IDataStore _data;
private EmbedService _embeds;
private ProxyCacheService _proxyCache;
public SystemCommands(SystemStore systems, MemberStore members, SwitchStore switches, EmbedService embeds, ProxyCacheService proxyCache)
public SystemCommands(EmbedService embeds, ProxyCacheService proxyCache, IDataStore data)
{
_systems = systems;
_members = members;
_switches = switches;
_embeds = embeds;
_proxyCache = proxyCache;
_data = data;
}
public async Task Query(Context ctx, PKSystem system) {
@ -41,8 +36,8 @@ namespace PluralKit.Bot.Commands
{
ctx.CheckNoSystem();
var system = await _systems.Create(ctx.RemainderOrNull());
await _systems.Link(system, ctx.Author.Id);
var system = await _data.CreateSystem(ctx.RemainderOrNull());
await _data.AddAccount(system, ctx.Author.Id);
await ctx.Reply($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
}
@ -54,7 +49,7 @@ namespace PluralKit.Bot.Commands
if (newSystemName != null && newSystemName.Length > Limits.MaxSystemNameLength) throw Errors.SystemNameTooLongError(newSystemName.Length);
ctx.System.Name = newSystemName;
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
await ctx.Reply($"{Emojis.Success} System name {(newSystemName != null ? "changed" : "cleared")}.");
}
@ -65,7 +60,7 @@ namespace PluralKit.Bot.Commands
if (newDescription != null && newDescription.Length > Limits.MaxDescriptionLength) throw Errors.DescriptionTooLongError(newDescription.Length);
ctx.System.Description = newDescription;
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
await ctx.Reply($"{Emojis.Success} System description {(newDescription != null ? "changed" : "cleared")}.");
}
@ -80,17 +75,18 @@ namespace PluralKit.Bot.Commands
{
if (newTag.Length > Limits.MaxSystemTagLength) throw Errors.SystemNameTooLongError(newTag.Length);
// Check unproxyable messages *after* changing the tag (so it's seen in the method) but *before* we save to DB (so we can cancel)
var unproxyableMembers = await _members.GetUnproxyableMembers(ctx.System);
if (unproxyableMembers.Count > 0)
{
var msg = await ctx.Reply(
$"{Emojis.Warn} Changing your system tag to '{newTag.SanitizeMentions()}' will result in the following members being unproxyable, since the tag would bring their name over {Limits.MaxProxyNameLength} characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name.SanitizeMentions()))}**\nDo you want to continue anyway?");
if (!await ctx.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
}
// TODO: The proxy name limit is long enough now that this probably doesn't matter much.
// // Check unproxyable messages *after* changing the tag (so it's seen in the method) but *before* we save to DB (so we can cancel)
// var unproxyableMembers = await _data.GetUnproxyableMembers(ctx.System);
// if (unproxyableMembers.Count > 0)
// {
// var msg = await ctx.Reply(
// $"{Emojis.Warn} Changing your system tag to '{newTag.SanitizeMentions()}' will result in the following members being unproxyable, since the tag would bring their name over {Limits.MaxProxyNameLength} characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name.SanitizeMentions()))}**\nDo you want to continue anyway?");
// if (!await ctx.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
// }
}
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
await ctx.Reply($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}.");
await _proxyCache.InvalidateResultsForSystem(ctx.System);
@ -105,7 +101,7 @@ namespace PluralKit.Bot.Commands
{
if (member.AvatarId == null) throw Errors.UserHasNoAvatar;
ctx.System.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
var embed = new EmbedBuilder().WithImageUrl(ctx.System.AvatarUrl).Build();
await ctx.Reply(
@ -117,7 +113,7 @@ namespace PluralKit.Bot.Commands
if (url != null) await ctx.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
ctx.System.AvatarUrl = url;
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null;
await ctx.Reply($"{Emojis.Success} System avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
@ -133,7 +129,7 @@ namespace PluralKit.Bot.Commands
var reply = await ctx.AwaitMessage(ctx.Channel, ctx.Author, timeout: TimeSpan.FromMinutes(1));
if (reply.Content != ctx.System.Hid) throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{ctx.System.Hid}`) *verbatim*.");
await _systems.Delete(ctx.System);
await _data.DeleteSystem(ctx.System);
await ctx.Reply($"{Emojis.Success} System deleted.");
await _proxyCache.InvalidateResultsForSystem(ctx.System);
@ -142,7 +138,7 @@ namespace PluralKit.Bot.Commands
public async Task MemberShortList(Context ctx, PKSystem system) {
if (system == null) throw Errors.NoSystemError;
var members = await _members.GetBySystem(system);
var members = await _data.GetSystemMembers(system);
var embedTitle = system.Name != null ? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
await ctx.Paginate<PKMember>(
members.OrderBy(m => m.Name.ToLower()).ToList(),
@ -158,7 +154,7 @@ namespace PluralKit.Bot.Commands
public async Task MemberLongList(Context ctx, PKSystem system) {
if (system == null) throw Errors.NoSystemError;
var members = await _members.GetBySystem(system);
var members = await _data.GetSystemMembers(system);
var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
await ctx.Paginate<PKMember>(
members.OrderBy(m => m.Name.ToLower()).ToList(),
@ -181,7 +177,7 @@ namespace PluralKit.Bot.Commands
{
if (system == null) throw Errors.NoSystemError;
var sw = await _switches.GetLatestSwitch(system);
var sw = await _data.GetLatestSwitch(system);
if (sw == null) throw Errors.NoRegisteredSwitches;
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone));
@ -191,7 +187,7 @@ namespace PluralKit.Bot.Commands
{
if (system == null) throw Errors.NoSystemError;
var sws = (await _switches.GetSwitches(system, 10)).ToList();
var sws = (await _data.GetSwitches(system, 10)).ToList();
if (sws.Count == 0) throw Errors.NoRegisteredSwitches;
await ctx.Reply(embed: await _embeds.CreateFrontHistoryEmbed(sws, system.Zone));
@ -209,7 +205,7 @@ namespace PluralKit.Bot.Commands
if (rangeStart == null) throw Errors.InvalidDateTime(durationStr);
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
var frontpercent = await _switches.GetPerMemberSwitchDuration(system, rangeStart.Value.ToInstant(), now);
var frontpercent = await _data.GetFrontBreakdown(system, rangeStart.Value.ToInstant(), now);
await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone));
}
@ -221,7 +217,7 @@ namespace PluralKit.Bot.Commands
if (zoneStr == null)
{
ctx.System.UiTz = "UTC";
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
await ctx.Reply($"{Emojis.Success} System time zone cleared.");
return;
}
@ -234,7 +230,7 @@ namespace PluralKit.Bot.Commands
$"This will change the system time zone to {zone.Id}. The current time is {Formats.ZonedDateTimeFormat.Format(currentTime)}. Is this correct?");
if (!await ctx.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled;
ctx.System.UiTz = zone.Id;
await _systems.Save(ctx.System);
await _data.SaveSystem(ctx.System);
await ctx.Reply($"System time zone changed to {zone.Id}.");
}

@ -9,39 +9,34 @@ using Humanizer;
using NodaTime;
namespace PluralKit.Bot {
public class EmbedService {
private SystemStore _systems;
private MemberStore _members;
private SwitchStore _switches;
private MessageStore _messages;
public class EmbedService
{
private IDataStore _data;
private IDiscordClient _client;
public EmbedService(SystemStore systems, MemberStore members, IDiscordClient client, SwitchStore switches, MessageStore messages)
public EmbedService(IDiscordClient client, IDataStore data)
{
_systems = systems;
_members = members;
_client = client;
_switches = switches;
_messages = messages;
_data = data;
}
public async Task<Embed> CreateSystemEmbed(PKSystem system) {
var accounts = await _systems.GetLinkedAccountIds(system);
var accounts = await _data.GetSystemAccounts(system);
// Fetch/render info for all accounts simultaneously
var users = await Task.WhenAll(accounts.Select(async uid => (await _client.GetUserAsync(uid))?.NameAndMention() ?? $"(deleted account {uid})"));
var memberCount = await _members.MemberCount(system);
var memberCount = await _data.GetSystemMemberCount(system);
var eb = new EmbedBuilder()
.WithColor(Color.Blue)
.WithTitle(system.Name ?? null)
.WithThumbnailUrl(system.AvatarUrl ?? null)
.WithFooter($"System ID: {system.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}");
var latestSwitch = await _switches.GetLatestSwitch(system);
var latestSwitch = await _data.GetLatestSwitch(system);
if (latestSwitch != null)
{
var switchMembers = (await _switches.GetSwitchMembers(latestSwitch)).ToList();
var switchMembers = (await _data.GetSwitchMembers(latestSwitch)).ToList();
if (switchMembers.Count > 0)
eb.AddField("Fronter".ToQuantity(switchMembers.Count(), ShowQuantityAs.None),
string.Join(", ", switchMembers.Select(m => m.Name)));
@ -85,7 +80,7 @@ namespace PluralKit.Bot {
color = Color.Default;
}
var messageCount = await _members.MessageCount(member);
var messageCount = await _data.GetMemberMessageCount(member);
var eb = new EmbedBuilder()
// TODO: add URL of website when that's up
@ -108,7 +103,7 @@ namespace PluralKit.Bot {
public async Task<Embed> CreateFronterEmbed(PKSwitch sw, DateTimeZone zone)
{
var members = (await _switches.GetSwitchMembers(sw)).ToList();
var members = (await _data.GetSwitchMembers(sw)).ToList();
var timeSinceSwitch = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp;
return new EmbedBuilder()
.WithColor(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? Color.Blue)
@ -125,7 +120,7 @@ namespace PluralKit.Bot {
foreach (var sw in sws)
{
// Fetch member list and format
var members = (await _switches.GetSwitchMembers(sw)).ToList();
var members = (await _data.GetSwitchMembers(sw)).ToList();
var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.Name)) : "no fronter";
var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp;
@ -151,7 +146,7 @@ namespace PluralKit.Bot {
.Build();
}
public async Task<Embed> CreateMessageInfoEmbed(MessageStore.StoredMessage msg)
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg)
{
var channel = await _client.GetChannelAsync(msg.Message.Channel) as ITextChannel;
var serverMsg = channel != null ? await channel.GetMessageAsync(msg.Message.Mid) : null;
@ -193,20 +188,20 @@ namespace PluralKit.Bot {
return eb.Build();
}
public Task<Embed> CreateFrontPercentEmbed(SwitchStore.PerMemberSwitchDuration frontpercent, DateTimeZone tz)
public Task<Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz)
{
var actualPeriod = frontpercent.RangeEnd - frontpercent.RangeStart;
var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart;
var eb = new EmbedBuilder()
.WithColor(Color.Blue)
.WithFooter($"Since {Formats.ZonedDateTimeFormat.Format(frontpercent.RangeStart.InZone(tz))} ({Formats.DurationFormat.Format(actualPeriod)} ago)");
.WithFooter($"Since {Formats.ZonedDateTimeFormat.Format(breakdown.RangeStart.InZone(tz))} ({Formats.DurationFormat.Format(actualPeriod)} ago)");
var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others"
// We convert to a list of pairs so we can add the no-fronter value
// Dictionary doesn't allow for null keys so we instead have a pair with a null key ;)
var pairs = frontpercent.MemberSwitchDurations.ToList();
if (frontpercent.NoFronterDuration != Duration.Zero)
pairs.Add(new KeyValuePair<PKMember, Duration>(null, frontpercent.NoFronterDuration));
var pairs = breakdown.MemberSwitchDurations.ToList();
if (breakdown.NoFronterDuration != Duration.Zero)
pairs.Add(new KeyValuePair<PKMember, Duration>(null, breakdown.NoFronterDuration));
var membersOrdered = pairs.OrderByDescending(pair => pair.Value).Take(maxEntriesToDisplay).ToList();
foreach (var pair in membersOrdered)

@ -17,10 +17,7 @@ namespace PluralKit.Bot
private DiscordShardedClient _client;
private IMetrics _metrics;
private SystemStore _systems;
private MemberStore _members;
private SwitchStore _switches;
private MessageStore _messages;
private IDataStore _data;
private WebhookCacheService _webhookCache;
@ -28,16 +25,13 @@ namespace PluralKit.Bot
private ILogger _logger;
public PeriodicStatCollector(IDiscordClient client, IMetrics metrics, SystemStore systems, MemberStore members, SwitchStore switches, MessageStore messages, ILogger logger, WebhookCacheService webhookCache, DbConnectionCountHolder countHolder)
public PeriodicStatCollector(IDiscordClient client, IMetrics metrics, ILogger logger, WebhookCacheService webhookCache, DbConnectionCountHolder countHolder, IDataStore data)
{
_client = (DiscordShardedClient) client;
_metrics = metrics;
_systems = systems;
_members = members;
_switches = switches;
_messages = messages;
_webhookCache = webhookCache;
_countHolder = countHolder;
_data = data;
_logger = logger.ForContext<PeriodicStatCollector>();
}
@ -65,10 +59,10 @@ namespace PluralKit.Bot
_metrics.Measure.Gauge.SetValue(BotMetrics.MembersOnline, usersOnline.Count);
// Aggregate DB stats
_metrics.Measure.Gauge.SetValue(CoreMetrics.SystemCount, await _systems.Count());
_metrics.Measure.Gauge.SetValue(CoreMetrics.MemberCount, await _members.Count());
_metrics.Measure.Gauge.SetValue(CoreMetrics.SwitchCount, await _switches.Count());
_metrics.Measure.Gauge.SetValue(CoreMetrics.MessageCount, await _messages.Count());
_metrics.Measure.Gauge.SetValue(CoreMetrics.SystemCount, await _data.GetTotalSystems());
_metrics.Measure.Gauge.SetValue(CoreMetrics.MemberCount, await _data.GetTotalMembers());
_metrics.Measure.Gauge.SetValue(CoreMetrics.SwitchCount, await _data.GetTotalSwitches());
_metrics.Measure.Gauge.SetValue(CoreMetrics.MessageCount, await _data.GetTotalMessages());
// Process info
var process = Process.GetCurrentProcess();

@ -23,7 +23,7 @@ namespace PluralKit.Bot
class ProxyService: IDisposable {
private IDiscordClient _client;
private LogChannelService _logChannel;
private MessageStore _messageStorage;
private IDataStore _data;
private EmbedService _embeds;
private ILogger _logger;
private WebhookExecutorService _webhookExecutor;
@ -31,11 +31,11 @@ namespace PluralKit.Bot
private HttpClient _httpClient;
public ProxyService(IDiscordClient client, LogChannelService logChannel, MessageStore messageStorage, EmbedService embeds, ILogger logger, ProxyCacheService cache, WebhookExecutorService webhookExecutor)
public ProxyService(IDiscordClient client, LogChannelService logChannel, IDataStore data, EmbedService embeds, ILogger logger, ProxyCacheService cache, WebhookExecutorService webhookExecutor)
{
_client = client;
_logChannel = logChannel;
_messageStorage = messageStorage;
_data = data;
_embeds = embeds;
_cache = cache;
_webhookExecutor = webhookExecutor;
@ -115,7 +115,7 @@ namespace PluralKit.Bot
);
// Store the message in the database, and log it in the log channel (if applicable)
await _messageStorage.Store(message.Author.Id, hookMessageId, message.Channel.Id, message.Id, match.Member);
await _data.AddMessage(message.Author.Id, hookMessageId, message.Channel.Id, message.Id, match.Member);
await _logChannel.LogMessage(match.System, match.Member, hookMessageId, message.Id, message.Channel as IGuildChannel, message.Author, match.InnerText);
// Wait a second or so before deleting the original message
@ -184,7 +184,7 @@ namespace PluralKit.Bot
if (user == null) return;
// Find the message in the DB
var msg = await _messageStorage.Get(message.Id);
var msg = await _data.GetMessage(message.Id);
if (msg == null) return;
// DM them the message card
@ -199,7 +199,7 @@ namespace PluralKit.Bot
public async Task HandleMessageDeletionByReaction(Cacheable<IUserMessage, ulong> message, ulong userWhoReacted)
{
// Find the message in the database
var storedMessage = await _messageStorage.Get(message.Id);
var storedMessage = await _data.GetMessage(message.Id);
if (storedMessage == null) return; // (if we can't, that's ok, no worries)
// Make sure it's the actual sender of that message deleting the message
@ -215,7 +215,7 @@ namespace PluralKit.Bot
}
// Finally, delete it from our database.
await _messageStorage.Delete(message.Id);
await _data.DeleteMessage(message.Id);
}
public async Task HandleMessageDeletedAsync(Cacheable<IMessage, ulong> message, ISocketMessageChannel channel)
@ -224,13 +224,13 @@ namespace PluralKit.Bot
// Non-webhook messages will never be stored anyway.
// If we're not sure (eg. message outside of cache), delete just to be sure.
if (message.HasValue && !message.Value.Author.IsWebhook) return;
await _messageStorage.Delete(message.Id);
await _data.DeleteMessage(message.Id);
}
public async Task HandleMessageBulkDeleteAsync(IReadOnlyCollection<Cacheable<IMessage, ulong>> messages, IMessageChannel channel)
{
_logger.Information("Bulk deleting {Count} messages in channel {Channel}", messages.Count, channel.Id);
await _messageStorage.BulkDelete(messages.Select(m => m.Id).ToList());
await _data.DeleteMessagesBulk(messages.Select(m => m.Id).ToList());
}
public void Dispose()

@ -12,16 +12,12 @@ namespace PluralKit.Bot
{
public class DataFileService
{
private SystemStore _systems;
private MemberStore _members;
private SwitchStore _switches;
private IDataStore _data;
private ILogger _logger;
public DataFileService(SystemStore systems, MemberStore members, SwitchStore switches, ILogger logger)
public DataFileService(ILogger logger, IDataStore data)
{
_systems = systems;
_members = members;
_switches = switches;
_data = data;
_logger = logger.ForContext<DataFileService>();
}
@ -29,8 +25,8 @@ namespace PluralKit.Bot
{
// Export members
var members = new List<DataFileMember>();
var pkMembers = await _members.GetBySystem(system); // Read all members in the system
var messageCounts = await _members.MessageCountsPerMember(system); // Count messages proxied by all members in the system
var pkMembers = await _data.GetSystemMembers(system); // Read all members in the system
var messageCounts = await _data.GetMemberMessageCountBulk(system); // Count messages proxied by all members in the system
members.AddRange(pkMembers.Select(m => new DataFileMember
{
Id = m.Hid,
@ -49,7 +45,7 @@ namespace PluralKit.Bot
// Export switches
var switches = new List<DataFileSwitch>();
var switchList = await _switches.GetTruncatedSwitchList(system, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant());
var switchList = await _data.GetPeriodFronters(system, Instant.FromDateTimeUtc(DateTime.MinValue.ToUniversalTime()), SystemClock.Instance.GetCurrentInstant());
switches.AddRange(switchList.Select(x => new DataFileSwitch
{
Timestamp = Formats.TimestampExportFormat.Format(x.TimespanStart),
@ -67,7 +63,7 @@ namespace PluralKit.Bot
Members = members,
Switches = switches,
Created = Formats.TimestampExportFormat.Format(system.Created),
LinkedAccounts = (await _systems.GetLinkedAccountIds(system)).ToList()
LinkedAccounts = (await _data.GetSystemAccounts(system)).ToList()
};
}
@ -85,7 +81,7 @@ namespace PluralKit.Bot
// If we don't already have a system to save to, create one
if (system == null)
system = await _systems.Create(data.Name);
system = await _data.CreateSystem(data.Name);
result.System = system;
// Apply system info
@ -94,13 +90,13 @@ namespace PluralKit.Bot
if (data.Tag != null) system.Tag = data.Tag;
if (data.AvatarUrl != null) system.AvatarUrl = data.AvatarUrl;
if (data.TimeZone != null) system.UiTz = data.TimeZone ?? "UTC";
await _systems.Save(system);
await _data.SaveSystem(system);
// Make sure to link the sender account, too
await _systems.Link(system, accountId);
await _data.AddAccount(system, accountId);
// Determine which members already exist and which ones need to be created
var existingMembers = await _members.GetBySystem(system);
var existingMembers = await _data.GetSystemMembers(system);
foreach (var d in data.Members)
{
// Try to look up the member with the given ID
@ -134,7 +130,7 @@ namespace PluralKit.Bot
// These consist of members from another PluralKit system or another framework (e.g. Tupperbox)
var membersToCreate = new Dictionary<string, string>();
unmappedMembers.ForEach(x => membersToCreate.Add(x.Id, x.Name));
var newMembers = await _members.CreateMultiple(system, membersToCreate);
var newMembers = await _data.CreateMembersBulk(system, membersToCreate);
foreach (var member in newMembers)
dataFileToMemberMapping.Add(member.Key, member.Value);
@ -164,23 +160,26 @@ namespace PluralKit.Bot
member.Birthday = birthdayParse.Success ? (LocalDate?)birthdayParse.Value : null;
}
await _members.Save(member);
await _data.SaveMember(member);
}
// Re-map the switch members in the likely case IDs have changed
var mappedSwitches = new List<Tuple<Instant, ICollection<PKMember>>>();
var mappedSwitches = new List<ImportedSwitch>();
foreach (var sw in data.Switches)
{
var timestamp = InstantPattern.ExtendedIso.Parse(sw.Timestamp).Value;
var swMembers = new List<PKMember>();
swMembers.AddRange(sw.Members.Select(x =>
dataFileToMemberMapping.FirstOrDefault(y => y.Key.Equals(x)).Value));
var mapped = new Tuple<Instant, ICollection<PKMember>>(timestamp, swMembers);
mappedSwitches.Add(mapped);
mappedSwitches.Add(new ImportedSwitch
{
Timestamp = timestamp,
Members = swMembers
});
}
// Import switches
if (mappedSwitches.Any())
await _switches.BulkImportSwitches(system, mappedSwitches);
await _data.AddSwitchesBulk(system, mappedSwitches);
_logger.Information("Imported system {System}", system.Hid);
return result;

@ -11,22 +11,324 @@ using PluralKit.Core;
using Serilog;
namespace PluralKit {
public class SystemStore {
public class FullMessage
{
public PKMessage Message;
public PKMember Member;
public PKSystem System;
}
public struct PKMessage
{
public ulong Mid;
public ulong Channel;
public ulong Sender;
public ulong? OriginalMid;
}
public struct ImportedSwitch
{
public Instant Timestamp;
public IReadOnlyCollection<PKMember> Members;
}
public struct SwitchListEntry
{
public ICollection<PKMember> Members;
public Instant TimespanStart;
public Instant TimespanEnd;
}
public struct MemberMessageCount
{
public PKMember Member;
public int MessageCount;
}
public struct FrontBreakdown
{
public Dictionary<PKMember, Duration> MemberSwitchDurations;
public Duration NoFronterDuration;
public Instant RangeStart;
public Instant RangeEnd;
}
public interface IDataStore
{
/// <summary>
/// Gets a system by its internal system ID.
/// </summary>
/// <returns>The <see cref="PKSystem"/> with the given internal ID, or null if no system was found.</returns>
Task<PKSystem> GetSystemById(int systemId);
/// <summary>
/// Gets a system by its user-facing human ID.
/// </summary>
/// <returns>The <see cref="PKSystem"/> with the given human ID, or null if no system was found.</returns>
Task<PKSystem> GetSystemByHid(string systemHid);
/// <summary>
/// Gets a system by one of its linked Discord account IDs. Multiple IDs can return the same system.
/// </summary>
/// <returns>The <see cref="PKSystem"/> with the given linked account, or null if no system was found.</returns>
Task<PKSystem> GetSystemByAccount(ulong linkedAccount);
/// <summary>
/// Gets a system by its API token.
/// </summary>
/// <returns>The <see cref="PKSystem"/> with the given API token, or null if no corresponding system was found.</returns>
Task<PKSystem> GetSystemByToken(string apiToken);
/// <summary>
/// Gets the Discord account IDs linked to a system.
/// </summary>
/// <returns>An enumerable of Discord account IDs linked to this system.</returns>
Task<IEnumerable<ulong>> GetSystemAccounts(PKSystem system);
/// <summary>
/// Gets the member count of a system.
/// </summary>
Task<int> GetSystemMemberCount(PKSystem system);
/// <summary>
/// Creates a system, auto-generating its corresponding IDs.
/// </summary>
/// <param name="systemName">An optional system name to set. If `null`, will not set a system name.</param>
/// <returns>The created system model.</returns>
Task<PKSystem> CreateSystem(string systemName);
// TODO: throw exception if account is present (when adding) or account isn't present (when removing)
/// <summary>
/// Links a Discord account to a system.
/// </summary>
/// <exception>Throws an exception (TODO: which?) if the given account is already linked to a system.</exception>
Task AddAccount(PKSystem system, ulong accountToAdd);
/// <summary>
/// Unlinks a Discord account from a system.
///
/// Will *not* throw if this results in an orphaned system - this is the caller's responsibility to ensure.
/// </summary>
/// <exception>Throws an exception (TODO: which?) if the given account is not linked to the given system.</exception>
Task RemoveAccount(PKSystem system, ulong accountToRemove);
/// <summary>
/// Saves the information within the given <see cref="PKSystem"/> struct to the data store.
/// </summary>
Task SaveSystem(PKSystem system);
/// <summary>
/// Deletes the given system from the database.
/// </summary>
/// <para>
/// This will also delete all the system's members, all system switches, and every message that has been proxied
/// by members in the system.
/// </para>
Task DeleteSystem(PKSystem system);
/// <summary>
/// Gets a system by its internal member ID.
/// </summary>
/// <returns>The <see cref="PKMember"/> with the given internal ID, or null if no member was found.</returns>
Task<PKMember> GetMemberById(int memberId);
/// <summary>
/// Gets a member by its user-facing human ID.
/// </summary>
/// <returns>The <see cref="PKMember"/> with the given human ID, or null if no member was found.</returns>
Task<PKMember> GetMemberByHid(string memberHid);
/// <summary>
/// Gets a member by its member name within one system.
/// </summary>
/// <para>
/// Member names are *usually* unique within a system (but not always), whereas member names
/// are almost certainly *not* unique globally - therefore only intra-system lookup is
/// allowed.
/// </para>
/// <returns>The <see cref="PKMember"/> with the given name, or null if no member was found.</returns>
Task<PKMember> GetMemberByName(PKSystem system, string name);
/// <summary>
/// Gets all members inside a given system.
/// </summary>
/// <returns>An enumerable of <see cref="PKMember"/> structs representing each member in the system, in no particular order.</returns>
Task<IEnumerable<PKMember>> GetSystemMembers(PKSystem system);
/// <summary>
/// Gets the amount of messages proxied by a given member.
/// </summary>
/// <returns>The message count of the given member.</returns>
Task<ulong> GetMemberMessageCount(PKMember member);
/// <summary>
/// Collects a breakdown of each member in a system's message count.
/// </summary>
/// <returns>An enumerable of members along with their message counts.</returns>
Task<IEnumerable<MemberMessageCount>> GetMemberMessageCountBulk(PKSystem system);
/// <summary>
/// Creates a member, auto-generating its corresponding IDs.
/// </summary>
/// <param name="system">The system in which to create the member.</param>
/// <param name="name">The name of the member to create.</param>
/// <returns>The created system model.</returns>
Task<PKMember> CreateMember(PKSystem system, string name);
/// <summary>
/// Creates multiple members, auto-generating each corresponding ID.
/// </summary>
/// <param name="system">The system to create the member in.</param>
/// <param name="memberNames">A dictionary containing a mapping from an arbitrary key to the member's name.</param>
/// <returns>A dictionary containing the resulting member structs, each mapped to the key given in the argument dictionary.</returns>
Task<Dictionary<string, PKMember>> CreateMembersBulk(PKSystem system, Dictionary<string, string> memberNames);
/// <summary>
/// Saves the information within the given <see cref="PKMember"/> struct to the data store.
/// </summary>
Task SaveMember(PKMember member);
/// <summary>
/// Deletes the given member from the database.
/// </summary>
/// <para>
/// This will remove this member from any switches it's involved in, as well as all the messages
/// proxied by this member.
/// </para>
Task DeleteMember(PKMember member);
/// <summary>
/// Gets a message and its information by its ID.
/// </summary>
/// <param name="id">The message ID to look up. This can be either the ID of the trigger message containing the proxy tags or the resulting proxied webhook message.</param>
/// <returns>An extended message object, containing not only the message data itself but the associated system and member structs.</returns>
Task<FullMessage> GetMessage(ulong id); // id is both original and trigger, also add return type struct
/// <summary>
/// Saves a posted message to the database.
/// </summary>
/// <param name="senderAccount">The ID of the account that sent the original trigger message.</param>
/// <param name="channelId">The ID of the channel the message was posted to.</param>
/// <param name="postedMessageId">The ID of the message posted by the webhook.</param>
/// <param name="triggerMessageId">The ID of the original trigger message containing the proxy tags.</param>
/// <param name="proxiedMember">The member (and by extension system) that was proxied.</param>
/// <returns></returns>
Task AddMessage(ulong senderAccount, ulong channelId, ulong postedMessageId, ulong triggerMessageId, PKMember proxiedMember);
/// <summary>
/// Deletes a message from the data store.
/// </summary>
/// <param name="postedMessageId">The ID of the webhook message to delete.</param>
Task DeleteMessage(ulong postedMessageId);
/// <summary>
/// Deletes messages from the data store in bulk.
/// </summary>
/// <param name="postedMessageIds">The IDs of the webhook messages to delete.</param>
Task DeleteMessagesBulk(IEnumerable<ulong> postedMessageIds);
/// <summary>
/// Gets switches from a system.
/// </summary>
/// <returns>An enumerable of the *count* latest switches in the system, in latest-first order. May contain fewer elements than requested.</returns>
Task<IEnumerable<PKSwitch>> GetSwitches(PKSystem system, int count);
/// <summary>
/// Gets the latest (temporally; closest to now) switch of a given system.
/// </summary>
Task<PKSwitch> GetLatestSwitch(PKSystem system);
/// <summary>
/// Gets the members a given switch consists of.
/// </summary>
Task<IEnumerable<PKMember>> GetSwitchMembers(PKSwitch sw);
/// <summary>
/// Gets a list of fronters over a given period of time.
/// </summary>
/// <para>
/// This list is returned as an enumerable of "switch members", each containing a timestamp
/// and a member ID. <seealso cref="GetMemberById"/>
///
/// Switches containing multiple members will be returned as multiple switch members each with the same
/// timestamp, and a change in timestamp should be interpreted as the start of a new switch.
/// </para>
/// <returns>An enumerable of the aforementioned "switch members".</returns>
Task<IEnumerable<SwitchListEntry>> GetPeriodFronters(PKSystem system, Instant periodStart, Instant periodEnd);
/// <summary>
/// Calculates a breakdown of a system's fronters over a given period, including how long each member has
/// been fronting, and how long *no* member has been fronting.
/// </summary>
/// <para>
/// Switches containing multiple members will count the full switch duration for all members, meaning
/// the total duration may add up to longer than the breakdown period.
/// </para>
/// <param name="system"></param>
/// <param name="periodStart"></param>
/// <param name="periodEnd"></param>
/// <returns></returns>
Task<FrontBreakdown> GetFrontBreakdown(PKSystem system, Instant periodStart, Instant periodEnd);
/// <summary>
/// Registers a switch with the given members in the given system.
/// </summary>
/// <exception>Throws an exception (TODO: which?) if any of the members are not in the given system.</exception>
Task AddSwitch(PKSystem system, IEnumerable<PKMember> switchMembers);
/// <summary>
/// Registers switches in bulk.
/// </summary>
/// <param name="switches">A list of switch structs, each containing a timestamp and a list of members.</param>
/// <exception>Throws an exception (TODO: which?) if any of the given members are not in the given system.</exception>
Task AddSwitchesBulk(PKSystem system, IEnumerable<ImportedSwitch> switches);
/// <summary>
/// Updates the timestamp of a given switch.
/// </summary>
Task MoveSwitch(PKSwitch sw, Instant time);
/// <summary>
/// Deletes a given switch from the data store.
/// </summary>
Task DeleteSwitch(PKSwitch sw);
/// <summary>
/// Gets the total amount of systems in the data store.
/// </summary>
Task<ulong> GetTotalSystems();
/// <summary>
/// Gets the total amount of members in the data store.
/// </summary>
Task<ulong> GetTotalMembers();
/// <summary>
/// Gets the total amount of switches in the data store.
/// </summary>
Task<ulong> GetTotalSwitches();
/// <summary>
/// Gets the total amount of messages in the data store.
/// </summary>
Task<ulong> GetTotalMessages();
}
public class PostgresDataStore: IDataStore {
private DbConnectionFactory _conn;
private ILogger _logger;
public SystemStore(DbConnectionFactory conn, ILogger logger)
public PostgresDataStore(DbConnectionFactory conn, ILogger logger)
{
this._conn = conn;
_logger = logger.ForContext<SystemStore>();
_conn = conn;
_logger = logger;
}
public async Task<PKSystem> Create(string systemName = null) {
public async Task<PKSystem> CreateSystem(string systemName = null) {
string hid;
do
{
hid = Utils.GenerateHid();
} while (await GetByHid(hid) != null);
} while (await GetSystemByHid(hid) != null);
PKSystem system;
using (var conn = await _conn.Obtain())
@ -36,7 +338,7 @@ namespace PluralKit {
return system;
}
public async Task Link(PKSystem system, ulong accountId) {
public async Task AddAccount(PKSystem system, ulong accountId) {
// We have "on conflict do nothing" since linking an account when it's already linked to the same system is idempotent
// This is used in import/export, although the pk;link command checks for this case beforehand
using (var conn = await _conn.Obtain())
@ -45,76 +347,65 @@ namespace PluralKit {
_logger.Information("Linked system {System} to account {Account}", system.Id, accountId);
}
public async Task Unlink(PKSystem system, ulong accountId) {
public async Task RemoveAccount(PKSystem system, ulong accountId) {
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("delete from accounts where uid = @Id and system = @SystemId", new { Id = accountId, SystemId = system.Id });
_logger.Information("Unlinked system {System} from account {Account}", system.Id, accountId);
}
public async Task<PKSystem> GetByAccount(ulong accountId) {
public async Task<PKSystem> GetSystemByAccount(ulong accountId) {
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKSystem>("select systems.* from systems, accounts where accounts.system = systems.id and accounts.uid = @Id", new { Id = accountId });
}
public async Task<PKSystem> GetByHid(string hid) {
public async Task<PKSystem> GetSystemByHid(string hid) {
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where systems.hid = @Hid", new { Hid = hid.ToLower() });
}
public async Task<PKSystem> GetByToken(string token) {
public async Task<PKSystem> GetSystemByToken(string token) {
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where token = @Token", new { Token = token });
}
public async Task<PKSystem> GetById(int id)
public async Task<PKSystem> GetSystemById(int id)
{
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where id = @Id", new { Id = id });
}
public async Task Save(PKSystem system) {
public async Task SaveSystem(PKSystem system) {
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("update systems set name = @Name, description = @Description, tag = @Tag, avatar_url = @AvatarUrl, token = @Token, ui_tz = @UiTz where id = @Id", system);
_logger.Information("Updated system {@System}", system);
}
public async Task Delete(PKSystem system) {
public async Task DeleteSystem(PKSystem system) {
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("delete from systems where id = @Id", system);
_logger.Information("Deleted system {System}", system.Id);
}
public async Task<IEnumerable<ulong>> GetLinkedAccountIds(PKSystem system)
public async Task<IEnumerable<ulong>> GetSystemAccounts(PKSystem system)
{
using (var conn = await _conn.Obtain())
return await conn.QueryAsync<ulong>("select uid from accounts where system = @Id", new { Id = system.Id });
}
public async Task<ulong> Count()
public async Task<ulong> GetTotalSystems()
{
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(id) from systems");
}
}
public class MemberStore {
private DbConnectionFactory _conn;
private ILogger _logger;
public MemberStore(DbConnectionFactory conn, ILogger logger)
{
this._conn = conn;
_logger = logger.ForContext<MemberStore>();
}
public async Task<PKMember> Create(PKSystem system, string name) {
public async Task<PKMember> CreateMember(PKSystem system, string name) {
string hid;
do
{
hid = Utils.GenerateHid();
} while (await GetByHid(hid) != null);
} while (await GetMemberByHid(hid) != null);
PKMember member;
using (var conn = await _conn.Obtain())
@ -128,7 +419,7 @@ namespace PluralKit {
return member;
}
public async Task<Dictionary<string,PKMember>> CreateMultiple(PKSystem system, Dictionary<string,string> names)
public async Task<Dictionary<string,PKMember>> CreateMembersBulk(PKSystem system, Dictionary<string,string> names)
{
using (var conn = await _conn.Obtain())
using (var tx = conn.BeginTransaction())
@ -159,60 +450,59 @@ namespace PluralKit {
}
}
public async Task<PKMember> GetByHid(string hid) {
public async Task<PKMember> GetMemberById(int id) {
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where id = @Id", new { Id = id });
}
public async Task<PKMember> GetMemberByHid(string hid) {
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where hid = @Hid", new { Hid = hid.ToLower() });
}
public async Task<PKMember> GetByName(PKSystem system, string name) {
public async Task<PKMember> GetMemberByName(PKSystem system, string name) {
// QueryFirst, since members can (in rare cases) share names
using (var conn = await _conn.Obtain())
return await conn.QueryFirstOrDefaultAsync<PKMember>("select * from members where lower(name) = lower(@Name) and system = @SystemID", new { Name = name, SystemID = system.Id });
}
public async Task<ICollection<PKMember>> GetUnproxyableMembers(PKSystem system) {
return (await GetBySystem(system))
return (await GetSystemMembers(system))
.Where((m) => {
var proxiedName = $"{m.Name} {system.Tag}";
return proxiedName.Length > Limits.MaxProxyNameLength || proxiedName.Length < 2;
}).ToList();
}
public async Task<IEnumerable<PKMember>> GetBySystem(PKSystem system) {
public async Task<IEnumerable<PKMember>> GetSystemMembers(PKSystem system) {
using (var conn = await _conn.Obtain())
return await conn.QueryAsync<PKMember>("select * from members where system = @SystemID", new { SystemID = system.Id });
}
public async Task Save(PKMember member) {
public async Task SaveMember(PKMember member) {
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, prefix = @Prefix, suffix = @Suffix where id = @Id", member);
_logger.Information("Updated member {@Member}", member);
}
public async Task Delete(PKMember member) {
public async Task DeleteMember(PKMember member) {
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("delete from members where id = @Id", member);
_logger.Information("Deleted member {@Member}", member);
}
public async Task<int> MessageCount(PKMember member)
public async Task<ulong> GetMemberMessageCount(PKMember member)
{
using (var conn = await _conn.Obtain())
return await conn.QuerySingleAsync<int>("select count(*) from messages where member = @Id", member);
return await conn.QuerySingleAsync<ulong>("select count(*) from messages where member = @Id", member);
}
public struct MessageBreakdownListEntry
{
public int Member;
public int MessageCount;
}
public async Task<IEnumerable<MessageBreakdownListEntry>> MessageCountsPerMember(PKSystem system)
public async Task<IEnumerable<MemberMessageCount>> GetMemberMessageCountBulk(PKSystem system)
{
using (var conn = await _conn.Obtain())
return await conn.QueryAsync<MessageBreakdownListEntry>(
return await conn.QueryAsync<MemberMessageCount>(
@"SELECT messages.member, COUNT(messages.member) messagecount
FROM members
JOIN messages
@ -222,44 +512,18 @@ namespace PluralKit {
new { System = system.Id });
}
public async Task<int> MemberCount(PKSystem system)
public async Task<int> GetSystemMemberCount(PKSystem system)
{
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<int>("select count(*) from members where system = @Id", system);
}
public async Task<ulong> Count()
public async Task<ulong> GetTotalMembers()
{
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(id) from members");
}
}
public class MessageStore {
public struct PKMessage
{
public ulong Mid;
public ulong Channel;
public ulong Sender;
public ulong? OriginalMid;
}
public class StoredMessage
{
public PKMessage Message;
public PKMember Member;
public PKSystem System;
}
private DbConnectionFactory _conn;
private ILogger _logger;
public MessageStore(DbConnectionFactory conn, ILogger logger)
{
this._conn = conn;
_logger = logger.ForContext<MessageStore>();
}
public async Task Store(ulong senderId, ulong messageId, ulong channelId, ulong originalMessage, PKMember member) {
public async Task AddMessage(ulong senderId, ulong messageId, ulong channelId, ulong originalMessage, PKMember member) {
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("insert into messages(mid, channel, member, sender, original_mid) values(@MessageId, @ChannelId, @MemberId, @SenderId, @OriginalMid)", new {
MessageId = messageId,
@ -272,10 +536,10 @@ namespace PluralKit {
_logger.Information("Stored message {Message} in channel {Channel}", messageId, channelId);
}
public async Task<StoredMessage> Get(ulong id)
public async Task<FullMessage> GetMessage(ulong id)
{
using (var conn = await _conn.Obtain())
return (await conn.QueryAsync<PKMessage, PKMember, PKSystem, StoredMessage>("select messages.*, members.*, systems.* from messages, members, systems where (mid = @Id or original_mid = @Id) and messages.member = members.id and systems.id = members.system", (msg, member, system) => new StoredMessage
return (await conn.QueryAsync<PKMessage, PKMember, PKSystem, FullMessage>("select messages.*, members.*, systems.* from messages, members, systems where (mid = @Id or original_mid = @Id) and messages.member = members.id and systems.id = members.system", (msg, member, system) => new FullMessage
{
Message = msg,
System = system,
@ -283,13 +547,13 @@ namespace PluralKit {
}, new { Id = id })).FirstOrDefault();
}
public async Task Delete(ulong id) {
public async Task DeleteMessage(ulong id) {
using (var conn = await _conn.Obtain())
if (await conn.ExecuteAsync("delete from messages where mid = @Id", new { Id = id }) > 0)
_logger.Information("Deleted message {Message}", id);
}
public async Task BulkDelete(IReadOnlyCollection<ulong> ids)
public async Task DeleteMessagesBulk(IEnumerable<ulong> ids)
{
using (var conn = await _conn.Obtain())
{
@ -301,25 +565,13 @@ namespace PluralKit {
}
}
public async Task<ulong> Count()
public async Task<ulong> GetTotalMessages()
{
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(mid) from messages");
}
}
public class SwitchStore
{
private DbConnectionFactory _conn;
private ILogger _logger;
public SwitchStore(DbConnectionFactory conn, ILogger logger)
{
_conn = conn;
_logger = logger.ForContext<SwitchStore>();
}
public async Task RegisterSwitch(PKSystem system, ICollection<PKMember> members)
public async Task AddSwitch(PKSystem system, IEnumerable<PKMember> members)
{
// Use a transaction here since we're doing multiple executed commands in one
using (var conn = await _conn.Obtain())
@ -345,7 +597,7 @@ namespace PluralKit {
}
}
public async Task BulkImportSwitches(PKSystem system, ICollection<Tuple<Instant, ICollection<PKMember>>> switches)
public async Task AddSwitchesBulk(PKSystem system, IEnumerable<ImportedSwitch> switches)
{
// Read existing switches to enforce unique timestamps
var priorSwitches = await GetSwitches(system);
@ -363,13 +615,13 @@ namespace PluralKit {
foreach (var sw in switches)
{
// If there's already a switch at this time, move on
if (priorSwitches.Any(x => x.Timestamp.Equals(sw.Item1)))
if (priorSwitches.Any(x => x.Timestamp.Equals(sw.Timestamp)))
continue;
// Otherwise, add it to the importer
importer.StartRow();
importer.Write(system.Id, NpgsqlTypes.NpgsqlDbType.Integer);
importer.Write(sw.Item1, NpgsqlTypes.NpgsqlDbType.Timestamp);
importer.Write(sw.Members, NpgsqlTypes.NpgsqlDbType.Timestamp);
}
importer.Complete(); // Commits the copy operation so dispose won't roll it back
}
@ -392,12 +644,12 @@ namespace PluralKit {
foreach (var pkSwitch in switchesWithoutMembers)
{
// If this isn't in our import set, move on
var sw = switches.FirstOrDefault(x => x.Item1.Equals(pkSwitch.Timestamp));
var sw = switches.Select(x => (ImportedSwitch?) x).FirstOrDefault(x => x.Value.Timestamp.Equals(pkSwitch.Timestamp));
if (sw == null)
continue;
// Loop through associated members to add each to the switch
foreach (var m in sw.Item2)
foreach (var m in sw.Value.Members)
{
// Skip switch-outs - these don't have switch_members
if (m == null)
@ -534,20 +786,13 @@ namespace PluralKit {
_logger.Information("Deleted switch {Switch}");
}
public async Task<ulong> Count()
public async Task<ulong> GetTotalSwitches()
{
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(id) from switches");
}
public struct SwitchListEntry
{
public ICollection<PKMember> Members;
public Instant TimespanStart;
public Instant TimespanEnd;
}
public async Task<IEnumerable<SwitchListEntry>> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd)
public async Task<IEnumerable<SwitchListEntry>> GetPeriodFronters(PKSystem system, Instant periodStart, Instant periodEnd)
{
// Returns the timestamps and member IDs of switches overlapping the range, in chronological (newest first) order
var switchMembers = await GetSwitchMembersList(system, periodStart, periodEnd);
@ -599,17 +844,7 @@ namespace PluralKit {
return outList;
}
public struct PerMemberSwitchDuration
{
public Dictionary<PKMember, Duration> MemberSwitchDurations;
public Duration NoFronterDuration;
public Instant RangeStart;
public Instant RangeEnd;
}
public async Task<PerMemberSwitchDuration> GetPerMemberSwitchDuration(PKSystem system, Instant periodStart,
Instant periodEnd)
public async Task<FrontBreakdown> GetFrontBreakdown(PKSystem system, Instant periodStart, Instant periodEnd)
{
var dict = new Dictionary<PKMember, Duration>();
@ -621,7 +856,7 @@ namespace PluralKit {
var actualStart = periodEnd; // will be "pulled" down
var actualEnd = periodStart; // will be "pulled" up
foreach (var sw in await GetTruncatedSwitchList(system, periodStart, periodEnd))
foreach (var sw in await GetPeriodFronters(system, periodStart, periodEnd))
{
var span = sw.TimespanEnd - sw.TimespanStart;
foreach (var member in sw.Members)
@ -636,7 +871,7 @@ namespace PluralKit {
if (sw.TimespanEnd > actualEnd) actualEnd = sw.TimespanEnd;
}
return new PerMemberSwitchDuration
return new FrontBreakdown
{
MemberSwitchDurations = dict,
NoFronterDuration = noFronterDuration,

@ -7,13 +7,11 @@ namespace PluralKit.Web.Pages
{
public class ViewSystem : PageModel
{
private SystemStore _systems;
private MemberStore _members;
private IDataStore _data;
public ViewSystem(SystemStore systems, MemberStore members)
public ViewSystem(IDataStore data)
{
_systems = systems;
_members = members;
_data = data;
}
public PKSystem System { get; set; }
@ -21,10 +19,10 @@ namespace PluralKit.Web.Pages
public async Task<IActionResult> OnGet(string systemId)
{
System = await _systems.GetByHid(systemId);
System = await _data.GetSystemByHid(systemId);
if (System == null) return NotFound();
Members = await _members.GetBySystem(System);
Members = await _data.GetSystemMembers(System);
return Page();
}

@ -28,8 +28,7 @@ namespace PluralKit.Web
services
.AddScoped<IDbConnection, NpgsqlConnection>(_ => new NpgsqlConnection(config.Database))
.AddTransient<SystemStore>()
.AddTransient<MemberStore>()
.AddTransient<IDataStore, PostgresDataStore>()
.AddSingleton(config);
}