2020-02-12 14:16:19 +00:00
using System.Collections.Generic ;
2019-08-14 05:16:48 +00:00
using System.Linq ;
using System.Threading.Tasks ;
2019-12-21 20:42:06 +00:00
2019-08-14 05:16:48 +00:00
using Dapper ;
2020-02-12 14:16:19 +00:00
using NodaTime ;
2020-02-01 13:40:57 +00:00
2019-08-14 05:16:48 +00:00
using Serilog ;
2020-02-12 14:16:19 +00:00
namespace PluralKit.Core {
2019-10-26 17:45:30 +00:00
public class PostgresDataStore : IDataStore {
2019-08-14 05:16:48 +00:00
private DbConnectionFactory _conn ;
private ILogger _logger ;
2020-02-01 13:40:57 +00:00
private ProxyCache _cache ;
2019-08-14 05:16:48 +00:00
2020-02-01 13:40:57 +00:00
public PostgresDataStore ( DbConnectionFactory conn , ILogger logger , ProxyCache cache )
2019-08-14 05:16:48 +00:00
{
2019-10-26 17:45:30 +00:00
_conn = conn ;
_logger = logger ;
2020-02-01 13:40:57 +00:00
_cache = cache ;
2019-08-14 05:16:48 +00:00
}
2019-12-21 20:42:06 +00:00
public async Task < IEnumerable < PKMember > > GetConflictingProxies ( PKSystem system , ProxyTag tag )
{
using ( var conn = await _conn . Obtain ( ) )
// return await conn.QueryAsync<PKMember>("select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix ilike @Prefix and suffix ilike @Suffix", new
// {
// System = system.Id,
// Prefix = tag.Prefix.Replace("%", "\\%") + "%",
// Suffix = "%" + tag.Suffix.Replace("%", "\\%")
// });
return await conn . QueryAsync < PKMember > ( "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix = @Prefix and suffix = @Suffix" , new
{
System = system . Id ,
Prefix = tag . Prefix ,
Suffix = tag . Suffix
} ) ;
}
2019-12-22 13:15:56 +00:00
public async Task < SystemGuildSettings > GetSystemGuildSettings ( PKSystem system , ulong guild )
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . QuerySingleOrDefaultAsync < SystemGuildSettings > (
2020-02-12 14:16:19 +00:00
"select * from system_guild where system = @System and guild = @Guild" ,
new { System = system . Id , Guild = guild } ) ? ? new SystemGuildSettings ( ) ;
2019-12-22 13:15:56 +00:00
}
2019-12-26 19:39:47 +00:00
public async Task SetSystemGuildSettings ( PKSystem system , ulong guild , SystemGuildSettings settings )
2019-12-22 13:15:56 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
2020-01-23 20:20:22 +00:00
await conn . ExecuteAsync ( "insert into system_guild (system, guild, proxy_enabled, autoproxy_mode, autoproxy_member) values (@System, @Guild, @ProxyEnabled, @AutoproxyMode, @AutoproxyMember) on conflict (system, guild) do update set proxy_enabled = @ProxyEnabled, autoproxy_mode = @AutoproxyMode, autoproxy_member = @AutoproxyMember" , new
2019-12-22 13:15:56 +00:00
{
System = system . Id ,
Guild = guild ,
2020-01-23 20:20:22 +00:00
settings . ProxyEnabled ,
settings . AutoproxyMode ,
settings . AutoproxyMember
2019-12-22 13:15:56 +00:00
} ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( system ) ;
2020-02-12 18:07:40 +00:00
_logger . Information ( "Updated system guild settings {@SystemGuildSettings}" , settings ) ;
2019-12-22 13:15:56 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task < PKSystem > CreateSystem ( string systemName = null ) {
2019-08-14 05:16:48 +00:00
string hid ;
do
{
2020-02-12 14:16:19 +00:00
hid = StringUtils . GenerateHid ( ) ;
2019-10-26 17:45:30 +00:00
} while ( await GetSystemByHid ( hid ) ! = null ) ;
2019-08-14 05:16:48 +00:00
PKSystem system ;
using ( var conn = await _conn . Obtain ( ) )
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 ) ;
2020-02-01 13:40:57 +00:00
// New system has no accounts, therefore nothing gets cached, therefore no need to invalidate caches right here
2019-08-14 05:16:48 +00:00
return system ;
}
2019-10-26 17:45:30 +00:00
public async Task AddAccount ( PKSystem system , ulong accountId ) {
2019-08-14 05:16:48 +00:00
// 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 ( ) )
await conn . ExecuteAsync ( "insert into accounts (uid, system) values (@Id, @SystemId) on conflict do nothing" , new { Id = accountId , SystemId = system . Id } ) ;
_logger . Information ( "Linked system {System} to account {Account}" , system . Id , accountId ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( system ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task RemoveAccount ( PKSystem system , ulong accountId ) {
2019-08-14 05:16:48 +00:00
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 ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( system ) ;
2020-02-03 14:04:53 +00:00
_cache . InvalidateAccounts ( new [ ] { accountId } ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task < PKSystem > GetSystemByAccount ( ulong accountId ) {
2019-08-14 05:16:48 +00:00
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 } ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < PKSystem > GetSystemByHid ( string hid ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
return await conn . QuerySingleOrDefaultAsync < PKSystem > ( "select * from systems where systems.hid = @Hid" , new { Hid = hid . ToLower ( ) } ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < PKSystem > GetSystemByToken ( string token ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
return await conn . QuerySingleOrDefaultAsync < PKSystem > ( "select * from systems where token = @Token" , new { Token = token } ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < PKSystem > GetSystemById ( int id )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . QuerySingleOrDefaultAsync < PKSystem > ( "select * from systems where id = @Id" , new { Id = id } ) ;
}
2019-10-26 17:45:30 +00:00
public async Task SaveSystem ( PKSystem system ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
2020-01-11 15:49:20 +00:00
await conn . ExecuteAsync ( "update systems set name = @Name, description = @Description, tag = @Tag, avatar_url = @AvatarUrl, token = @Token, ui_tz = @UiTz, description_privacy = @DescriptionPrivacy, member_list_privacy = @MemberListPrivacy, front_privacy = @FrontPrivacy, front_history_privacy = @FrontHistoryPrivacy where id = @Id" , system ) ;
2019-08-14 05:16:48 +00:00
_logger . Information ( "Updated system {@System}" , system ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( system ) ;
2019-08-14 05:16:48 +00:00
}
2020-02-03 13:47:29 +00:00
public async Task DeleteSystem ( PKSystem system )
{
using var conn = await _conn . Obtain ( ) ;
// Fetch the list of accounts *before* deletion so we can cache-bust all of those
var accounts = ( await conn . QueryAsync < ulong > ( "select uid from accounts where system = @Id" , system ) ) . ToArray ( ) ;
await conn . ExecuteAsync ( "delete from systems where id = @Id" , system ) ;
2020-02-01 13:40:57 +00:00
2019-08-14 05:16:48 +00:00
_logger . Information ( "Deleted system {System}" , system . Id ) ;
2020-02-03 13:47:29 +00:00
_cache . InvalidateDeletedSystem ( system . Id , accounts ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task < IEnumerable < ulong > > GetSystemAccounts ( PKSystem system )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . QueryAsync < ulong > ( "select uid from accounts where system = @Id" , new { Id = system . Id } ) ;
}
2019-11-02 21:46:51 +00:00
public async Task DeleteAllSwitches ( PKSystem system )
{
using ( var conn = await _conn . Obtain ( ) )
await conn . ExecuteAsync ( "delete from switches where system = @Id" , system ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < ulong > GetTotalSystems ( )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . ExecuteScalarAsync < ulong > ( "select count(id) from systems" ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < PKMember > CreateMember ( PKSystem system , string name ) {
2019-08-14 05:16:48 +00:00
string hid ;
do
{
2020-02-12 14:16:19 +00:00
hid = StringUtils . GenerateHid ( ) ;
2019-10-26 17:45:30 +00:00
} while ( await GetMemberByHid ( hid ) ! = null ) ;
2019-08-14 05:16:48 +00:00
PKMember member ;
using ( var conn = await _conn . Obtain ( ) )
member = await conn . QuerySingleAsync < PKMember > ( "insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *" , new {
Hid = hid ,
SystemID = system . Id ,
Name = name
} ) ;
_logger . Information ( "Created member {Member}" , member . Id ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( system ) ;
2019-08-14 05:16:48 +00:00
return member ;
}
2019-10-26 17:45:30 +00:00
public async Task < Dictionary < string , PKMember > > CreateMembersBulk ( PKSystem system , Dictionary < string , string > names )
2019-10-20 07:16:57 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
using ( var tx = conn . BeginTransaction ( ) )
{
var results = new Dictionary < string , PKMember > ( ) ;
foreach ( var name in names )
{
string hid ;
do
{
hid = await conn . QuerySingleOrDefaultAsync < string > ( "SELECT @Hid WHERE NOT EXISTS (SELECT id FROM members WHERE hid = @Hid LIMIT 1)" , new
{
2020-02-12 14:16:19 +00:00
Hid = StringUtils . GenerateHid ( )
2019-10-20 07:16:57 +00:00
} ) ;
} while ( hid = = null ) ;
var member = await conn . QuerySingleAsync < PKMember > ( "INSERT INTO members (hid, system, name) VALUES (@Hid, @SystemId, @Name) RETURNING *" , new
{
Hid = hid ,
SystemID = system . Id ,
Name = name . Value
} ) ;
results . Add ( name . Key , member ) ;
}
tx . Commit ( ) ;
_logger . Information ( "Created {MemberCount} members for system {SystemID}" , names . Count ( ) , system . Hid ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( system ) ;
2019-10-20 07:16:57 +00:00
return results ;
}
}
2019-10-26 17:45:30 +00:00
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 ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
return await conn . QuerySingleOrDefaultAsync < PKMember > ( "select * from members where hid = @Hid" , new { Hid = hid . ToLower ( ) } ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < PKMember > GetMemberByName ( PKSystem system , string name ) {
2019-08-14 05:16:48 +00:00
// 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 } ) ;
}
2020-01-17 23:58:35 +00:00
public IAsyncEnumerable < PKMember > GetSystemMembers ( PKSystem system , bool orderByName )
{
var sql = "select * from members where system = @SystemID" ;
if ( orderByName ) sql + = " order by lower(name) asc" ;
return _conn . QueryStreamAsync < PKMember > ( sql , new { SystemID = system . Id } ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task SaveMember ( PKMember member ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
2020-01-11 15:49:20 +00:00
await conn . ExecuteAsync ( "update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, proxy_tags = @ProxyTags, keep_proxy = @KeepProxy, member_privacy = @MemberPrivacy where id = @Id" , member ) ;
2019-08-14 05:16:48 +00:00
_logger . Information ( "Updated member {@Member}" , member ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( member . System ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task DeleteMember ( PKMember member ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
await conn . ExecuteAsync ( "delete from members where id = @Id" , member ) ;
_logger . Information ( "Deleted member {@Member}" , member ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( member . System ) ;
2019-08-14 05:16:48 +00:00
}
2019-12-26 19:39:47 +00:00
public async Task < MemberGuildSettings > GetMemberGuildSettings ( PKMember member , ulong guild )
{
using var conn = await _conn . Obtain ( ) ;
return await conn . QuerySingleOrDefaultAsync < MemberGuildSettings > (
2020-02-12 14:16:19 +00:00
"select * from member_guild where member = @Member and guild = @Guild" , new { Member = member . Id , Guild = guild } )
2020-02-12 16:42:12 +00:00
? ? new MemberGuildSettings { Guild = guild , Member = member . Id } ;
2019-12-26 19:39:47 +00:00
}
public async Task SetMemberGuildSettings ( PKMember member , ulong guild , MemberGuildSettings settings )
{
using var conn = await _conn . Obtain ( ) ;
await conn . ExecuteAsync (
2020-02-12 16:42:12 +00:00
"insert into member_guild (member, guild, display_name, avatar_url) values (@Member, @Guild, @DisplayName, @AvatarUrl) on conflict (member, guild) do update set display_name = @DisplayName, avatar_url = @AvatarUrl" ,
settings ) ;
2020-02-12 18:07:40 +00:00
_logger . Information ( "Updated member guild settings {@MemberGuildSettings}" , settings ) ;
2020-02-01 13:40:57 +00:00
await _cache . InvalidateSystem ( member . System ) ;
2019-12-26 19:39:47 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task < ulong > GetMemberMessageCount ( PKMember member )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
2019-10-26 17:45:30 +00:00
return await conn . QuerySingleAsync < ulong > ( "select count(*) from messages where member = @Id" , member ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task < IEnumerable < MemberMessageCount > > GetMemberMessageCountBulk ( PKSystem system )
2019-10-06 07:03:28 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
2019-10-26 17:45:30 +00:00
return await conn . QueryAsync < MemberMessageCount > (
2019-10-06 07:03:28 +00:00
@ "SELECT messages.member, COUNT(messages.member) messagecount
FROM members
JOIN messages
ON members . id = messages . member
WHERE members . system = @System
GROUP BY messages . member ",
new { System = system . Id } ) ;
}
2020-01-11 15:49:20 +00:00
public async Task < int > GetSystemMemberCount ( PKSystem system , bool includePrivate )
2019-08-14 05:16:48 +00:00
{
2020-01-11 15:49:20 +00:00
var query = "select count(*) from members where system = @Id" ;
2020-01-17 16:54:07 +00:00
if ( ! includePrivate ) query + = " and member_privacy = 1" ; // 1 = public
2020-01-11 15:49:20 +00:00
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
2020-01-11 15:49:20 +00:00
return await conn . ExecuteScalarAsync < int > ( query , system ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-26 17:45:30 +00:00
public async Task < ulong > GetTotalMembers ( )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . ExecuteScalarAsync < ulong > ( "select count(id) from members" ) ;
}
2020-01-24 19:28:48 +00:00
public async Task AddMessage ( ulong senderId , ulong messageId , ulong guildId , ulong channelId , ulong originalMessage , PKMember member ) {
2019-08-14 05:16:48 +00:00
using ( var conn = await _conn . Obtain ( ) )
2020-01-24 19:28:48 +00:00
await conn . ExecuteAsync ( "insert into messages(mid, guild, channel, member, sender, original_mid) values(@MessageId, @GuildId, @ChannelId, @MemberId, @SenderId, @OriginalMid)" , new {
2019-08-14 05:16:48 +00:00
MessageId = messageId ,
2020-01-24 19:28:48 +00:00
GuildId = guildId ,
2019-08-14 05:16:48 +00:00
ChannelId = channelId ,
MemberId = member . Id ,
SenderId = senderId ,
OriginalMid = originalMessage
} ) ;
_logger . Information ( "Stored message {Message} in channel {Channel}" , messageId , channelId ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < FullMessage > GetMessage ( ulong id )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
2019-10-26 17:45:30 +00:00
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
2019-08-14 05:16:48 +00:00
{
Message = msg ,
System = system ,
Member = member
} , new { Id = id } ) ) . FirstOrDefault ( ) ;
}
2019-10-26 17:45:30 +00:00
public async Task DeleteMessage ( ulong id ) {
2019-08-14 05:16:48 +00:00
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 ) ;
}
2019-10-26 17:45:30 +00:00
public async Task DeleteMessagesBulk ( IEnumerable < ulong > ids )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
{
// 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)
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 ) ;
}
}
2020-01-24 19:28:48 +00:00
public async Task < FullMessage > GetLastMessageInGuild ( ulong account , ulong guild )
{
using var conn = await _conn . Obtain ( ) ;
return ( await conn . QueryAsync < PKMessage , PKMember , PKSystem , FullMessage > ( "select messages.*, members.*, systems.* from messages, members, systems where messages.guild = @Guild and messages.sender = @Uid and messages.member = members.id and systems.id = members.system order by mid desc limit 1" , ( msg , member , system ) = > new FullMessage
{
Message = msg ,
System = system ,
Member = member
} , new { Uid = account , Guild = guild } ) ) . FirstOrDefault ( ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < ulong > GetTotalMessages ( )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . ExecuteScalarAsync < ulong > ( "select count(mid) from messages" ) ;
}
2019-11-03 18:15:50 +00:00
// Same as GuildConfig, but with ISet<ulong> as long[] instead.
2020-02-01 13:40:57 +00:00
public struct DatabaseCompatibleGuildConfig
2019-11-03 18:15:50 +00:00
{
public ulong Id { get ; set ; }
public ulong? LogChannel { get ; set ; }
public long [ ] LogBlacklist { get ; set ; }
public long [ ] Blacklist { get ; set ; }
2020-02-14 23:12:03 +00:00
public bool LogCleanupEnabled { get ; set ; }
2019-12-28 11:00:52 +00:00
public GuildConfig Into ( ) = >
new GuildConfig
{
Id = Id ,
LogChannel = LogChannel ,
LogBlacklist = new HashSet < ulong > ( LogBlacklist ? . Select ( c = > ( ulong ) c ) ? ? new ulong [ ] { } ) ,
2020-02-14 23:12:03 +00:00
Blacklist = new HashSet < ulong > ( Blacklist ? . Select ( c = > ( ulong ) c ) ? ? new ulong [ ] { } ) ,
LogCleanupEnabled = LogCleanupEnabled
2019-12-28 11:00:52 +00:00
} ;
2019-11-03 18:15:50 +00:00
}
public async Task < GuildConfig > GetOrCreateGuildConfig ( ulong guild )
2019-10-27 22:01:20 +00:00
{
2020-02-01 13:40:57 +00:00
// When changing this, also see ProxyCache::GetGuildDataCached
2019-10-27 22:01:20 +00:00
using ( var conn = await _conn . Obtain ( ) )
{
2019-12-28 11:00:52 +00:00
return ( await conn . QuerySingleOrDefaultAsync < DatabaseCompatibleGuildConfig > (
2019-11-03 18:15:50 +00:00
"insert into servers (id) values (@Id) on conflict do nothing; select * from servers where id = @Id" ,
2019-12-28 11:00:52 +00:00
new { Id = guild } ) ) . Into ( ) ;
2019-10-27 22:01:20 +00:00
}
}
public async Task SaveGuildConfig ( GuildConfig cfg )
{
using ( var conn = await _conn . Obtain ( ) )
2020-02-14 23:12:03 +00:00
await conn . ExecuteAsync ( "insert into servers (id, log_channel, log_blacklist, blacklist, log_cleanup_enabled) values (@Id, @LogChannel, @LogBlacklist, @Blacklist, @LogCleanupEnabled) on conflict (id) do update set log_channel = @LogChannel, log_blacklist = @LogBlacklist, blacklist = @Blacklist, log_cleanup_enabled = @LogCleanupEnabled" , new
2019-11-03 18:15:50 +00:00
{
cfg . Id ,
cfg . LogChannel ,
2020-02-14 23:12:03 +00:00
cfg . LogCleanupEnabled ,
2019-11-03 18:15:50 +00:00
LogBlacklist = cfg . LogBlacklist . Select ( c = > ( long ) c ) . ToList ( ) ,
Blacklist = cfg . Blacklist . Select ( c = > ( long ) c ) . ToList ( )
} ) ;
2019-10-27 22:01:20 +00:00
_logger . Information ( "Updated guild configuration {@GuildCfg}" , cfg ) ;
2020-02-01 13:40:57 +00:00
_cache . InvalidateGuild ( cfg . Id ) ;
2019-10-27 22:01:20 +00:00
}
2019-12-28 11:00:52 +00:00
2020-01-24 19:28:48 +00:00
public async Task < PKMember > GetFirstFronter ( PKSystem system )
{
// TODO: move to extension method since it doesn't rely on internals
var lastSwitch = await GetLatestSwitch ( system ) ;
if ( lastSwitch = = null ) return null ;
return await GetSwitchMembers ( lastSwitch ) . FirstOrDefaultAsync ( ) ;
}
2019-10-26 17:45:30 +00:00
public async Task AddSwitch ( PKSystem system , IEnumerable < PKMember > members )
2019-08-14 05:16:48 +00:00
{
// Use a transaction here since we're doing multiple executed commands in one
using ( var conn = await _conn . Obtain ( ) )
using ( var tx = conn . BeginTransaction ( ) )
{
// First, we insert the switch itself
var sw = await conn . QuerySingleAsync < PKSwitch > ( "insert into switches(system) values (@System) returning *" ,
new { System = system . Id } ) ;
// Then we insert each member in the switch in the switch_members table
// TODO: can we parallelize this or send it in bulk somehow?
foreach ( var member in members )
{
await conn . ExecuteAsync (
"insert into switch_members(switch, member) values(@Switch, @Member)" ,
new { Switch = sw . Id , Member = member . 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 ) ) ;
}
}
2019-10-26 17:45:30 +00:00
public async Task AddSwitchesBulk ( PKSystem system , IEnumerable < ImportedSwitch > switches )
2019-10-20 19:38:43 +00:00
{
// Read existing switches to enforce unique timestamps
2020-01-17 23:02:17 +00:00
var priorSwitches = new List < PKSwitch > ( ) ;
await foreach ( var sw in GetSwitches ( system ) ) priorSwitches . Add ( sw ) ;
2019-10-20 19:38:43 +00:00
var lastSwitchId = priorSwitches . Any ( )
? priorSwitches . Max ( x = > x . Id )
: 0 ;
using ( var conn = ( PerformanceTrackingConnection ) await _conn . Obtain ( ) )
{
using ( var tx = conn . BeginTransaction ( ) )
{
// Import switches in bulk
using ( var importer = conn . BeginBinaryImport ( "COPY switches (system, timestamp) FROM STDIN (FORMAT BINARY)" ) )
{
foreach ( var sw in switches )
{
// If there's already a switch at this time, move on
2019-10-26 17:45:30 +00:00
if ( priorSwitches . Any ( x = > x . Timestamp . Equals ( sw . Timestamp ) ) )
2019-10-20 19:38:43 +00:00
continue ;
// Otherwise, add it to the importer
importer . StartRow ( ) ;
importer . Write ( system . Id , NpgsqlTypes . NpgsqlDbType . Integer ) ;
2019-10-27 10:37:16 +00:00
importer . Write ( sw . Timestamp , NpgsqlTypes . NpgsqlDbType . Timestamp ) ;
2019-10-20 19:38:43 +00:00
}
importer . Complete ( ) ; // Commits the copy operation so dispose won't roll it back
}
// Get all switches that were created above and don't have members for ID lookup
var switchesWithoutMembers =
await conn . QueryAsync < PKSwitch > ( @ "
SELECT switches . *
FROM switches
LEFT JOIN switch_members
ON switch_members . switch = switches . id
WHERE switches . id > @LastSwitchId
AND switches . system = @System
AND switch_members . id IS NULL ", new { LastSwitchId = lastSwitchId, System = system.Id });
// Import switch_members in bulk
using ( var importer = conn . BeginBinaryImport ( "COPY switch_members (switch, member) FROM STDIN (FORMAT BINARY)" ) )
{
// Iterate over the switches we created above and set their members
foreach ( var pkSwitch in switchesWithoutMembers )
{
// If this isn't in our import set, move on
2019-10-26 17:45:30 +00:00
var sw = switches . Select ( x = > ( ImportedSwitch ? ) x ) . FirstOrDefault ( x = > x . Value . Timestamp . Equals ( pkSwitch . Timestamp ) ) ;
2019-10-20 19:38:43 +00:00
if ( sw = = null )
continue ;
// Loop through associated members to add each to the switch
2019-10-26 17:45:30 +00:00
foreach ( var m in sw . Value . Members )
2019-10-20 19:38:43 +00:00
{
// Skip switch-outs - these don't have switch_members
if ( m = = null )
continue ;
importer . StartRow ( ) ;
importer . Write ( pkSwitch . Id , NpgsqlTypes . NpgsqlDbType . Integer ) ;
importer . Write ( m . Id , NpgsqlTypes . NpgsqlDbType . Integer ) ;
}
}
importer . Complete ( ) ; // Commits the copy operation so dispose won't roll it back
}
tx . Commit ( ) ;
}
}
_logger . Information ( "Completed bulk import of switches for system {0}" , system . Hid ) ;
}
2020-01-17 23:02:17 +00:00
public IAsyncEnumerable < PKSwitch > GetSwitches ( PKSystem system )
2019-08-14 05:16:48 +00:00
{
// TODO: refactor the PKSwitch data structure to somehow include a hydrated member list
// (maybe when we get caching in?)
2020-01-17 23:02:17 +00:00
return _conn . QueryStreamAsync < PKSwitch > (
"select * from switches where system = @System order by timestamp desc" ,
new { System = system . Id } ) ;
2019-08-14 05:16:48 +00:00
}
2020-01-17 23:58:35 +00:00
public async Task < int > GetSwitchCount ( PKSystem system )
{
using var conn = await _conn . Obtain ( ) ;
return await conn . QuerySingleAsync < int > ( "select count(*) from switches where system = @Id" , system ) ;
}
public async IAsyncEnumerable < SwitchMembersListEntry > GetSwitchMembersList ( PKSystem system , Instant start , Instant end )
2019-10-05 20:08:27 +00:00
{
// Wrap multiple commands in a single transaction for performance
2020-01-17 23:58:35 +00:00
using var conn = await _conn . Obtain ( ) ;
using var tx = conn . BeginTransaction ( ) ;
// Find the time of the last switch outside the range as it overlaps the range
// If no prior switch exists, the lower bound of the range remains the start time
var lastSwitch = await conn . QuerySingleOrDefaultAsync < Instant > (
@ "SELECT COALESCE(MAX(timestamp), @Start)
2019-10-05 20:08:27 +00:00
FROM switches
WHERE switches . system = @System
AND switches . timestamp < @Start ",
2020-01-17 23:58:35 +00:00
new { System = system . Id , Start = start } ) ;
2019-10-05 20:08:27 +00:00
2020-01-17 23:58:35 +00:00
// Then collect the time and members of all switches that overlap the range
var switchMembersEntries = conn . QueryStreamAsync < SwitchMembersListEntry > (
@ "SELECT switch_members.member, switches.timestamp
2019-10-05 20:08:27 +00:00
FROM switches
2019-10-06 07:03:28 +00:00
LEFT JOIN switch_members
2019-10-05 20:08:27 +00:00
ON switches . id = switch_members . switch
WHERE switches . system = @System
AND (
switches . timestamp > = @Start
OR switches . timestamp = @LastSwitch
)
AND switches . timestamp < @End
ORDER BY switches . timestamp DESC ",
2020-01-17 23:58:35 +00:00
new { System = system . Id , Start = start , End = end , LastSwitch = lastSwitch } ) ;
2019-10-05 20:08:27 +00:00
2020-01-17 23:58:35 +00:00
// Yield each value here
await foreach ( var entry in switchMembersEntries )
yield return entry ;
// Don't really need to worry about the transaction here, we're not doing any *writes*
2019-10-05 20:08:27 +00:00
}
2020-01-17 23:02:17 +00:00
public IAsyncEnumerable < PKMember > GetSwitchMembers ( PKSwitch sw )
2019-08-14 05:16:48 +00:00
{
2020-01-17 23:02:17 +00:00
return _conn . QueryStreamAsync < PKMember > (
"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 } ) ;
2019-08-14 05:16:48 +00:00
}
2020-01-17 23:02:17 +00:00
public async Task < PKSwitch > GetLatestSwitch ( PKSystem system ) = >
await GetSwitches ( system ) . FirstOrDefaultAsync ( ) ;
2019-08-14 05:16:48 +00:00
public async Task MoveSwitch ( PKSwitch sw , Instant time )
{
using ( var conn = await _conn . Obtain ( ) )
await conn . ExecuteAsync ( "update switches set timestamp = @Time where id = @Id" ,
new { Time = time , Id = sw . Id } ) ;
_logger . Information ( "Moved switch {Switch} to {Time}" , sw . Id , time ) ;
}
public async Task DeleteSwitch ( PKSwitch sw )
{
using ( var conn = await _conn . Obtain ( ) )
await conn . ExecuteAsync ( "delete from switches where id = @Id" , new { Id = sw . Id } ) ;
_logger . Information ( "Deleted switch {Switch}" ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < ulong > GetTotalSwitches ( )
2019-08-14 05:16:48 +00:00
{
using ( var conn = await _conn . Obtain ( ) )
return await conn . ExecuteScalarAsync < ulong > ( "select count(id) from switches" ) ;
}
2019-10-26 17:45:30 +00:00
public async Task < IEnumerable < SwitchListEntry > > GetPeriodFronters ( PKSystem system , Instant periodStart , Instant periodEnd )
2019-08-14 05:16:48 +00:00
{
2020-01-17 23:58:35 +00:00
// TODO: IAsyncEnumerable-ify this one
2019-10-05 20:08:27 +00:00
// Returns the timestamps and member IDs of switches overlapping the range, in chronological (newest first) order
2020-01-17 23:58:35 +00:00
var switchMembers = await GetSwitchMembersList ( system , periodStart , periodEnd ) . ToListAsync ( ) ;
2019-08-14 05:16:48 +00:00
// query DB for all members involved in any of the switches above and collect into a dictionary for future use
// this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary
// key used in GetPerMemberSwitchDuration below
Dictionary < int , PKMember > memberObjects ;
using ( var conn = await _conn . Obtain ( ) )
{
2019-10-05 20:08:27 +00:00
memberObjects = (
await conn . QueryAsync < PKMember > (
"select * from members where id = any(@Switches)" , // lol postgres specific `= any()` syntax
2020-01-25 15:42:45 +00:00
new { Switches = switchMembers . Select ( m = > m . Member ) . Distinct ( ) . ToList ( ) } )
2020-02-12 14:16:19 +00:00
) . ToDictionary ( m = > m . Id ) ;
2019-08-14 05:16:48 +00:00
}
2019-10-05 20:08:27 +00:00
// Initialize entries - still need to loop to determine the TimespanEnd below
var entries =
from item in switchMembers
group item by item . Timestamp into g
select new SwitchListEntry
{
TimespanStart = g . Key ,
2019-10-06 07:03:28 +00:00
Members = g . Where ( x = > x . Member ! = 0 ) . Select ( x = > memberObjects [ x . Member ] ) . ToList ( )
2019-10-05 20:08:27 +00:00
} ;
2019-08-14 05:16:48 +00:00
2019-10-05 20:08:27 +00:00
// Loop through every switch that overlaps the range and add it to the output list
// end time is the *FOLLOWING* switch's timestamp - we cheat by working backwards from the range end, so no dates need to be compared
2019-08-14 05:16:48 +00:00
var endTime = periodEnd ;
2019-10-05 20:08:27 +00:00
var outList = new List < SwitchListEntry > ( ) ;
foreach ( var e in entries )
2019-08-14 05:16:48 +00:00
{
2019-10-05 20:08:27 +00:00
// Override the start time of the switch if it's outside the range (only true for the "out of range" switch we included above)
var switchStartClamped = e . TimespanStart < periodStart
? periodStart
: e . TimespanStart ;
2019-08-14 05:16:48 +00:00
outList . Add ( new SwitchListEntry
{
2019-10-05 20:08:27 +00:00
Members = e . Members ,
2019-08-14 05:16:48 +00:00
TimespanStart = switchStartClamped ,
TimespanEnd = endTime
} ) ;
2019-10-05 20:08:27 +00:00
// next switch's end is this switch's start (we're working backward in time)
endTime = e . TimespanStart ;
2019-08-14 05:16:48 +00:00
}
return outList ;
}
2019-10-26 17:45:30 +00:00
public async Task < FrontBreakdown > GetFrontBreakdown ( PKSystem system , Instant periodStart , Instant periodEnd )
2019-08-14 05:16:48 +00:00
{
var dict = new Dictionary < PKMember , Duration > ( ) ;
var noFronterDuration = Duration . Zero ;
// 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 actualStart = periodEnd ; // will be "pulled" down
var actualEnd = periodStart ; // will be "pulled" up
2019-10-26 17:45:30 +00:00
foreach ( var sw in await GetPeriodFronters ( system , periodStart , periodEnd ) )
2019-08-14 05:16:48 +00:00
{
var span = sw . TimespanEnd - sw . TimespanStart ;
foreach ( var member in sw . Members )
{
if ( ! dict . ContainsKey ( member ) ) dict . Add ( member , span ) ;
else dict [ member ] + = span ;
}
if ( sw . Members . Count = = 0 ) noFronterDuration + = span ;
if ( sw . TimespanStart < actualStart ) actualStart = sw . TimespanStart ;
if ( sw . TimespanEnd > actualEnd ) actualEnd = sw . TimespanEnd ;
}
2019-10-26 17:45:30 +00:00
return new FrontBreakdown
2019-08-14 05:16:48 +00:00
{
MemberSwitchDurations = dict ,
NoFronterDuration = noFronterDuration ,
RangeStart = actualStart ,
RangeEnd = actualEnd
} ;
}
}
2019-04-19 18:48:37 +00:00
}