Compare commits
46 Commits
3c0ffd145c
...
ce73a18374
Author | SHA1 | Date |
---|---|---|
Anna | ce73a18374 | |
Anna | 6f340836d9 | |
Anna | e91a446361 | |
Anna | 500687194c | |
Anna | 8cd95f7b48 | |
Anna | 685c3570d4 | |
Anna | 168c299e0c | |
Anna | 12c81bdceb | |
Anna | 58b4def737 | |
Anna | c4d062af69 | |
Anna | e3adfaf545 | |
Anna | 4d7988d136 | |
Anna | 5c2cd8a2ce | |
Anna | 6ab4384a7f | |
Anna | 5027362621 | |
Anna | 5121bb67e9 | |
Anna | fc4a57efcd | |
Anna | 52a4fd2a9f | |
Anna | abe467e69b | |
Anna | d91eaefe99 | |
Anna | d3dfd18ab2 | |
Anna | 4a3fab32e0 | |
Anna | eb3a0c659d | |
Anna | b0dba5bd55 | |
Anna | 5b9d4a5121 | |
Anna | e9d9bab0da | |
Anna | 6f34c0ac38 | |
Anna | ce6149be04 | |
Anna | 748d63a8d2 | |
Anna | d8ce434aea | |
Anna | f4b4acae52 | |
Anna | 27746a8744 | |
Anna | b55bffce7a | |
Anna | bab1044a04 | |
Anna | 8682ffdfec | |
Anna | 4d8dbf50b7 | |
Anna | 8344fe4a68 | |
Anna | 456c942fa0 | |
Anna | ace08c13fe | |
Anna | 5173f3cb49 | |
Anna | 77322ccbeb | |
Anna | 2026a33ff6 | |
Anna | e8e9206f10 | |
Anna | 30853e15ba | |
Anna | 08a3e3a02e | |
Anna | d16eb7f084 |
|
@ -42,7 +42,7 @@ internal class Client : IDisposable {
|
|||
|
||||
private KeyPair KeyPair { get; }
|
||||
|
||||
private readonly Mutex _waitersMutex = new();
|
||||
private readonly SemaphoreSlim _waitersSemaphore = new(1, 1);
|
||||
private Dictionary<uint, ChannelWriter<ResponseKind>> Waiters { get; set; } = new();
|
||||
private Channel<(RequestContainer, ChannelWriter<ChannelReader<ResponseKind>>?)> ToSend { get; set; } = System.Threading.Channels.Channel.CreateUnbounded<(RequestContainer, ChannelWriter<ChannelReader<ResponseKind>>?)>();
|
||||
|
||||
|
@ -69,6 +69,7 @@ internal class Client : IDisposable {
|
|||
|
||||
this._active = false;
|
||||
this.WebSocket.Dispose();
|
||||
this._waitersSemaphore.Dispose();
|
||||
}
|
||||
|
||||
private void Login(object? sender, EventArgs e) {
|
||||
|
@ -112,11 +113,15 @@ internal class Client : IDisposable {
|
|||
});
|
||||
}
|
||||
|
||||
private ChannelReader<ResponseKind> RegisterWaiter(uint number) {
|
||||
private async Task<ChannelReader<ResponseKind>> RegisterWaiter(uint number) {
|
||||
var channel = System.Threading.Channels.Channel.CreateBounded<ResponseKind>(1);
|
||||
this._waitersMutex.WaitOne();
|
||||
this.Waiters[number] = channel.Writer;
|
||||
this._waitersMutex.ReleaseMutex();
|
||||
await this._waitersSemaphore.WaitAsync();
|
||||
try {
|
||||
this.Waiters[number] = channel.Writer;
|
||||
} finally {
|
||||
this._waitersSemaphore.Release();
|
||||
}
|
||||
|
||||
return channel.Reader;
|
||||
}
|
||||
|
||||
|
@ -201,9 +206,7 @@ internal class Client : IDisposable {
|
|||
|
||||
internal async Task<(Channel, byte[])> Create(string name) {
|
||||
var shared = SodiumSecretBoxXChaCha20Poly1305.GenerateKey();
|
||||
var nonce = SodiumSecretBoxXChaCha20Poly1305.GenerateNonce();
|
||||
var ciphertext = SodiumSecretBoxXChaCha20Poly1305.Create(Encoding.UTF8.GetBytes(name), nonce, shared);
|
||||
var encryptedName = nonce.Concat(ciphertext);
|
||||
var encryptedName = SecretBox.Encrypt(shared, Encoding.UTF8.GetBytes(name));
|
||||
|
||||
var response = await this.QueueMessageAndWait(new RequestKind.Create(new CreateRequest {
|
||||
Name = encryptedName,
|
||||
|
@ -286,6 +289,28 @@ internal class Client : IDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
internal async Task<string?> DeleteAccount() {
|
||||
var response = await this.QueueMessageAndWait(new RequestKind.DeleteAccount(new DeleteAccountRequest()));
|
||||
return response switch {
|
||||
ResponseKind.Error { Response.Error: var error } => error,
|
||||
ResponseKind.DeleteAccount => null,
|
||||
_ => throw new Exception("Unexpected response"),
|
||||
};
|
||||
}
|
||||
|
||||
internal async Task DeleteAccountToast() {
|
||||
var message = await this.DeleteAccount();
|
||||
if (message != null) {
|
||||
this.Plugin.ShowError($"Could not delete account: {message}");
|
||||
return;
|
||||
}
|
||||
|
||||
this.Plugin.Config.Configs.Remove(this.Plugin.ClientState.LocalContentId);
|
||||
this.Plugin.SaveConfig();
|
||||
this.StopLoop();
|
||||
this.Status = State.NotAuthenticated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to register the user after the challenge has been completed.
|
||||
/// </summary>
|
||||
|
@ -334,6 +359,7 @@ internal class Client : IDisposable {
|
|||
var response = await this.QueueMessageAndWait(new RequestKind.Authenticate(new AuthenticateRequest {
|
||||
Key = key,
|
||||
PublicKey = this.KeyPair.GetPublicKey(),
|
||||
AllowInvites = this.Plugin.ConfigInfo.AllowInvites,
|
||||
}));
|
||||
|
||||
var success = response switch {
|
||||
|
@ -485,6 +511,21 @@ internal class Client : IDisposable {
|
|||
}));
|
||||
}
|
||||
|
||||
internal async Task<bool> AllowInvites(bool allow) {
|
||||
var resp = await this.QueueMessageAndWait(new RequestKind.AllowInvites(new AllowInvitesRequest {
|
||||
Allowed = allow,
|
||||
}));
|
||||
|
||||
|
||||
return resp is ResponseKind.AllowInvites { Response.Allowed: var respAllowed } && respAllowed == allow;
|
||||
}
|
||||
|
||||
internal async Task AllowInvitesToast(bool allow) {
|
||||
if (!await this.AllowInvites(allow)) {
|
||||
this.Plugin.ShowError("Could not set invite permissions.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool _up;
|
||||
|
||||
#pragma warning disable CS4014
|
||||
|
@ -500,10 +541,17 @@ internal class Client : IDisposable {
|
|||
return;
|
||||
}
|
||||
|
||||
this.Channels.Clear();
|
||||
this.InvitedChannels.Clear();
|
||||
this.ChannelRanks.Clear();
|
||||
this.Waiters.Clear();
|
||||
this.ToSend = System.Threading.Channels.Channel.CreateUnbounded<(RequestContainer, ChannelWriter<ChannelReader<ResponseKind>>?)>();
|
||||
this._waitersMutex.WaitOne();
|
||||
this.Waiters = new Dictionary<uint, ChannelWriter<ResponseKind>>();
|
||||
this._waitersMutex.ReleaseMutex();
|
||||
await this._waitersSemaphore.WaitAsync();
|
||||
try {
|
||||
this.Waiters = new Dictionary<uint, ChannelWriter<ResponseKind>>();
|
||||
} finally {
|
||||
this._waitersSemaphore.Release();
|
||||
}
|
||||
|
||||
// If the websocket is closed, we need to reconnect
|
||||
this.WebSocket.Dispose();
|
||||
|
@ -585,13 +633,13 @@ internal class Client : IDisposable {
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
this._waitersMutex.WaitOne();
|
||||
await this._waitersSemaphore.WaitAsync();
|
||||
try {
|
||||
if (this.Waiters.Remove(response.Number, out var waiter)) {
|
||||
await waiter.WriteAsync(response.Kind);
|
||||
}
|
||||
} finally {
|
||||
this._waitersMutex.ReleaseMutex();
|
||||
this._waitersSemaphore.Release();
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -603,7 +651,7 @@ internal class Client : IDisposable {
|
|||
|
||||
await this.WebSocket.SendMessage(req);
|
||||
if (update != null) {
|
||||
await update.WriteAsync(this.RegisterWaiter(req.Number));
|
||||
await update.WriteAsync(await this.RegisterWaiter(req.Number));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -888,6 +936,7 @@ internal class Client : IDisposable {
|
|||
}
|
||||
|
||||
this.Plugin.Commands.ReregisterAll();
|
||||
this.Plugin.Ipc.BroadcastChannelNames();
|
||||
}
|
||||
|
||||
private void HandleMessage(MessageResponse resp) {
|
||||
|
@ -900,6 +949,9 @@ internal class Client : IDisposable {
|
|||
var message = SeString.Parse(SecretBox.Decrypt(info.SharedSecret, resp.Message));
|
||||
|
||||
var output = new SeStringBuilder();
|
||||
// add a tag payload for filtering
|
||||
output.Add(PayloadUtil.CreateTagPayload(resp.Channel));
|
||||
output.Add(RawPayload.LinkTerminator);
|
||||
|
||||
var colour = config.GetUiColour(resp.Channel);
|
||||
output.AddUiForeground(colour);
|
||||
|
@ -915,7 +967,9 @@ internal class Client : IDisposable {
|
|||
output.Add(new PlayerPayload(resp.Sender, resp.World));
|
||||
}
|
||||
|
||||
if (!isSelf && resp.World != this.Plugin.LocalPlayer?.CurrentWorld.Id) {
|
||||
var homeWorldsSame = resp.World == this.Plugin.LocalPlayer?.HomeWorld.Id;
|
||||
var homeWorldsSameAndOnHomeWorld = homeWorldsSame && this.Plugin.LocalPlayer?.CurrentWorld.Id == resp.World;
|
||||
if (!isSelf && !homeWorldsSameAndOnHomeWorld) {
|
||||
output.AddIcon(BitmapFontIcon.CrossWorld);
|
||||
var world = this.Plugin.DataManager.GetExcelSheet<World>()?.GetRow(resp.World)?.Name.ToDalamudString();
|
||||
if (world != null) {
|
||||
|
|
|
@ -11,6 +11,7 @@ internal class Configuration : IPluginConfiguration {
|
|||
public int Version { get; set; } = 1;
|
||||
|
||||
public bool UseNativeToasts = true;
|
||||
public bool ShowContextMenuItem = true;
|
||||
public XivChatType DefaultChannel = XivChatType.Debug;
|
||||
public Dictionary<ulong, ConfigInfo> Configs { get; } = new();
|
||||
|
||||
|
@ -41,6 +42,7 @@ internal class ConfigInfo {
|
|||
public Dictionary<Guid, string> ChannelMarkers = new();
|
||||
public Dictionary<Guid, XivChatType> ChannelChannels = new();
|
||||
public int TutorialStep;
|
||||
public bool AllowInvites = true;
|
||||
|
||||
internal string GetName(Guid id) => this.Channels.TryGetValue(id, out var channel)
|
||||
? channel.Name
|
||||
|
|
|
@ -1,64 +1,65 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>1.1.0</Version>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<Version>1.3.0</Version>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<DebugType>full</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Dalamud>$(AppData)\XIVLauncher\addon\Hooks\dev</Dalamud>
|
||||
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
||||
<Dalamud>$(DALAMUD_HOME)</Dalamud>
|
||||
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsCI)' == 'true'">
|
||||
<Dalamud>$(HOME)/dalamud</Dalamud>
|
||||
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(Dalamud)\Dalamud.dll</HintPath>
|
||||
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(Dalamud)\FFXIVClientStructs.dll</HintPath>
|
||||
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGui.NET">
|
||||
<HintPath>$(Dalamud)\ImGui.NET.dll</HintPath>
|
||||
<HintPath>$(DalamudLibPath)\ImGui.NET.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGuiScene">
|
||||
<HintPath>$(Dalamud)\ImGuiScene.dll</HintPath>
|
||||
<HintPath>$(DalamudLibPath)\ImGuiScene.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(Dalamud)\Lumina.dll</HintPath>
|
||||
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(Dalamud)\Lumina.Excel.dll</HintPath>
|
||||
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ASodium" Version="0.5.3" />
|
||||
<PackageReference Include="Dalamud.ContextMenu" Version="1.0.0" />
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.7" />
|
||||
<PackageReference Include="MessagePack" Version="2.3.112" />
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="6.0.0" />
|
||||
<PackageReference Include="ASodium" Version="0.5.3"/>
|
||||
<PackageReference Include="Dalamud.ContextMenu" Version="1.2.1"/>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.8"/>
|
||||
<PackageReference Include="MessagePack" Version="2.4.35"/>
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2"/>
|
||||
<PackageReference Include="System.Threading.Channels" Version="6.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
name: ExtraChat
|
||||
author: ascclemens
|
||||
description: |-
|
||||
It's more chat. Very alpha. /ecl# is the LS command.
|
||||
punchline: '[ALPHA] Cross-data-centre linkshells with unlimited members.'
|
||||
ExtraChat adds end-to-end encrypted, cross-data-centre, unlimited linkshells
|
||||
to the game.
|
||||
punchline: 'Cross-data-centre linkshells with unlimited members.'
|
||||
repo_url: https://git.annaclemens.io/ascclemens/ExtraChat
|
||||
accepts_feedback: false
|
||||
feedback_message: Submit only bugs to https://github.com/ascclemens/plugin-issues
|
||||
|
|
|
@ -25,6 +25,8 @@ public class RequestKindFormatter : IMessagePackFormatter<RequestKind> {
|
|||
RequestKind.Promote => "promote",
|
||||
RequestKind.Update => "update",
|
||||
RequestKind.Version => "version",
|
||||
RequestKind.DeleteAccount => "delete_account",
|
||||
RequestKind.AllowInvites => "allow_invites",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(value)),
|
||||
};
|
||||
|
||||
|
@ -82,6 +84,12 @@ public class RequestKindFormatter : IMessagePackFormatter<RequestKind> {
|
|||
case RequestKind.Version version:
|
||||
options.Resolver.GetFormatterWithVerify<VersionRequest>().Serialize(ref writer, version.Request, options);
|
||||
break;
|
||||
case RequestKind.DeleteAccount deleteAccount:
|
||||
options.Resolver.GetFormatterWithVerify<DeleteAccountRequest>().Serialize(ref writer, deleteAccount.Request, options);
|
||||
break;
|
||||
case RequestKind.AllowInvites allowInvites:
|
||||
options.Resolver.GetFormatterWithVerify<AllowInvitesRequest>().Serialize(ref writer, allowInvites.Request, options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,7 +102,7 @@ public class RequestKindFormatter : IMessagePackFormatter<RequestKind> {
|
|||
|
||||
switch (key) {
|
||||
case "ping": {
|
||||
var request = MessagePackSerializer.Deserialize<PingRequest>(ref reader, options);
|
||||
var request = options.Resolver.GetFormatterWithVerify<PingRequest>().Deserialize(ref reader, options);
|
||||
return new RequestKind.Ping(request);
|
||||
}
|
||||
case "authenticate": {
|
||||
|
@ -161,6 +169,14 @@ public class RequestKindFormatter : IMessagePackFormatter<RequestKind> {
|
|||
var request = options.Resolver.GetFormatterWithVerify<VersionRequest>().Deserialize(ref reader, options);
|
||||
return new RequestKind.Version(request);
|
||||
}
|
||||
case "delete_account": {
|
||||
var request = options.Resolver.GetFormatterWithVerify<DeleteAccountRequest>().Deserialize(ref reader, options);
|
||||
return new RequestKind.DeleteAccount(request);
|
||||
}
|
||||
case "allow_invites": {
|
||||
var request = options.Resolver.GetFormatterWithVerify<AllowInvitesRequest>().Deserialize(ref reader, options);
|
||||
return new RequestKind.AllowInvites(request);
|
||||
}
|
||||
default:
|
||||
throw new MessagePackSerializationException("Invalid RequestKind");
|
||||
}
|
||||
|
|
|
@ -106,6 +106,14 @@ public class ResponseKindFormatter : IMessagePackFormatter<ResponseKind> {
|
|||
var response = options.Resolver.GetFormatterWithVerify<AnnounceResponse>().Deserialize(ref reader, options);
|
||||
return new ResponseKind.Announce(response);
|
||||
}
|
||||
case "delete_account": {
|
||||
var response = options.Resolver.GetFormatterWithVerify<DeleteAccountResponse>().Deserialize(ref reader, options);
|
||||
return new ResponseKind.DeleteAccount(response);
|
||||
}
|
||||
case "allow_invites": {
|
||||
var response = options.Resolver.GetFormatterWithVerify<AllowInvitesResponse>().Deserialize(ref reader, options);
|
||||
return new ResponseKind.AllowInvites(response);
|
||||
}
|
||||
default:
|
||||
throw new MessagePackSerializationException("Invalid ResponseKind");
|
||||
}
|
||||
|
|
|
@ -24,13 +24,24 @@ internal class ChatTwo : IDisposable {
|
|||
this.Available = this.Plugin.Interface.GetIpcSubscriber<object?>("ChatTwo.Available");
|
||||
|
||||
this.Available.Subscribe(this.DoRegister);
|
||||
this.DoRegister();
|
||||
try {
|
||||
this.DoRegister();
|
||||
} catch (Exception) {
|
||||
// try to register if chat 2 is already loaded
|
||||
// if not, just ignore exception
|
||||
}
|
||||
|
||||
this.Invoke.Subscribe(this.Integration);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (this._id != null) {
|
||||
this.Unregister.InvokeAction(this._id);
|
||||
try {
|
||||
this.Unregister.InvokeAction(this._id);
|
||||
} catch (Exception) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
this._id = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,17 +14,21 @@ internal class Ipc : IDisposable {
|
|||
private Plugin Plugin { get; }
|
||||
private ICallGateProvider<OverrideInfo, object> OverrideChannelColour { get; }
|
||||
private ICallGateProvider<Dictionary<string, uint>, Dictionary<string, uint>> ChannelCommandColours { get; }
|
||||
private ICallGateProvider<Dictionary<Guid, string>, Dictionary<Guid, string>> ChannelNames { get; }
|
||||
|
||||
internal Ipc(Plugin plugin) {
|
||||
this.Plugin = plugin;
|
||||
|
||||
this.OverrideChannelColour = this.Plugin.Interface.GetIpcProvider<OverrideInfo, object>("ExtraChat.OverrideChannelColour");
|
||||
this.ChannelCommandColours = this.Plugin.Interface.GetIpcProvider<Dictionary<string, uint>, Dictionary<string, uint>>("ExtraChat.ChannelCommandColours");
|
||||
this.ChannelNames = this.Plugin.Interface.GetIpcProvider<Dictionary<Guid, string>, Dictionary<Guid, string>>("ExtraChat.ChannelNames");
|
||||
|
||||
this.ChannelCommandColours.RegisterFunc(_ => this.GetChannelColours());
|
||||
this.ChannelNames.RegisterFunc(_ => this.GetChannelNames());
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
this.ChannelNames.UnregisterFunc();
|
||||
this.ChannelCommandColours.UnregisterFunc();
|
||||
}
|
||||
|
||||
|
@ -41,10 +45,23 @@ internal class Ipc : IDisposable {
|
|||
return dict;
|
||||
}
|
||||
|
||||
private Dictionary<Guid, string> GetChannelNames() {
|
||||
return this.Plugin.Client.Channels
|
||||
.Values
|
||||
.ToDictionary(
|
||||
channel => channel.Id,
|
||||
channel => this.Plugin.ConfigInfo.Channels.TryGetValue(channel.Id, out var info) ? info.Name : "???"
|
||||
);
|
||||
}
|
||||
|
||||
internal void BroadcastChannelCommandColours() {
|
||||
this.ChannelCommandColours.SendMessage(this.GetChannelColours());
|
||||
}
|
||||
|
||||
internal void BroadcastChannelNames() {
|
||||
this.ChannelNames.SendMessage(this.GetChannelNames());
|
||||
}
|
||||
|
||||
internal void BroadcastOverride() {
|
||||
var over = this.Plugin.GameFunctions.OverrideChannel;
|
||||
if (over == Guid.Empty) {
|
||||
|
|
|
@ -60,7 +60,7 @@ public class Plugin : IDalamudPlugin {
|
|||
internal Client Client { get; }
|
||||
internal Commands Commands { get; }
|
||||
internal PluginUi PluginUi { get; }
|
||||
internal DalamudContextMenuBase ContextMenu { get; }
|
||||
internal DalamudContextMenu ContextMenu { get; }
|
||||
internal GameFunctions GameFunctions { get; }
|
||||
internal Ipc Ipc { get; }
|
||||
private IDisposable[] Integrations { get; }
|
||||
|
@ -85,7 +85,7 @@ public class Plugin : IDalamudPlugin {
|
|||
public Plugin() {
|
||||
SodiumInit.Init();
|
||||
WorldUtil.Initialise(this.DataManager!);
|
||||
this.ContextMenu = new DalamudContextMenuBase();
|
||||
this.ContextMenu = new DalamudContextMenu();
|
||||
this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
this.Client = new Client(this);
|
||||
this.Commands = new Commands(this);
|
||||
|
@ -98,13 +98,13 @@ public class Plugin : IDalamudPlugin {
|
|||
};
|
||||
|
||||
this.Framework!.Update += this.FrameworkUpdate;
|
||||
this.ContextMenu.Functions.ContextMenu.OnOpenGameObjectContextMenu += this.OnOpenGameObjectContextMenu;
|
||||
this.ContextMenu.OnOpenGameObjectContextMenu += this.OnOpenGameObjectContextMenu;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
this.GameFunctions.ResetOverride();
|
||||
|
||||
this.ContextMenu.Functions.ContextMenu.OnOpenGameObjectContextMenu -= this.OnOpenGameObjectContextMenu;
|
||||
this.ContextMenu.OnOpenGameObjectContextMenu -= this.OnOpenGameObjectContextMenu;
|
||||
this.Framework.Update -= this.FrameworkUpdate;
|
||||
this._localPlayerLock.Dispose();
|
||||
|
||||
|
@ -130,6 +130,10 @@ public class Plugin : IDalamudPlugin {
|
|||
}
|
||||
|
||||
private void OnOpenGameObjectContextMenu(GameObjectContextMenuOpenArgs args) {
|
||||
if (!this.Config.ShowContextMenuItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ObjectId != 0xE0000000) {
|
||||
this.ObjectContext(args);
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace ExtraChat.Protocol;
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class AllowInvitesRequest {
|
||||
[Key(0)]
|
||||
public bool Allowed;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class AllowInvitesResponse {
|
||||
[Key(0)]
|
||||
public bool Allowed;
|
||||
}
|
|
@ -2,6 +2,7 @@ using MessagePack;
|
|||
|
||||
namespace ExtraChat.Protocol;
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class AnnounceResponse {
|
||||
[Key(0)]
|
||||
|
|
|
@ -10,4 +10,7 @@ public class AuthenticateRequest {
|
|||
|
||||
[Key(1)]
|
||||
public byte[] PublicKey;
|
||||
|
||||
[Key(2)]
|
||||
public bool AllowInvites;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace ExtraChat.Protocol;
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class DeleteAccountRequest {
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace ExtraChat.Protocol;
|
||||
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class DeleteAccountResponse {
|
||||
}
|
|
@ -57,4 +57,10 @@ public abstract record RequestKind {
|
|||
|
||||
[MessagePackObject]
|
||||
public record Version(VersionRequest Request) : RequestKind;
|
||||
|
||||
[MessagePackObject]
|
||||
public record DeleteAccount(DeleteAccountRequest Request) : RequestKind;
|
||||
|
||||
[MessagePackObject]
|
||||
public record AllowInvites(AllowInvitesRequest Request) : RequestKind;
|
||||
}
|
||||
|
|
|
@ -72,4 +72,10 @@ public abstract record ResponseKind {
|
|||
|
||||
[MessagePackObject]
|
||||
public record Announce(AnnounceResponse Response) : ResponseKind;
|
||||
|
||||
[MessagePackObject]
|
||||
public record DeleteAccount(DeleteAccountResponse Response) : ResponseKind;
|
||||
|
||||
[MessagePackObject]
|
||||
public record AllowInvites(AllowInvitesResponse Response) : ResponseKind;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,405 @@
|
|||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Dalamud.Interface;
|
||||
using ExtraChat.Protocol;
|
||||
using ExtraChat.Protocol.Channels;
|
||||
using ExtraChat.Util;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace ExtraChat.Ui;
|
||||
|
||||
internal class ChannelList {
|
||||
private Plugin Plugin { get; }
|
||||
|
||||
private readonly List<(string, List<World>)> _worlds;
|
||||
|
||||
private string _createName = string.Empty;
|
||||
private Guid _selectedChannel = Guid.Empty;
|
||||
private string _inviteName = string.Empty;
|
||||
private ushort _inviteWorld;
|
||||
private string _rename = string.Empty;
|
||||
|
||||
internal ChannelList(Plugin plugin) {
|
||||
this.Plugin = plugin;
|
||||
|
||||
this._worlds = this.Plugin.DataManager.GetExcelSheet<World>()!
|
||||
.Where(row => row.IsPublic)
|
||||
.GroupBy(row => row.DataCenter.Value!)
|
||||
.Where(grouping => grouping.Key.Region != 0)
|
||||
.OrderBy(grouping => grouping.Key.Region)
|
||||
.ThenBy(grouping => grouping.Key.Name.RawString)
|
||||
.Select(grouping => (grouping.Key.Name.RawString, grouping.OrderBy(row => row.Name.RawString).ToList()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal void Draw() {
|
||||
var anyChanged = false;
|
||||
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
|
||||
var syncButton = ImGui.CalcTextSize(FontAwesomeIcon.Sync.ToIconString()).X
|
||||
+ ImGui.GetStyle().FramePadding.X * 2;
|
||||
// PluginLog.Log($"syncButton: {syncButton}");
|
||||
var addButton = ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).X
|
||||
+ ImGui.GetStyle().FramePadding.X * 2;
|
||||
// PluginLog.Log($"addButton: {addButton}");
|
||||
var syncOffset = ImGui.GetContentRegionAvail().X - syncButton;
|
||||
var addOffset = ImGui.GetContentRegionAvail().X - syncButton - ImGui.GetStyle().ItemSpacing.X - addButton;
|
||||
ImGui.SameLine(syncOffset);
|
||||
|
||||
if (ImGui.Button(FontAwesomeIcon.Sync.ToIconString())) {
|
||||
Task.Run(async () => await this.Plugin.Client.ListAll());
|
||||
}
|
||||
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 1);
|
||||
|
||||
ImGui.SameLine(addOffset);
|
||||
|
||||
if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString())) {
|
||||
ImGui.OpenPopup("create-channel-popup");
|
||||
}
|
||||
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 0);
|
||||
|
||||
ImGui.PopFont();
|
||||
|
||||
if (ImGui.BeginPopup("create-channel-popup")) {
|
||||
ImGui.TextUnformatted("Create a new ExtraChat Linkshell");
|
||||
|
||||
ImGui.SetNextItemWidth(350 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputTextWithHint("##linkshell-name", "Linkshell name", ref this._createName, 64);
|
||||
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
ImGui.SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this._createName) && ImGui.Button("Create") && !this.Plugin.PluginUi.Busy) {
|
||||
this.Plugin.PluginUi.Busy = true;
|
||||
var name = this._createName;
|
||||
Task.Run(async () => await this.Plugin.Client.Create(name))
|
||||
.ContinueWith(_ => this.Plugin.PluginUi.Busy = false);
|
||||
ImGui.CloseCurrentPopup();
|
||||
this._createName = string.Empty;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
if (this.Plugin.Client.Channels.Count == 0 && this.Plugin.Client.InvitedChannels.Count == 0) {
|
||||
ImGui.TextUnformatted("You aren't in any linkshells yet. Try creating or joining one first.");
|
||||
goto AfterTable;
|
||||
}
|
||||
|
||||
this.DrawTable(ref anyChanged);
|
||||
|
||||
AfterTable:
|
||||
if (anyChanged) {
|
||||
this.Plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTable(ref bool anyChanged) {
|
||||
if (!ImGui.BeginTable("ecls-list", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingFixedFit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.TableSetupColumn("##channels", ImGuiTableColumnFlags.WidthFixed, 125 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableSetupColumn("##members", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableNextRow();
|
||||
|
||||
var channelOrder = this.Plugin.ConfigInfo.ChannelOrder.ToDictionary(
|
||||
entry => entry.Value,
|
||||
entry => entry.Key
|
||||
);
|
||||
|
||||
var orderedChannels = this.Plugin.Client.Channels.Keys
|
||||
.OrderBy(id => channelOrder.ContainsKey(id) ? channelOrder[id] : int.MaxValue)
|
||||
.Concat(this.Plugin.Client.InvitedChannels.Keys);
|
||||
|
||||
var childSize = new Vector2(
|
||||
-1,
|
||||
ImGui.GetContentRegionAvail().Y
|
||||
- ImGui.GetStyle().WindowPadding.Y
|
||||
- ImGui.GetStyle().ItemSpacing.Y
|
||||
);
|
||||
|
||||
if (ImGui.TableSetColumnIndex(0)) {
|
||||
this.DrawChannelList(ref anyChanged, childSize, orderedChannels, channelOrder);
|
||||
}
|
||||
|
||||
if (ImGui.TableSetColumnIndex(1) && this._selectedChannel != Guid.Empty) {
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
Task.Run(async () => await this.Plugin.Client.ListMembers(this._selectedChannel));
|
||||
}
|
||||
|
||||
if (ImGui.BeginChild("channel-info", childSize)) {
|
||||
this.DrawInfo(ref anyChanged);
|
||||
ImGui.EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
private void DrawChannelList(ref bool anyChanged, Vector2 childSize, IEnumerable<Guid> orderedChannels, Dictionary<Guid, int> channelOrder) {
|
||||
if (!ImGui.BeginChild("channel-list", childSize)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var first = true;
|
||||
foreach (var id in orderedChannels) {
|
||||
this.DrawChannel(ref anyChanged, channelOrder, id, ref first);
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
private void DrawChannel(ref bool anyChanged, IReadOnlyDictionary<Guid, int> channelOrder, Guid id, ref bool first) {
|
||||
this.Plugin.ConfigInfo.Channels.TryGetValue(id, out var info);
|
||||
var name = info?.Name ?? "???";
|
||||
|
||||
var order = "?";
|
||||
if (channelOrder.TryGetValue(id, out var o)) {
|
||||
order = (o + 1).ToString();
|
||||
}
|
||||
|
||||
if (!this.Plugin.Client.ChannelRanks.TryGetValue(id, out var rank)) {
|
||||
rank = Rank.Member;
|
||||
}
|
||||
|
||||
if (ImGui.Selectable($"{order}. {rank.Symbol()}{name}###{id}", this._selectedChannel == id)) {
|
||||
this._selectedChannel = id;
|
||||
|
||||
Task.Run(async () => await this.Plugin.Client.ListMembers(id));
|
||||
}
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 2);
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 3);
|
||||
}
|
||||
|
||||
if (!ImGui.BeginPopupContextItem()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var invited = this.Plugin.Client.InvitedChannels.ContainsKey(id);
|
||||
if (invited) {
|
||||
if (ImGui.Selectable("Accept invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Join(id));
|
||||
}
|
||||
|
||||
if (ImGuiUtil.SelectableConfirm("Decline invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Leave(id));
|
||||
}
|
||||
} else {
|
||||
if (ImGuiUtil.SelectableConfirm("Leave")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Leave(id));
|
||||
}
|
||||
|
||||
if (rank == Rank.Admin) {
|
||||
if (ImGuiUtil.SelectableConfirm("Disband")) {
|
||||
Task.Run(async () => {
|
||||
if (await this.Plugin.Client.Disband(id) is { } error) {
|
||||
this.Plugin.ShowError($"Could not disband \"{name}\": {error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rank == Rank.Admin && info != null && ImGui.BeginMenu($"Rename##{id}-rename")) {
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
this._rename = string.Empty;
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(350 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputTextWithHint($"##{id}-rename-input", "New name", ref this._rename, 64);
|
||||
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
ImGui.SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
if (ImGui.Button($"Rename##{id}-rename-button") && !string.IsNullOrWhiteSpace(this._rename)) {
|
||||
var newName = SecretBox.Encrypt(info.SharedSecret, Encoding.UTF8.GetBytes(this._rename));
|
||||
Task.Run(async () => await this.Plugin.Client.UpdateToast(id, new UpdateKind.Name(newName)));
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui.BeginMenu($"Invite##{id}-invite")) {
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
this._inviteName = string.Empty;
|
||||
this._inviteWorld = 0;
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputTextWithHint("##invite-name", "Name", ref this._inviteName, 32);
|
||||
|
||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||
var preview = this._inviteWorld == 0 ? "World" : WorldUtil.WorldName(this._inviteWorld);
|
||||
if (ImGui.BeginCombo("##invite-world", preview)) {
|
||||
foreach (var (dc, worlds) in this._worlds) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
|
||||
ImGui.TextUnformatted(dc);
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.Separator();
|
||||
|
||||
foreach (var world in worlds) {
|
||||
if (ImGui.Selectable(world.Name.RawString, this._inviteWorld == world.RowId)) {
|
||||
this._inviteWorld = (ushort) world.RowId;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
if (ImGui.Button($"Invite##{id}-invite-button") && !string.IsNullOrWhiteSpace(this._inviteName) && this._inviteWorld != 0) {
|
||||
var inviteName = this._inviteName;
|
||||
var inviteWorld = this._inviteWorld;
|
||||
|
||||
Task.Run(async () => await this.Plugin.Client.InviteToast(inviteName, inviteWorld, id));
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.BeginMenu("Change number")) {
|
||||
ImGui.SetNextItemWidth(150 * ImGuiHelpers.GlobalScale);
|
||||
channelOrder.TryGetValue(id, out var refOrder);
|
||||
var old = refOrder;
|
||||
refOrder += 1;
|
||||
if (ImGui.InputInt($"##{id}-order", ref refOrder)) {
|
||||
refOrder = Math.Max(1, refOrder) - 1;
|
||||
|
||||
if (this.Plugin.ConfigInfo.ChannelOrder.TryGetValue(refOrder, out var other) && other != id) {
|
||||
// another channel already has this number, so swap
|
||||
this.Plugin.ConfigInfo.ChannelOrder[old] = other;
|
||||
} else {
|
||||
this.Plugin.ConfigInfo.ChannelOrder.Remove(old);
|
||||
}
|
||||
|
||||
this.Plugin.ConfigInfo.ChannelOrder[refOrder] = id;
|
||||
this.Plugin.SaveConfig();
|
||||
this.Plugin.Commands.ReregisterAll();
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
if (ImGui.Selectable("Request secrets")) {
|
||||
Task.Run(async () => await this.Plugin.Client.RequestSecrets(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
private void DrawInfo(ref bool anyChanged) {
|
||||
if (!this.Plugin.Client.TryGetChannel(this._selectedChannel, out var channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector4 disabledColour;
|
||||
unsafe {
|
||||
disabledColour = *ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled);
|
||||
}
|
||||
|
||||
if (!this.Plugin.Client.ChannelRanks.TryGetValue(this._selectedChannel, out var rank)) {
|
||||
rank = Rank.Member;
|
||||
}
|
||||
|
||||
var first = true;
|
||||
foreach (var member in channel.Members) {
|
||||
if (!member.Online) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, disabledColour);
|
||||
}
|
||||
|
||||
try {
|
||||
ImGui.TextUnformatted($"{member.Rank.Symbol()}{member.Name}{PluginUi.CrossWorld}{WorldUtil.WorldName(member.World)}");
|
||||
} finally {
|
||||
if (!member.Online) {
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
this.DrawMemberContextMenu(member, rank);
|
||||
|
||||
if (!first) {
|
||||
continue;
|
||||
}
|
||||
|
||||
first = false;
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 4);
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 5);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMemberContextMenu(Member member, Rank rank) {
|
||||
if (!ImGui.BeginPopupContextItem($"{this._selectedChannel}-{member.Name}@{member.World}-context")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cursor = ImGui.GetCursorPos();
|
||||
|
||||
if (rank == Rank.Admin) {
|
||||
if (member.Rank is not (Rank.Admin or Rank.Invited)) {
|
||||
if (ImGuiUtil.SelectableConfirm("Promote to admin", tooltip: "This will demote you to moderator.")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Promote(this._selectedChannel, member.Name, member.World, Rank.Admin));
|
||||
}
|
||||
}
|
||||
|
||||
if (member.Rank == Rank.Moderator && ImGuiUtil.SelectableConfirm("Demote")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Promote(this._selectedChannel, member.Name, member.World, Rank.Member));
|
||||
}
|
||||
|
||||
if (member.Rank == Rank.Member && ImGuiUtil.SelectableConfirm("Promote to moderator")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Promote(this._selectedChannel, member.Name, member.World, Rank.Moderator));
|
||||
}
|
||||
}
|
||||
|
||||
if (rank >= Rank.Moderator) {
|
||||
var canKick = member.Rank < rank && member.Rank != Rank.Invited;
|
||||
if (canKick && ImGuiUtil.SelectableConfirm("Kick")) {
|
||||
Task.Run(async () => {
|
||||
if (await this.Plugin.Client.Kick(this._selectedChannel, member.Name, member.World) is { } error) {
|
||||
this.Plugin.ShowError($"Could not kick {member.Name}: {error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (member.Rank == Rank.Invited && ImGuiUtil.SelectableConfirm("Cancel invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Kick(this._selectedChannel, member.Name, member.World));
|
||||
}
|
||||
}
|
||||
|
||||
if (rank == Rank.Invited && member.Rank == Rank.Invited) {
|
||||
if (member.Name == this.Plugin.LocalPlayer?.Name.TextValue && member.World == this.Plugin.LocalPlayer?.HomeWorld.Id) {
|
||||
if (ImGui.Selectable("Accept invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Join(this._selectedChannel));
|
||||
}
|
||||
|
||||
if (ImGuiUtil.SelectableConfirm("Decline invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Leave(this._selectedChannel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor == ImGui.GetCursorPos()) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
|
||||
ImGui.TextUnformatted("No options available");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using Dalamud;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ExtraChat.Protocol;
|
||||
using ExtraChat.Protocol.Channels;
|
||||
using ExtraChat.Util;
|
||||
using ImGuiNET;
|
||||
|
@ -18,23 +16,15 @@ internal class PluginUi : IDisposable {
|
|||
internal const string CrossWorld = "\ue05d";
|
||||
|
||||
private Plugin Plugin { get; }
|
||||
private ChannelList ChannelList { get; }
|
||||
|
||||
internal bool Visible;
|
||||
|
||||
private readonly List<(string, List<World>)> _worlds;
|
||||
private readonly List<(uint Id, Vector4 Abgr)> _uiColours;
|
||||
|
||||
internal PluginUi(Plugin plugin) {
|
||||
this.Plugin = plugin;
|
||||
|
||||
this._worlds = this.Plugin.DataManager.GetExcelSheet<World>()!
|
||||
.Where(row => row.IsPublic)
|
||||
.GroupBy(row => row.DataCenter.Value!)
|
||||
.Where(grouping => grouping.Key.Region != 0)
|
||||
.OrderBy(grouping => grouping.Key.Region)
|
||||
.ThenBy(grouping => grouping.Key.Name.RawString)
|
||||
.Select(grouping => (grouping.Key.Name.RawString, grouping.OrderBy(row => row.Name.RawString).ToList()))
|
||||
.ToList();
|
||||
this.ChannelList = new ChannelList(this.Plugin);
|
||||
|
||||
this._uiColours = this.Plugin.DataManager.GetExcelSheet<UIColor>()!
|
||||
.Where(row => row.UIForeground is not (0 or 0x000000FF))
|
||||
|
@ -66,9 +56,8 @@ internal class PluginUi : IDisposable {
|
|||
|
||||
internal (string, ushort)? InviteInfo;
|
||||
|
||||
private volatile bool _busy;
|
||||
internal volatile bool Busy;
|
||||
private string? _challenge;
|
||||
private string _createName = string.Empty;
|
||||
private Guid? _inviteId;
|
||||
private readonly Channel<string?> _challengeChannel = Channel.CreateUnbounded<string?>();
|
||||
|
||||
|
@ -104,20 +93,20 @@ internal class PluginUi : IDisposable {
|
|||
var status = this.Plugin.Client.Status;
|
||||
ImGui.TextUnformatted($"Status: {status}");
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Wifi, tooltip: "Reconnect") && !this._busy) {
|
||||
this._busy = true;
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Wifi, tooltip: "Reconnect") && !this.Busy) {
|
||||
this.Busy = true;
|
||||
|
||||
Task.Run(async () => {
|
||||
this.Plugin.Client.StopLoop();
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
this.Plugin.Client.StartLoop();
|
||||
this._busy = false;
|
||||
this.Busy = false;
|
||||
});
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Client.State.Connected:
|
||||
this.DrawList();
|
||||
this.ChannelList.Draw();
|
||||
break;
|
||||
case Client.State.NotAuthenticated:
|
||||
case Client.State.RetrievingChallenge:
|
||||
|
@ -182,6 +171,7 @@ internal class PluginUi : IDisposable {
|
|||
|
||||
private void DrawSettingsGeneral(ref bool anyChanged) {
|
||||
anyChanged |= ImGui.Checkbox("Use native toasts", ref this.Plugin.Config.UseNativeToasts);
|
||||
anyChanged |= ImGui.Checkbox("Add invite context menu item", ref this.Plugin.Config.ShowContextMenuItem);
|
||||
// ImGui.Spacing();
|
||||
//
|
||||
// ImGui.TextUnformatted("Default channel");
|
||||
|
@ -196,6 +186,35 @@ internal class PluginUi : IDisposable {
|
|||
//
|
||||
// ImGui.EndCombo();
|
||||
// }
|
||||
|
||||
if (this.Plugin.LocalPlayer is { } player) {
|
||||
if (ImGui.TreeNodeEx($"Settings for {player.Name}{CrossWorld}{player.HomeWorld.GameData?.Name}")) {
|
||||
if (ImGui.Checkbox("Allow receiving invites", ref this.Plugin.ConfigInfo.AllowInvites)) {
|
||||
anyChanged = true;
|
||||
Task.Run(async () => await this.Plugin.Client.AllowInvitesToast(this.Plugin.ConfigInfo.AllowInvites));
|
||||
}
|
||||
|
||||
ImGui.TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.Plugin.Client.Status == Client.State.Connected && ImGui.TreeNodeEx("Delete account")) {
|
||||
ImGui.PushTextWrapPos();
|
||||
|
||||
if (this.Plugin.Client.Channels.Count > 0) {
|
||||
ImGui.TextUnformatted("You must leave or disband all ExtraChat linkshells you are currently in before you can delete your account.");
|
||||
} else {
|
||||
ImGui.TextUnformatted("Clicking the button below will permanently and irreversibly delete your account from ExtraChat's servers.");
|
||||
|
||||
if (ImGui.Button("Delete account##actual-delete")) {
|
||||
Task.Run(async () => await this.Plugin.Client.DeleteAccountToast());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
|
||||
ImGui.TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSettingsLinkshells(ref bool anyChanged) {
|
||||
|
@ -394,18 +413,18 @@ internal class PluginUi : IDisposable {
|
|||
if (this.Plugin.ConfigInfo.Key != null) {
|
||||
ImGui.TextUnformatted("Please wait...");
|
||||
} else {
|
||||
if (ImGui.Button($"Register {player.Name}") && !this._busy) {
|
||||
this._busy = true;
|
||||
if (ImGui.Button($"Register {player.Name}") && !this.Busy) {
|
||||
this.Busy = true;
|
||||
Task.Run(async () => {
|
||||
var challenge = await this.Plugin.Client.GetChallenge();
|
||||
await this._challengeChannel.Writer.WriteAsync(challenge);
|
||||
}).ContinueWith(_ => this._busy = false);
|
||||
}).ContinueWith(_ => this.Busy = false);
|
||||
}
|
||||
|
||||
ImGui.PushTextWrapPos();
|
||||
ImGui.TextUnformatted("ExtraChat is a third-party service that allows for functionally unlimited extra linkshells that work across data centres.");
|
||||
ImGui.TextUnformatted("In order to use ExtraChat, characters must be registered and verified using their Lodestone profile.");
|
||||
ImGui.TextUnformatted("ExtraChat stores your character's name, home world, and Lodestone ID, as well as what linkshells your character is a part of and has been invited to.");
|
||||
ImGui.TextUnformatted("ExtraChat stores your character's name, home world, and Lodestone ID, as well as what ExtraChat linkshells your character is a part of and has been invited to.");
|
||||
ImGui.TextUnformatted("Messages and linkshell names are end-to-end encrypted; the server cannot decrypt them and does not store messages.");
|
||||
ImGui.TextUnformatted("In the event of a legal subpoena, ExtraChat will provide any information available to the legal system.");
|
||||
ImGui.PopTextWrapPos();
|
||||
|
@ -451,363 +470,18 @@ internal class PluginUi : IDisposable {
|
|||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("Verify") && !this._busy) {
|
||||
this._busy = true;
|
||||
if (ImGui.Button("Verify") && !this.Busy) {
|
||||
this.Busy = true;
|
||||
Task.Run(async () => {
|
||||
var key = await this.Plugin.Client.Register();
|
||||
this.Plugin.ConfigInfo.Key = key;
|
||||
this.Plugin.SaveConfig();
|
||||
await this.Plugin.Client.AuthenticateAndList();
|
||||
}).ContinueWith(_ => this._busy = false);
|
||||
}).ContinueWith(_ => this.Busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
private Guid _selectedChannel = Guid.Empty;
|
||||
private string _inviteName = string.Empty;
|
||||
private ushort _inviteWorld;
|
||||
private string _rename = string.Empty;
|
||||
|
||||
private void DrawList() {
|
||||
var anyChanged = false;
|
||||
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
|
||||
var syncButton = ImGui.CalcTextSize(FontAwesomeIcon.Sync.ToIconString()).X
|
||||
+ ImGui.GetStyle().FramePadding.X * 2;
|
||||
// PluginLog.Log($"syncButton: {syncButton}");
|
||||
var addButton = ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).X
|
||||
+ ImGui.GetStyle().FramePadding.X * 2;
|
||||
// PluginLog.Log($"addButton: {addButton}");
|
||||
var syncOffset = ImGui.GetContentRegionAvail().X - syncButton;
|
||||
var addOffset = ImGui.GetContentRegionAvail().X - syncButton - ImGui.GetStyle().ItemSpacing.X - addButton;
|
||||
ImGui.SameLine(syncOffset);
|
||||
|
||||
if (ImGui.Button(FontAwesomeIcon.Sync.ToIconString())) {
|
||||
Task.Run(async () => await this.Plugin.Client.ListAll());
|
||||
}
|
||||
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 1);
|
||||
|
||||
ImGui.SameLine(addOffset);
|
||||
|
||||
if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString())) {
|
||||
ImGui.OpenPopup("create-channel-popup");
|
||||
}
|
||||
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 0);
|
||||
|
||||
ImGui.PopFont();
|
||||
|
||||
if (ImGui.BeginPopup("create-channel-popup")) {
|
||||
ImGui.TextUnformatted("Create a new ExtraChat Linkshell");
|
||||
|
||||
ImGui.SetNextItemWidth(350 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputTextWithHint("##linkshell-name", "Linkshell name", ref this._createName, 64);
|
||||
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
ImGui.SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this._createName) && ImGui.Button("Create") && !this._busy) {
|
||||
this._busy = true;
|
||||
var name = this._createName;
|
||||
Task.Run(async () => await this.Plugin.Client.Create(name))
|
||||
.ContinueWith(_ => this._busy = false);
|
||||
ImGui.CloseCurrentPopup();
|
||||
this._createName = string.Empty;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
if (this.Plugin.Client.Channels.Count == 0) {
|
||||
ImGui.TextUnformatted("You aren't in any linkshells yet. Try creating or joining one first.");
|
||||
goto AfterTable;
|
||||
}
|
||||
|
||||
if (ImGui.BeginTable("ecls-list", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingFixedFit)) {
|
||||
ImGui.TableSetupColumn("##channels", ImGuiTableColumnFlags.WidthFixed, 125 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableSetupColumn("##members", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableNextRow();
|
||||
|
||||
var channelOrder = this.Plugin.ConfigInfo.ChannelOrder.ToDictionary(
|
||||
entry => entry.Value,
|
||||
entry => entry.Key
|
||||
);
|
||||
|
||||
var orderedChannels = this.Plugin.Client.Channels.Keys
|
||||
.OrderBy(id => channelOrder.ContainsKey(id) ? channelOrder[id] : int.MaxValue)
|
||||
.Concat(this.Plugin.Client.InvitedChannels.Keys);
|
||||
|
||||
var childSize = new Vector2(
|
||||
-1,
|
||||
ImGui.GetContentRegionAvail().Y
|
||||
- ImGui.GetStyle().WindowPadding.Y
|
||||
- ImGui.GetStyle().ItemSpacing.Y
|
||||
);
|
||||
|
||||
if (ImGui.TableSetColumnIndex(0)) {
|
||||
if (ImGui.BeginChild("channel-list", childSize)) {
|
||||
var first = true;
|
||||
foreach (var id in orderedChannels) {
|
||||
this.Plugin.ConfigInfo.Channels.TryGetValue(id, out var info);
|
||||
var name = info?.Name ?? "???";
|
||||
|
||||
var order = "?";
|
||||
if (channelOrder.TryGetValue(id, out var o)) {
|
||||
order = (o + 1).ToString();
|
||||
}
|
||||
|
||||
if (!this.Plugin.Client.ChannelRanks.TryGetValue(id, out var rank)) {
|
||||
rank = Rank.Member;
|
||||
}
|
||||
|
||||
if (ImGui.Selectable($"{order}. {rank.Symbol()}{name}###{id}", this._selectedChannel == id)) {
|
||||
this._selectedChannel = id;
|
||||
|
||||
Task.Run(async () => await this.Plugin.Client.ListMembers(id));
|
||||
}
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 2);
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 3);
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopupContextItem()) {
|
||||
var invited = this.Plugin.Client.InvitedChannels.ContainsKey(id);
|
||||
if (invited) {
|
||||
if (ImGui.Selectable("Accept invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Join(id));
|
||||
}
|
||||
|
||||
if (ImGuiUtil.SelectableConfirm("Decline invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Leave(id));
|
||||
}
|
||||
} else {
|
||||
if (ImGuiUtil.SelectableConfirm("Leave")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Leave(id));
|
||||
}
|
||||
|
||||
if (rank == Rank.Admin) {
|
||||
if (ImGuiUtil.SelectableConfirm("Disband")) {
|
||||
Task.Run(async () => {
|
||||
if (await this.Plugin.Client.Disband(id) is { } error) {
|
||||
this.Plugin.ShowError($"Could not disband \"{name}\": {error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rank == Rank.Admin && info != null && ImGui.BeginMenu($"Rename##{id}-rename")) {
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
this._rename = string.Empty;
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(350 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputTextWithHint($"##{id}-rename-input", "New name", ref this._rename, 64);
|
||||
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
ImGui.SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
if (ImGui.Button($"Rename##{id}-rename-button") && !string.IsNullOrWhiteSpace(this._rename)) {
|
||||
var newName = SecretBox.Encrypt(info.SharedSecret, Encoding.UTF8.GetBytes(this._rename));
|
||||
Task.Run(async () => await this.Plugin.Client.UpdateToast(id, new UpdateKind.Name(newName)));
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui.BeginMenu($"Invite##{id}-invite")) {
|
||||
if (ImGui.IsWindowAppearing()) {
|
||||
this._inviteName = string.Empty;
|
||||
this._inviteWorld = 0;
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputTextWithHint("##invite-name", "Name", ref this._inviteName, 32);
|
||||
|
||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||
var preview = this._inviteWorld == 0 ? "World" : WorldUtil.WorldName(this._inviteWorld);
|
||||
if (ImGui.BeginCombo("##invite-world", preview)) {
|
||||
foreach (var (dc, worlds) in this._worlds) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
|
||||
ImGui.TextUnformatted(dc);
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.Separator();
|
||||
|
||||
foreach (var world in worlds) {
|
||||
if (ImGui.Selectable(world.Name.RawString, this._inviteWorld == world.RowId)) {
|
||||
this._inviteWorld = (ushort) world.RowId;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
if (ImGui.Button($"Invite##{id}-invite-button") && !string.IsNullOrWhiteSpace(this._inviteName) && this._inviteWorld != 0) {
|
||||
var inviteName = this._inviteName;
|
||||
var inviteWorld = this._inviteWorld;
|
||||
|
||||
Task.Run(async () => await this.Plugin.Client.InviteToast(inviteName, inviteWorld, id));
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.BeginMenu("Change number")) {
|
||||
ImGui.SetNextItemWidth(150 * ImGuiHelpers.GlobalScale);
|
||||
channelOrder.TryGetValue(id, out var refOrder);
|
||||
var old = refOrder;
|
||||
refOrder += 1;
|
||||
if (ImGui.InputInt($"##{id}-order", ref refOrder)) {
|
||||
refOrder = Math.Max(1, refOrder) - 1;
|
||||
|
||||
if (this.Plugin.ConfigInfo.ChannelOrder.TryGetValue(refOrder, out var other) && other != id) {
|
||||
// another channel already has this number, so swap
|
||||
this.Plugin.ConfigInfo.ChannelOrder[old] = other;
|
||||
} else {
|
||||
this.Plugin.ConfigInfo.ChannelOrder.Remove(old);
|
||||
}
|
||||
|
||||
this.Plugin.ConfigInfo.ChannelOrder[refOrder] = id;
|
||||
this.Plugin.SaveConfig();
|
||||
this.Plugin.Commands.ReregisterAll();
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
if (ImGui.Selectable("Request secrets")) {
|
||||
Task.Run(async () => await this.Plugin.Client.RequestSecrets(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.TableSetColumnIndex(1) && this._selectedChannel != Guid.Empty) {
|
||||
void DrawInfo() {
|
||||
if (!this.Plugin.Client.TryGetChannel(this._selectedChannel, out var channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector4 disabledColour;
|
||||
unsafe {
|
||||
disabledColour = *ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled);
|
||||
}
|
||||
|
||||
if (!this.Plugin.Client.ChannelRanks.TryGetValue(this._selectedChannel, out var rank)) {
|
||||
rank = Rank.Member;
|
||||
}
|
||||
|
||||
var first = true;
|
||||
foreach (var member in channel.Members) {
|
||||
if (!member.Online) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, disabledColour);
|
||||
}
|
||||
|
||||
try {
|
||||
ImGui.TextUnformatted($"{member.Rank.Symbol()}{member.Name}{CrossWorld}{WorldUtil.WorldName(member.World)}");
|
||||
} finally {
|
||||
if (!member.Online) {
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 4);
|
||||
anyChanged |= ImGuiUtil.Tutorial(this.Plugin, 5);
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopupContextItem($"{this._selectedChannel}-{member.Name}@{member.World}-context")) {
|
||||
var cursor = ImGui.GetCursorPos();
|
||||
|
||||
if (rank == Rank.Admin) {
|
||||
if (member.Rank is not (Rank.Admin or Rank.Invited)) {
|
||||
if (ImGuiUtil.SelectableConfirm("Promote to admin", tooltip: "This will demote you to moderator.")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Promote(this._selectedChannel, member.Name, member.World, Rank.Admin));
|
||||
}
|
||||
}
|
||||
|
||||
if (member.Rank == Rank.Moderator && ImGuiUtil.SelectableConfirm("Demote")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Promote(this._selectedChannel, member.Name, member.World, Rank.Member));
|
||||
}
|
||||
|
||||
if (member.Rank == Rank.Member && ImGuiUtil.SelectableConfirm("Promote to moderator")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Promote(this._selectedChannel, member.Name, member.World, Rank.Moderator));
|
||||
}
|
||||
}
|
||||
|
||||
if (rank >= Rank.Moderator) {
|
||||
var canKick = member.Rank < rank && member.Rank != Rank.Invited;
|
||||
if (canKick && ImGuiUtil.SelectableConfirm("Kick")) {
|
||||
Task.Run(async () => {
|
||||
if (await this.Plugin.Client.Kick(this._selectedChannel, member.Name, member.World) is { } error) {
|
||||
this.Plugin.ShowError($"Could not kick {member.Name}: {error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (member.Rank == Rank.Invited && ImGuiUtil.SelectableConfirm("Cancel invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Kick(this._selectedChannel, member.Name, member.World));
|
||||
}
|
||||
}
|
||||
|
||||
if (rank == Rank.Invited && member.Rank == Rank.Invited) {
|
||||
if (member.Name == this.Plugin.LocalPlayer?.Name.TextValue && member.World == this.Plugin.LocalPlayer?.HomeWorld.Id) {
|
||||
if (ImGui.Selectable("Accept invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Join(this._selectedChannel));
|
||||
}
|
||||
|
||||
if (ImGuiUtil.SelectableConfirm("Decline invite")) {
|
||||
Task.Run(async () => await this.Plugin.Client.Leave(this._selectedChannel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor == ImGui.GetCursorPos()) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
|
||||
ImGui.TextUnformatted("No options available");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.BeginChild("channel-info", childSize)) {
|
||||
DrawInfo();
|
||||
ImGui.EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
AfterTable:
|
||||
if (anyChanged) {
|
||||
this.Plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
|
||||
namespace ExtraChat.Util;
|
||||
|
||||
internal static class PayloadUtil {
|
||||
internal static RawPayload CreateTagPayload(Guid id) {
|
||||
var bytes = new List<byte> {
|
||||
2, // start byte
|
||||
0x27, // interactable
|
||||
3 + 16, // chunk length (3 bytes plus data length)
|
||||
0x20, // embedded info type (custom)
|
||||
};
|
||||
|
||||
// now add data, we always know it's 16 bytes
|
||||
bytes.AddRange(id.ToByteArray());
|
||||
|
||||
// end byte
|
||||
bytes.Add(3);
|
||||
|
||||
return new RawPayload(bytes.ToArray());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,800 @@
|
|||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net6.0-windows7.0": {
|
||||
"ASodium": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.5.3, )",
|
||||
"resolved": "0.5.3",
|
||||
"contentHash": "pSg92BLqGxeFTE0YPc11xZnJSB/c6DEIvzUK8hf7ofbXAh/NC6+aS8fED3W8aPDb02Tco2JjI53aDnOq6Gx83Q==",
|
||||
"dependencies": {
|
||||
"libsodium": "1.0.18"
|
||||
}
|
||||
},
|
||||
"Dalamud.ContextMenu": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.1, )",
|
||||
"resolved": "1.2.1",
|
||||
"contentHash": "RiBkn1OYRTnVbfUGYolLBE8MOeXjok+JiZaryb27oGa7YARCTu0XgUzkRiCglujknsHOn5kAaXsT3TUJmqMigg=="
|
||||
},
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.1.8, )",
|
||||
"resolved": "2.1.8",
|
||||
"contentHash": "YqagNXs9InxmqkXzq7kLveImxnodkBEicAhydMXVp7dFjC7xb76U6zGgAax4/BWIWfZeWzr5DJyQSev31kj81A=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.4.35, )",
|
||||
"resolved": "2.4.35",
|
||||
"contentHash": "sWyuByDues5fXghrCXXt5BSiqsQSjFWC+srBU97iwWWCV3JhiI3HoML5/Uj4zK9yD8pnAk2oobuuvwYfHhUruA==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "2.4.35",
|
||||
"Microsoft.NET.StringTools": "1.0.0"
|
||||
}
|
||||
},
|
||||
"System.Net.WebSockets.Client": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.3.2, )",
|
||||
"resolved": "4.3.2",
|
||||
"contentHash": "LqSrocFY47SxEmu1fYWbUmXcFJ2B/PqnMtc6zudOTUhNINgo75hgmoHv46ynkJLmqjkpxPzl251SGWHc+jofFQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.Win32.Primitives": "4.3.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Diagnostics.Tracing": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Net.NameResolution": "4.3.0",
|
||||
"System.Net.Primitives": "4.3.0",
|
||||
"System.Net.Security": "4.3.0",
|
||||
"System.Net.Sockets": "4.3.0",
|
||||
"System.Net.WebHeaderCollection": "4.3.0",
|
||||
"System.Net.WebSockets": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Cryptography.Algorithms": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Security.Cryptography.X509Certificates": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Text.Encoding.Extensions": "4.3.0",
|
||||
"System.Threading": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0",
|
||||
"System.Threading.Timer": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Channels": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q=="
|
||||
},
|
||||
"libsodium": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.18",
|
||||
"contentHash": "Ajv3AR9Qg/C4SQcE2ONx/UieeKnn5lSvVNc6egC3p6NP6qjZzWJ+Xg2vJURNYjkpHui/KctBwQjMPqpZK8/CHA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.0.1"
|
||||
}
|
||||
},
|
||||
"MessagePack.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.4.35",
|
||||
"contentHash": "6wfQ3Law5TMPeMm/HpYpMLal5HzEj70QFmbXWzHJjRsxF+nEol8RP3mqGUIctN9pItdtKCpbrpP8iEpSCkORCA=="
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "ZYVcoDM0LnSyT5nWoRGfShYdOecCw2sOXWwP6j1Z0u48Xq3+BVvZ+EiPCX9/8Gz439giW+O1H1kWF9Eb/w6rVg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.4",
|
||||
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
||||
},
|
||||
"Microsoft.Win32.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q=="
|
||||
},
|
||||
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA=="
|
||||
},
|
||||
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw=="
|
||||
},
|
||||
"runtime.native.System": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"runtime.native.System.Net.Http": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"runtime.native.System.Net.Security": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "M2nN92ePS8BgQ2oi6Jj3PlTUzadYSIWLdZrHY1n1ZcW9o4wAQQ6W+aQ2lfq1ysZQfVCgDwY58alUdowrzezztg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"runtime.native.System.Security.Cryptography.Apple": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==",
|
||||
"dependencies": {
|
||||
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0"
|
||||
}
|
||||
},
|
||||
"runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==",
|
||||
"dependencies": {
|
||||
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A=="
|
||||
},
|
||||
"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ=="
|
||||
},
|
||||
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ=="
|
||||
},
|
||||
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g=="
|
||||
},
|
||||
"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg=="
|
||||
},
|
||||
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ=="
|
||||
},
|
||||
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A=="
|
||||
},
|
||||
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg=="
|
||||
},
|
||||
"System.Collections": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Collections.Concurrent": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==",
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Diagnostics.Tracing": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Threading": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.Debug": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.Tracing": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Globalization": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Globalization.Calendars": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Globalization.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.IO": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.IO.FileSystem": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.IO.FileSystem.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.IO.FileSystem.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Linq": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==",
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
|
||||
},
|
||||
"System.Net.NameResolution": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "AFYl08R7MrsrEjqpQWTZWBadqXyTzNDaWpMqyxhb0d6sGhV6xMDKueuBXlLL30gz+DIRY6MpdgnHWlCh5wmq9w==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Tracing": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Net.Primitives": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Principal.Windows": "4.3.0",
|
||||
"System.Threading": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0",
|
||||
"runtime.native.System": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Net.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Net.Security": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "IgJKNfALqw7JRWp5LMQ5SWHNKvXVz094U6wNE3c1i8bOkMQvgBL+MMQuNt3xl9Qg9iWpj3lFxPZEY6XHmROjMQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.Win32.Primitives": "4.3.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Collections.Concurrent": "4.3.0",
|
||||
"System.Diagnostics.Tracing": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Globalization.Extensions": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Net.Primitives": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Claims": "4.3.0",
|
||||
"System.Security.Cryptography.Algorithms": "4.3.0",
|
||||
"System.Security.Cryptography.Encoding": "4.3.0",
|
||||
"System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Security.Cryptography.X509Certificates": "4.3.0",
|
||||
"System.Security.Principal": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0",
|
||||
"System.Threading.ThreadPool": "4.3.0",
|
||||
"runtime.native.System": "4.3.0",
|
||||
"runtime.native.System.Net.Security": "4.3.0",
|
||||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Net.Sockets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Net.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Net.WebHeaderCollection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "XZrXYG3c7QV/GpWeoaRC02rM6LH2JJetfVYskf35wdC/w2fFDFMphec4gmVH2dkll6abtW14u9Rt96pxd9YH2A==",
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Net.WebSockets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "u6fFNY5q4T8KerUAVbya7bR6b7muBuSTAersyrihkcmE5QhEOiH3t5rh4il15SexbVlpXFHGuMwr/m8fDrnkQg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.Primitives": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Resources.ResourceManager": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
|
||||
},
|
||||
"System.Runtime.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.Handles": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.InteropServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.Numerics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==",
|
||||
"dependencies": {
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Claims": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "P/+BR/2lnc4PNDHt/TPBAWHVMLMRHsyYZbU1NphW4HIWzCggz8mJbTQQ3MKljFE7LS3WagmVFuBgoLcFzYXlkA==",
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Security.Principal": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Algorithms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Runtime.Numerics": "4.3.0",
|
||||
"System.Security.Cryptography.Encoding": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"runtime.native.System.Security.Cryptography.Apple": "4.3.0",
|
||||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Cng": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Cryptography.Algorithms": "4.3.0",
|
||||
"System.Security.Cryptography.Encoding": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Csp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Cryptography.Algorithms": "4.3.0",
|
||||
"System.Security.Cryptography.Encoding": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Collections.Concurrent": "4.3.0",
|
||||
"System.Linq": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.OpenSsl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==",
|
||||
"dependencies": {
|
||||
"System.Collections": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Runtime.Numerics": "4.3.0",
|
||||
"System.Security.Cryptography.Algorithms": "4.3.0",
|
||||
"System.Security.Cryptography.Encoding": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Threading": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.X509Certificates": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Globalization.Calendars": "4.3.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.IO.FileSystem": "4.3.0",
|
||||
"System.IO.FileSystem.Primitives": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Runtime.Numerics": "4.3.0",
|
||||
"System.Security.Cryptography.Algorithms": "4.3.0",
|
||||
"System.Security.Cryptography.Cng": "4.3.0",
|
||||
"System.Security.Cryptography.Csp": "4.3.0",
|
||||
"System.Security.Cryptography.Encoding": "4.3.0",
|
||||
"System.Security.Cryptography.OpenSsl": "4.3.0",
|
||||
"System.Security.Cryptography.Primitives": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading": "4.3.0",
|
||||
"runtime.native.System": "4.3.0",
|
||||
"runtime.native.System.Net.Http": "4.3.0",
|
||||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Principal": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "I1tkfQlAoMM2URscUtpcRo/hX0jinXx6a/KUtEQoz3owaYwl3qwsO8cbzYVVnjxrzxjHo3nJC+62uolgeGIS9A==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Principal.Windows": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "HVL1rvqYtnRCxFsYag/2le/ZfKLK4yMw79+s6FmKXbSCNN0JeAhrYxnRAHFoWRa0dEojsDcbBSpH3l22QxAVyw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.Win32.Primitives": "4.3.0",
|
||||
"System.Collections": "4.3.0",
|
||||
"System.Diagnostics.Debug": "4.3.0",
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Resources.ResourceManager": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Extensions": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0",
|
||||
"System.Runtime.InteropServices": "4.3.0",
|
||||
"System.Security.Claims": "4.3.0",
|
||||
"System.Security.Principal": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.ThreadPool": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "k/+g4b7vjdd4aix83sTgC9VG6oXYKAktSfNIJUNGxPEj7ryEOfzHHhfnmsZvjxawwcD9HyWXKCXmPjX8U4zeSw==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Runtime.Handles": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Timer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -336,6 +336,7 @@ dependencies = [
|
|||
"prefixed-api-key",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rmp-serde",
|
||||
"rustyline",
|
||||
"serde",
|
||||
|
@ -346,6 +347,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
|
@ -18,16 +18,17 @@ parking_lot = "0.12"
|
|||
prefixed-api-key = { git = "https://git.annaclemens.io/ascclemens/prefixed-api-key.git" }
|
||||
rand = "0.8"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.11", default-features = false }
|
||||
rmp-serde = "1"
|
||||
rustyline = { version = "9", default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
serde_repr = "0.1"
|
||||
sha3 = "0.10"
|
||||
#sodiumoxide = "0.2"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite", "chrono"] }
|
||||
tokio-tungstenite = "0.17"
|
||||
toml = "0.5"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dependencies.tokio]
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{ClientState, State, util, WsStream};
|
||||
use crate::types::protocol::{AllowInvitesRequest, AllowInvitesResponse};
|
||||
|
||||
pub async fn allow_invites(_state: Arc<RwLock<State>>, client_state: Arc<RwLock<ClientState>>, conn: &mut WsStream, number: u32, req: AllowInvitesRequest) -> anyhow::Result<()> {
|
||||
client_state.write().await.allow_invites = req.allowed;
|
||||
util::send(conn, number, AllowInvitesResponse {
|
||||
allowed: req.allowed,
|
||||
}).await
|
||||
}
|
|
@ -3,7 +3,6 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::Context;
|
||||
use chrono::{Duration, Utc};
|
||||
use lodestone_scraper::LodestoneScraper;
|
||||
use log::trace;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
|
@ -11,8 +10,7 @@ use crate::{AuthenticateRequest, AuthenticateResponse, ClientState, State, User,
|
|||
|
||||
pub async fn authenticate(state: Arc<RwLock<State>>, client_state: Arc<RwLock<ClientState>>, conn: &mut WsStream, number: u32, req: AuthenticateRequest) -> anyhow::Result<()> {
|
||||
if client_state.read().await.user.is_some() {
|
||||
util::send(conn, number, AuthenticateResponse::error("already logged in")).await?;
|
||||
return Ok(());
|
||||
return util::send(conn, number, AuthenticateResponse::error("already logged in")).await;
|
||||
}
|
||||
|
||||
let key = prefixed_api_key::parse(&*req.key)
|
||||
|
@ -27,38 +25,20 @@ pub async fn authenticate(state: Arc<RwLock<State>>, client_state: Arc<RwLock<Cl
|
|||
.fetch_optional(&state.read().await.db)
|
||||
.await
|
||||
.context("could not query database for user")?;
|
||||
let mut user = match user {
|
||||
let user = match user {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
util::send(conn, number, AuthenticateResponse::error("invalid key")).await?;
|
||||
return Ok(());
|
||||
}
|
||||
None => return util::send(conn, number, AuthenticateResponse::error("invalid key")).await,
|
||||
};
|
||||
|
||||
if Utc::now().naive_utc().signed_duration_since(user.last_updated) >= Duration::hours(2) {
|
||||
let info = LodestoneScraper::default()
|
||||
.character(user.lodestone_id as u64)
|
||||
.await
|
||||
.context("could not get character info")?;
|
||||
let world_name = info.world.as_str();
|
||||
|
||||
user.name = info.name.clone();
|
||||
user.world = world_name.to_string();
|
||||
|
||||
sqlx::query!(
|
||||
// language=sqlite
|
||||
"update users set name = ?, world = ?, last_updated = current_timestamp where lodestone_id = ?",
|
||||
info.name,
|
||||
world_name,
|
||||
user.lodestone_id,
|
||||
)
|
||||
.execute(&state.read().await.db)
|
||||
.await
|
||||
.context("could not update user")?;
|
||||
}
|
||||
|
||||
let world = World::from_str(&user.world).map_err(|_| anyhow::anyhow!("invalid world in db"))?;
|
||||
|
||||
if let Some(old_client_state) = state.read().await.clients.get(&(user.lodestone_id as u64)) {
|
||||
let mut lock = old_client_state.write().await;
|
||||
// this prevents the old client thread from removing info from the global state
|
||||
lock.user = None;
|
||||
lock.shutdown_tx.send(()).await.ok();
|
||||
}
|
||||
|
||||
trace!(" [authenticate] before user write");
|
||||
let mut c_state = client_state.write().await;
|
||||
c_state.user = Some(User {
|
||||
|
@ -69,6 +49,7 @@ pub async fn authenticate(state: Arc<RwLock<State>>, client_state: Arc<RwLock<Cl
|
|||
});
|
||||
|
||||
c_state.pk = req.pk.into_inner();
|
||||
c_state.allow_invites = req.allow_invites;
|
||||
|
||||
// release lock asap
|
||||
drop(c_state);
|
||||
|
@ -80,7 +61,9 @@ pub async fn authenticate(state: Arc<RwLock<State>>, client_state: Arc<RwLock<Cl
|
|||
state.write().await.ids.insert((user.name, util::id_from_world(world)), user.lodestone_id as u64);
|
||||
trace!(" [authenticate] after state writes");
|
||||
|
||||
util::send(conn, number, AuthenticateResponse::success()).await?;
|
||||
if Utc::now().naive_utc().signed_duration_since(user.last_updated) >= Duration::hours(2) {
|
||||
state.read().await.updater_tx.send(user.lodestone_id).ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
util::send(conn, number, AuthenticateResponse::success()).await
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{ClientState, ErrorResponse, State, WsStream};
|
||||
use crate::types::protocol::{DeleteAccountRequest, DeleteAccountResponse};
|
||||
|
||||
pub async fn delete_account(state: Arc<RwLock<State>>, client_state: Arc<RwLock<ClientState>>, conn: &mut WsStream, number: u32, _req: DeleteAccountRequest) -> Result<()> {
|
||||
let id = match client_state.read().await.lodestone_id() {
|
||||
Some(id) => id,
|
||||
None => return crate::util::send(conn, number, ErrorResponse::new(None, "no Lodestone ID? this is a bug")).await,
|
||||
};
|
||||
let lodestone_id = id as i64;
|
||||
|
||||
let channels = sqlx::query!(
|
||||
// language=sqlite
|
||||
"select count(*) as count from user_channels where lodestone_id = ?",
|
||||
lodestone_id,
|
||||
)
|
||||
.fetch_one(&state.read().await.db)
|
||||
.await
|
||||
.context("could not get channel count")?;
|
||||
|
||||
if channels.count > 0 {
|
||||
return crate::util::send(conn, number, ErrorResponse::new(None, "leave all linkshells first")).await;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
// language=sqlite
|
||||
"delete from users where lodestone_id = ?",
|
||||
lodestone_id,
|
||||
)
|
||||
.execute(&state.read().await.db)
|
||||
.await
|
||||
.context("could not delete user")?;
|
||||
|
||||
crate::util::send(conn, number, DeleteAccountResponse {}).await
|
||||
}
|
|
@ -23,12 +23,19 @@ pub async fn invite(state: Arc<RwLock<State>>, client_state: Arc<RwLock<ClientSt
|
|||
return crate::util::send(conn, number, ErrorResponse::new(req.channel, "not enough permissions to invite")).await;
|
||||
}
|
||||
|
||||
const NOT_ONLINE: &str = "user not online";
|
||||
let target_id = match state.read().await.ids.get(&(req.name.clone(), req.world)) {
|
||||
Some(id) => *id,
|
||||
None => return crate::util::send(conn, number, ErrorResponse::new(req.channel, "user not online")).await,
|
||||
None => return crate::util::send(conn, number, ErrorResponse::new(req.channel, NOT_ONLINE)).await,
|
||||
};
|
||||
let target_id_i = target_id as i64;
|
||||
|
||||
if let Some(client) = state.read().await.clients.get(&target_id) {
|
||||
if !client.read().await.allow_invites {
|
||||
return crate::util::send(conn, number, ErrorResponse::new(req.channel, NOT_ONLINE)).await;
|
||||
}
|
||||
}
|
||||
|
||||
if target_id_i == lodestone_id {
|
||||
return crate::util::send(conn, number, ErrorResponse::new(req.channel, "cannot invite self")).await;
|
||||
}
|
||||
|
@ -110,7 +117,7 @@ pub async fn invite(state: Arc<RwLock<State>>, client_state: Arc<RwLock<ClientSt
|
|||
}),
|
||||
}).await?;
|
||||
}
|
||||
None => return crate::util::send(conn, number, ErrorResponse::new(req.channel, "user not online")).await,
|
||||
None => return crate::util::send(conn, number, ErrorResponse::new(req.channel, NOT_ONLINE)).await,
|
||||
}
|
||||
|
||||
crate::util::send(conn, number, InviteResponse {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
pub use self::{
|
||||
allow_invites::*,
|
||||
authenticate::*,
|
||||
create::*,
|
||||
delete_account::*,
|
||||
disband::*,
|
||||
invite::*,
|
||||
join::*,
|
||||
|
@ -18,8 +20,10 @@ pub use self::{
|
|||
version::*,
|
||||
};
|
||||
|
||||
pub mod allow_invites;
|
||||
pub mod authenticate;
|
||||
pub mod create;
|
||||
pub mod delete_account;
|
||||
pub mod disband;
|
||||
pub mod invite;
|
||||
pub mod join;
|
||||
|
|
|
@ -10,25 +10,20 @@ use crate::util::redacted::Redacted;
|
|||
pub async fn public_key(state: Arc<RwLock<State>>, conn: &mut WsStream, number: u32, req: PublicKeyRequest) -> Result<()> {
|
||||
let id = match state.read().await.ids.get(&(req.name.clone(), req.world)) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
crate::util::send(conn, number, PublicKeyResponse {
|
||||
name: req.name,
|
||||
world: req.world,
|
||||
pk: None,
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
None => return crate::util::send(conn, number, PublicKeyResponse {
|
||||
name: req.name,
|
||||
world: req.world,
|
||||
pk: None,
|
||||
}).await,
|
||||
};
|
||||
|
||||
let pk = match state.read().await.clients.get(&id) {
|
||||
Some(client) => Some(client.read().await.pk.clone()),
|
||||
None => None,
|
||||
Some(client) if client.read().await.allow_invites => Some(client.read().await.pk.clone()),
|
||||
_ => None,
|
||||
};
|
||||
crate::util::send(conn, number, PublicKeyResponse {
|
||||
name: req.name,
|
||||
world: req.world,
|
||||
pk: pk.map(Redacted),
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
pk: pk.map(Redacted::new),
|
||||
}).await
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{debug, error};
|
||||
use reqwest::Client;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{Config, State};
|
||||
|
||||
pub fn spawn(config: &Config, state: Arc<RwLock<State>>) {
|
||||
let influx = match &config.influx {
|
||||
Some(i) => i,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut url = match influx.url.join("/api/v2/write") {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
error!("Failed to parse influxdb url: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("org", &influx.org)
|
||||
.append_pair("bucket", &influx.bucket);
|
||||
|
||||
let influx_token = influx.token.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let mut last_messages = 0;
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
loop {
|
||||
let messages = state.read().await.messages_sent.load(Ordering::SeqCst);
|
||||
let diff = messages - last_messages;
|
||||
last_messages = messages;
|
||||
|
||||
let clients = state.read().await.clients.len();
|
||||
|
||||
let num_users = sqlx::query!(
|
||||
// language=sqlite
|
||||
"select count(*) as count from users"
|
||||
)
|
||||
.fetch_one(&state.read().await.db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let in_one = sqlx::query!(
|
||||
// language=sqlite
|
||||
"select count(distinct lodestone_id) as count from user_channels"
|
||||
)
|
||||
.fetch_one(&state.read().await.db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let num_linkshells = sqlx::query!(
|
||||
// language=sqlite
|
||||
"select count(*) as count from channels"
|
||||
)
|
||||
.fetch_one(&state.read().await.db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let outstanding_invites = sqlx::query!(
|
||||
// language=sqlite
|
||||
"select count(*) as count from channel_invites"
|
||||
)
|
||||
.fetch_one(&state.read().await.db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let timestamp = Utc::now().timestamp_nanos();
|
||||
|
||||
let mut line_format = format!(
|
||||
"logged_in value={logged_in}u {timestamp}\nmessages_this_instance value={messages_this_instance}u {timestamp}\nmessages_new value={messages_new}u {timestamp}\n",
|
||||
logged_in = clients,
|
||||
messages_this_instance = messages,
|
||||
messages_new = diff,
|
||||
timestamp = timestamp,
|
||||
);
|
||||
|
||||
if let Some(num_users) = num_users {
|
||||
line_format.push_str(&format!(
|
||||
"users value={users}u {timestamp}\n",
|
||||
users = num_users.count,
|
||||
timestamp = timestamp,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(in_one) = in_one {
|
||||
line_format.push_str(&format!(
|
||||
"users_in_at_least_one_linkshell value={in_one}u {timestamp}\n",
|
||||
in_one = in_one.count,
|
||||
timestamp = timestamp,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(num_linkshells) = num_linkshells {
|
||||
line_format.push_str(&format!(
|
||||
"linkshells value={linkshells}u {timestamp}\n",
|
||||
linkshells = num_linkshells.count,
|
||||
timestamp = timestamp,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(outstanding_invites) = outstanding_invites {
|
||||
line_format.push_str(&format!(
|
||||
"outstanding_invites value={outstanding_invites}u {timestamp}\n",
|
||||
outstanding_invites = outstanding_invites.count,
|
||||
timestamp = timestamp,
|
||||
));
|
||||
}
|
||||
|
||||
debug!("line_format: {}", line_format);
|
||||
|
||||
let res = client.post(url.clone())
|
||||
.header("Authorization", format!("Token {}", influx_token))
|
||||
.body(line_format)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|resp| resp.error_for_status());
|
||||
|
||||
if let Err(e) = res {
|
||||
error!("failed to send to influxdb: {}", e);
|
||||
} else {
|
||||
debug!("sent to influxdb");
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -17,7 +17,7 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
|||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_tungstenite::{
|
||||
tungstenite::Message as WsMessage,
|
||||
|
@ -49,6 +49,7 @@ pub mod handlers;
|
|||
pub mod util;
|
||||
pub mod updater;
|
||||
pub mod logging;
|
||||
pub mod influx;
|
||||
|
||||
pub type WsStream = WebSocketStream<TcpStream>;
|
||||
|
||||
|
@ -58,6 +59,7 @@ pub struct State {
|
|||
pub ids: HashMap<(String, u16), u64>,
|
||||
pub secrets_requests: HashMap<Uuid, SecretsRequestInfo>,
|
||||
pub messages_sent: AtomicU64,
|
||||
pub updater_tx: UnboundedSender<i64>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
@ -125,6 +127,9 @@ async fn main() -> Result<()> {
|
|||
.await
|
||||
.context("could not run database migrations")?;
|
||||
|
||||
// set up updater channel
|
||||
let (updater_tx, updater_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
// set up server
|
||||
let server = TcpListener::bind(&config.server.address).await?;
|
||||
let state = Arc::new(RwLock::new(State {
|
||||
|
@ -133,6 +138,7 @@ async fn main() -> Result<()> {
|
|||
ids: Default::default(),
|
||||
secrets_requests: Default::default(),
|
||||
messages_sent: AtomicU64::default(),
|
||||
updater_tx,
|
||||
}));
|
||||
|
||||
info!("Listening on ws://{}/", server.local_addr()?);
|
||||
|
@ -188,18 +194,29 @@ async fn main() -> Result<()> {
|
|||
{
|
||||
let state = Arc::clone(&state);
|
||||
tokio::task::spawn(async move {
|
||||
let mut last_messages = 0;
|
||||
|
||||
loop {
|
||||
let messages = state.read().await.messages_sent.load(Ordering::SeqCst);
|
||||
let diff = messages - last_messages;
|
||||
last_messages = messages;
|
||||
|
||||
let clients = state.read().await.clients.len();
|
||||
|
||||
info!(
|
||||
"Clients: {}, messages sent: {}",
|
||||
state.read().await.clients.len(),
|
||||
state.read().await.messages_sent.load(Ordering::SeqCst),
|
||||
"Clients: {}, messages sent: {} (+{})",
|
||||
clients,
|
||||
messages,
|
||||
diff,
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updater::spawn(Arc::clone(&state));
|
||||
influx::spawn(&config, Arc::clone(&state));
|
||||
|
||||
updater::spawn(Arc::clone(&state), updater_rx);
|
||||
|
||||
loop {
|
||||
let res: Result<()> = try {
|
||||
|
@ -244,7 +261,9 @@ async fn main() -> Result<()> {
|
|||
pub struct ClientState {
|
||||
user: Option<User>,
|
||||
tx: Sender<ResponseContainer>,
|
||||
shutdown_tx: Sender<()>,
|
||||
pk: Vec<u8>,
|
||||
allow_invites: bool,
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
|
@ -327,16 +346,23 @@ impl ClientState {
|
|||
|
||||
async fn client_loop(state: Arc<RwLock<State>>, mut conn: WsStream) -> Result<()> {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(10);
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
let client_state = Arc::new(RwLock::new(ClientState {
|
||||
user: None,
|
||||
tx,
|
||||
shutdown_tx,
|
||||
pk: Default::default(),
|
||||
allow_invites: false,
|
||||
}));
|
||||
|
||||
loop {
|
||||
let res: Result<()> = try {
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
debug!("break due to new login");
|
||||
break;
|
||||
}
|
||||
msg = rx.recv() => {
|
||||
if let Some(msg) = msg {
|
||||
let encoded = rmp_serde::to_vec(&msg)?;
|
||||
|
@ -410,6 +436,12 @@ async fn client_loop(state: Arc<RwLock<State>>, mut conn: WsStream) -> Result<()
|
|||
RequestKind::SendSecrets(req) if logged_in => {
|
||||
crate::handlers::send_secrets(Arc::clone(&state), Arc::clone(&client_state), &mut conn, msg.number, req).await?;
|
||||
}
|
||||
RequestKind::AllowInvites(req) if logged_in => {
|
||||
crate::handlers::allow_invites(Arc::clone(&state), Arc::clone(&client_state), &mut conn, msg.number, req).await?;
|
||||
}
|
||||
RequestKind::DeleteAccount(req) if logged_in => {
|
||||
crate::handlers::delete_account(Arc::clone(&state), Arc::clone(&client_state), &mut conn, msg.number, req).await?;
|
||||
}
|
||||
_ if !logged_in => {
|
||||
util::send(&mut conn, msg.number, ErrorResponse::new(None, "not logged in")).await?;
|
||||
}
|
||||
|
@ -434,10 +466,14 @@ async fn client_loop(state: Arc<RwLock<State>>, mut conn: WsStream) -> Result<()
|
|||
}
|
||||
}
|
||||
|
||||
debug!("ending client thread");
|
||||
|
||||
if let Some(user) = &client_state.read().await.user {
|
||||
state.write().await.clients.remove(&user.lodestone_id);
|
||||
state.write().await.ids.remove(&(user.name.clone(), util::id_from_world(user.world)));
|
||||
}
|
||||
|
||||
debug!("client thread ended");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub server: Server,
|
||||
pub database: Database,
|
||||
#[serde(default)]
|
||||
pub influx: Option<Influx>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -15,3 +18,11 @@ pub struct Server {
|
|||
pub struct Database {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Influx {
|
||||
pub url: Url,
|
||||
pub org: String,
|
||||
pub bucket: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AllowInvitesRequest {
|
||||
pub allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AllowInvitesResponse {
|
||||
pub allowed: bool,
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::util::redacted::Redacted;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -6,6 +7,8 @@ pub struct AuthenticateRequest {
|
|||
pub key: Redacted<String>,
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub pk: Redacted<Vec<u8>>,
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_invites: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -26,3 +29,7 @@ impl AuthenticateResponse {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ pub enum RequestKind {
|
|||
PublicKey(PublicKeyRequest),
|
||||
Secrets(SecretsRequest),
|
||||
SendSecrets(SendSecretsRequest),
|
||||
AllowInvites(AllowInvitesRequest),
|
||||
DeleteAccount(DeleteAccountRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -61,6 +63,8 @@ pub enum ResponseKind {
|
|||
Secrets(SecretsResponse),
|
||||
SendSecrets(SendSecretsResponse),
|
||||
Announce(AnnounceResponse),
|
||||
AllowInvites(AllowInvitesResponse),
|
||||
DeleteAccount(DeleteAccountResponse),
|
||||
}
|
||||
|
||||
macro_rules! request_container {
|
||||
|
@ -90,6 +94,8 @@ request_container!(Update, UpdateRequest);
|
|||
request_container!(PublicKey, PublicKeyRequest);
|
||||
request_container!(Secrets, SecretsRequest);
|
||||
request_container!(SendSecrets, SendSecretsRequest);
|
||||
request_container!(AllowInvites, AllowInvitesRequest);
|
||||
request_container!(DeleteAccount, DeleteAccountRequest);
|
||||
|
||||
macro_rules! response_container {
|
||||
($name:ident, $response:ty) => {
|
||||
|
@ -123,3 +129,5 @@ response_container!(MemberChange, MemberChangeResponse);
|
|||
response_container!(Secrets, SecretsResponse);
|
||||
response_container!(SendSecrets, SendSecretsResponse);
|
||||
response_container!(Announce, AnnounceResponse);
|
||||
response_container!(AllowInvites, AllowInvitesResponse);
|
||||
response_container!(DeleteAccount, DeleteAccountResponse);
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteAccountRequest {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteAccountResponse {
|
||||
}
|
|
@ -1,7 +1,34 @@
|
|||
pub use self::{
|
||||
allow_invites::*,
|
||||
announce::*,
|
||||
authenticate::*,
|
||||
container::*,
|
||||
create::*,
|
||||
delete_account::*,
|
||||
disband::*,
|
||||
error::*,
|
||||
invite::*,
|
||||
join::*,
|
||||
kick::*,
|
||||
leave::*,
|
||||
list::*,
|
||||
member_change::*,
|
||||
message::*,
|
||||
ping::*,
|
||||
promote::*,
|
||||
public_key::*,
|
||||
register::*,
|
||||
secrets::*,
|
||||
update::*,
|
||||
version::*,
|
||||
};
|
||||
|
||||
pub mod allow_invites;
|
||||
pub mod announce;
|
||||
pub mod authenticate;
|
||||
pub mod container;
|
||||
pub mod create;
|
||||
pub mod delete_account;
|
||||
pub mod disband;
|
||||
pub mod error;
|
||||
pub mod invite;
|
||||
|
@ -21,25 +48,3 @@ pub mod version;
|
|||
|
||||
pub mod channel;
|
||||
|
||||
pub use self::{
|
||||
announce::*,
|
||||
authenticate::*,
|
||||
container::*,
|
||||
create::*,
|
||||
disband::*,
|
||||
error::*,
|
||||
invite::*,
|
||||
join::*,
|
||||
kick::*,
|
||||
leave::*,
|
||||
list::*,
|
||||
member_change::*,
|
||||
message::*,
|
||||
ping::*,
|
||||
promote::*,
|
||||
public_key::*,
|
||||
register::*,
|
||||
secrets::*,
|
||||
update::*,
|
||||
version::*,
|
||||
};
|
||||
|
|
|
@ -1,57 +1,47 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use lodestone_scraper::LodestoneScraper;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
use log::{error, info, trace};
|
||||
use log::{debug, error, trace};
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::UnboundedReceiver,
|
||||
RwLock,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::State;
|
||||
|
||||
pub fn spawn(state: Arc<RwLock<State>>) -> JoinHandle<()> {
|
||||
pub fn spawn(state: Arc<RwLock<State>>, mut rx: UnboundedReceiver<i64>) -> JoinHandle<()> {
|
||||
const WAIT_TIME: u64 = 5;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let lodestone = LodestoneScraper::default();
|
||||
|
||||
loop {
|
||||
match inner(&state, &lodestone).await {
|
||||
Ok(results) => {
|
||||
let successful = results.values().filter(|result| result.is_ok()).count();
|
||||
info!("Updated {}/{} characters", successful, results.len());
|
||||
for (id, result) in results {
|
||||
if let Err(e) = result {
|
||||
error!("error updating user {}: {:?}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("error updating users: {:?}", e);
|
||||
}
|
||||
let mut last_update = Instant::now();
|
||||
while let Some(id) = rx.recv().await {
|
||||
// make sure to wait five seconds between each request
|
||||
let elapsed = last_update.elapsed();
|
||||
if elapsed < Duration::from_secs(WAIT_TIME) {
|
||||
let left = Duration::from_secs(WAIT_TIME) - elapsed;
|
||||
tokio::time::sleep(left).await;
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
match update(&*state, &lodestone, id).await {
|
||||
Ok(()) => debug!("updated user {}", id),
|
||||
Err(e) => error!("error updating user {}: {:?}", id, e),
|
||||
}
|
||||
|
||||
last_update = Instant::now();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn inner(state: &RwLock<State>, lodestone: &LodestoneScraper) -> Result<HashMap<u32, Result<()>>> {
|
||||
let users = sqlx::query!(
|
||||
// language=sqlite
|
||||
"select * from users where (julianday(current_timestamp) - julianday(last_updated)) * 24 >= 2",
|
||||
)
|
||||
.fetch_all(&state.read().await.db)
|
||||
.await
|
||||
.context("could not query database for users")?;
|
||||
|
||||
let mut results = HashMap::with_capacity(users.len());
|
||||
for user in users {
|
||||
results.insert(user.lodestone_id as u32, update(state, lodestone, user.lodestone_id).await);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn update(state: &RwLock<State>, lodestone: &LodestoneScraper, lodestone_id: i64) -> Result<()> {
|
||||
let info = lodestone
|
||||
.character(lodestone_id as u64)
|
||||
|
@ -83,4 +73,4 @@ async fn update(state: &RwLock<State>, lodestone: &LodestoneScraper, lodestone_i
|
|||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -7,9 +7,13 @@ use sqlx::database::HasArguments;
|
|||
use sqlx::encode::IsNull;
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct Redacted<T>(pub T);
|
||||
pub struct Redacted<T>(T);
|
||||
|
||||
impl<T> Redacted<T> {
|
||||
pub fn new(t: T) -> Self {
|
||||
Self(t)
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue