Add importing and exporting function

This commit is contained in:
Ske 2019-06-14 22:48:19 +02:00
parent cd9a3e0abd
commit 652afffb8c
7 changed files with 321 additions and 2 deletions

View File

@ -68,6 +68,8 @@ namespace PluralKit.Bot
.AddTransient<EmbedService>()
.AddTransient<ProxyService>()
.AddTransient<LogChannelService>()
.AddTransient<DataFileService>()
.AddSingleton<WebhookCacheService>()
.AddTransient<SystemStore>()

View File

@ -0,0 +1,85 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Discord.Commands;
using Newtonsoft.Json;
namespace PluralKit.Bot.Commands
{
public class ImportExportCommands : ModuleBase<PKCommandContext>
{
public DataFileService DataFiles { get; set; }
[Command("import")]
[Remarks("import [fileurl]")]
public async Task Import([Remainder] string url = null)
{
if (url == null) url = Context.Message.Attachments.FirstOrDefault()?.Filename;
if (url == null) throw Errors.NoImportFilePassed;
await Context.BusyIndicator(async () =>
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode) throw Errors.InvalidImportFile;
var str = await response.Content.ReadAsStringAsync();
var data = TryDeserialize(str);
if (!data.HasValue || !data.Value.Valid) throw Errors.InvalidImportFile;
if (Context.SenderSystem != null && Context.SenderSystem.Hid != data.Value.Id)
{
// TODO: prompt "are you sure you want to import someone else's system?
}
// If passed system is null, it'll create a new one
// (and that's okay!)
var result = await DataFiles.ImportSystem(data.Value, Context.SenderSystem);
if (Context.SenderSystem == null)
{
await Context.Channel.SendMessageAsync($"{Emojis.Success} PluralKit has created a system for you based on the given file. Your system ID is `{result.System.Hid}`. Type `pk;system` for more information.");
}
else
{
await Context.Channel.SendMessageAsync($"{Emojis.Success} Updated {result.ModifiedNames.Count} members, created {result.AddedNames.Count} members. Type `pk;system list` to check!");
}
}
});
}
[Command("export")]
[Remarks("export")]
[MustHaveSystem]
public async Task Export()
{
await Context.BusyIndicator(async () =>
{
var data = await DataFiles.ExportSystem(Context.SenderSystem);
var json = JsonConvert.SerializeObject(data, Formatting.None);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
await Context.Channel.SendFileAsync(stream, "system.json", $"{Emojis.Success} Here you go!");
});
}
private DataFileSystem? TryDeserialize(string json)
{
try
{
return JsonConvert.DeserializeObject<DataFileSystem>(json);
}
catch (JsonException e)
{
Console.WriteLine("uww");
}
return null;
}
}
}

View File

@ -60,7 +60,8 @@ namespace PluralKit.Bot {
public static PKError InvalidTimeZone(string zoneStr) => new PKError($"Invalid time zone ID '{zoneStr}'. To find your time zone ID, use the following website: <https://xske.github.io/tz>");
public static PKError TimezoneChangeCancelled => new PKError("Time zone change cancelled.");
public static PKError AmbiguousTimeZone(string zoneStr, int count) => new PKError($"The time zone query '{zoneStr}' resulted in **{count}** different time zone regions. Try being more specific - e.g. pass an exact time zone specifier from the following website: <https://xske.github.io/tz>");
public static Exception NoImportFilePassed => new PKError("You must either pass an URL to a file as a command parameter, or as an attachment to the message containing the command.");
public static Exception InvalidImportFile => new PKError("Imported data file invalid. Make sure this is a .json file directly exported from PluralKit or Tupperbox.");
}
}

223
PluralKit.Core/DataFiles.cs Normal file
View File

@ -0,0 +1,223 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Text;
using NodaTime.TimeZones;
namespace PluralKit.Bot
{
public class DataFileService
{
private SystemStore _systems;
private MemberStore _members;
private SwitchStore _switches;
public DataFileService(SystemStore systems, MemberStore members, SwitchStore switches)
{
_systems = systems;
_members = members;
_switches = switches;
}
public async Task<DataFileSystem> ExportSystem(PKSystem system)
{
var members = new List<DataFileMember>();
foreach (var member in await _members.GetBySystem(system)) members.Add(await ExportMember(member));
var switches = new List<DataFileSwitch>();
foreach (var sw in await _switches.GetSwitches(system, 999999)) switches.Add(await ExportSwitch(sw));
return new DataFileSystem
{
Id = system.Hid,
Name = system.Name,
Description = system.Description,
Tag = system.Tag,
AvatarUrl = system.AvatarUrl,
TimeZone = system.UiTz,
Members = members,
Switches = switches,
Created = system.Created.ToString(Formats.TimestampExportFormat, null),
LinkedAccounts = (await _systems.GetLinkedAccountIds(system)).ToList()
};
}
private async Task<DataFileMember> ExportMember(PKMember member) => new DataFileMember
{
Id = member.Hid,
Name = member.Name,
Description = member.Description,
Birthdate = member.Birthday?.ToString(Formats.DateExportFormat, null),
Pronouns = member.Pronouns,
Color = member.Color,
AvatarUrl = member.AvatarUrl,
Prefix = member.Prefix,
Suffix = member.Suffix,
Created = member.Created.ToString(Formats.TimestampExportFormat, null),
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 = sw.Timestamp.ToString(Formats.TimestampExportFormat, null)
};
public async Task<ImportResult> ImportSystem(DataFileSystem data, PKSystem system)
{
var result = new ImportResult { AddedNames = new List<string>(), ModifiedNames = new List<string>() };
// If we don't already have a system to save to, create one
if (system == null) system = await _systems.Create(data.Name);
// Apply system info
system.Name = data.Name;
system.Description = data.Description;
system.Tag = data.Tag;
system.AvatarUrl = data.AvatarUrl;
system.UiTz = data.TimeZone ?? "UTC";
await _systems.Save(system);
// Apply members
// TODO: parallelize?
foreach (var dataMember 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)
{
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);
}
else
{
result.ModifiedNames.Add(dataMember.Name);
}
// Apply member info
member.Name = dataMember.Name;
member.Description = dataMember.Description;
member.Color = dataMember.Color;
member.AvatarUrl = dataMember.AvatarUrl;
member.Prefix = dataMember.Prefix;
member.Suffix = dataMember.Suffix;
var birthdayParse = LocalDatePattern.CreateWithInvariantCulture(Formats.DateExportFormat).Parse(dataMember.Birthdate);
member.Birthday = birthdayParse.Success ? (LocalDate?) birthdayParse.Value : null;
await _members.Save(member);
}
// TODO: import switches, too?
result.System = system;
return result;
}
}
public struct ImportResult
{
public ICollection<string> AddedNames;
public ICollection<string> ModifiedNames;
public PKSystem System;
}
public struct DataFileSystem
{
[JsonProperty("id")]
public string Id;
[JsonProperty("name")]
public string Name;
[JsonProperty("description")]
public string Description;
[JsonProperty("tag")]
public string Tag;
[JsonProperty("avatar_url")]
public string AvatarUrl;
[JsonProperty("timezone")]
public string TimeZone;
[JsonProperty("members")]
public ICollection<DataFileMember> Members;
[JsonProperty("switches")]
public ICollection<DataFileSwitch> Switches;
[JsonProperty("accounts")]
public ICollection<ulong> LinkedAccounts;
[JsonProperty("created")]
public string Created;
private bool TimeZoneValid => TimeZone == null || DateTimeZoneProviders.Tzdb.GetZoneOrNull(TimeZone) != null;
[JsonIgnore]
public bool Valid => TimeZoneValid && Members.All(m => m.Valid);
}
public struct DataFileMember
{
[JsonProperty("id")]
public string Id;
[JsonProperty("name")]
public string Name;
[JsonProperty("description")]
public string Description;
[JsonProperty("birthday")]
public string Birthdate;
[JsonProperty("pronouns")]
public string Pronouns;
[JsonProperty("color")]
public string Color;
[JsonProperty("avatar_url")]
public string AvatarUrl;
[JsonProperty("prefix")]
public string Prefix;
[JsonProperty("suffix")]
public string Suffix;
[JsonProperty("message_count")]
public int MessageCount;
[JsonProperty("created")]
public string Created;
[JsonIgnore]
public bool Valid => Name != null;
}
public struct DataFileSwitch
{
[JsonProperty("timestamp")]
public string Timestamp;
[JsonProperty("members")]
public ICollection<string> Members;
}
}

View File

@ -15,4 +15,10 @@
<PackageReference Include="Npgsql.NodaTime" Version="4.0.6" />
</ItemGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\..\..\.nuget\packages\newtonsoft.json\11.0.2\lib\netstandard2.0\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -84,7 +84,7 @@ namespace PluralKit {
public async Task<PKMember> GetByName(PKSystem system, string name) {
// QueryFirst, since members can (in rare cases) share names
return await conn.QueryFirstOrDefaultAsync<PKMember>("select * from members where lower(name) = @Name and system = @SystemID", new { Name = name, SystemID = system.Id });
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) {

View File

@ -224,6 +224,8 @@ namespace PluralKit
public static class Formats
{
public static string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
public static string DateExportFormat = "yyyy-MM-dd";
public static string TimestampExportFormat = "g";
public static string DurationFormat = "D'd' h'h' m'm' s's'";
}
}