Increase webhook name limit to 80

This commit is contained in:
Ske 2019-08-14 07:16:48 +02:00
parent 5ef8a9303d
commit 74e0508065
7 changed files with 476 additions and 457 deletions

View File

@ -134,18 +134,16 @@ namespace PluralKit.Bot
private IServiceProvider _services; private IServiceProvider _services;
private DiscordShardedClient _client; private DiscordShardedClient _client;
private CommandService _commands; private CommandService _commands;
private ProxyService _proxy;
private Timer _updateTimer; private Timer _updateTimer;
private IMetrics _metrics; private IMetrics _metrics;
private PeriodicStatCollector _collector; private PeriodicStatCollector _collector;
private ILogger _logger; private ILogger _logger;
public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy, IMetrics metrics, PeriodicStatCollector collector, ILogger logger) public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, IMetrics metrics, PeriodicStatCollector collector, ILogger logger)
{ {
_services = services; _services = services;
_client = client as DiscordShardedClient; _client = client as DiscordShardedClient;
_commands = commands; _commands = commands;
_proxy = proxy;
_metrics = metrics; _metrics = metrics;
_collector = collector; _collector = collector;
_logger = logger.ForContext<Bot>(); _logger = logger.ForContext<Bot>();
@ -367,7 +365,14 @@ namespace PluralKit.Bot
else else
{ {
// If not, try proxying anyway // If not, try proxying anyway
await _proxy.HandleMessageAsync(arg); try
{
await _proxy.HandleMessageAsync(arg);
}
catch (PKError e)
{
await msg.Channel.SendMessageAsync($"{Emojis.Error} {e.Message}");
}
} }
} }

View File

@ -89,7 +89,7 @@ namespace PluralKit.Bot.Commands
if (unproxyableMembers.Count > 0) if (unproxyableMembers.Count > 0)
{ {
var msg = await Context.Channel.SendMessageAsync( var msg = await Context.Channel.SendMessageAsync(
$"{Emojis.Warn} Changing your system tag to '{newTag}' will result in the following members being unproxyable, since the tag would bring their name over 32 characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name))}**\nDo you want to continue anyway?"); $"{Emojis.Warn} Changing your system tag to '{newTag}' 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))}**\nDo you want to continue anyway?");
if (!await Context.PromptYesNo(msg)) throw new PKError("Tag change cancelled."); if (!await Context.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
} }
} }

View File

@ -76,5 +76,7 @@ namespace PluralKit.Bot {
public static PKError DisplayNameTooLong(string displayName, int maxLength) => new PKError( public static PKError DisplayNameTooLong(string displayName, int maxLength) => new PKError(
$"Display name too long ({displayName.Length} > {maxLength} characters). Use a shorter display name, or shorten your system tag."); $"Display name too long ({displayName.Length} > {maxLength} characters). Use a shorter display name, or shorten your system tag.");
public static PKError ProxyNameTooShort(string name) => new PKError($"The webhook's name, `{name}`, is shorter than two characters, and thus cannot be proxied. Please change the member name or use a longer system tag.");
public static PKError ProxyNameTooLong(string name) => new PKError($"The webhook's name, `{name}`, is too long ({name.Length} > {Limits.MaxProxyNameLength} characters), and thus cannot be proxied. Please change the member name or use a shorter system tag.");
} }
} }

View File

