Add importing and exporting function
This commit is contained in:
parent
cd9a3e0abd
commit
652afffb8c
@ -68,6 +68,8 @@ namespace PluralKit.Bot
|
||||
.AddTransient<EmbedService>()
|
||||
.AddTransient<ProxyService>()
|
||||
.AddTransient<LogChannelService>()
|
||||
.AddTransient<DataFileService>()
|
||||
|
||||
.AddSingleton<WebhookCacheService>()
|
||||
|
||||
.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 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
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" />
|
||||
</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>
|
||||
|
@ -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) {
|
||||
|
@ -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'";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user