Added max member count to limits

A given system can now have up to 1000 members. Within 50 members of that limit, a warning will display whenever a new member is created via the bot. Once the limit is reached, a final warning will appear indicating that no additional members can be created unless members are first deleted. Attempting to create a new member at that point by any method will result in an error message indicating that the limit has been reached.

Respecting this in pk;import required some restructuring to tease apart which members already exist and which ones need to be created prior to creating any members as it seems preferable to fail early and give the user the ability to intervene rather than pushing the system to the member cap and requiring manual deletion of "lower priority" members before others can be created. One consequence of the restructure is that existing members are being read in bulk which is a performance improvement of 25-70% depending on how many switches need to be imported (the more members you have, the more noticeable this is).
This commit is contained in:
Noko
2019-10-20 02:16:57 -05:00
parent a277f4fb87
commit 397da2e1fa
7 changed files with 111 additions and 60 deletions

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Text;
using PluralKit.Core;
using Serilog;
namespace PluralKit.Bot
@@ -70,38 +71,22 @@ namespace PluralKit.Bot
};
}
private async Task<DataFileMember> ExportMember(PKMember member) => new DataFileMember
{
Id = member.Hid,
Name = member.Name,
DisplayName = member.DisplayName,
Description = member.Description,
Birthday = member.Birthday != null ? Formats.DateExportFormat.Format(member.Birthday.Value) : null,
Pronouns = member.Pronouns,
Color = member.Color,
AvatarUrl = member.AvatarUrl,
Prefix = member.Prefix,
Suffix = member.Suffix,
Created = Formats.TimestampExportFormat.Format(member.Created),
MessageCount = await _members.MessageCount(member)
};
private async Task<DataFileSwitch> ExportSwitch(PKSwitch sw) => new DataFileSwitch
{
Members = (await _switches.GetSwitchMembers(sw)).Select(m => m.Hid).ToList(),
Timestamp = Formats.TimestampExportFormat.Format(sw.Timestamp)
};
public async Task<ImportResult> ImportSystem(DataFileSystem data, PKSystem system, ulong accountId)
{
// TODO: make atomic, somehow - we'd need to obtain one IDbConnection and reuse it
// which probably means refactoring SystemStore.Save and friends etc
var result = new ImportResult {AddedNames = new List<string>(), ModifiedNames = new List<string>()};
var hidMapping = new Dictionary<string, PKMember>();
var result = new ImportResult {
AddedNames = new List<string>(),
ModifiedNames = new List<string>(),
Success = true // Assume success unless indicated otherwise
};
var dataFileToMemberMapping = new Dictionary<string, PKMember>();
var unmappedMembers = new List<DataFileMember>();
// If we don't already have a system to save to, create one
if (system == null) system = await _systems.Create(data.Name);
if (system == null)
system = await _systems.Create(data.Name);
result.System = system;
// Apply system info
system.Name = data.Name;
@@ -110,41 +95,56 @@ namespace PluralKit.Bot
if (data.AvatarUrl != null) system.AvatarUrl = data.AvatarUrl;
if (data.TimeZone != null) system.UiTz = data.TimeZone ?? "UTC";
await _systems.Save(system);
// Make sure to link the sender account, too
await _systems.Link(system, accountId);
// Apply members
// TODO: parallelize?
foreach (var dataMember in data.Members)
// Determine which members already exist and which ones need to be created
var existingMembers = await _members.GetBySystem(system);
foreach (var d in data.Members)
{
// If member's given an ID, we try to look up the member with the given ID
PKMember member = null;
if (dataMember.Id != null)
// Try to look up the member with the given ID
var match = existingMembers.FirstOrDefault(m => m.Hid.Equals(d.Id));
if (match == null)
match = existingMembers.FirstOrDefault(m => m.Name.Equals(d.Name)); // Try with the name instead
if (match != null)
{
member = await _members.GetByHid(dataMember.Id);
// ...but if it's a different system's member, we just make a new one anyway
if (member != null && member.System != system.Id) member = null;
}
// Try to look up by name, too
if (member == null) member = await _members.GetByName(system, dataMember.Name);
// And if all else fails (eg. fresh import from Tupperbox, etc) we just make a member lol
if (member == null)
{
member = await _members.Create(system, dataMember.Name);
result.AddedNames.Add(dataMember.Name);
dataFileToMemberMapping.Add(d.Id, match); // Relate the data file ID to the PKMember for importing switches
result.ModifiedNames.Add(d.Name);
}
else
{
result.ModifiedNames.Add(dataMember.Name);
unmappedMembers.Add(d); // Track members that weren't found so we can create them all
result.AddedNames.Add(d.Name);
}
}
// Keep track of what the data file's member ID maps to for switch import
if (!hidMapping.ContainsKey(dataMember.Id))
hidMapping.Add(dataMember.Id, member);
// If creating the unmatched members would put us over the member limit, abort before creating any members
// new total: # in the system + (# in the file - # in the file that already exist)
if (data.Members.Count - dataFileToMemberMapping.Count + existingMembers.Count() >= Limits.MaxMemberCount)
{
result.Success = false;
result.Message = $"Import would exceed the maximum number of members ({Limits.MaxMemberCount}).";
result.AddedNames.Clear();
result.ModifiedNames.Clear();
return result;
}
// Create all unmapped members in one transaction
// 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);
foreach (var member in newMembers)
dataFileToMemberMapping.Add(member.Key, member.Value);
// Update members with data file properties
// TODO: parallelize?
foreach (var dataMember in data.Members)
{
dataFileToMemberMapping.TryGetValue(dataMember.Id, out PKMember member);
if (member == null)
continue;
// Apply member info
member.Name = dataMember.Name;
@@ -161,7 +161,7 @@ namespace PluralKit.Bot
if (dataMember.Birthday != null)
{
var birthdayParse = Formats.DateExportFormat.Parse(dataMember.Birthday);
member.Birthday = birthdayParse.Success ? (LocalDate?) birthdayParse.Value : null;
member.Birthday = birthdayParse.Success ? (LocalDate?)birthdayParse.Value : null;
}
await _members.Save(member);
@@ -174,7 +174,7 @@ namespace PluralKit.Bot
var timestamp = InstantPattern.ExtendedIso.Parse(sw.Timestamp).Value;
var swMembers = new List<PKMember>();
swMembers.AddRange(sw.Members.Select(x =>
hidMapping.FirstOrDefault(y => y.Key.Equals(x)).Value));
dataFileToMemberMapping.FirstOrDefault(y => y.Key.Equals(x)).Value));
var mapped = new Tuple<Instant, ICollection<PKMember>>(timestamp, swMembers);
mappedSwitches.Add(mapped);
}
@@ -182,8 +182,6 @@ namespace PluralKit.Bot
await _switches.RegisterSwitches(system, mappedSwitches);
_logger.Information("Imported system {System}", system.Id);
result.System = system;
return result;
}
}
@@ -193,6 +191,8 @@ namespace PluralKit.Bot
public ICollection<string> AddedNames;
public ICollection<string> ModifiedNames;
public PKSystem System;
public bool Success;
public string Message;
}
public struct DataFileSystem