@ -2,15 +2,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using App.Metrics;
using Dapper;
using Discord; using Discord;
using Discord.Net; using Discord.Net;
using Discord.Webhook;
using Discord.WebSocket; using Discord.WebSocket;
using Microsoft.Extensions.Caching.Memory;
using PluralKit.Core;
using Serilog; using Serilog;
namespace PluralKit.Bot namespace PluralKit.Bot
@ -100,6 +99,10 @@ namespace PluralKit.Bot
var proxyName = match.Member.ProxyName(match.System.Tag); var proxyName = match.Member.ProxyName(match.System.Tag);
var avatarUrl = match.Member.AvatarUrl ?? match.System.AvatarUrl; var avatarUrl = match.Member.AvatarUrl ?? match.System.AvatarUrl;
// If the name's too long (or short), bail
if (proxyName.Length < 2) throw Errors.ProxyNameTooShort(proxyName);
if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName);
// Sanitize @everyone, but only if the original user wouldn't have permission to // Sanitize @everyone, but only if the original user wouldn't have permission to
var messageContents = SanitizeEveryoneMaybe(message, match.InnerText); var messageContents = SanitizeEveryoneMaybe(message, match.InnerText);
@ -143,6 +146,7 @@ namespace PluralKit.Bot
if (!permissions.ManageWebhooks) if (!permissions.ManageWebhooks)
{ {
// todo: PKError-ify these
await channel.SendMessageAsync( await channel.SendMessageAsync(
$"{Emojis.Error} PluralKit does not have the *Manage Webhooks* permission in this channel, and thus cannot proxy messages. Please contact a server administrator to remedy this."); $"{Emojis.Error} PluralKit does not have the *Manage Webhooks* permission in this channel, and thus cannot proxy messages. Please contact a server administrator to remedy this.");
return false; return false;

View File

@ -1,9 +1,12 @@
namespace PluralKit.Core { namespace PluralKit.Core {
public static class Limits { public static class Limits
{
public static readonly int MaxProxyNameLength = 80;
public static readonly int MaxSystemNameLength = 100; public static readonly int MaxSystemNameLength = 100;
public static readonly int MaxSystemTagLength = 31; public static readonly int MaxSystemTagLength = MaxProxyNameLength - 1;
public static readonly int MaxDescriptionLength = 1000; public static readonly int MaxDescriptionLength = 1000;
public static readonly int MaxMemberNameLength = 50; public static readonly int MaxMemberNameLength = 100; // Fair bit larger than MaxProxyNameLength for bookkeeping
public static readonly int MaxPronounsLength = 100; public static readonly int MaxPronounsLength = 100;
public static readonly long AvatarFileSizeLimit = 1024 * 1024; public static readonly long AvatarFileSizeLimit = 1024 * 1024;

View File

@ -3,6 +3,8 @@ using Newtonsoft.Json;
using NodaTime; using NodaTime;
using NodaTime.Text; using NodaTime.Text;
using PluralKit.Core;
namespace PluralKit namespace PluralKit
{ {
public class PKSystem public class PKSystem
@ -18,7 +20,7 @@ namespace PluralKit
[JsonProperty("created")] public Instant Created { get; set; } [JsonProperty("created")] public Instant Created { get; set; }
[JsonProperty("tz")] public string UiTz { get; set; } [JsonProperty("tz")] public string UiTz { get; set; }
[JsonIgnore] public int MaxMemberNameLength => Tag != null ? 32 - Tag.Length - 1 : 32; [JsonIgnore] public int MaxMemberNameLength => Tag != null ? Limits.MaxProxyNameLength - Tag.Length - 1 : Limits.MaxProxyNameLength;
[JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
} }

View File

@ -1,444 +1,447 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using App.Metrics.Logging; using App.Metrics.Logging;
using Dapper; using Dapper;
using NodaTime; using NodaTime;
using Serilog;
using PluralKit.Core;
namespace PluralKit {
public class SystemStore { using Serilog;
private DbConnectionFactory _conn;
private ILogger _logger; namespace PluralKit {
public class SystemStore {
public SystemStore(DbConnectionFactory conn, ILogger logger) private DbConnectionFactory _conn;
{ private ILogger _logger;
this._conn = conn;
_logger = logger.ForContext<SystemStore>(); public SystemStore(DbConnectionFactory conn, ILogger logger)
} {
this._conn = conn;
public async Task<PKSystem> Create(string systemName = null) { _logger = logger.ForContext<SystemStore>();
string hid; }
do
{ public async Task<PKSystem> Create(string systemName = null) {
hid = Utils.GenerateHid(); string hid;
} while (await GetByHid(hid) != null); do
{
PKSystem system; hid = Utils.GenerateHid();
using (var conn = await _conn.Obtain()) } while (await GetByHid(hid) != null);
system = await conn.QuerySingleAsync<PKSystem>("insert into systems (hid, name) values (@Hid, @Name) returning *", new { Hid = hid, Name = systemName });
PKSystem system;
_logger.Information("Created system {System}", system.Id); using (var conn = await _conn.Obtain())
return system; system = await conn.QuerySingleAsync<PKSystem>("insert into systems (hid, name) values (@Hid, @Name) returning *", new { Hid = hid, Name = systemName });
}
_logger.Information("Created system {System}", system.Id);
public async Task Link(PKSystem system, ulong accountId) { return system;
// 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()) public async Task Link(PKSystem system, ulong accountId) {
await conn.ExecuteAsync("insert into accounts (uid, system) values (@Id, @SystemId) on conflict do nothing", new { Id = accountId, SystemId = system.Id }); // 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
_logger.Information("Linked system {System} to account {Account}", system.Id, accountId); using (var conn = await _conn.Obtain())
} await conn.ExecuteAsync("insert into accounts (uid, system) values (@Id, @SystemId) on conflict do nothing", new { Id = accountId, SystemId = system.Id });
public async Task Unlink(PKSystem system, ulong accountId) { _logger.Information("Linked system {System} to account {Account}", system.Id, 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 });
public async Task Unlink(PKSystem system, ulong accountId) {
_logger.Information("Unlinked system {System} from account {Account}", system.Id, 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 });
public async Task<PKSystem> GetByAccount(ulong accountId) { _logger.Information("Unlinked system {System} from account {Account}", system.Id, 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> GetByAccount(ulong accountId) {
using (var conn = await _conn.Obtain())
public async Task<PKSystem> GetByHid(string hid) { return await conn.QuerySingleOrDefaultAsync<PKSystem>("select systems.* from systems, accounts where accounts.system = systems.id and accounts.uid = @Id", new { Id = accountId });
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> GetByHid(string hid) {
using (var conn = await _conn.Obtain())
public async Task<PKSystem> GetByToken(string token) { return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where systems.hid = @Hid", new { Hid = hid.ToLower() });
using (var conn = await _conn.Obtain()) }
return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where token = @Token", new { Token = token });
} public async Task<PKSystem> GetByToken(string token) {
using (var conn = await _conn.Obtain())
public async Task<PKSystem> GetById(int id) return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where token = @Token", new { Token = token });
{ }
using (var conn = await _conn.Obtain())
return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where id = @Id", new { Id = id }); public async Task<PKSystem> GetById(int id)
} {
using (var conn = await _conn.Obtain())
public async Task Save(PKSystem system) { return await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where id = @Id", new { Id = id });
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);
public async Task Save(PKSystem system) {
_logger.Information("Updated system {@System}", 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);
public async Task Delete(PKSystem system) { _logger.Information("Updated system {@System}", 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 Delete(PKSystem system) {
} using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("delete from systems where id = @Id", system);
public async Task<IEnumerable<ulong>> GetLinkedAccountIds(PKSystem system) _logger.Information("Deleted system {System}", system.Id);
{ }
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<IEnumerable<ulong>> GetLinkedAccountIds(PKSystem system)
} {
using (var conn = await _conn.Obtain())
public async Task<ulong> Count() return await conn.QueryAsync<ulong>("select uid from accounts where system = @Id", new { Id = system.Id });
{ }
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(id) from systems"); public async Task<ulong> Count()
} {
} 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 class MemberStore {
public MemberStore(DbConnectionFactory conn, ILogger logger) private DbConnectionFactory _conn;
{ private ILogger _logger;
this._conn = conn;
_logger = logger.ForContext<MemberStore>(); public MemberStore(DbConnectionFactory conn, ILogger logger)
} {
this._conn = conn;
public async Task<PKMember> Create(PKSystem system, string name) { _logger = logger.ForContext<MemberStore>();
string hid; }
do
{ public async Task<PKMember> Create(PKSystem system, string name) {
hid = Utils.GenerateHid(); string hid;
} while (await GetByHid(hid) != null); do
{
PKMember member; hid = Utils.GenerateHid();
using (var conn = await _conn.Obtain()) } while (await GetByHid(hid) != null);
member = await conn.QuerySingleAsync<PKMember>("insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *", new {
Hid = hid, PKMember member;
SystemID = system.Id, using (var conn = await _conn.Obtain())
Name = name member = await conn.QuerySingleAsync<PKMember>("insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *", new {
}); Hid = hid,
SystemID = system.Id,
_logger.Information("Created member {Member}", member.Id); Name = name
return member; });
}
_logger.Information("Created member {Member}", member.Id);
public async Task<PKMember> GetByHid(string hid) { return member;
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> GetByHid(string hid) {
using (var conn = await _conn.Obtain())
public async Task<PKMember> GetByName(PKSystem system, string name) { return await conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where hid = @Hid", new { Hid = hid.ToLower() });
// 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<PKMember> GetByName(PKSystem system, string name) {
} // QueryFirst, since members can (in rare cases) share names
using (var conn = await _conn.Obtain())
public async Task<ICollection<PKMember>> GetUnproxyableMembers(PKSystem system) { return await conn.QueryFirstOrDefaultAsync<PKMember>("select * from members where lower(name) = lower(@Name) and system = @SystemID", new { Name = name, SystemID = system.Id });
return (await GetBySystem(system)) }
.Where((m) => {
var proxiedName = $"{m.Name} {system.Tag}"; public async Task<ICollection<PKMember>> GetUnproxyableMembers(PKSystem system) {
return proxiedName.Length > 32 || proxiedName.Length < 2; return (await GetBySystem(system))
}).ToList(); .Where((m) => {
} var proxiedName = $"{m.Name} {system.Tag}";
return proxiedName.Length > Limits.MaxProxyNameLength || proxiedName.Length < 2;
public async Task<IEnumerable<PKMember>> GetBySystem(PKSystem system) { }).ToList();
using (var conn = await _conn.Obtain()) }
return await conn.QueryAsync<PKMember>("select * from members where system = @SystemID", new { SystemID = system.Id });
} public async Task<IEnumerable<PKMember>> GetBySystem(PKSystem system) {
using (var conn = await _conn.Obtain())
public async Task Save(PKMember member) { return await conn.QueryAsync<PKMember>("select * from members where system = @SystemID", new { SystemID = system.Id });
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);
public async Task Save(PKMember member) {
_logger.Information("Updated member {@Member}", 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);
public async Task Delete(PKMember member) { _logger.Information("Updated member {@Member}", member);
using (var conn = await _conn.Obtain()) }
await conn.ExecuteAsync("delete from members where id = @Id", member);
public async Task Delete(PKMember member) {
_logger.Information("Deleted member {@Member}", member); using (var conn = await _conn.Obtain())
} await conn.ExecuteAsync("delete from members where id = @Id", member);
public async Task<int> MessageCount(PKMember member) _logger.Information("Deleted member {@Member}", member);
{ }
using (var conn = await _conn.Obtain())
return await conn.QuerySingleAsync<int>("select count(*) from messages where member = @Id", member); public async Task<int> MessageCount(PKMember member)
} {
using (var conn = await _conn.Obtain())
public async Task<int> MemberCount(PKSystem system) return await conn.QuerySingleAsync<int>("select count(*) from messages where member = @Id", member);
{ }
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<int>("select count(*) from members where system = @Id", system); public async Task<int> MemberCount(PKSystem system)
} {
using (var conn = await _conn.Obtain())
public async Task<ulong> Count() return await conn.ExecuteScalarAsync<int>("select count(*) from members where system = @Id", system);
{ }
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(id) from members"); public async Task<ulong> Count()
} {
} 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 class MessageStore {
public ulong Channel; public struct PKMessage
public ulong Sender; {
public ulong? OriginalMid; public ulong Mid;
} public ulong Channel;
public class StoredMessage public ulong Sender;
{ public ulong? OriginalMid;
public PKMessage Message; }
public PKMember Member; public class StoredMessage
public PKSystem System; {
} public PKMessage Message;
public PKMember Member;
private DbConnectionFactory _conn; public PKSystem System;
private ILogger _logger; }
public MessageStore(DbConnectionFactory conn, ILogger logger) private DbConnectionFactory _conn;
{ private ILogger _logger;
this._conn = conn;
_logger = logger.ForContext<MessageStore>(); public MessageStore(DbConnectionFactory conn, ILogger logger)
} {
this._conn = conn;
public async Task Store(ulong senderId, ulong messageId, ulong channelId, ulong originalMessage, PKMember member) { _logger = logger.ForContext<MessageStore>();
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, public async Task Store(ulong senderId, ulong messageId, ulong channelId, ulong originalMessage, PKMember member) {
ChannelId = channelId, using (var conn = await _conn.Obtain())
MemberId = member.Id, await conn.ExecuteAsync("insert into messages(mid, channel, member, sender, original_mid) values(@MessageId, @ChannelId, @MemberId, @SenderId, @OriginalMid)", new {
SenderId = senderId, MessageId = messageId,
OriginalMid = originalMessage ChannelId = channelId,
}); MemberId = member.Id,
SenderId = senderId,
_logger.Information("Stored message {Message} in channel {Channel}", messageId, channelId); OriginalMid = originalMessage
} });
public async Task<StoredMessage> Get(ulong id) _logger.Information("Stored message {Message} in channel {Channel}", messageId, channelId);
{ }
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 public async Task<StoredMessage> Get(ulong id)
{ {
Message = msg, using (var conn = await _conn.Obtain())
System = system, 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
Member = member {
}, new { Id = id })).FirstOrDefault(); Message = msg,
} System = system,
Member = member
public async Task Delete(ulong id) { }, new { Id = id })).FirstOrDefault();
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 Delete(ulong id) {
} using (var conn = await _conn.Obtain())
if (await conn.ExecuteAsync("delete from messages where mid = @Id", new { Id = id }) > 0)
public async Task BulkDelete(IReadOnlyCollection<ulong> ids) _logger.Information("Deleted message {Message}", id);
{ }
using (var conn = await _conn.Obtain())
{ public async Task BulkDelete(IReadOnlyCollection<ulong> ids)
// Npgsql doesn't support ulongs in general - we hacked around it for plain ulongs but tbh not worth it for collections of ulong {
// Hence we map them to single longs, which *are* supported (this is ok since they're Technically (tm) stored as signed longs in the db anyway) using (var conn = await _conn.Obtain())
var foundCount = await conn.ExecuteAsync("delete from messages where mid = any(@Ids)", new {Ids = ids.Select(id => (long) id).ToArray()}); {
if (foundCount > 0) // Npgsql doesn't support ulongs in general - we hacked around it for plain ulongs but tbh not worth it for collections of ulong
_logger.Information("Bulk deleted messages {Messages}, {FoundCount} found", ids, foundCount); // Hence we map them to single longs, which *are* supported (this is ok since they're Technically (tm) stored as signed longs in the db anyway)
} var foundCount = await conn.ExecuteAsync("delete from messages where mid = any(@Ids)", new {Ids = ids.Select(id => (long) id).ToArray()});
} if (foundCount > 0)
_logger.Information("Bulk deleted messages {Messages}, {FoundCount} found", ids, foundCount);
public async Task<ulong> Count() }
{ }
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(mid) from messages"); public async Task<ulong> Count()
} {
} 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 class SwitchStore
{
public SwitchStore(DbConnectionFactory conn, ILogger logger) private DbConnectionFactory _conn;
{ private ILogger _logger;
_conn = conn;
_logger = logger.ForContext<SwitchStore>(); public SwitchStore(DbConnectionFactory conn, ILogger logger)
} {
_conn = conn;
public async Task RegisterSwitch(PKSystem system, ICollection<PKMember> members) _logger = logger.ForContext<SwitchStore>();
{ }
// Use a transaction here since we're doing multiple executed commands in one
using (var conn = await _conn.Obtain()) public async Task RegisterSwitch(PKSystem system, ICollection<PKMember> members)
using (var tx = conn.BeginTransaction()) {
{ // Use a transaction here since we're doing multiple executed commands in one
// First, we insert the switch itself using (var conn = await _conn.Obtain())
var sw = await conn.QuerySingleAsync<PKSwitch>("insert into switches(system) values (@System) returning *", using (var tx = conn.BeginTransaction())
new {System = system.Id}); {
// First, we insert the switch itself
// Then we insert each member in the switch in the switch_members table var sw = await conn.QuerySingleAsync<PKSwitch>("insert into switches(system) values (@System) returning *",
// TODO: can we parallelize this or send it in bulk somehow? new {System = system.Id});
foreach (var member in members)
{ // Then we insert each member in the switch in the switch_members table
await conn.ExecuteAsync( // TODO: can we parallelize this or send it in bulk somehow?
"insert into switch_members(switch, member) values(@Switch, @Member)", foreach (var member in members)
new {Switch = sw.Id, Member = member.Id}); {
} await conn.ExecuteAsync(
"insert into switch_members(switch, member) values(@Switch, @Member)",
// Finally we commit the tx, since the using block will otherwise rollback it new {Switch = sw.Id, Member = member.Id});
tx.Commit(); }
_logger.Information("Registered switch {Switch} in system {System} with members {@Members}", sw.Id, system.Id, members.Select(m => m.Id)); // Finally we commit the tx, since the using block will otherwise rollback it
} tx.Commit();
}
_logger.Information("Registered switch {Switch} in system {System} with members {@Members}", sw.Id, system.Id, members.Select(m => m.Id));
public async Task<IEnumerable<PKSwitch>> GetSwitches(PKSystem system, int count = 9999999) }
{ }
// TODO: refactor the PKSwitch data structure to somehow include a hydrated member list
// (maybe when we get caching in?) public async Task<IEnumerable<PKSwitch>> GetSwitches(PKSystem system, int count = 9999999)
using (var conn = await _conn.Obtain()) {
return await conn.QueryAsync<PKSwitch>("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count}); // TODO: refactor the PKSwitch data structure to somehow include a hydrated member list
} // (maybe when we get caching in?)
using (var conn = await _conn.Obtain())
public async Task<IEnumerable<int>> GetSwitchMemberIds(PKSwitch sw) return await conn.QueryAsync<PKSwitch>("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count});
{ }
using (var conn = await _conn.Obtain())
return await conn.QueryAsync<int>("select member from switch_members where switch = @Switch order by switch_members.id", public async Task<IEnumerable<int>> GetSwitchMemberIds(PKSwitch sw)
new {Switch = sw.Id}); {
} using (var conn = await _conn.Obtain())
return await conn.QueryAsync<int>("select member from switch_members where switch = @Switch order by switch_members.id",
public async Task<IEnumerable<PKMember>> GetSwitchMembers(PKSwitch sw) new {Switch = sw.Id});
{ }
using (var conn = await _conn.Obtain())
return await conn.QueryAsync<PKMember>( public async Task<IEnumerable<PKMember>> GetSwitchMembers(PKSwitch sw)
"select * from switch_members, members where switch_members.member = members.id and switch_members.switch = @Switch order by switch_members.id", {
new {Switch = sw.Id}); using (var conn = await _conn.Obtain())
} return await conn.QueryAsync<PKMember>(
"select * from switch_members, members where switch_members.member = members.id and switch_members.switch = @Switch order by switch_members.id",
public async Task<PKSwitch> GetLatestSwitch(PKSystem system) => (await GetSwitches(system, 1)).FirstOrDefault(); new {Switch = sw.Id});
}
public async Task MoveSwitch(PKSwitch sw, Instant time)
{ public async Task<PKSwitch> GetLatestSwitch(PKSystem system) => (await GetSwitches(system, 1)).FirstOrDefault();
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("update switches set timestamp = @Time where id = @Id", public async Task MoveSwitch(PKSwitch sw, Instant time)
new {Time = time, Id = sw.Id}); {
using (var conn = await _conn.Obtain())
_logger.Information("Moved switch {Switch} to {Time}", sw.Id, time); await conn.ExecuteAsync("update switches set timestamp = @Time where id = @Id",
} new {Time = time, Id = sw.Id});
public async Task DeleteSwitch(PKSwitch sw) _logger.Information("Moved switch {Switch} to {Time}", sw.Id, time);
{ }
using (var conn = await _conn.Obtain())
await conn.ExecuteAsync("delete from switches where id = @Id", new {Id = sw.Id}); public async Task DeleteSwitch(PKSwitch sw)
{
_logger.Information("Deleted switch {Switch}"); using (var conn = await _conn.Obtain())
} await conn.ExecuteAsync("delete from switches where id = @Id", new {Id = sw.Id});
public async Task<ulong> Count() _logger.Information("Deleted switch {Switch}");
{ }
using (var conn = await _conn.Obtain())
return await conn.ExecuteScalarAsync<ulong>("select count(id) from switches"); public async Task<ulong> Count()
} {
using (var conn = await _conn.Obtain())
public struct SwitchListEntry return await conn.ExecuteScalarAsync<ulong>("select count(id) from switches");
{ }
public ICollection<PKMember> Members;
public Instant TimespanStart; public struct SwitchListEntry
public Instant TimespanEnd; {
} public ICollection<PKMember> Members;
public Instant TimespanStart;
public async Task<IEnumerable<SwitchListEntry>> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd) public Instant TimespanEnd;
{ }
// TODO: only fetch the necessary switches here
// todo: this is in general not very efficient LOL public async Task<IEnumerable<SwitchListEntry>> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd)
// returns switches in chronological (newest first) order {
var switches = await GetSwitches(system); // TODO: only fetch the necessary switches here
// todo: this is in general not very efficient LOL
// we skip all switches that happened later than the range end, and taking all the ones that happened after the range start // returns switches in chronological (newest first) order
// *BUT ALSO INCLUDING* the last switch *before* the range (that partially overlaps the range period) var switches = await GetSwitches(system);
var switchesInRange = switches.SkipWhile(sw => sw.Timestamp >= periodEnd).TakeWhileIncluding(sw => sw.Timestamp > periodStart).ToList();
// we skip all switches that happened later than the range end, and taking all the ones that happened after the range start
// query DB for all members involved in any of the switches above and collect into a dictionary for future use // *BUT ALSO INCLUDING* the last switch *before* the range (that partially overlaps the range period)
// this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary var switchesInRange = switches.SkipWhile(sw => sw.Timestamp >= periodEnd).TakeWhileIncluding(sw => sw.Timestamp > periodStart).ToList();
// key used in GetPerMemberSwitchDuration below
Dictionary<int, PKMember> memberObjects; // query DB for all members involved in any of the switches above and collect into a dictionary for future use
using (var conn = await _conn.Obtain()) // this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary
{ // key used in GetPerMemberSwitchDuration below
memberObjects = (await conn.QueryAsync<PKMember>( Dictionary<int, PKMember> memberObjects;
"select distinct members.* from members, switch_members where switch_members.switch = any(@Switches) and switch_members.member = members.id", // lol postgres specific `= any()` syntax using (var conn = await _conn.Obtain())
new {Switches = switchesInRange.Select(sw => sw.Id).ToList()})) {
.ToDictionary(m => m.Id); memberObjects = (await conn.QueryAsync<PKMember>(
} "select distinct members.* from members, switch_members where switch_members.switch = any(@Switches) and switch_members.member = members.id", // lol postgres specific `= any()` syntax
new {Switches = switchesInRange.Select(sw => sw.Id).ToList()}))
.ToDictionary(m => m.Id);
// we create the entry objects }
var outList = new List<SwitchListEntry>();
// loop through every switch that *occurred* in-range and add it to the list // we create the entry objects
// end time is the switch *after*'s timestamp - we cheat and start it out at the range end so the first switch in-range "ends" there instead of the one after's start point var outList = new List<SwitchListEntry>();
var endTime = periodEnd;
foreach (var switchInRange in switchesInRange) // loop through every switch that *occurred* in-range and add it to the list
{ // end time is the switch *after*'s timestamp - we cheat and start it out at the range end so the first switch in-range "ends" there instead of the one after's start point
// find the start time of the switch, but clamp it to the range (only applicable to the Last Switch Before Range we include in the TakeWhileIncluding call above) var endTime = periodEnd;
var switchStartClamped = switchInRange.Timestamp; foreach (var switchInRange in switchesInRange)
if (switchStartClamped < periodStart) switchStartClamped = periodStart; {
// find the start time of the switch, but clamp it to the range (only applicable to the Last Switch Before Range we include in the TakeWhileIncluding call above)
outList.Add(new SwitchListEntry var switchStartClamped = switchInRange.Timestamp;
{ if (switchStartClamped < periodStart) switchStartClamped = periodStart;
Members = (await GetSwitchMemberIds(switchInRange)).Select(id => memberObjects[id]).ToList(),
TimespanStart = switchStartClamped, outList.Add(new SwitchListEntry
TimespanEnd = endTime {
}); Members = (await GetSwitchMemberIds(switchInRange)).Select(id => memberObjects[id]).ToList(),
TimespanStart = switchStartClamped,
// next switch's end is this switch's start TimespanEnd = endTime
endTime = switchInRange.Timestamp; });
}
// next switch's end is this switch's start
return outList; endTime = switchInRange.Timestamp;
} }
public struct PerMemberSwitchDuration return outList;
{ }
public Dictionary<PKMember, Duration> MemberSwitchDurations;
public Duration NoFronterDuration; public struct PerMemberSwitchDuration
public Instant RangeStart; {
public Instant RangeEnd; public Dictionary<PKMember, Duration> MemberSwitchDurations;
} public Duration NoFronterDuration;
public Instant RangeStart;
public async Task<PerMemberSwitchDuration> GetPerMemberSwitchDuration(PKSystem system, Instant periodStart, public Instant RangeEnd;
Instant periodEnd) }
{
var dict = new Dictionary<PKMember, Duration>(); public async Task<PerMemberSwitchDuration> GetPerMemberSwitchDuration(PKSystem system, Instant periodStart,
Instant periodEnd)
var noFronterDuration = Duration.Zero; {
var dict = new Dictionary<PKMember, Duration>();
// Sum up all switch durations for each member
// switches with multiple members will result in the duration to add up to more than the actual period range var noFronterDuration = Duration.Zero;
var actualStart = periodEnd; // will be "pulled" down // Sum up all switch durations for each member
var actualEnd = periodStart; // will be "pulled" up // switches with multiple members will result in the duration to add up to more than the actual period range
foreach (var sw in await GetTruncatedSwitchList(system, periodStart, periodEnd)) var actualStart = periodEnd; // will be "pulled" down
{ var actualEnd = periodStart; // will be "pulled" up
var span = sw.TimespanEnd - sw.TimespanStart;
foreach (var member in sw.Members) foreach (var sw in await GetTruncatedSwitchList(system, periodStart, periodEnd))
{ {
if (!dict.ContainsKey(member)) dict.Add(member, span); var span = sw.TimespanEnd - sw.TimespanStart;
else dict[member] += span; foreach (var member in sw.Members)
} {
if (!dict.ContainsKey(member)) dict.Add(member, span);
if (sw.Members.Count == 0) noFronterDuration += span; else dict[member] += span;
}
if (sw.TimespanStart < actualStart) actualStart = sw.TimespanStart;
if (sw.TimespanEnd > actualEnd) actualEnd = sw.TimespanEnd; if (sw.Members.Count == 0) noFronterDuration += span;
}
if (sw.TimespanStart < actualStart) actualStart = sw.TimespanStart;
return new PerMemberSwitchDuration if (sw.TimespanEnd > actualEnd) actualEnd = sw.TimespanEnd;
{ }
MemberSwitchDurations = dict,
NoFronterDuration = noFronterDuration, return new PerMemberSwitchDuration
RangeStart = actualStart, {
RangeEnd = actualEnd MemberSwitchDurations = dict,
}; NoFronterDuration = noFronterDuration,
} RangeStart = actualStart,
} RangeEnd = actualEnd
};
}
}
} }