mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
3xpl websocket client in case anyone wanted one. Don't bother using it though, their websocket service is a piece of shit that's totally broken which I only found out after wasting a day on it.
This commit is contained in:
@@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KickWsClient", "KickWsClien
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetKickBot", "KfChatDotNetKickBot\KfChatDotNetKickBot.csproj", "{4734E0A4-150E-4915-B905-928BB4BE3FF6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetKickBot", "KfChatDotNetKickBot\KfChatDotNetKickBot.csproj", "{4734E0A4-150E-4915-B905-928BB4BE3FF6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThreeXplWsClient", "ThreeXplWsClient\ThreeXplWsClient.csproj", "{3D72D70A-48AD-4EE8-89DC-C78153EEA879}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThreeXplCliClient", "ThreeXplCliClient\ThreeXplCliClient.csproj", "{D098E281-5535-4A07-9514-57AF78704B0C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -36,5 +40,13 @@ Global
|
|||||||
{4734E0A4-150E-4915-B905-928BB4BE3FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4734E0A4-150E-4915-B905-928BB4BE3FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4734E0A4-150E-4915-B905-928BB4BE3FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4734E0A4-150E-4915-B905-928BB4BE3FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4734E0A4-150E-4915-B905-928BB4BE3FF6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4734E0A4-150E-4915-B905-928BB4BE3FF6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3D72D70A-48AD-4EE8-89DC-C78153EEA879}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3D72D70A-48AD-4EE8-89DC-C78153EEA879}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3D72D70A-48AD-4EE8-89DC-C78153EEA879}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3D72D70A-48AD-4EE8-89DC-C78153EEA879}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D098E281-5535-4A07-9514-57AF78704B0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D098E281-5535-4A07-9514-57AF78704B0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D098E281-5535-4A07-9514-57AF78704B0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D098E281-5535-4A07-9514-57AF78704B0C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
15
ThreeXplCliClient/NLog.config
Normal file
15
ThreeXplCliClient/NLog.config
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
|
||||||
|
autoReload="true"
|
||||||
|
throwExceptions="false"
|
||||||
|
internalLogLevel="Off" internalLogFile="~/nlog-internal.log">
|
||||||
|
<targets>
|
||||||
|
<target xsi:type="Console" name="console"/>
|
||||||
|
</targets>
|
||||||
|
|
||||||
|
<rules>
|
||||||
|
<logger name="*" minlevel="Debug" writeTo="console" />
|
||||||
|
</rules>
|
||||||
|
</nlog>
|
||||||
3483
ThreeXplCliClient/NLog.xsd
Normal file
3483
ThreeXplCliClient/NLog.xsd
Normal file
File diff suppressed because it is too large
Load Diff
18
ThreeXplCliClient/Program.cs
Normal file
18
ThreeXplCliClient/Program.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Spectre.Console;
|
||||||
|
using ThreeXplWsClient.Events;
|
||||||
|
|
||||||
|
namespace ThreeXplCliClient
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
|
AnsiConsole.MarkupLine("[green]3xpl test client started[/]");
|
||||||
|
var cliClient = new ThreeXplClient();
|
||||||
|
await cliClient.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
ThreeXplCliClient/ThreeXplCliClient.csproj
Normal file
30
ThreeXplCliClient/ThreeXplCliClient.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NLog" Version="5.3.2" />
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.49.1" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ThreeXplWsClient\ThreeXplWsClient.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="NLog.config">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<None Remove="NLog.config" />
|
||||||
|
<Content Include="NLog.config">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
38
ThreeXplCliClient/ThreeXplClient.cs
Normal file
38
ThreeXplCliClient/ThreeXplClient.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Spectre.Console;
|
||||||
|
using ThreeXplWsClient.Events;
|
||||||
|
|
||||||
|
namespace ThreeXplCliClient;
|
||||||
|
|
||||||
|
public class ThreeXplClient
|
||||||
|
{
|
||||||
|
private List<string> _addresses =
|
||||||
|
[
|
||||||
|
"MC8TiBEsnQVjxbvLtTsUXjTBZTQaR8fe8X",
|
||||||
|
"ltc1qks2m7hvmhs3c20zrfvptv9pvk82p8g70sgw5mk"
|
||||||
|
];
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
var client = new ThreeXplWsClient.ThreeXplWsClient();
|
||||||
|
client.OnThreeXplPush += OnThreeXplEvent;
|
||||||
|
await client.StartWsClient();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var prompt = AnsiConsole.Ask<string>("Channel: ");
|
||||||
|
client.SendSubscribeRequest(prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThreeXplEvent(object sender, ThreeXplPushModel e, int connectionId)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[blue]Received event from 3xpl[/]");
|
||||||
|
foreach (var txn in e.Pub.Data.Data)
|
||||||
|
{
|
||||||
|
if (txn.Address == null) return;
|
||||||
|
if (_addresses.Contains(txn.Address))
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[green]Saw txn I'm interested in: {txn.Address}, effect {txn.Effect}, currency {txn.Currency}[/]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
ThreeXplWsClient/Events/EventHandlers.cs
Normal file
22
ThreeXplWsClient/Events/EventHandlers.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Websocket.Client;
|
||||||
|
|
||||||
|
namespace ThreeXplWsClient.Events;
|
||||||
|
|
||||||
|
public class EventHandlers
|
||||||
|
{
|
||||||
|
public delegate void OnThreeXplPing(object sender, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnThreeXplPush(object sender, ThreeXplPushModel e, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnWsReconnectEventHandler(object sender, ReconnectionInfo e, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnWsMessageReceivedEventHandler(object sender, ResponseMessage e, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnThreeXplConnect(object sender, ThreeXplConnectDataModel e, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnThreeXplError(object sender, ThreeXplErrorModel e, int connectionId);
|
||||||
|
|
||||||
|
public delegate void OnThreeXplSubscribe(object sender, ThreeXplSubscribeModel e, int connectionId);
|
||||||
|
}
|
||||||
110
ThreeXplWsClient/Events/EventModels.cs
Normal file
110
ThreeXplWsClient/Events/EventModels.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ThreeXplWsClient.Events;
|
||||||
|
|
||||||
|
public class BaseThreeXplPacketModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("connect")]
|
||||||
|
public ThreeXplConnectDataModel? Connect { get; set; }
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int? Id { get; set; }
|
||||||
|
[JsonPropertyName("error")]
|
||||||
|
public ThreeXplErrorModel? Error { get; set; }
|
||||||
|
[JsonPropertyName("subscribe")]
|
||||||
|
public ThreeXplSubscribeModel? Subscribe { get; set; }
|
||||||
|
[JsonPropertyName("push")]
|
||||||
|
public ThreeXplPushModel? Push { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplDataModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("blockchain")]
|
||||||
|
public string? Blockchain { get; set; }
|
||||||
|
[JsonPropertyName("module")]
|
||||||
|
public string? Module { get; set; }
|
||||||
|
[JsonPropertyName("block")]
|
||||||
|
public int? Block { get; set; }
|
||||||
|
[JsonPropertyName("transaction")]
|
||||||
|
public string? Transaction { get; set; }
|
||||||
|
[JsonPropertyName("sort_key")]
|
||||||
|
public int? SortKey { get; set; }
|
||||||
|
[JsonPropertyName("time")]
|
||||||
|
public DateTimeOffset? Time { get; set; }
|
||||||
|
[JsonPropertyName("currency")]
|
||||||
|
public string? Currency { get; set; }
|
||||||
|
[JsonPropertyName("effect")]
|
||||||
|
public string? Effect { get; set; }
|
||||||
|
[JsonPropertyName("failed")]
|
||||||
|
public bool? Failed { get; set; }
|
||||||
|
[JsonPropertyName("extra")]
|
||||||
|
public object? Extra { get; set; }
|
||||||
|
[JsonPropertyName("extra_indexed")]
|
||||||
|
public object? ExtraIndexed { get; set; }
|
||||||
|
[JsonPropertyName("address")]
|
||||||
|
public string? Address { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplContextModel
|
||||||
|
{
|
||||||
|
// "time":"0.21778600 1718465848"
|
||||||
|
[JsonPropertyName("time")]
|
||||||
|
public string? Time { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplConnectDataModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("client")]
|
||||||
|
public string? Client { get; set; }
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string? Version { get; set; }
|
||||||
|
[JsonPropertyName("ping")]
|
||||||
|
public int? Ping { get; set; }
|
||||||
|
[JsonPropertyName("pong")]
|
||||||
|
public bool? Pong { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplErrorModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int? Code { get; set; }
|
||||||
|
[JsonPropertyName("Message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
[JsonPropertyName("temporary")]
|
||||||
|
public bool? Temporary { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplSubscribeModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("recoverable")]
|
||||||
|
public bool? Recoverable { get; set; }
|
||||||
|
[JsonPropertyName("epoch")]
|
||||||
|
public string? Epoch { get; set; }
|
||||||
|
[JsonPropertyName("positioned")]
|
||||||
|
public bool? Positioned { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplPushModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("channel")]
|
||||||
|
public required string Channel { get; set; }
|
||||||
|
[JsonPropertyName("pub")]
|
||||||
|
public required ThreeXplPushPubModel Pub { get; set; }
|
||||||
|
[JsonPropertyName("offset")]
|
||||||
|
public int? Offset { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplPushPubModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public required ThreeXplPushDataModel Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreeXplPushDataModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public required List<ThreeXplDataModel> Data { get; set; }
|
||||||
|
[JsonPropertyName("context")]
|
||||||
|
public required ThreeXplContextModel Context { get; set; }
|
||||||
|
}
|
||||||
17
ThreeXplWsClient/Models/JwtResponseModels.cs
Normal file
17
ThreeXplWsClient/Models/JwtResponseModels.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ThreeXplWsClient.Models;
|
||||||
|
|
||||||
|
public class GetWebsocketTokenModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public required string Data { get; set; }
|
||||||
|
[JsonPropertyName("context")]
|
||||||
|
public GetWebSocketTokenContextModel? Context { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetWebSocketTokenContextModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int? Code { get; set; }
|
||||||
|
}
|
||||||
31
ThreeXplWsClient/Models/RequestModels.cs
Normal file
31
ThreeXplWsClient/Models/RequestModels.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ThreeXplWsClient.Models;
|
||||||
|
|
||||||
|
public class ConnectRequestModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("connect")]
|
||||||
|
public required ConnectRequestTokenModel Connect { get; set; }
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConnectRequestTokenModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("token")]
|
||||||
|
public required string Token { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribeRequestModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("subscribe")]
|
||||||
|
public required SubscribeRequestChannelModel Subscribe { get; set; }
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; } = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscribeRequestChannelModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("channel")]
|
||||||
|
public required string Channel { get; set; }
|
||||||
|
}
|
||||||
235
ThreeXplWsClient/ThreeXplWsClient.cs
Normal file
235
ThreeXplWsClient/ThreeXplWsClient.cs
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
using NLog;
|
||||||
|
using ThreeXplWsClient.Events;
|
||||||
|
using ThreeXplWsClient.Models;
|
||||||
|
using Websocket.Client;
|
||||||
|
|
||||||
|
namespace ThreeXplWsClient;
|
||||||
|
|
||||||
|
public class ThreeXplWsClient
|
||||||
|
{
|
||||||
|
public event EventHandlers.OnWsMessageReceivedEventHandler OnWsMessageReceived;
|
||||||
|
public event EventHandlers.OnWsDisconnectionEventHandler OnWsDisconnection;
|
||||||
|
public event EventHandlers.OnWsReconnectEventHandler OnWsReconnect;
|
||||||
|
public event EventHandlers.OnThreeXplPing OnThreeXplPing;
|
||||||
|
public event EventHandlers.OnThreeXplPush OnThreeXplPush;
|
||||||
|
public event EventHandlers.OnThreeXplConnect OnThreeXplConnect;
|
||||||
|
public event EventHandlers.OnThreeXplError OnThreeXplError;
|
||||||
|
public event EventHandlers.OnThreeXplSubscribe OnThreeXplSubscribe;
|
||||||
|
|
||||||
|
private WebsocketClient _wsClient;
|
||||||
|
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||||
|
private string? _wsJwt;
|
||||||
|
private DateTime _wsJwtLastRetrieved = DateTime.Now;
|
||||||
|
private int _wsJwtValidityPeriodSeconds;
|
||||||
|
private string? _proxy;
|
||||||
|
private int _reconnectTimeout;
|
||||||
|
private Uri _wsUri;
|
||||||
|
// Basically they have a limit of 10 subscriptions per connection and I have more than 10 addresses to monitor, so
|
||||||
|
// I give each connection an ID number as that way I know what addresses need to be resubscribed in the event of a
|
||||||
|
// connection drop. This ID is included with every event fired and set when the class is constructed.
|
||||||
|
private int _connectionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client for the 3xpl WebSocket API
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="threeXplWsUri">URI for the websocket API, published at https://3xpl.com/data/websocket-api</param>
|
||||||
|
/// <param name="proxy">Web proxy to use for the WebSocket connection</param>
|
||||||
|
/// <param name="reconnectTimeout">Reconnect timeout, defaults to 30 seconds as 3xpl tells us to expect a ping every 25 seconds</param>
|
||||||
|
/// <param name="jwtValidityPeriodSeconds">How long the JWT is valid for. Set to int.MaxValue if you've manually provided a non-expiring token</param>
|
||||||
|
/// <param name="jwtApiToken">Manually provide a JWT if you have access to create your own</param>
|
||||||
|
/// <param name="connectionId">ID that can be used to differentiate multiple 3xpl connections</param>
|
||||||
|
public ThreeXplWsClient(string threeXplWsUri = "wss://stream.3xpl.net", string? proxy = null,
|
||||||
|
int reconnectTimeout = 30, int jwtValidityPeriodSeconds = 600, string? jwtApiToken = null, int connectionId = 0)
|
||||||
|
{
|
||||||
|
_wsUri = new Uri(threeXplWsUri);
|
||||||
|
_proxy = proxy;
|
||||||
|
_reconnectTimeout = reconnectTimeout;
|
||||||
|
_wsJwtValidityPeriodSeconds = jwtValidityPeriodSeconds;
|
||||||
|
_wsJwt = jwtApiToken;
|
||||||
|
_connectionId = connectionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshApiToken()
|
||||||
|
{
|
||||||
|
_logger.Debug("Refreshing the API token");
|
||||||
|
if (_wsJwtValidityPeriodSeconds == int.MaxValue)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Token is non expiring as it is set to {int.MaxValue}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_wsJwt != null && _wsJwtLastRetrieved.AddSeconds(_wsJwtValidityPeriodSeconds) >= DateTime.Now)
|
||||||
|
{
|
||||||
|
_logger.Debug(
|
||||||
|
$"Token has not yet expired. Its expiration date is {_wsJwtLastRetrieved.AddSeconds(_wsJwtValidityPeriodSeconds):yyyy-MM-dd HH:mm:ss}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All };
|
||||||
|
if (_proxy != null)
|
||||||
|
{
|
||||||
|
handler.Proxy = new WebProxy(_proxy);
|
||||||
|
handler.UseProxy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var client = new HttpClient(handler);
|
||||||
|
var token = await client.GetFromJsonAsync<GetWebsocketTokenModel>("https://3xpl.com/get-websockets-token");
|
||||||
|
if (token == null)
|
||||||
|
{
|
||||||
|
_logger.Error("Caught a null when retrieving a WebSocket JWT from 3xpl");
|
||||||
|
throw new InvalidOperationException("Caught a null when retrieving a WebSocket JWT from 3xp");
|
||||||
|
}
|
||||||
|
|
||||||
|
_wsJwt = token.Data;
|
||||||
|
_wsJwtLastRetrieved = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartWsClient()
|
||||||
|
{
|
||||||
|
_logger.Debug("StartWsClient() called, creating client");
|
||||||
|
await CreateWsClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateWsClient()
|
||||||
|
{
|
||||||
|
var factory = new Func<ClientWebSocket>(() =>
|
||||||
|
{
|
||||||
|
var clientWs = new ClientWebSocket();
|
||||||
|
if (_proxy == null) return clientWs;
|
||||||
|
clientWs.Options.Proxy = new WebProxy(_proxy);
|
||||||
|
return clientWs;
|
||||||
|
});
|
||||||
|
var client = new WebsocketClient(_wsUri, factory)
|
||||||
|
{
|
||||||
|
ReconnectTimeout = TimeSpan.FromSeconds(_reconnectTimeout)
|
||||||
|
};
|
||||||
|
_wsClient = client;
|
||||||
|
|
||||||
|
client.ReconnectionHappened.Subscribe(WsReconnection);
|
||||||
|
client.MessageReceived.Subscribe(WsMessageReceived);
|
||||||
|
client.DisconnectionHappened.Subscribe(WsDisconnection);
|
||||||
|
|
||||||
|
_logger.Debug("Websocket client has been built, about to start");
|
||||||
|
await client.Start();
|
||||||
|
_logger.Debug("Websocket client started!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnected()
|
||||||
|
{
|
||||||
|
return _wsClient is { IsRunning: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WsDisconnection(DisconnectionInfo disconnectionInfo)
|
||||||
|
{
|
||||||
|
_logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}");
|
||||||
|
_logger.Error(disconnectionInfo.Exception);
|
||||||
|
OnWsDisconnection?.Invoke(this, disconnectionInfo, _connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendConnectRequest()
|
||||||
|
{
|
||||||
|
if (_wsJwt == null)
|
||||||
|
{
|
||||||
|
_logger.Error("JWT was null.");
|
||||||
|
throw new InvalidOperationException("JWT was null");
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new ConnectRequestModel { Connect = new ConnectRequestTokenModel { Token = _wsJwt } };
|
||||||
|
var payload = JsonSerializer.Serialize(data);
|
||||||
|
_logger.Debug("Sending the following payload to 3xpl");
|
||||||
|
_logger.Debug(payload);
|
||||||
|
_wsClient.Send(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WsReconnection(ReconnectionInfo reconnectionInfo)
|
||||||
|
{
|
||||||
|
_logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}");
|
||||||
|
_logger.Info("Refreshing JWT");
|
||||||
|
RefreshApiToken().Wait();
|
||||||
|
_logger.Info("Sending connect request");
|
||||||
|
SendConnectRequest();
|
||||||
|
OnWsReconnect?.Invoke(this, reconnectionInfo, _connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendSubscribeRequest(string channel)
|
||||||
|
{
|
||||||
|
var data = new SubscribeRequestModel { Subscribe = new SubscribeRequestChannelModel { Channel = channel }};
|
||||||
|
var payload = JsonSerializer.Serialize(data);
|
||||||
|
_logger.Debug("Sending the following subscription payload to 3xpl");
|
||||||
|
_logger.Debug(payload);
|
||||||
|
_wsClient.Send(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WsMessageReceived(ResponseMessage message)
|
||||||
|
{
|
||||||
|
OnWsMessageReceived?.Invoke(this, message, _connectionId);
|
||||||
|
_logger.Debug("Received JSON from 3xpl");
|
||||||
|
_logger.Debug(message.Text);
|
||||||
|
|
||||||
|
if (message.Text == null)
|
||||||
|
{
|
||||||
|
_logger.Info("Websocket message was null, ignoring packet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Text == "{}")
|
||||||
|
{
|
||||||
|
_logger.Debug("Received ping from 3xpl. Sending back a pong and invoking event");
|
||||||
|
_wsClient.Send("{}");
|
||||||
|
OnThreeXplPing?.Invoke(this, _connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseThreeXplPacketModel threeXplPacket;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
threeXplPacket = JsonSerializer.Deserialize<BaseThreeXplPacketModel>(message.Text) ??
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Error("Failed to parse 3xpl payload. Exception follows:");
|
||||||
|
_logger.Error(e);
|
||||||
|
_logger.Error("--- Message from 3xpl follows ---");
|
||||||
|
_logger.Error(message.Text);
|
||||||
|
_logger.Error("--- /end of message ---");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threeXplPacket.Connect != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Received connect packet from 3xpl, invoking event");
|
||||||
|
OnThreeXplConnect?.Invoke(this, threeXplPacket.Connect, _connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threeXplPacket.Push != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Received data event from 3xpl");
|
||||||
|
OnThreeXplPush?.Invoke(this, threeXplPacket.Push, _connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threeXplPacket.Error != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Received error packet from 3xpl");
|
||||||
|
OnThreeXplError?.Invoke(this, threeXplPacket.Error, _connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threeXplPacket.Subscribe != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Received subscribe packet from 3xpl");
|
||||||
|
OnThreeXplSubscribe?.Invoke(this, threeXplPacket.Subscribe, _connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Error("Failed to handle 3xpl packet");
|
||||||
|
_logger.Error("--- Message from 3xpl follows ---");
|
||||||
|
_logger.Error(message.Text);
|
||||||
|
_logger.Error("--- /end of message ---");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ThreeXplWsClient/ThreeXplWsClient.csproj
Normal file
15
ThreeXplWsClient/ThreeXplWsClient.csproj
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NLog" Version="5.3.2" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.0.3" />
|
||||||
|
<PackageReference Include="Websocket.Client" Version="5.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user