From 5cdab275c352119377641ace62bd26303080c9b3 Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Sun, 16 Jun 2024 12:18:56 +0800 Subject: [PATCH] 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. --- KfChatDotNet.sln | 12 + ThreeXplCliClient/NLog.config | 15 + ThreeXplCliClient/NLog.xsd | 3483 ++++++++++++++++++ ThreeXplCliClient/Program.cs | 18 + ThreeXplCliClient/ThreeXplCliClient.csproj | 30 + ThreeXplCliClient/ThreeXplClient.cs | 38 + ThreeXplWsClient/Events/EventHandlers.cs | 22 + ThreeXplWsClient/Events/EventModels.cs | 110 + ThreeXplWsClient/Models/JwtResponseModels.cs | 17 + ThreeXplWsClient/Models/RequestModels.cs | 31 + ThreeXplWsClient/ThreeXplWsClient.cs | 235 ++ ThreeXplWsClient/ThreeXplWsClient.csproj | 15 + 12 files changed, 4026 insertions(+) create mode 100644 ThreeXplCliClient/NLog.config create mode 100644 ThreeXplCliClient/NLog.xsd create mode 100644 ThreeXplCliClient/Program.cs create mode 100644 ThreeXplCliClient/ThreeXplCliClient.csproj create mode 100644 ThreeXplCliClient/ThreeXplClient.cs create mode 100644 ThreeXplWsClient/Events/EventHandlers.cs create mode 100644 ThreeXplWsClient/Events/EventModels.cs create mode 100644 ThreeXplWsClient/Models/JwtResponseModels.cs create mode 100644 ThreeXplWsClient/Models/RequestModels.cs create mode 100644 ThreeXplWsClient/ThreeXplWsClient.cs create mode 100644 ThreeXplWsClient/ThreeXplWsClient.csproj diff --git a/KfChatDotNet.sln b/KfChatDotNet.sln index ee16e3e..33a8819 100644 --- a/KfChatDotNet.sln +++ b/KfChatDotNet.sln @@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KickWsClient", "KickWsClien EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetKickBot", "KfChatDotNetKickBot\KfChatDotNetKickBot.csproj", "{4734E0A4-150E-4915-B905-928BB4BE3FF6}" 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 GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 EndGlobal diff --git a/ThreeXplCliClient/NLog.config b/ThreeXplCliClient/NLog.config new file mode 100644 index 0000000..ab4e010 --- /dev/null +++ b/ThreeXplCliClient/NLog.config @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/ThreeXplCliClient/NLog.xsd b/ThreeXplCliClient/NLog.xsd new file mode 100644 index 0000000..e2b7858 --- /dev/null +++ b/ThreeXplCliClient/NLog.xsd @@ -0,0 +1,3483 @@ + + + + + + + + + + + + + + + Watch config file for changes and reload automatically. + + + + + Print internal NLog messages to the console. Default value is: false + + + + + Print internal NLog messages to the console error output. Default value is: false + + + + + Write internal NLog messages to the specified file. + + + + + Log level threshold for internal log messages. Default value is: Info. + + + + + Global log level threshold for application log messages. Messages below this level won't be logged. + + + + + Throw an exception when there is an internal error. Default value is: false. Not recommend to set to true in production! + + + + + Throw an exception when there is a configuration error. If not set, determined by throwExceptions. + + + + + Gets or sets a value indicating whether Variables should be kept on configuration reload. Default value is: false. + + + + + Write internal NLog messages to the System.Diagnostics.Trace. Default value is: false. + + + + + Write timestamps for internal NLog messages. Default value is: true. + + + + + Use InvariantCulture as default culture instead of CurrentCulture. Default value is: false. + + + + + Perform message template parsing and formatting of LogEvent messages (true = Always, false = Never, empty = Auto Detect). Default value is: empty. + + + + + + + + + + + + + + Make all targets within this section asynchronous (creates additional threads but the calling thread isn't blocked by any target writes). + + + + + + + + + + + + + + + + + Prefix for targets/layout renderers/filters/conditions loaded from this assembly. + + + + + Load NLog extensions from the specified file (*.dll) + + + + + Load NLog extensions from the specified assembly. Assembly name should be fully qualified. + + + + + + + + + + Filter on the name of the logger. May include wildcard characters ('*' or '?'). + + + + + Comma separated list of levels that this rule matches. + + + + + Minimum level that this rule matches. + + + + + Maximum level that this rule matches. + + + + + Level that this rule matches. + + + + + Comma separated list of target names. + + + + + Ignore further rules if this one matches. + + + + + Enable this rule. Note: disabled rules aren't available from the API. + + + + + Rule identifier to allow rule lookup with Configuration.FindRuleByName and Configuration.RemoveRuleByName. + + + + + Loggers matching will be restricted to specified minimum level for following rules. + + + + + + + + + + + + + + + Default action if none of the filters match. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the file to be included. You could use * wildcard. The name is relative to the name of the current config file. + + + + + Ignore any errors in the include file. + + + + + + + + Variable value. Note, the 'value' attribute has precedence over this one. + + + + + + Variable name. + + + + + Variable value. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Action to be taken when the lazy writer thread request queue count exceeds the set limit. + + + + + Limit on the number of requests in the lazy writer thread request queue. + + + + + Number of log events that should be processed in a batch by the lazy writer thread. + + + + + Whether to use the locking queue, instead of a lock-free concurrent queue + + + + + Number of batches of P:NLog.Targets.Wrappers.AsyncTargetWrapper.BatchSize to write before yielding into P:NLog.Targets.Wrappers.AsyncTargetWrapper.TimeToSleepBetweenBatches + + + + + Time in milliseconds to sleep between batches. (1 or less means trigger on new activity) + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Delay the flush until the LogEvent has been confirmed as written + + + + + Condition expression. Log events who meet this condition will cause a flush on the wrapped target. + + + + + Only flush when LogEvent matches condition. Ignore explicit-flush, config-reload-flush and shutdown-flush + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Number of log events to be buffered. + + + + + Action to take if the buffer overflows. + + + + + Timeout (in milliseconds) after which the contents of buffer will be flushed if there's no write in the specified period of time. Use -1 to disable timed flushes. + + + + + Indicates whether to use sliding timeout. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Separator for T:NLog.ScopeContext operation-states-stack. + + + + + Stack separator for log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Renderer for log4j:event logger-xml-attribute (Default ${logger}) + + + + + Whether to include the contents of the T:NLog.ScopeContext properties-dictionary. + + + + + Whether to include log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Indicates whether to include source info (file name and line number) in the information sent over the network. + + + + + Whether to include log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Option to include all properties from the log events + + + + + Indicates whether to include call site (class and method name) in the information sent over the network. + + + + + AppInfo field. By default it's the friendly name of the current AppDomain. + + + + + Instance of T:NLog.Layouts.Log4JXmlEventLayout that is used to format log messages. + + + + + Indicates whether to include NLog-specific extensions to log4j schema. + + + + + Action that should be taken, when more connections than P:NLog.Targets.NetworkTarget.MaxConnections. + + + + + SSL/TLS protocols. Default no SSL/TLS is used. Currently only implemented for TCP. + + + + + Action that should be taken, when more pending messages than P:NLog.Targets.NetworkTarget.MaxQueueSize. + + + + + Action that should be taken if the message is larger than P:NLog.Targets.NetworkTarget.MaxMessageSize + + + + + Maximum queue size for a single connection. Requires P:NLog.Targets.NetworkTarget.KeepConnection = true + + + + + Network address. + + + + + Indicates whether to keep connection open whenever possible. + + + + + The number of seconds a connection will remain idle before the first keep-alive probe is sent + + + + + Size of the connection cache (number of connections which are kept alive). Requires P:NLog.Targets.NetworkTarget.KeepConnection = true + + + + + Maximum simultaneous connections. Requires P:NLog.Targets.NetworkTarget.KeepConnection = false + + + + + Type of compression for protocol payload. Useful for UDP where datagram max-size is 8192 bytes. + + + + + Skip compression when protocol payload is below limit to reduce overhead in cpu-usage and additional headers + + + + + Maximum message size in bytes. On limit breach then P:NLog.Targets.NetworkTarget.OnOverflow action is activated. + + + + + Encoding to be used. + + + + + End of line value if a newline is appended at the end of log message P:NLog.Targets.NetworkTarget.NewLine. + + + + + Indicates whether to append newline at the end of log message. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Viewer parameter name. + + + + + Layout that should be use to calculate the value for the parameter. + + + + + Whether an attribute with empty value should be included in the output + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether to auto-check if the console is available. - Disables console writing if Environment.UserInteractive = False (Windows Service) - Disables console writing if Console Standard Input is not available (Non-Console-App) + + + + + Enables output using ANSI Color Codes + + + + + The encoding for writing messages to the T:System.Console. + + + + + Indicates whether to send the log messages to the standard error instead of the standard output. + + + + + Indicates whether to auto-flush after M:System.Console.WriteLine + + + + + Indicates whether to auto-check if the console has been redirected to file - Disables coloring logic when System.Console.IsOutputRedirected = true + + + + + Indicates whether to use default row highlighting rules. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Background color. + + + + + Condition that must be met in order to set the specified foreground and background color. + + + + + Foreground color. + + + + + + + + + + + + + + + + + Background color. + + + + + Compile the P:NLog.Targets.ConsoleWordHighlightingRule.Regex? This can improve the performance, but at the costs of more memory usage. If false, the Regex Cache is used. + + + + + Condition that must be met before scanning the row for highlight of words + + + + + Foreground color. + + + + + Indicates whether to ignore case when comparing texts. + + + + + Regular expression to be matched. You must specify either text or regex. + + + + + Text to be matched. You must specify either text or regex. + + + + + Indicates whether to match whole words only. + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether to auto-flush after M:System.Console.WriteLine + + + + + Indicates whether to auto-check if the console is available - Disables console writing if Environment.UserInteractive = False (Windows Service) - Disables console writing if Console Standard Input is not available (Non-Console-App) + + + + + The encoding for writing messages to the T:System.Console. + + + + + Indicates whether to send the log messages to the standard error instead of the standard output. + + + + + Whether to activate internal buffering to allow batch writing, instead of using M:System.Console.WriteLine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Database user name. If the ConnectionString is not provided this value will be used to construct the "User ID=" part of the connection string. + + + + + Database password. If the ConnectionString is not provided this value will be used to construct the "Password=" part of the connection string. + + + + + Database name. If the ConnectionString is not provided this value will be used to construct the "Database=" part of the connection string. + + + + + Name of the connection string (as specified in <connectionStrings> configuration section. + + + + + Database host name. If the ConnectionString is not provided this value will be used to construct the "Server=" part of the connection string. + + + + + Indicates whether to keep the database connection open between the log events. + + + + + Name of the database provider. + + + + + Connection string. When provided, it overrides the values specified in DBHost, DBUserName, DBPassword, DBDatabase. + + + + + Connection string using for installation and uninstallation. If not provided, regular ConnectionString is being used. + + + + + Configures isolated transaction batch writing. If supported by the database, then it will improve insert performance. + + + + + Text of the SQL command to be run on each log level. + + + + + Type of the SQL command to be run on each log level. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Convert format of the property value + + + + + Culture used for parsing property string-value for type-conversion + + + + + Value to assign on the object-property + + + + + Name for the object-property + + + + + Type of the object-property + + + + + + + + + + + + + + Type of the command. + + + + + Connection string to run the command against. If not provided, connection string from the target is used. + + + + + Indicates whether to ignore failures. + + + + + Command text. + + + + + + + + + + + + + + + + + + + + Database parameter name. + + + + + Layout that should be use to calculate the value for the parameter. + + + + + Database parameter DbType. + + + + + Database parameter size. + + + + + Database parameter precision. + + + + + Database parameter scale. + + + + + Type of the parameter. + + + + + Fallback value when result value is not available + + + + + Convert format of the database parameter value. + + + + + Culture used for parsing parameter string-value for type-conversion + + + + + Whether empty value should translate into DbNull. Requires database column to allow NULL values. + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Layout that renders event Category. + + + + + Optional entry type. When not set, or when not convertible to T:System.Diagnostics.EventLogEntryType then determined by T:NLog.LogLevel + + + + + Layout that renders event ID. + + + + + Name of the Event Log to write to. This can be System, Application or any user-defined name. + + + + + Name of the machine on which Event Log service is running. + + + + + Maximum Event log size in kilobytes. + + + + + Message length limit to write to the Event Log. + + + + + Value to be used as the event Source. + + + + + Action to take if the message is larger than the P:NLog.Targets.EventLogTarget.MaxMessageLength option. + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Indicates whether to return to the first target after any successful write. + + + + + Whether to enable batching, but fallback will be handled individually + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Name of the file to write to. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether the footer should be written only when the file is archived. + + + + + Maximum number of archive files that should be kept. + + + + + Maximum days of archive files that should be kept. + + + + + Value of the file size threshold to archive old log file on startup. + + + + + Indicates whether to archive old log file on startup. + + + + + Indicates whether to compress archive files into the zip archive format. + + + + + Name of the file to be used for an archive. + + + + + Is the P:NLog.Targets.FileTarget.ArchiveFileName an absolute or relative path? + + + + + Indicates whether to automatically archive log files every time the specified time passes. + + + + + Value specifying the date format to use when archiving files. + + + + + Size in bytes above which log files will be automatically archived. + + + + + Way file archives are numbered. + + + + + Indicates whether to create directories if they do not exist. + + + + + Indicates whether file creation calls should be synchronized by a system global mutex. + + + + + Gets or set a value indicating whether a managed file stream is forced, instead of using the native implementation. + + + + + Is the P:NLog.Targets.FileTarget.FileName an absolute or relative path? + + + + + File attributes (Windows only). + + + + + Cleanup invalid values in a filename, e.g. slashes in a filename. If set to true, this can impact the performance of massive writes. If set to false, nothing gets written when the filename is wrong. + + + + + Indicates whether to write BOM (byte order mark) in created files. Defaults to true for UTF-16 and UTF-32 + + + + + Indicates whether to enable log file(s) to be deleted. + + + + + Indicates whether to delete old log file on startup. + + + + + File encoding. + + + + + Indicates whether to replace file contents on each write instead of appending log message at the end. + + + + + Line ending mode. + + + + + Number of times the write is appended on the file before NLog discards the log message. + + + + + Delay in milliseconds to wait before attempting to write to the file again. + + + + + Maximum number of seconds before open files are flushed. Zero or negative means disabled. + + + + + Maximum number of seconds that files are kept open. Zero or negative means disabled. + + + + + Indicates whether concurrent writes to the log file by multiple processes on different network hosts. + + + + + Log file buffer size in bytes. + + + + + Indicates whether to automatically flush the file buffers after each log message. + + + + + Indicates whether to keep log file open instead of opening and closing it on each logging event. + + + + + Indicates whether concurrent writes to the log file by multiple processes on the same host. + + + + + Whether or not this target should just discard all data that its asked to write. Mostly used for when testing NLog Stack except final write + + + + + Number of files to be kept open. Setting this to a higher value may improve performance in a situation where a single File target is writing to many files (such as splitting by level or by logger). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Condition expression. Log events who meet this condition will be forwarded to the wrapped target. + + + + + + + + + + + + + + + Name of the target. + + + + + Identifier to perform group-by + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Windows domain name to change context to. + + + + + Required impersonation level. + + + + + Type of the logon provider. + + + + + Logon Type. + + + + + User account password. + + + + + Indicates whether to revert to the credentials of the process instead of impersonating another user. + + + + + Username to change context to. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Interval in which messages will be written up to the P:NLog.Targets.Wrappers.LimitingTargetWrapper.MessageLimit number of messages. + + + + + Maximum allowed number of messages written per P:NLog.Targets.Wrappers.LimitingTargetWrapper.Interval. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether NewLine characters in the body should be replaced with tags. + + + + + Priority used for sending mails. + + + + + Encoding to be used for sending e-mail. + + + + + BCC email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). + + + + + CC email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). + + + + + Indicates whether to add new lines between log entries. + + + + + Indicates whether to send message as HTML instead of plain text. + + + + + Sender's email address (e.g. joe@domain.com). + + + + + Mail message body (repeated for each log message send in one mail). + + + + + Mail subject. + + + + + Recipients' email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). + + + + + Specifies how outgoing email messages will be handled. + + + + + SMTP Server to be used for sending. + + + + + SMTP Authentication mode. + + + + + Username used to connect to SMTP server (used when SmtpAuthentication is set to "basic"). + + + + + Password used to authenticate against SMTP server (used when SmtpAuthentication is set to "basic"). + + + + + Indicates whether SSL (secure sockets layer) should be used when communicating with SMTP server. + + + + + Port number that SMTP Server is listening on. + + + + + Indicates whether the default Settings from System.Net.MailSettings should be used. + + + + + Folder where applications save mail messages to be processed by the local SMTP server. + + + + + Indicates the SMTP client timeout. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Max number of items to have in memory + + + + + + + + + + + + + + + + + Name of the target. + + + + + Class name. + + + + + Method name. The method must be public and static. Use the AssemblyQualifiedName , https://msdn.microsoft.com/en-us/library/system.type.assemblyqualifiedname(v=vs.110).aspx e.g. + + + + + + + + + + + + + + + Name of the parameter. + + + + + Layout that should be use to calculate the value for the parameter. + + + + + Fallback value when result value is not available + + + + + Type of the parameter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + SSL/TLS protocols. Default no SSL/TLS is used. Currently only implemented for TCP. + + + + + Action that should be taken, when more pending messages than P:NLog.Targets.NetworkTarget.MaxQueueSize. + + + + + Action that should be taken if the message is larger than P:NLog.Targets.NetworkTarget.MaxMessageSize + + + + + Maximum queue size for a single connection. Requires P:NLog.Targets.NetworkTarget.KeepConnection = true + + + + + Action that should be taken, when more connections than P:NLog.Targets.NetworkTarget.MaxConnections. + + + + + Indicates whether to keep connection open whenever possible. + + + + + The number of seconds a connection will remain idle before the first keep-alive probe is sent + + + + + Size of the connection cache (number of connections which are kept alive). Requires P:NLog.Targets.NetworkTarget.KeepConnection = true + + + + + Network address. + + + + + Maximum simultaneous connections. Requires P:NLog.Targets.NetworkTarget.KeepConnection = false + + + + + Type of compression for protocol payload. Useful for UDP where datagram max-size is 8192 bytes. + + + + + Skip compression when protocol payload is below limit to reduce overhead in cpu-usage and additional headers + + + + + Maximum message size in bytes. On limit breach then P:NLog.Targets.NetworkTarget.OnOverflow action is activated. + + + + + Encoding to be used. + + + + + End of line value if a newline is appended at the end of log message P:NLog.Targets.NetworkTarget.NewLine. + + + + + Indicates whether to append newline at the end of log message. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Separator for T:NLog.ScopeContext operation-states-stack. + + + + + Stack separator for log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Renderer for log4j:event logger-xml-attribute (Default ${logger}) + + + + + Whether to include the contents of the T:NLog.ScopeContext properties-dictionary. + + + + + Whether to include log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Indicates whether to include source info (file name and line number) in the information sent over the network. + + + + + Whether to include log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Option to include all properties from the log events + + + + + Indicates whether to include call site (class and method name) in the information sent over the network. + + + + + AppInfo field. By default it's the friendly name of the current AppDomain. + + + + + Instance of T:NLog.Layouts.Log4JXmlEventLayout that is used to format log messages. + + + + + Indicates whether to include NLog-specific extensions to log4j schema. + + + + + Action that should be taken, when more connections than P:NLog.Targets.NetworkTarget.MaxConnections. + + + + + SSL/TLS protocols. Default no SSL/TLS is used. Currently only implemented for TCP. + + + + + Action that should be taken, when more pending messages than P:NLog.Targets.NetworkTarget.MaxQueueSize. + + + + + Action that should be taken if the message is larger than P:NLog.Targets.NetworkTarget.MaxMessageSize + + + + + Maximum queue size for a single connection. Requires P:NLog.Targets.NetworkTarget.KeepConnection = true + + + + + Network address. + + + + + Indicates whether to keep connection open whenever possible. + + + + + The number of seconds a connection will remain idle before the first keep-alive probe is sent + + + + + Size of the connection cache (number of connections which are kept alive). Requires P:NLog.Targets.NetworkTarget.KeepConnection = true + + + + + Maximum simultaneous connections. Requires P:NLog.Targets.NetworkTarget.KeepConnection = false + + + + + Type of compression for protocol payload. Useful for UDP where datagram max-size is 8192 bytes. + + + + + Skip compression when protocol payload is below limit to reduce overhead in cpu-usage and additional headers + + + + + Maximum message size in bytes. On limit breach then P:NLog.Targets.NetworkTarget.OnOverflow action is activated. + + + + + Encoding to be used. + + + + + End of line value if a newline is appended at the end of log message P:NLog.Targets.NetworkTarget.NewLine. + + + + + Indicates whether to append newline at the end of log message. + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Indicates whether to perform layout calculation. + + + + + + + + + + + + + + + + Name of the target. + + + + + Default filter to be applied when no specific rule matches. + + + + + + + + + + + + + Condition to be tested. + + + + + Resulting filter to be applied when the condition matches. + + + + + + + + + + + + Name of the target. + + + + + + + + + + + + + + + Name of the target. + + + + + Number of times to repeat each log message. + + + + + + + + + + + + + + + + + Name of the target. + + + + + Whether to enable batching, and only apply single delay when a whole batch fails + + + + + Number of retries that should be attempted on the wrapped target in case of a failure. + + + + + Time to wait between retries in milliseconds. + + + + + + + + + + + + + + Name of the target. + + + + + + + + + + + + + + Name of the target. + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Forward F:NLog.LogLevel.Fatal to M:System.Diagnostics.Trace.Fail(System.String) (Instead of M:System.Diagnostics.Trace.TraceError(System.String)) + + + + + Force use M:System.Diagnostics.Trace.WriteLine(System.String) independent of T:NLog.LogLevel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Indicates whether to pre-authenticate the HttpWebRequest (Requires 'Authorization' in P:NLog.Targets.WebServiceTarget.Headers parameters) + + + + + Value whether escaping be done according to Rfc3986 (Supports Internationalized Resource Identifiers - IRIs) + + + + + Value whether escaping be done according to the old NLog style (Very non-standard) + + + + + Value of the User-agent HTTP header. + + + + + Web service URL. + + + + + Proxy configuration when calling web service + + + + + Custom proxy address, include port separated by a colon + + + + + Protocol to be used when calling web service. + + + + + Web service namespace. Only used with Soap. + + + + + Web service method name. Only used with Soap. + + + + + Should we include the BOM (Byte-order-mark) for UTF? Influences the P:NLog.Targets.WebServiceTarget.Encoding property. This will only work for UTF-8. + + + + + Encoding. + + + + + Name of the root XML element, if POST of XML document chosen. If so, this property must not be null. (see P:NLog.Targets.WebServiceTarget.Protocol and F:NLog.Targets.WebServiceProtocol.XmlPost). + + + + + (optional) root namespace of the XML document, if POST of XML document chosen. (see P:NLog.Targets.WebServiceTarget.Protocol and F:NLog.Targets.WebServiceProtocol.XmlPost). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom column delimiter value (valid when ColumnDelimiter is set to 'Custom'). + + + + + Column delimiter. + + + + + Footer layout. + + + + + Header layout. + + + + + Body layout (can be repeated multiple times). + + + + + Quote Character. + + + + + Quoting mode. + + + + + Indicates whether CVS should include header. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the column. + + + + + Layout of the column. + + + + + Override of Quoting mode + + + + + + + + + + + + + + Option to render the empty object value {} + + + + + Option to suppress the extra spaces in the output json + + + + + + + + + + + + + + + + + + + + + + + Option to include all properties from the log event (as JSON) + + + + + Indicates whether to include contents of the T:NLog.GlobalDiagnosticsContext dictionary. + + + + + Whether to include the contents of the T:NLog.ScopeContext dictionary. + + + + + Should forward slashes be escaped? If true, / will be converted to \/ + + + + + Option to exclude null/empty properties from the log event (as JSON) + + + + + List of property names to exclude when P:NLog.Layouts.JsonLayout.IncludeAllProperties is true + + + + + How far should the JSON serializer follow object references before backing off + + + + + Option to render the empty object value {} + + + + + Option to suppress the extra spaces in the output json + + + + + + + + + + + + + + + + + + + Name of the attribute. + + + + + Layout that will be rendered as the attribute's value. + + + + + Fallback value when result value is not available + + + + + Determines whether or not this attribute will be Json encoded. + + + + + Should forward slashes be escaped? If true, / will be converted to \/ + + + + + Indicates whether to escape non-ascii characters + + + + + Whether an attribute with empty value should be included in the output + + + + + Result value type, for conversion of layout rendering output + + + + + + + + + + + + + + Footer layout. + + + + + Header layout. + + + + + Body layout (can be repeated multiple times). + + + + + + + + + + + + + + + + + + + + + + + Option to include all properties from the log events + + + + + Whether to include log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Whether to include log4j:NDC in output from T:NLog.ScopeContext nested context. + + + + + Whether to include the contents of the T:NLog.ScopeContext properties-dictionary. + + + + + AppInfo field. By default it's the friendly name of the current AppDomain. + + + + + Indicates whether to include call site (class and method name) in the information sent over the network. + + + + + Indicates whether to include source info (file name and line number) in the information sent over the network. + + + + + Log4j:event logger-xml-attribute (Default ${logger}) + + + + + Whether the log4j:throwable xml-element should be written as CDATA + + + + + + + + + + + + + + Layout text. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the root XML element + + + + + Value inside the root XML element + + + + + Whether to include the contents of the T:NLog.ScopeContext dictionary. + + + + + Determines whether or not this attribute will be Xml encoded. + + + + + List of property names to exclude when P:NLog.Layouts.XmlElementBase.IncludeAllProperties is true + + + + + Whether a ElementValue with empty value should be included in the output + + + + + Auto indent and create new lines + + + + + How far should the XML serializer follow object references before backing off + + + + + XML element name to use for rendering IList-collections items + + + + + XML attribute name to use when rendering property-key When null (or empty) then key-attribute is not included + + + + + XML element name to use when rendering properties + + + + + XML attribute name to use when rendering property-value When null (or empty) then value-attribute is not included and value is formatted as XML-element-value + + + + + Option to include all properties from the log event (as XML) + + + + + + + + + + + + + + + + + Name of the attribute. + + + + + Layout that will be rendered as the attribute's value. + + + + + Fallback value when result value is not available + + + + + Determines whether or not this attribute will be Xml encoded. + + + + + Whether an attribute with empty value should be included in the output + + + + + Result value type, for conversion of layout rendering output + + + + + + + + + + + + + + + + + + + + + + + + Name of the element + + + + + Whether to include the contents of the T:NLog.ScopeContext dictionary. + + + + + Value inside the element + + + + + Determines whether or not this attribute will be Xml encoded. + + + + + List of property names to exclude when P:NLog.Layouts.XmlElementBase.IncludeAllProperties is true + + + + + Whether a ElementValue with empty value should be included in the output + + + + + Auto indent and create new lines + + + + + How far should the XML serializer follow object references before backing off + + + + + XML element name to use for rendering IList-collections items + + + + + XML attribute name to use when rendering property-key When null (or empty) then key-attribute is not included + + + + + XML element name to use when rendering properties + + + + + XML attribute name to use when rendering property-value When null (or empty) then value-attribute is not included and value is formatted as XML-element-value + + + + + Option to include all properties from the log event (as XML) + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Condition expression. + + + + + + + + + + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + Substring to be matched. + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + String to compare the layout to. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + Substring to be matched. + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + String to compare the layout to. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + + + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Append FilterCount to the P:NLog.LogEventInfo.Message when an event is no longer filtered + + + + + Insert FilterCount value into P:NLog.LogEventInfo.Properties when an event is no longer filtered + + + + + Applies the configured action to the initial logevent that starts the timeout period. Used to configure that it should ignore all events until timeout. + + + + + Layout to be used to filter log messages. + + + + + Max length of filter values, will truncate if above limit + + + + + How long before a filter expires, and logging is accepted again + + + + + Default number of unique filter values to expect, will automatically increase if needed + + + + + Max number of unique filter values to expect simultaneously + + + + + Default buffer size for the internal buffers + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ThreeXplCliClient/Program.cs b/ThreeXplCliClient/Program.cs new file mode 100644 index 0000000..2b773c5 --- /dev/null +++ b/ThreeXplCliClient/Program.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/ThreeXplCliClient/ThreeXplCliClient.csproj b/ThreeXplCliClient/ThreeXplCliClient.csproj new file mode 100644 index 0000000..16aef83 --- /dev/null +++ b/ThreeXplCliClient/ThreeXplCliClient.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + Always + + + + Always + + + + diff --git a/ThreeXplCliClient/ThreeXplClient.cs b/ThreeXplCliClient/ThreeXplClient.cs new file mode 100644 index 0000000..150a373 --- /dev/null +++ b/ThreeXplCliClient/ThreeXplClient.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using Spectre.Console; +using ThreeXplWsClient.Events; + +namespace ThreeXplCliClient; + +public class ThreeXplClient +{ + private List _addresses = + [ + "MC8TiBEsnQVjxbvLtTsUXjTBZTQaR8fe8X", + "ltc1qks2m7hvmhs3c20zrfvptv9pvk82p8g70sgw5mk" + ]; + public async Task Start() + { + var client = new ThreeXplWsClient.ThreeXplWsClient(); + client.OnThreeXplPush += OnThreeXplEvent; + await client.StartWsClient(); + while (true) + { + var prompt = AnsiConsole.Ask("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}[/]"); + } + } + } +} \ No newline at end of file diff --git a/ThreeXplWsClient/Events/EventHandlers.cs b/ThreeXplWsClient/Events/EventHandlers.cs new file mode 100644 index 0000000..a360196 --- /dev/null +++ b/ThreeXplWsClient/Events/EventHandlers.cs @@ -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); +} \ No newline at end of file diff --git a/ThreeXplWsClient/Events/EventModels.cs b/ThreeXplWsClient/Events/EventModels.cs new file mode 100644 index 0000000..f18a953 --- /dev/null +++ b/ThreeXplWsClient/Events/EventModels.cs @@ -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 Data { get; set; } + [JsonPropertyName("context")] + public required ThreeXplContextModel Context { get; set; } +} \ No newline at end of file diff --git a/ThreeXplWsClient/Models/JwtResponseModels.cs b/ThreeXplWsClient/Models/JwtResponseModels.cs new file mode 100644 index 0000000..5b61075 --- /dev/null +++ b/ThreeXplWsClient/Models/JwtResponseModels.cs @@ -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; } +} \ No newline at end of file diff --git a/ThreeXplWsClient/Models/RequestModels.cs b/ThreeXplWsClient/Models/RequestModels.cs new file mode 100644 index 0000000..11691f1 --- /dev/null +++ b/ThreeXplWsClient/Models/RequestModels.cs @@ -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; } +} \ No newline at end of file diff --git a/ThreeXplWsClient/ThreeXplWsClient.cs b/ThreeXplWsClient/ThreeXplWsClient.cs new file mode 100644 index 0000000..ae3a4ad --- /dev/null +++ b/ThreeXplWsClient/ThreeXplWsClient.cs @@ -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; + + /// + /// Client for the 3xpl WebSocket API + /// + /// URI for the websocket API, published at https://3xpl.com/data/websocket-api + /// Web proxy to use for the WebSocket connection + /// Reconnect timeout, defaults to 30 seconds as 3xpl tells us to expect a ping every 25 seconds + /// How long the JWT is valid for. Set to int.MaxValue if you've manually provided a non-expiring token + /// Manually provide a JWT if you have access to create your own + /// ID that can be used to differentiate multiple 3xpl connections + 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("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(() => + { + 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(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 ---"); + } +} \ No newline at end of file diff --git a/ThreeXplWsClient/ThreeXplWsClient.csproj b/ThreeXplWsClient/ThreeXplWsClient.csproj new file mode 100644 index 0000000..c9ea1c5 --- /dev/null +++ b/ThreeXplWsClient/ThreeXplWsClient.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + +