Add importing and exporting function
This commit is contained in:
parent
cd9a3e0abd
commit
652afffb8c
@ -68,6 +68,8 @@ namespace PluralKit.Bot
|
|||||||
.AddTransient<EmbedService>()
|
.AddTransient<EmbedService>()
|
||||||
.AddTransient<ProxyService>()
|
.AddTransient<ProxyService>()
|
||||||
.AddTransient<LogChannelService>()
|
.AddTransient<LogChannelService>()
|
||||||
|
.AddTransient<DataFileService>()
|
||||||
|
|
||||||
.AddSingleton<WebhookCacheService>()
|
.AddSingleton<WebhookCacheService>()
|
||||||
|
|
||||||
.AddTransient<SystemStore>()
|
.AddTransient<SystemStore>()
|
||||||
|
85
PluralKit.Bot/Commands/ImportExportCommands.cs
Normal file
85
PluralKit.Bot/Commands/ImportExportCommands.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 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 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 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
223
PluralKit.Core/DataFiles.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,4 +15,10 @@
|
|||||||
<PackageReference Include="Npgsql.NodaTime" Version="4.0.6" />
|
<PackageReference Include="Npgsql.NodaTime" Version="4.0.6" />
|
||||||
</ItemGroup>
|
</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>
|
</Project>
|
||||||
|
@ -84,7 +84,7 @@ namespace PluralKit {
|
|||||||
|
|
||||||
public async Task<PKMember> GetByName(PKSystem system, string name) {
|
public async Task<PKMember> GetByName(PKSystem system, string name) {
|
||||||
// QueryFirst, since members can (in rare cases) share names
|
// 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) {
|
public async Task<ICollection<PKMember>> GetUnproxyableMembers(PKSystem system) {
|
||||||
|
@ -224,6 +224,8 @@ namespace PluralKit
|
|||||||
public static class Formats
|
public static class Formats
|
||||||
{
|
{
|
||||||
public static string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
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'";
|
public static string DurationFormat = "D'd' h'h' m'm' s's'";
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user