commit 9f92fc8e27f501b3549976bd1ecdc0a56f72bff1
Author: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com>
Date: Mon Mar 25 20:11:49 2024 +0800
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9998c3d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# Common IntelliJ Platform excludes
+
+# User specific
+**/.idea/**/workspace.xml
+**/.idea/**/tasks.xml
+**/.idea/shelf/*
+**/.idea/dictionaries
+**/.idea/httpRequests/
+
+# Sensitive or high-churn files
+**/.idea/**/dataSources/
+**/.idea/**/dataSources.ids
+**/.idea/**/dataSources.xml
+**/.idea/**/dataSources.local.xml
+**/.idea/**/sqlDataSources.xml
+**/.idea/**/dynamic.xml
+
+# Rider
+# Rider auto-generates .iml files, and contentModel.xml
+**/.idea/**/*.iml
+**/.idea/**/contentModel.xml
+**/.idea/**/modules.xml
+
+*.suo
+*.user
+.vs/
+[Bb]in/
+[Oo]bj/
+_UpgradeReport_Files/
+[Pp]ackages/
+
+Thumbs.db
+Desktop.ini
+.DS_Store
diff --git a/.idea/.idea.KfChatDotNet/.idea/.gitignore b/.idea/.idea.KfChatDotNet/.idea/.gitignore
new file mode 100644
index 0000000..fb77034
--- /dev/null
+++ b/.idea/.idea.KfChatDotNet/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/.idea.KfChatDotNet.iml
+/modules.xml
+/contentModel.xml
+/projectSettingsUpdater.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.KfChatDotNet/.idea/avalonia.xml b/.idea/.idea.KfChatDotNet/.idea/avalonia.xml
new file mode 100644
index 0000000..d13e3a0
--- /dev/null
+++ b/.idea/.idea.KfChatDotNet/.idea/avalonia.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.KfChatDotNet/.idea/encodings.xml b/.idea/.idea.KfChatDotNet/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.KfChatDotNet/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.KfChatDotNet/.idea/indexLayout.xml b/.idea/.idea.KfChatDotNet/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.KfChatDotNet/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.KfChatDotNet/.idea/sqldialects.xml b/.idea/.idea.KfChatDotNet/.idea/sqldialects.xml
new file mode 100644
index 0000000..80f40a6
--- /dev/null
+++ b/.idea/.idea.KfChatDotNet/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.KfChatDotNet/.idea/vcs.xml b/.idea/.idea.KfChatDotNet/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/.idea.KfChatDotNet/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/KfChatDotNet.sln b/KfChatDotNet.sln
new file mode 100644
index 0000000..ee16e3e
--- /dev/null
+++ b/KfChatDotNet.sln
@@ -0,0 +1,40 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetWsClient", "KfChatDotNetWsClient\KfChatDotNetWsClient.csproj", "{B3BC806A-7FFC-47BD-9C18-45CD2B99F9F8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetCli", "KfChatDotNetCli\KfChatDotNetCli.csproj", "{A4D19F8E-5A1F-4A66-BC42-214DB9D5429B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetGui", "KfChatDotNetGui\KfChatDotNetGui.csproj", "{B2A5D4EE-5EB6-4F0B-BB5E-2B87AAFBFB5B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KickWsClient", "KickWsClient\KickWsClient.csproj", "{DECBB95C-2C9F-44C2-AFA3-3741986FBA38}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KfChatDotNetKickBot", "KfChatDotNetKickBot\KfChatDotNetKickBot.csproj", "{4734E0A4-150E-4915-B905-928BB4BE3FF6}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B3BC806A-7FFC-47BD-9C18-45CD2B99F9F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B3BC806A-7FFC-47BD-9C18-45CD2B99F9F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B3BC806A-7FFC-47BD-9C18-45CD2B99F9F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B3BC806A-7FFC-47BD-9C18-45CD2B99F9F8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A4D19F8E-5A1F-4A66-BC42-214DB9D5429B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A4D19F8E-5A1F-4A66-BC42-214DB9D5429B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A4D19F8E-5A1F-4A66-BC42-214DB9D5429B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A4D19F8E-5A1F-4A66-BC42-214DB9D5429B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2A5D4EE-5EB6-4F0B-BB5E-2B87AAFBFB5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B2A5D4EE-5EB6-4F0B-BB5E-2B87AAFBFB5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B2A5D4EE-5EB6-4F0B-BB5E-2B87AAFBFB5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B2A5D4EE-5EB6-4F0B-BB5E-2B87AAFBFB5B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DECBB95C-2C9F-44C2-AFA3-3741986FBA38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DECBB95C-2C9F-44C2-AFA3-3741986FBA38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DECBB95C-2C9F-44C2-AFA3-3741986FBA38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DECBB95C-2C9F-44C2-AFA3-3741986FBA38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4734E0A4-150E-4915-B905-928BB4BE3FF6}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/KfChatDotNetCli/ChatCliMain.cs b/KfChatDotNetCli/ChatCliMain.cs
new file mode 100644
index 0000000..e24feb9
--- /dev/null
+++ b/KfChatDotNetCli/ChatCliMain.cs
@@ -0,0 +1,85 @@
+using KfChatDotNetWsClient;
+using KfChatDotNetWsClient.Models;
+using KfChatDotNetWsClient.Models.Events;
+using KfChatDotNetWsClient.Models.Json;
+using NLog;
+using Spectre.Console;
+using Websocket.Client;
+
+namespace KfChatDotNetCli;
+
+public class ChatCliMain
+{
+ private ChatClient _client;
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+ private int _roomId;
+
+ public ChatCliMain(string xfSessionToken, int roomId)
+ {
+ _roomId = roomId;
+ _client = new ChatClient(new ChatClientConfigModel
+ {
+ WsUri = new Uri("wss://kiwifarms.st/chat.ws"),
+ XfSessionToken = xfSessionToken
+ });
+
+ _client.OnMessages += OnMessages;
+ _client.OnDeleteMessages += OnDeleteMessages;
+ _client.OnUsersJoined += OnUsersJoined;
+ _client.OnUsersParted += OnUsersParted;
+ _client.OnWsReconnect += OnWsReconnected;
+
+ _client.StartWsClient().Wait();
+ _client.JoinRoom(_roomId);
+
+ while (true)
+ {
+ var input = AnsiConsole.Prompt(new TextPrompt("Enter Message:"));
+ _client.SendMessage(input);
+ }
+ // ReSharper disable once FunctionNeverReturns
+ }
+
+ private void OnMessages(object sender, List messages, MessagesJsonModel jsonPayload)
+ {
+ _logger.Debug($"Received {messages.Count} message(s)");
+ foreach (var message in messages)
+ {
+ AnsiConsole.MarkupLine($"<{message.Author.Username}> {message.Message.EscapeMarkup()} ({message.MessageDate.LocalDateTime.ToShortTimeString()})");
+ }
+ }
+
+ private void OnDeleteMessages(object sender, List messageIds)
+ {
+ _logger.Debug($"Received delete event for {messageIds}");
+ foreach (var id in messageIds)
+ {
+ AnsiConsole.MarkupLine($"[red]{id} message deleted![/]");
+ }
+ }
+
+ private void OnUsersJoined(object sender, List users, UsersJsonModel jsonPayload)
+ {
+ _logger.Debug($"Received {users.Count} user join events");
+ foreach (var user in users)
+ {
+ AnsiConsole.MarkupLine($"[green]{user.Username.EscapeMarkup()} joined![/]");
+ }
+ }
+
+ private void OnUsersParted(object sender, List userIds)
+ {
+ _logger.Debug($"Received {userIds.Count} user part events");
+ foreach (var id in userIds)
+ {
+ AnsiConsole.MarkupLine($"[red]{id} left the chat...[/]");
+ }
+ }
+
+ private void OnWsReconnected(object sender, ReconnectionInfo reconnectionInfo)
+ {
+ AnsiConsole.MarkupLine($"[red]Reconnected due to {reconnectionInfo.Type}[/]");
+ AnsiConsole.MarkupLine($"[green]Rejoining {_roomId}[/]");
+ _client.JoinRoom(_roomId);
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetCli/KfChatDotNetCli.csproj b/KfChatDotNetCli/KfChatDotNetCli.csproj
new file mode 100644
index 0000000..13801b6
--- /dev/null
+++ b/KfChatDotNetCli/KfChatDotNetCli.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/KfChatDotNetCli/NLog.config b/KfChatDotNetCli/NLog.config
new file mode 100644
index 0000000..e255848
--- /dev/null
+++ b/KfChatDotNetCli/NLog.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/KfChatDotNetCli/NLog.xsd b/KfChatDotNetCli/NLog.xsd
new file mode 100644
index 0000000..e2b7858
--- /dev/null
+++ b/KfChatDotNetCli/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/KfChatDotNetCli/Program.cs b/KfChatDotNetCli/Program.cs
new file mode 100644
index 0000000..f6bb823
--- /dev/null
+++ b/KfChatDotNetCli/Program.cs
@@ -0,0 +1,40 @@
+using System.Net;
+using System.Text;
+using CommandLine;
+using NLog;
+
+namespace KfChatDotNetCli
+{
+ public class Program
+ {
+ public class Options
+ {
+ [Option('t', "token", Required = false, Default = null, HelpText = "XF session token from the 'xf_session' cookie")]
+ public string XfSessionToken { get; set; } = null!;
+
+ [Option("debug", Required = false, Default = false, HelpText = "Enable debug logging")]
+ public bool Debug { get; set; }
+ [Option('r', "room", Required = true, HelpText = "Room ID to join on start")]
+ public int RoomId { get; set; }
+ }
+ static void Main(string[] args)
+ {
+ Console.OutputEncoding = Encoding.UTF8;
+ Parser.Default.ParseArguments(args).WithParsed(CliOptions);
+ }
+
+ static void CliOptions(Options options)
+ {
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13;
+ if (options.Debug)
+ {
+ foreach (var rule in LogManager.Configuration.LoggingRules)
+ {
+ rule.EnableLoggingForLevel(LogLevel.Debug);
+ }
+ }
+
+ new ChatCliMain(options.XfSessionToken, options.RoomId);
+ }
+ }
+}
diff --git a/KfChatDotNetGui/.gitignore b/KfChatDotNetGui/.gitignore
new file mode 100644
index 0000000..8afdcb6
--- /dev/null
+++ b/KfChatDotNetGui/.gitignore
@@ -0,0 +1,454 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/KfChatDotNetGui/App.axaml b/KfChatDotNetGui/App.axaml
new file mode 100644
index 0000000..17b4d69
--- /dev/null
+++ b/KfChatDotNetGui/App.axaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/KfChatDotNetGui/App.axaml.cs b/KfChatDotNetGui/App.axaml.cs
new file mode 100644
index 0000000..6f32462
--- /dev/null
+++ b/KfChatDotNetGui/App.axaml.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using KfChatDotNetGui.Models;
+using KfChatDotNetGui.ViewModels;
+using KfChatDotNetGui.Views;
+using Newtonsoft.Json;
+using NLog;
+
+namespace KfChatDotNetGui
+{
+ public partial class App : Application
+ {
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ var logger = LogManager.GetCurrentClassLogger();
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var dataContext = new MainWindowViewModel();
+ if (File.Exists("rooms.json"))
+ {
+ var rooms = JsonConvert.DeserializeObject(File.ReadAllText("rooms.json"));
+ dataContext.RoomList = rooms!.Rooms;
+
+ }
+ dataContext.Messages.Add(new MainWindowViewModel.MessageViewModel
+ {
+ Author = "SneedChat",
+ Messages = new ObservableCollection{
+ new(){
+ Message = "Welcome to my shitty chat client.",
+ MessageId = 0,
+ OwnMessage = false
+ },
+ new()
+ {
+ Message = "Click on Settings -> Identity to configure your XenForo token so you may connect to SneedChat",
+ MessageId = 0,
+ OwnMessage = false
+ }
+ },
+ PostedAt = DateTimeOffset.Now,
+ AuthorId = -1
+ });
+ if (dataContext.RoomList.Count == 0)
+ {
+ dataContext.Messages[0].Messages.Add(new MainWindowViewModel.InnerMessageViewModel
+ {
+ Message = "Also it looks like you have no rooms configured. Click on Settings -> Rooms to configure the room list",
+ MessageId = 0,
+ OwnMessage = false
+ });
+ }
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = dataContext
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/Assets/avalonia-logo.ico b/KfChatDotNetGui/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..da8d49f
Binary files /dev/null and b/KfChatDotNetGui/Assets/avalonia-logo.ico differ
diff --git a/KfChatDotNetGui/Helpers/ForumIdentity.cs b/KfChatDotNetGui/Helpers/ForumIdentity.cs
new file mode 100644
index 0000000..dc9fb6e
--- /dev/null
+++ b/KfChatDotNetGui/Helpers/ForumIdentity.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using KfChatDotNetGui.Models;
+using Newtonsoft.Json;
+
+namespace KfChatDotNetGui.Helpers;
+
+public static class ForumIdentity
+{
+ public static async Task GetForumIdentity(string xfSession, Uri sneedChatUri, string? antiDdosPowCookie = null)
+ {
+ CookieContainer cookies = new CookieContainer();
+ cookies.Add(new Cookie("xf_session", xfSession, "/", sneedChatUri.Host));
+ if (antiDdosPowCookie != null)
+ {
+ cookies.Add(new Cookie("z_ddos_pow", antiDdosPowCookie, "/", sneedChatUri.Host));
+ }
+ using (var client = new HttpClient(new HttpClientHandler {AutomaticDecompression = DecompressionMethods.All, CookieContainer = cookies}))
+ {
+ client.DefaultRequestHeaders.UserAgent.TryParseAdd("KfChatDotNetGui/1.0");
+ client.DefaultRequestHeaders.Accept.Clear();
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
+ client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US"));
+ var response = await client.GetAsync(sneedChatUri);
+ response.EnsureSuccessStatusCode();
+ var html = await response.Content.ReadAsStringAsync();
+ var accountRegex = new Regex(@"user: (.+),");
+ var match = accountRegex.Match(html);
+ if (!match.Success)
+ {
+ throw new Exception("Shitty regex failed to extract account information");
+ }
+
+ var accountJs = match.Groups[1].Value;
+ return JsonConvert.DeserializeObject(accountJs);
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/KfChatDotNetGui.csproj b/KfChatDotNetGui/KfChatDotNetGui.csproj
new file mode 100644
index 0000000..aa4ad12
--- /dev/null
+++ b/KfChatDotNetGui/KfChatDotNetGui.csproj
@@ -0,0 +1,43 @@
+
+
+ WinExe
+ net8.0
+ enable
+
+ copyused
+ true
+ default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
+
diff --git a/KfChatDotNetGui/Models/ForumIdentityModel.cs b/KfChatDotNetGui/Models/ForumIdentityModel.cs
new file mode 100644
index 0000000..79e7097
--- /dev/null
+++ b/KfChatDotNetGui/Models/ForumIdentityModel.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace KfChatDotNetGui.Models;
+
+public class ForumIdentityModel
+{
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ [JsonProperty("username")]
+ public string Username { get; set; }
+ [JsonProperty("avatar_url")]
+ public Uri AvatarUrl { get; set; }
+ // Guessing it'll be the user ID as an int but no idea as this list is empty for me
+ [JsonProperty("ignored_users")]
+ public List IgnoredUsers { get; set; }
+ [JsonProperty("is_staff")]
+ public bool IsStaff { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/Models/RoomSettingsModel.cs b/KfChatDotNetGui/Models/RoomSettingsModel.cs
new file mode 100644
index 0000000..cfeb305
--- /dev/null
+++ b/KfChatDotNetGui/Models/RoomSettingsModel.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace KfChatDotNetGui.Models;
+
+public class RoomSettingsModel
+{
+ public class RoomList
+ {
+ public string Name { get; set; }
+ public int Id { get; set; }
+ }
+
+ public List Rooms { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/Models/SettingsModel.cs b/KfChatDotNetGui/Models/SettingsModel.cs
new file mode 100644
index 0000000..2725b26
--- /dev/null
+++ b/KfChatDotNetGui/Models/SettingsModel.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace KfChatDotNetGui.Models;
+
+public class SettingsModel
+{
+ public string XfSessionToken { get; set; }
+ public Uri WsUri { get; set; }
+ public int ReconnectTimeout { get; set; }
+ public string AntiDdosPow { get; set; }
+ public string Username { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/NLog.config b/KfChatDotNetGui/NLog.config
new file mode 100644
index 0000000..e255848
--- /dev/null
+++ b/KfChatDotNetGui/NLog.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/KfChatDotNetGui/NLog.xsd b/KfChatDotNetGui/NLog.xsd
new file mode 100644
index 0000000..e2b7858
--- /dev/null
+++ b/KfChatDotNetGui/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/KfChatDotNetGui/Program.cs b/KfChatDotNetGui/Program.cs
new file mode 100644
index 0000000..ce5b262
--- /dev/null
+++ b/KfChatDotNetGui/Program.cs
@@ -0,0 +1,23 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+using System;
+
+namespace KfChatDotNetGui
+{
+ class Program
+ {
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace()
+ .UseReactiveUI();
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/ViewLocator.cs b/KfChatDotNetGui/ViewLocator.cs
new file mode 100644
index 0000000..099121d
--- /dev/null
+++ b/KfChatDotNetGui/ViewLocator.cs
@@ -0,0 +1,28 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using KfChatDotNetGui.ViewModels;
+
+namespace KfChatDotNetGui
+{
+ public class ViewLocator : IDataTemplate
+ {
+ public Control Build(object data)
+ {
+ var name = data.GetType().FullName!.Replace("ViewModel", "View");
+ var type = Type.GetType(name);
+
+ if (type != null)
+ {
+ return (Control) Activator.CreateInstance(type)!;
+ }
+
+ return new TextBlock {Text = "Not Found: " + name};
+ }
+
+ public bool Match(object data)
+ {
+ return data is ViewModelBase;
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/ViewModels/IdentitySettingsWindowViewModel.cs b/KfChatDotNetGui/ViewModels/IdentitySettingsWindowViewModel.cs
new file mode 100644
index 0000000..5fff1f7
--- /dev/null
+++ b/KfChatDotNetGui/ViewModels/IdentitySettingsWindowViewModel.cs
@@ -0,0 +1,47 @@
+using System;
+using ReactiveUI;
+
+namespace KfChatDotNetGui.ViewModels;
+
+public class IdentitySettingsWindowViewModel : ViewModelBase
+{
+ private Uri _wsUri = new ("wss://kiwifarms.net/chat.ws");
+
+ public Uri WsUri
+ {
+ get => _wsUri;
+ set => this.RaiseAndSetIfChanged(ref _wsUri, value);
+ }
+
+ private string _xfSessionToken;
+
+ public string XfSessionToken
+ {
+ get => _xfSessionToken;
+ set => this.RaiseAndSetIfChanged(ref _xfSessionToken, value);
+ }
+
+ private string _antiDdosPow;
+
+ public string AntiDdosPow
+ {
+ get => _antiDdosPow;
+ set => this.RaiseAndSetIfChanged(ref _antiDdosPow, value);
+ }
+
+ private string _username;
+
+ public string Username
+ {
+ get => _username;
+ set => this.RaiseAndSetIfChanged(ref _username, value);
+ }
+
+ private int _reconnectTimeout = 30;
+
+ public int ReconnectTimeout
+ {
+ get => _reconnectTimeout;
+ set => this.RaiseAndSetIfChanged(ref _reconnectTimeout, value);
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/ViewModels/MainWindowViewModel.cs b/KfChatDotNetGui/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..110fb23
--- /dev/null
+++ b/KfChatDotNetGui/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Avalonia.Media;
+using JetBrains.Annotations;
+using KfChatDotNetGui.Models;
+using ReactiveUI;
+
+namespace KfChatDotNetGui.ViewModels
+{
+ public class MainWindowViewModel : ViewModelBase
+ {
+ public class InnerMessageViewModel : INotifyPropertyChanged
+ {
+ private string _message;
+
+ public string Message
+ {
+ get => _message;
+ set
+ {
+ if (_message == value) return;
+ _message = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private bool _isHighlighted = false;
+
+ public bool IsHighlighted
+ {
+ get => _isHighlighted;
+ set
+ {
+ if (_isHighlighted == value) return;
+ _isHighlighted = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public int MessageId { get; set; }
+ public bool OwnMessage { get; set; }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ public class MessageViewModel : INotifyPropertyChanged
+ {
+ private ObservableCollection _messages;
+
+ public ObservableCollection Messages
+ {
+ get => _messages;
+ set
+ {
+ if (_messages == value) return;
+ _messages = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public DateTimeOffset PostedAt { get; set; }
+ public string Author { get; set; }
+ public int AuthorId { get; set; }
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ public class UserListViewModel
+ {
+ public string Name { get; set; }
+ public int Id { get; set; }
+ }
+
+ private string _statusText = "Not connected";
+
+ public string Status
+ {
+ get => _statusText;
+ set => this.RaiseAndSetIfChanged(ref _statusText, value);
+ }
+
+ private int _userId;
+
+ public int UserId
+ {
+ get => _userId;
+ set => this.RaiseAndSetIfChanged(ref _userId, value);
+ }
+
+ private List _roomList = new();
+
+ public List RoomList
+ {
+ get => _roomList;
+ set => this.RaiseAndSetIfChanged(ref _roomList, value);
+ }
+
+ private ObservableCollection _userList = new();
+
+ public ObservableCollection UserList
+ {
+ get => _userList;
+ set => this.RaiseAndSetIfChanged(ref _userList, value);
+ }
+
+ private ObservableCollection _messages = new();
+
+ public ObservableCollection Messages
+ {
+ get => _messages;
+ set => this.RaiseAndSetIfChanged(ref _messages, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/ViewModels/RoomSettingsWindowViewModel.cs b/KfChatDotNetGui/ViewModels/RoomSettingsWindowViewModel.cs
new file mode 100644
index 0000000..b6470af
--- /dev/null
+++ b/KfChatDotNetGui/ViewModels/RoomSettingsWindowViewModel.cs
@@ -0,0 +1,20 @@
+using System.Collections.ObjectModel;
+using KfChatDotNetGui.Models;
+using ReactiveUI;
+
+namespace KfChatDotNetGui.ViewModels;
+
+public class RoomSettingsWindowViewModel : ViewModelBase
+{
+
+ private ObservableCollection _roomList = new()
+ {
+ new RoomSettingsModel.RoomList {Id = 1, Name = "General"}
+ };
+
+ public ObservableCollection RoomList
+ {
+ get => _roomList;
+ set => this.RaiseAndSetIfChanged(ref _roomList, value);
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/ViewModels/ViewModelBase.cs b/KfChatDotNetGui/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..7bf7f83
--- /dev/null
+++ b/KfChatDotNetGui/ViewModels/ViewModelBase.cs
@@ -0,0 +1,8 @@
+using ReactiveUI;
+
+namespace KfChatDotNetGui.ViewModels
+{
+ public class ViewModelBase : ReactiveObject
+ {
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/Views/IdentitySettingsWindow.axaml b/KfChatDotNetGui/Views/IdentitySettingsWindow.axaml
new file mode 100644
index 0000000..fe34f51
--- /dev/null
+++ b/KfChatDotNetGui/Views/IdentitySettingsWindow.axaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/KfChatDotNetGui/Views/IdentitySettingsWindow.axaml.cs b/KfChatDotNetGui/Views/IdentitySettingsWindow.axaml.cs
new file mode 100644
index 0000000..5d228f1
--- /dev/null
+++ b/KfChatDotNetGui/Views/IdentitySettingsWindow.axaml.cs
@@ -0,0 +1,116 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Threading;
+using KfChatDotNetGui.Helpers;
+using KfChatDotNetGui.Models;
+using KfChatDotNetGui.ViewModels;
+using Newtonsoft.Json;
+using NLog;
+
+namespace KfChatDotNetGui.Views;
+
+public partial class IdentitySettingsWindow : Window
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+ public IdentitySettingsWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var saveResult = this.FindControl("SaveResult");
+ try
+ {
+ var settings = new SettingsModel
+ {
+ XfSessionToken = (DataContext as IdentitySettingsWindowViewModel).XfSessionToken,
+ WsUri = (DataContext as IdentitySettingsWindowViewModel).WsUri,
+ ReconnectTimeout = (DataContext as IdentitySettingsWindowViewModel).ReconnectTimeout,
+ AntiDdosPow = (DataContext as IdentitySettingsWindowViewModel).AntiDdosPow,
+ Username = (DataContext as IdentitySettingsWindowViewModel).Username
+ };
+ File.WriteAllText("settings.json", JsonConvert.SerializeObject(settings, Formatting.Indented));
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex);
+ saveResult.Foreground = Brushes.Red;
+ saveResult.Text = "Failed to save settings due to an error: " + ex.Message;
+ saveResult.IsVisible = true;
+ return;
+ }
+ saveResult.Foreground = Brushes.Green;
+ saveResult.Text = "Successfully saved settings!";
+ saveResult.IsVisible = true;
+ }
+
+ private void TestTokenButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var saveResult = this.FindControl("SaveResult");
+ saveResult.Foreground = Brushes.Yellow;
+ saveResult.Text = "Testing XenForo token";
+ saveResult.IsVisible = true;
+ var kfHost = (DataContext as IdentitySettingsWindowViewModel).WsUri.Host;
+ Dispatcher.UIThread.Post(
+ () => TestXfToken((DataContext as IdentitySettingsWindowViewModel).XfSessionToken, kfHost, (DataContext as IdentitySettingsWindowViewModel).AntiDdosPow),
+ DispatcherPriority.Background);
+ }
+
+ public void UpdateSaveText(ISolidColorBrush brush, string text)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ var saveResult = this.FindControl("SaveResult");
+ saveResult.Foreground = brush;
+ saveResult.Text = text;
+ if (!saveResult.IsVisible)
+ {
+ saveResult.IsVisible = true;
+ }
+ });
+ }
+
+ public async Task TestXfToken(string xfToken, string kfHost, string? antiDdosPowToken = null)
+ {
+ ForumIdentityModel forumIdentity;
+ try
+ {
+ forumIdentity = await ForumIdentity.GetForumIdentity(xfToken, new Uri($"https://{kfHost}/test-chat"), antiDdosPowToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex);
+ UpdateSaveText(Brushes.Red, "Caught exception while testing token: " + ex.Message);
+ return;
+ }
+
+ if (forumIdentity == null)
+ {
+ UpdateSaveText(Brushes.Red, "Failed to parse SneedChat page, got a null when deserializing the user info");
+ return;
+ }
+
+ if (forumIdentity.Id == 0)
+ {
+ UpdateSaveText(Brushes.Red, "Token is invalid, SneedChat page returned Guest");
+ return;
+ }
+
+ UpdateSaveText(Brushes.Green, "Success! Token belongs to " + forumIdentity.Username);
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/Views/MainWindow.axaml b/KfChatDotNetGui/Views/MainWindow.axaml
new file mode 100644
index 0000000..abde377
--- /dev/null
+++ b/KfChatDotNetGui/Views/MainWindow.axaml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/KfChatDotNetGui/Views/MainWindow.axaml.cs b/KfChatDotNetGui/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..bd9c38b
--- /dev/null
+++ b/KfChatDotNetGui/Views/MainWindow.axaml.cs
@@ -0,0 +1,499 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using KfChatDotNetGui.Models;
+using KfChatDotNetGui.ViewModels;
+using KfChatDotNetWsClient;
+using KfChatDotNetWsClient.Models;
+using KfChatDotNetWsClient.Models.Events;
+using KfChatDotNetWsClient.Models.Json;
+using Newtonsoft.Json;
+using NLog;
+using Websocket.Client;
+
+namespace KfChatDotNetGui.Views
+{
+ public partial class MainWindow : Window
+ {
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+ // Using an empty config as we can update it later through the UpdateConfig method
+ // Having this instance created early is handy for wiring up the events
+ private ChatClient _chatClient = new(new ChatClientConfigModel());
+ private SettingsModel _settings;
+ private int _currentRoom;
+ private ForumIdentityModel _forumIdentity;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ _chatClient.OnMessages += OnMessages;
+ _chatClient.OnUsersJoined += OnUsersJoined;
+ _chatClient.OnUsersParted += OnUsersParted;
+ _chatClient.OnWsReconnect += OnOnWsReconnect;
+ _chatClient.OnWsDisconnection += OnOnWsDisconnection;
+ _chatClient.OnDeleteMessages += OnDeleteMessages;
+ _chatClient.OnFailedToJoinRoom += OnFailedToJoinRoom;
+ }
+
+ private void OnFailedToJoinRoom(object sender, string message)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ UpdateStatus($"Failed to join room, room ID {_currentRoom} is probably invalid");
+ });
+ }
+
+ private void OnDeleteMessages(object sender, List messageIds)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ _logger.Info($"Received delete event for following message IDs: {string.Join(',', messageIds)}");
+ // Gotta make a copy of all the messages (annoyingly) as we'll be deleting stuff and .NET has a very obvious limitation there
+ var messages = (DataContext as MainWindowViewModel).Messages.ToList();
+ foreach (var message in messages)
+ {
+ foreach (var innerMessage in message.Messages.Where(m => messageIds.Contains(m.MessageId)))
+ {
+ // Remove the parent message thingy as otherwise it just shows as a blank item
+ if (message.Messages.Count == 1)
+ {
+ _logger.Info("Removing parent message box");
+ (DataContext as MainWindowViewModel).Messages.Remove(message);
+ }
+ // Go scavenging if there are multiple messages and we don't want to lose the lot
+ else
+ {
+ (DataContext as MainWindowViewModel)
+ .Messages[(DataContext as MainWindowViewModel).Messages.IndexOf(message)].Messages
+ .Remove(innerMessage);
+ }
+
+ _logger.Info($"Removed {innerMessage.MessageId}");
+ }
+ }
+ });
+ }
+
+ private void OnOnWsDisconnection(object sender, DisconnectionInfo disconnectionInfo)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ UpdateStatus($"Disconnected from SneedChat due to {disconnectionInfo.Type}. Client should automatically attempt to reconnect");
+ });
+ }
+
+ private void OnOnWsReconnect(object sender, ReconnectionInfo reconnectionInfo)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ UpdateStatus("Reconnected to SneedChat. Reason was " + reconnectionInfo.Type);
+ (DataContext as MainWindowViewModel).Messages.Clear();
+ (DataContext as MainWindowViewModel).UserList.Clear();
+ });
+
+ _chatClient.JoinRoom(_currentRoom);
+ }
+
+ private void IdentitySettings_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var context = new IdentitySettingsWindowViewModel();
+ if (File.Exists("settings.json"))
+ {
+ var settings = JsonConvert.DeserializeObject(File.ReadAllText("settings.json"));
+ context.WsUri = settings.WsUri;
+ context.XfSessionToken = settings.XfSessionToken;
+ context.ReconnectTimeout = settings.ReconnectTimeout;
+ context.AntiDdosPow = settings.AntiDdosPow;
+ context.Username = settings.Username;
+ }
+ var identitySettingsWindow = new IdentitySettingsWindow
+ {
+ DataContext = context
+ };
+ identitySettingsWindow.ShowDialog(this);
+ identitySettingsWindow.Closed += (o, args) =>
+ {
+ ReloadSettings();
+ };
+ }
+
+ private void ExitMenuItem_OnClick(object? sender, RoutedEventArgs e)
+ {
+ Environment.Exit(0);
+ }
+
+ private async Task ConnectToSneedChat()
+ {
+ UpdateStatus("Connecting to SneedChat");
+ if (!File.Exists("settings.json"))
+ {
+ _logger.Error("Cannot find settings.json and therefore unable to connect to SneedChat, notifying the user through the status bar");
+ UpdateStatus("Unable to connect as client has not been configured (settings.json missing)");
+ return;
+ }
+ ReloadSettings();
+ UpdateStatus("Testing XenForo token validity");
+ ForumIdentityModel forumIdentity;
+ if (string.IsNullOrEmpty(_settings.Username))
+ {
+ try
+ {
+ forumIdentity = await Helpers.ForumIdentity.GetForumIdentity(_settings.XfSessionToken,
+ new Uri($"https://{_settings.WsUri.Host}/test-chat"), _settings.AntiDdosPow);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex);
+ UpdateStatus("Failed to test XenForo token, caught exception " + ex.Message);
+ return;
+ }
+ }
+ else
+ {
+ forumIdentity = new ForumIdentityModel
+ {
+ Username = _settings.Username,
+ Id = int.MaxValue
+ };
+ }
+
+ if (forumIdentity == null)
+ {
+ UpdateStatus("Failed to deserialize account info on SneedChat page");
+ return;
+ }
+
+ if (forumIdentity.Id == 0)
+ {
+ UpdateStatus("Token failed, SneedChat page returned Guest");
+ return;
+ }
+
+ UpdateStatus("Token works! It belongs to " + forumIdentity.Username);
+ _forumIdentity = forumIdentity;
+ (DataContext as MainWindowViewModel).UserId = _forumIdentity.Id;
+ var roomListControl = this.FindControl("RoomList");
+ RoomSettingsModel.RoomList initialRoom;
+ if (roomListControl.SelectedItem == null)
+ {
+ initialRoom = (DataContext as MainWindowViewModel).RoomList.First();
+ }
+ else
+ {
+ initialRoom = (RoomSettingsModel.RoomList) roomListControl.SelectedItem;
+ }
+
+ _chatClient.UpdateConfig(new ChatClientConfigModel
+ {
+ CookieDomain = _settings.WsUri.Host,
+ ReconnectTimeout = _settings.ReconnectTimeout,
+ WsUri = _settings.WsUri,
+ XfSessionToken = _settings.XfSessionToken
+ });
+
+ await _chatClient.StartWsClient();
+ _chatClient.JoinRoom(initialRoom.Id);
+ _currentRoom = initialRoom.Id;
+ UpdateStatus("Connected!");
+ }
+
+ private void OnUsersJoined(object sender, List users, UsersJsonModel jsonPayload)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ foreach (var user in users)
+ {
+ if ((DataContext as MainWindowViewModel).UserList.FirstOrDefault(x => x.Id == user.Id) != null)
+ {
+ _logger.Info($"{user.Username} ({user.Id}) is already in the list but has joined again. New tab? Ignoring!");
+ continue;
+ }
+ (DataContext as MainWindowViewModel).UserList.Add(new MainWindowViewModel.UserListViewModel
+ {
+ Id = user.Id,
+ Name = user.Username
+ });
+ }
+ UpdateUserTotalStatus();
+ });
+ }
+
+ private void OnUsersParted(object sender, List userIds)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ foreach (var id in userIds)
+ {
+ var row = (DataContext as MainWindowViewModel).UserList.FirstOrDefault(x => x.Id == id);
+ if (row == null)
+ {
+ _logger.Info($"A user ({id}) who isn't in the list has parted, ignoring!");
+ continue;
+ }
+ (DataContext as MainWindowViewModel).UserList.Remove(row);
+ }
+ UpdateUserTotalStatus();
+ });
+ }
+
+ private void OnMessages(object sender, List messages, MessagesJsonModel jsonPayload)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ var previousMessage = (DataContext as MainWindowViewModel).Messages.LastOrDefault();
+ if (previousMessage == null)
+ {
+ previousMessage = new MainWindowViewModel.MessageViewModel {AuthorId = -1};
+ }
+ foreach (var message in messages)
+ {
+ _logger.Info("Received message, data payload next");
+ _logger.Info(JsonConvert.SerializeObject(message, Formatting.Indented));
+ if (message.RoomId != _currentRoom)
+ {
+ _logger.Info($"Message {message.MessageId} belongs to another room (we're in {_currentRoom}, this one was for {message.RoomId}), ignoring.");
+ continue;
+ }
+
+ if (message.MessageEditDate != null)
+ {
+ _logger.Info("Received an edit. Going to rewrite message if it already exists, " +
+ "if it doesn't, nothing will happen as this would occur when loading historically modified messages.");
+ foreach (var msg in (DataContext as MainWindowViewModel).Messages)
+ {
+ foreach (var innerMsg in msg.Messages.Where(m => m.MessageId == message.MessageId))
+ {
+ innerMsg.Message = WebUtility.HtmlDecode(message.MessageRaw);
+ _logger.Info("Found the original message, text has been overwritten");
+ return;
+ }
+ }
+ _logger.Info("Never ended up finding the original message so this was probably historical");
+ }
+
+ if (previousMessage.AuthorId == message.Author.Id)
+ {
+ _logger.Info("Found a message from the same author, merging");
+ var lastMessage = (DataContext as MainWindowViewModel).Messages.Last();
+ lastMessage.Messages.Add(new MainWindowViewModel.InnerMessageViewModel
+ {
+ Message = WebUtility.HtmlDecode(message.MessageRaw),
+ MessageId = message.MessageId,
+ OwnMessage = _forumIdentity.Username == message.Author.Username
+ });
+ continue;
+ }
+
+ var viewMessage = new MainWindowViewModel.MessageViewModel
+ {
+ Author = message.Author.Username,
+ Messages = new ObservableCollection
+ {
+ new()
+ {
+ Message = WebUtility.HtmlDecode(message.MessageRaw),
+ MessageId = message.MessageId,
+ OwnMessage = _forumIdentity.Username == message.Author.Username
+ }
+ },
+ PostedAt = message.MessageDate.LocalDateTime,
+ AuthorId = message.Author.Id
+ };
+ (DataContext as MainWindowViewModel).Messages.Add(viewMessage);
+ previousMessage = viewMessage;
+ }
+ var messagesControl = this.FindControl("ChatMessageList");
+ messagesControl.ScrollIntoView((DataContext as MainWindowViewModel).Messages.Last());
+ });
+ }
+
+ private void UpdateStatus(string newStatus)
+ {
+ (DataContext as MainWindowViewModel)!.Status = newStatus;
+ }
+
+ private void ConnectMenuItem_OnClick(object? sender, RoutedEventArgs e)
+ {
+ Dispatcher.UIThread.Post(() => ConnectToSneedChat(), DispatcherPriority.Background);
+ }
+
+ private void RoomSettingsMenuItem_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var context = new RoomSettingsWindowViewModel();
+ if (File.Exists("rooms.json"))
+ {
+ var settings = JsonConvert.DeserializeObject(File.ReadAllText("rooms.json"));
+ context.RoomList.Clear();
+ foreach (var room in settings.Rooms)
+ {
+ context.RoomList.Add(new RoomSettingsModel.RoomList
+ {
+ Id = room.Id,
+ Name = room.Name
+ });
+ }
+ }
+ var roomSettingsWindow = new RoomSettingsWindow
+ {
+ DataContext = context
+ };
+ roomSettingsWindow.ShowDialog(this);
+ roomSettingsWindow.Closed += (o, args) =>
+ {
+ ReloadRoomList();
+ };
+ }
+
+ private void ReloadSettings()
+ {
+ if (!File.Exists("settings.json"))
+ {
+ _logger.Error("Was asked to reload the settings but settings.json doesn't exist so I won't bother");
+ return;
+ }
+ var settings = JsonConvert.DeserializeObject(File.ReadAllText("settings.json"));
+ _settings = settings;
+ }
+
+ private void ReloadRoomList()
+ {
+ if (!File.Exists("rooms.json"))
+ {
+ _logger.Error("Was asked to reload the room list but rooms.json doesn't exist so I won't bother");
+ return;
+ }
+ var rooms = JsonConvert.DeserializeObject(File.ReadAllText("rooms.json"));
+ (DataContext as MainWindowViewModel)!.RoomList = rooms!.Rooms;
+ }
+
+ private void RoomList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (!_chatClient.IsConnected())
+ {
+ UpdateStatus("Cannot join room as client is not connected");
+ return;
+ }
+ var roomListControl = this.FindControl("RoomList");
+ var room = (RoomSettingsModel.RoomList) roomListControl.SelectedItem!;
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (room == null)
+ {
+ _logger.Info("Got a selection change on room list with a null selected item. This seems to just happen sometimes, ignoring.");
+ return;
+ }
+ UpdateStatus($"Connected! Changing to {room.Name}");
+ (DataContext as MainWindowViewModel).Messages.Clear();
+ (DataContext as MainWindowViewModel).UserList.Clear();
+ _chatClient.JoinRoom(room.Id);
+ _currentRoom = room.Id;
+ }
+
+ private MainWindowViewModel.MessageViewModel? GetCurrentlySelectedMessage()
+ {
+ var messagesControl = this.FindControl("ChatMessageList");
+ return messagesControl.SelectedItem as MainWindowViewModel.MessageViewModel;
+ }
+
+ private void NewChatMessage_OnKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (e.Key is not (Key.Enter or Key.Return))
+ {
+ return;
+ }
+ TrySendMessageFromTextBox();
+ }
+
+ private void NewChatMessageSubmitButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ TrySendMessageFromTextBox();
+ }
+
+ private void TrySendMessageFromTextBox()
+ {
+ if (!_chatClient.IsConnected())
+ {
+ UpdateStatus("Cannot send a message while disconnected");
+ return;
+ }
+ var newChatMessage = this.FindControl("NewChatMessage");
+ _chatClient.SendMessage(newChatMessage.Text);
+ newChatMessage.Clear();
+ }
+
+ private void UpdateUserTotalStatus()
+ {
+ var userCount = (DataContext as MainWindowViewModel).UserList.Count;
+ UpdateStatus($"Connected! {userCount} users in chat");
+ }
+
+ private void MessageEditButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ _logger.Info("Edit button clicked for " + ((e.Source as Button).DataContext as MainWindowViewModel.InnerMessageViewModel).MessageId);
+ }
+
+ private void CopyButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var message = (e.Source as Button).DataContext as MainWindowViewModel.InnerMessageViewModel;
+ if (message == null)
+ {
+ _logger.Info("Caught a null when trying to access the inner message model instance for the purposes of copying");
+ return;
+ }
+ _logger.Info($"Copying {message.MessageId} to clipboard");
+ GetTopLevel(e.Source as Button)?.Clipboard?.SetTextAsync(message.Message).Wait();
+ }
+
+ private void InnerMessageRow_OnPointerEnter(object? sender, PointerEventArgs e)
+ {
+ ((e.Source as ListBoxItem).DataContext as MainWindowViewModel.InnerMessageViewModel).IsHighlighted = true;
+ }
+
+ private void InnerMessageRow_OnPointerLeave(object? sender, PointerEventArgs e)
+ {
+ ((e.Source as ListBoxItem).DataContext as MainWindowViewModel.InnerMessageViewModel).IsHighlighted = false;
+ }
+
+ private void OuterMessageRow_OnPointerEnter(object? sender, PointerEventArgs e)
+ {
+ var children = (e.Source as ListBox).GetLogicalChildren().Cast();
+ foreach (var child in children)
+ {
+ // Bit of a ghetto hack but it ensures that there's only ever one subscriber
+ child.PointerEntered -= InnerMessageRow_OnPointerEnter;
+ child.PointerExited -= InnerMessageRow_OnPointerLeave;
+ child.PointerEntered += InnerMessageRow_OnPointerEnter;
+ child.PointerExited += InnerMessageRow_OnPointerLeave;
+ }
+ }
+
+ private void MessageDeleteButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var newChatMessage = this.FindControl("NewChatMessage");
+ var messageId = ((e.Source as Button).DataContext as MainWindowViewModel.InnerMessageViewModel).MessageId;
+ newChatMessage.Text = "/delete " + messageId;
+ }
+
+ private void AuthorNameButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var message = (e.Source as Button).DataContext as MainWindowViewModel.MessageViewModel;
+ var newChatMessage = this.FindControl("NewChatMessage");
+ newChatMessage.Text += $"@{message.Author}, ";
+ newChatMessage.Focus();
+ newChatMessage.SelectionStart = newChatMessage.Text.Length;
+ newChatMessage.SelectionEnd = newChatMessage.Text.Length;
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetGui/Views/RoomSettingsWindow.axaml b/KfChatDotNetGui/Views/RoomSettingsWindow.axaml
new file mode 100644
index 0000000..7a11202
--- /dev/null
+++ b/KfChatDotNetGui/Views/RoomSettingsWindow.axaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/KfChatDotNetGui/Views/RoomSettingsWindow.axaml.cs b/KfChatDotNetGui/Views/RoomSettingsWindow.axaml.cs
new file mode 100644
index 0000000..b26b5eb
--- /dev/null
+++ b/KfChatDotNetGui/Views/RoomSettingsWindow.axaml.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Threading;
+using HtmlAgilityPack;
+using KfChatDotNetGui.Models;
+using KfChatDotNetGui.ViewModels;
+using Newtonsoft.Json;
+using NLog;
+
+namespace KfChatDotNetGui.Views;
+
+public partial class RoomSettingsWindow : Window
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+ public RoomSettingsWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var saveResult = this.FindControl("SaveResult");
+ try
+ {
+ var roomSettings = new RoomSettingsModel
+ {
+ Rooms = new List()
+ };
+ foreach (var room in (DataContext as RoomSettingsWindowViewModel).RoomList)
+ {
+ roomSettings.Rooms.Add(new RoomSettingsModel.RoomList
+ {
+ Id = room.Id,
+ Name = room.Name
+ });
+ }
+ File.WriteAllText("rooms.json", JsonConvert.SerializeObject(roomSettings, Formatting.Indented));
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(e);
+ saveResult.Foreground = Brushes.Red;
+ saveResult.Text = "Failed to save rooms due to an error: " + ex.Message;
+ saveResult.IsVisible = true;
+ return;
+ }
+ saveResult.Foreground = Brushes.Green;
+ saveResult.Text = "Successfully saved rooms!";
+ saveResult.IsVisible = true;
+ }
+
+ private void AddRowButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ (DataContext as RoomSettingsWindowViewModel).RoomList.Add(new RoomSettingsModel.RoomList());
+ }
+
+ private void DeleteSelectedRowsButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var roomGrid = this.FindControl("RoomGrid");
+ var roomList = (DataContext as RoomSettingsWindowViewModel).RoomList.ToList();
+ foreach (var room in roomGrid.SelectedItems)
+ {
+ roomList.Remove(room as RoomSettingsModel.RoomList);
+ }
+
+ (DataContext as RoomSettingsWindowViewModel).RoomList =
+ new ObservableCollection(roomList);
+ }
+
+ private void AutoDetectButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var saveResult = this.FindControl("SaveResult");
+ saveResult.Foreground = Brushes.Yellow;
+ saveResult.Text = "Downloading the SneedChat page";
+ saveResult.IsVisible = true;
+
+ Dispatcher.UIThread.Post(() => AutoDetectRooms(), DispatcherPriority.Background);
+ }
+
+ private async Task AutoDetectRooms()
+ {
+ var kfDomain = "kiwifarms.net";
+ if (File.Exists("settings.json"))
+ {
+ var settings = JsonConvert.DeserializeObject(await File.ReadAllTextAsync("settings.json"));
+ kfDomain = settings.WsUri.Host;
+ }
+
+ Uri sneedChatUri = new Uri($"https://{kfDomain}/test-chat");
+ using (var client = new HttpClient(new HttpClientHandler {AutomaticDecompression = DecompressionMethods.All}))
+ {
+ client.DefaultRequestHeaders.UserAgent.TryParseAdd("KfChatDotNetGui/1.0");
+ client.DefaultRequestHeaders.Accept.Clear();
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
+ client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US"));
+
+ HttpResponseMessage response = await client.GetAsync(sneedChatUri);
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.Error($"Got HTTP error {response.StatusCode} when fetching {sneedChatUri}");
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ var saveResult = this.FindControl("SaveResult");
+ saveResult.Foreground = Brushes.Red;
+ saveResult.Text = $"Failed to load the SneedChat page due to an HTTP error (Status code {response.StatusCode})";
+ saveResult.IsVisible = true;
+ });
+ return;
+ }
+
+ var html = await response.Content.ReadAsStringAsync();
+ var document = new HtmlDocument();
+ document.LoadHtml(html);
+
+ var roomList = document.DocumentNode.SelectNodes("//a[@class=\"chat-room\"]");
+ if (roomList == null)
+ {
+ _logger.Error("Chat room list is null, xpath for it is probably broken");
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ var saveResult = this.FindControl("SaveResult");
+ saveResult.Foreground = Brushes.Red;
+ saveResult.Text = "Failed to parse the SneedChat page, list of rooms was null";
+ saveResult.IsVisible = true;
+ });
+ return;
+ }
+
+ List roomListModel = new List();
+ foreach (var element in roomList)
+ {
+ roomListModel.Add(new RoomSettingsModel.RoomList
+ {
+ Id = element.GetAttributeValue("data-id", 0),
+ Name = WebUtility.HtmlDecode(element.InnerText)
+ });
+ }
+
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ (DataContext as RoomSettingsWindowViewModel).RoomList.Clear();
+ foreach (var room in roomListModel)
+ {
+ (DataContext as RoomSettingsWindowViewModel).RoomList.Add(room);
+ }
+
+ var saveResult = this.FindControl("SaveResult");
+ saveResult.Foreground = Brushes.Green;
+ saveResult.Text = "Populated list using SneedChat page. Remember to hit Save when you're done!";
+ saveResult.IsVisible = true;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetKickBot/Helpers.cs b/KfChatDotNetKickBot/Helpers.cs
new file mode 100644
index 0000000..2b6fcf0
--- /dev/null
+++ b/KfChatDotNetKickBot/Helpers.cs
@@ -0,0 +1,36 @@
+using Microsoft.Data.Sqlite;
+using NLog;
+
+namespace KfChatDotNetKickBot;
+
+public static class Helpers
+{
+ // This ended up being pretty useless as it turns out Firefox doesn't store session cookies in cookies.sqlite
+ // But I'll leave it here in case it becomes useful one day
+ public static async Task GetXfToken(string cookieName, string cookieDomain, string containerPath)
+ {
+ var logger = LogManager.GetCurrentClassLogger();
+ await using var connection = new SqliteConnection($"Data Source={containerPath}");
+
+ await connection.OpenAsync();
+ logger.Debug($"Opened {containerPath}");
+
+ var command = connection.CreateCommand();
+ command.CommandText = "SELECT value FROM moz_cookies WHERE host = $host AND name = $name ORDER BY creationTime DESC LIMIT 1";
+ command.Parameters.AddWithValue("$host", cookieDomain);
+ command.Parameters.AddWithValue("$name", cookieName);
+ logger.Debug("Created command");
+ logger.Debug(command.CommandText);
+
+ await using var reader = await command.ExecuteReaderAsync();
+
+ while (await reader.ReadAsync())
+ {
+ logger.Debug("Reading first row, which will be immediately returned anyway");
+ return reader.GetString(0);
+ }
+
+ logger.Error("Fucked up while retrieving cookie. Cookie doesn't exist?");
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj b/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj
new file mode 100644
index 0000000..f2b0776
--- /dev/null
+++ b/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj
@@ -0,0 +1,32 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+ Always
+
+
+
+
diff --git a/KfChatDotNetKickBot/KickBot.cs b/KfChatDotNetKickBot/KickBot.cs
new file mode 100644
index 0000000..d1eb1a6
--- /dev/null
+++ b/KfChatDotNetKickBot/KickBot.cs
@@ -0,0 +1,164 @@
+using KfChatDotNetWsClient;
+using KfChatDotNetWsClient.Models;
+using KfChatDotNetWsClient.Models.Events;
+using KfChatDotNetWsClient.Models.Json;
+using KickWsClient.Models;
+using Newtonsoft.Json;
+using NLog;
+using Spectre.Console;
+using Websocket.Client;
+
+namespace KfChatDotNetKickBot;
+
+public class KickBot
+{
+ private ChatClient _kfClient;
+ private KickWsClient.KickWsClient _kickClient;
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+ private Models.ConfigModel _config;
+ private Thread _pingThread;
+ private bool _pingEnabled = true;
+
+ public KickBot()
+ {
+ _logger.Info("Bot starting!");
+ const string configPath = "config.json";
+ if (!Path.Exists(configPath))
+ {
+ _logger.Error($"{configPath} is missing! Exiting");
+ Environment.Exit(1);
+ }
+
+ _config = JsonConvert.DeserializeObject(File.ReadAllText(configPath)) ??
+ throw new InvalidOperationException();
+
+ _kfClient = new ChatClient(new ChatClientConfigModel
+ {
+ WsUri = _config.KfWsEndpoint,
+ XfSessionToken = GetXfToken(),
+ CookieDomain = _config.KfWsEndpoint.Host,
+ Proxy = _config.KfProxy,
+ ReconnectTimeout = _config.KfReconnectTimeout
+ });
+
+ _kickClient = new KickWsClient.KickWsClient(_config.PusherEndpoint.ToString(),
+ _config.PusherProxy, _config.PusherReconnectTimeout);
+
+ _kfClient.OnMessages += OnKfChatMessage;
+ _kfClient.OnUsersParted += OnUsersParted;
+ _kfClient.OnUsersJoined += OnUsersJoined;
+ _kfClient.OnWsDisconnection += OnKfWsDisconnected;
+ _kfClient.OnWsReconnect += OnKfWsReconnected;
+
+ _kickClient.OnStreamerIsLive += OnStreamerIsLive;
+ _kickClient.OnChatMessage += OnKickChatMessage;
+ _kickClient.OnWsReconnect += OnPusherWsReconnected;
+ _kickClient.OnPusherSubscriptionSucceeded += OnPusherSubscriptionSucceeded;
+
+ _kfClient.StartWsClient().Wait();
+ _kfClient.JoinRoom(_config.KfChatRoomId);
+
+ _kickClient.StartWsClient().Wait();
+ foreach (var channel in _config.PusherChannels)
+ {
+ _kickClient.SendPusherSubscribe(channel);
+ }
+
+ _pingThread = new Thread(PingThread);
+ _pingThread.Start();
+
+ while (true)
+ {
+ var input = AnsiConsole.Prompt(new TextPrompt("Enter Message:"));
+ _kfClient.SendMessage(input);
+ }
+ }
+
+ private void PingThread()
+ {
+ while (_pingEnabled)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(15));
+ _logger.Debug("Pinging KF and Pusher");
+ _kfClient.SendMessage("/ping");
+ _kickClient.SendPusherPing();
+ }
+ }
+
+ private string GetXfToken()
+ {
+ //return Helpers.GetXfToken("xf_session", _config.KfWsEndpoint.Host, _config.FirefoxCookieContainer).Result ??
+ // throw new InvalidOperationException();
+ return _config.XfTokenValue;
+ }
+
+ private void OnStreamerIsLive(object sender, KickModels.StreamerIsLiveEventModel? e)
+ {
+
+ }
+
+ private void OnKfChatMessage(object sender, List messages, MessagesJsonModel jsonPayload)
+ {
+ _logger.Debug($"Received {messages.Count} message(s)");
+ foreach (var message in messages)
+ {
+ AnsiConsole.MarkupLine($"[yellow]KF[/] <{message.Author.Username}> {message.Message.EscapeMarkup()} ({message.MessageDate.LocalDateTime.ToShortTimeString()})");
+ }
+ }
+
+ private void OnKickChatMessage(object sender, KickModels.ChatMessageEventModel? e)
+ {
+ if (e == null) return;
+ AnsiConsole.MarkupLine($"[green]Kick[/] <{e.Sender.Username}> {e.Content.EscapeMarkup()} ({e.CreatedAt.LocalDateTime.ToShortTimeString()})");
+
+ }
+
+ private void OnUsersJoined(object sender, List users, UsersJsonModel jsonPayload)
+ {
+ _logger.Debug($"Received {users.Count} user join events");
+ foreach (var user in users)
+ {
+ AnsiConsole.MarkupLine($"[green]{user.Username.EscapeMarkup()} joined![/]");
+ }
+ }
+
+ private void OnUsersParted(object sender, List userIds)
+ {
+ _logger.Debug($"Received {userIds.Count} user part events");
+ foreach (var id in userIds)
+ {
+ AnsiConsole.MarkupLine($"[red]{id} left the chat...[/]");
+ }
+ }
+
+ private void OnKfWsDisconnected(object sender, DisconnectionInfo disconnectionInfo)
+ {
+ AnsiConsole.MarkupLine($"[red]Sneedchat disconnected due to {disconnectionInfo.Type}[/]");
+ AnsiConsole.MarkupLine("[yellow]Grabbing fresh token from browser[/]");
+ var token = GetXfToken();
+ AnsiConsole.MarkupLine($"[green]Obtained token = {token.EscapeMarkup()}[/]");
+ _kfClient.UpdateToken(token);
+ }
+
+ private void OnKfWsReconnected(object sender, ReconnectionInfo reconnectionInfo)
+ {
+ AnsiConsole.MarkupLine($"[red]Sneedchat reconnected due to {reconnectionInfo.Type}[/]");
+ AnsiConsole.MarkupLine($"[green]Rejoining {_config.KfChatRoomId}[/]");
+ _kfClient.JoinRoom(_config.KfChatRoomId);
+ }
+
+ private void OnPusherWsReconnected(object sender, ReconnectionInfo reconnectionInfo)
+ {
+ AnsiConsole.MarkupLine($"[red]Pusher reconnected due to {reconnectionInfo.Type}[/]");
+ foreach (var channel in _config.PusherChannels)
+ {
+ AnsiConsole.MarkupLine($"[green]Rejoining {channel}[/]");
+ _kickClient.SendPusherSubscribe(channel);
+ }
+ }
+
+ private void OnPusherSubscriptionSucceeded(object sender, PusherModels.BasePusherEventModel? e)
+ {
+ AnsiConsole.MarkupLine($"[green]Pusher indicates subscription to {e?.Channel.EscapeMarkup()} was successful[/]");
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetKickBot/Models.cs b/KfChatDotNetKickBot/Models.cs
new file mode 100644
index 0000000..5e6c60f
--- /dev/null
+++ b/KfChatDotNetKickBot/Models.cs
@@ -0,0 +1,24 @@
+namespace KfChatDotNetKickBot;
+
+public class Models
+{
+ public class ConfigModel
+ {
+ public Uri PusherEndpoint { get; set; } =
+ new("wss://ws-us2.pusher.com/app/eb1d5f283081a78b932c?protocol=7&client=js&version=7.6.0&flash=false");
+
+ public Uri KfWsEndpoint { get; set; } = new("wss://kiwifarms.st:9443/chat.ws");
+
+ public List PusherChannels { get; set; } = [];
+ public int KfChatRoomId { get; set; }
+ // Proxy to use for connecting to Sneedchat
+ public string? KfProxy { get; set; }
+ // Proxy to use for the Pusher websocket
+ // e.g. socks5://blahblah:1080
+ public string? PusherProxy { get; set; }
+ public int KfReconnectTimeout { get; set; } = 30;
+ public int PusherReconnectTimeout { get; set; } = 30;
+ // Todo: Find a way to extract this from the browser as it's not valid forever
+ public string? XfTokenValue { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetKickBot/NLog.config b/KfChatDotNetKickBot/NLog.config
new file mode 100644
index 0000000..ab4e010
--- /dev/null
+++ b/KfChatDotNetKickBot/NLog.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/KfChatDotNetKickBot/NLog.xsd b/KfChatDotNetKickBot/NLog.xsd
new file mode 100644
index 0000000..e2b7858
--- /dev/null
+++ b/KfChatDotNetKickBot/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/KfChatDotNetKickBot/Program.cs b/KfChatDotNetKickBot/Program.cs
new file mode 100644
index 0000000..981e8e5
--- /dev/null
+++ b/KfChatDotNetKickBot/Program.cs
@@ -0,0 +1,15 @@
+using System.Net;
+using System.Text;
+using NLog;
+
+namespace KfChatDotNetKickBot
+{
+ public class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.OutputEncoding = Encoding.UTF8;
+ new KickBot();
+ }
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetKickBot/config.json b/KfChatDotNetKickBot/config.json
new file mode 100644
index 0000000..e1da35d
--- /dev/null
+++ b/KfChatDotNetKickBot/config.json
@@ -0,0 +1,6 @@
+{
+ "PusherChannels": ["chatrooms.2507974.v2", "channel.2515504"],
+ "KfProxy": "socks5://us-lax-wg-socks5-203.relays.mullvad.net:1080",
+ "KfChatRoomId": 15,
+ "XfTokenValue": "fill this in with the value from xf_session"
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/ChatClient.cs b/KfChatDotNetWsClient/ChatClient.cs
new file mode 100644
index 0000000..620bb4a
--- /dev/null
+++ b/KfChatDotNetWsClient/ChatClient.cs
@@ -0,0 +1,285 @@
+using System.Net;
+using System.Net.WebSockets;
+using KfChatDotNetWsClient.Models;
+using KfChatDotNetWsClient.Models.Events;
+using KfChatDotNetWsClient.Models.Json;
+using Newtonsoft.Json;
+using NLog;
+using Websocket.Client;
+// It's a fucking lie. You must use conditional access or you WILL get NullReferenceErrors if an event is not in use
+// ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
+
+namespace KfChatDotNetWsClient;
+
+public class ChatClient
+{
+ public event EventHandlers.OnMessagesEventHandler OnMessages;
+ public event EventHandlers.OnUsersPartedEventHandler OnUsersParted;
+ public event EventHandlers.OnUsersJoinedEventHandler OnUsersJoined;
+ public event EventHandlers.OnWsReconnectEventHandler OnWsReconnect;
+ public event EventHandlers.OnDeleteMessagesEventHandler OnDeleteMessages;
+ public event EventHandlers.OnWsDisconnectionEventHandler OnWsDisconnection;
+ public event EventHandlers.OnFailedToJoinRoom OnFailedToJoinRoom;
+ public event EventHandlers.OnUnknownCommand OnUnknownCommand;
+ private WebsocketClient _wsClient;
+ private readonly Logger _logger = LogManager.GetCurrentClassLogger();
+ private ChatClientConfigModel _config;
+
+ public ChatClient(ChatClientConfigModel config)
+ {
+ _config = config;
+ }
+
+ public void UpdateConfig(ChatClientConfigModel config)
+ {
+ _config = config;
+ }
+
+ public void UpdateToken(string newToken)
+ {
+ _config.XfSessionToken = newToken;
+ }
+
+ public async Task StartWsClient()
+ {
+ _wsClient = await CreateWsClient();
+ }
+
+ public void Disconnect()
+ {
+ _wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket").Wait();
+ }
+
+ private async Task CreateWsClient()
+ {
+ var factory = new Func(() =>
+ {
+ var clientWs = new ClientWebSocket();
+ // Guest mode
+ if (_config.XfSessionToken == null)
+ {
+ return clientWs;
+ }
+
+ var cookieContainer = new CookieContainer();
+ cookieContainer.Add(new Cookie("xf_session", _config.XfSessionToken, "/", _config.CookieDomain));
+ clientWs.Options.Cookies = cookieContainer;
+ if (_config.Proxy != null)
+ {
+ clientWs.Options.Proxy = new WebProxy(_config.Proxy);
+ }
+
+ return clientWs;
+ });
+
+ var client = new WebsocketClient(_config.WsUri, factory)
+ {
+ ReconnectTimeout = TimeSpan.FromSeconds(_config.ReconnectTimeout)
+ };
+
+ 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!");
+ return client;
+ }
+
+ 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);
+ }
+
+ private void WsReconnection(ReconnectionInfo reconnectionInfo)
+ {
+ _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}");
+ if (reconnectionInfo.Type == ReconnectionType.Initial)
+ {
+ _logger.Error("Not firing the reconnection event as this is the initial event");
+ return;
+ }
+ OnWsReconnect?.Invoke(this, reconnectionInfo);
+ }
+
+ private void WsMessageReceived(ResponseMessage message)
+ {
+ if (message.Text == null)
+ {
+ _logger.Info("Websocket message was null, ignoring packet");
+ return;
+ }
+
+ if (message.Text.StartsWith("You cannot join this room"))
+ {
+ _logger.Debug("Got a message saying we failed to join the room");
+ OnFailedToJoinRoom?.Invoke(this, message.Text);
+ return;
+ }
+
+ if (message.Text.StartsWith("Unknown command"))
+ {
+ _logger.Debug("Unknown command received");
+ OnUnknownCommand?.Invoke(this, message.Text);
+ return;
+ }
+
+ Dictionary packetType = new Dictionary();
+ try
+ {
+ packetType = JsonConvert.DeserializeObject>(message.Text)!;
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Failed to parse packet");
+ _logger.Error(e);
+ _logger.Error($"Packet contents: {message.Text}");
+ return;
+ }
+
+ _logger.Debug($"Received packet from KF: {string.Join(',', packetType.Keys)}");
+ // Message(s) received
+ if (packetType.ContainsKey("messages"))
+ {
+ _logger.Debug("Looks like it's a chat message");
+ WsChatMessagesReceived(message);
+ return;
+ }
+ // User(s) joined
+ if (packetType.ContainsKey("users"))
+ {
+ _logger.Debug("Looks like this is a user(s) joined packet");
+ WsChatUsersJoined(message);
+ return;
+ }
+ // User(s) parted
+ if (packetType.ContainsKey("user"))
+ {
+ _logger.Debug("Looks like this is a user(s) parted packet");
+ WsChatUsersParted(message);
+ return;
+ }
+
+ if (packetType.ContainsKey("delete"))
+ {
+ _logger.Debug($"Looks like this is a message deletion packet");
+ WsDeleteMessagesReceived(message);
+ return;
+ }
+
+ _logger.Info($"Received packet this was not handled: {message.Text}");
+ }
+
+ public void JoinRoom(int roomId)
+ {
+ _logger.Debug($"Joining {roomId}");
+ _wsClient.Send($"/join {roomId}");
+ }
+
+ public void SendMessage(string message)
+ {
+ _logger.Debug($"Sending '{message}'");
+ _wsClient.Send(message);
+ }
+
+ public void DeleteMessage(int messageId)
+ {
+ _logger.Debug($"Deleting {messageId}");
+ _wsClient.Send($"/delete {messageId}");
+ }
+
+ public void EditMessage(int messageId, string newMessage)
+ {
+ // Explicitly set formatting to none as it must be inline (Newtonsoft will do this by default but just wanting to be explicit)
+ var payload = JsonConvert.SerializeObject(new EditMessageJsonModel {Id = messageId, Message = newMessage},
+ Formatting.None);
+ _logger.Debug($"Editing {messageId} with '{newMessage}'");
+ _wsClient.Send($"/edit {payload}");
+ }
+
+ private void WsDeleteMessagesReceived(ResponseMessage message)
+ {
+ var data = JsonConvert.DeserializeObject(message.Text);
+ _logger.Debug($"Received delete packet for messages: {string.Join(',', data.MessageIdsToDelete)}");
+ OnDeleteMessages?.Invoke(this, data.MessageIdsToDelete);
+ }
+
+ private void WsChatMessagesReceived(ResponseMessage message)
+ {
+ var data = JsonConvert.DeserializeObject(message.Text);
+ var messages = new List();
+ foreach (var chatMessage in data.Messages)
+ {
+ var model = new MessageModel
+ {
+ Author = new UserModel
+ {
+ Id = chatMessage.Author.Id,
+ Username = chatMessage.Author.Username,
+ AvatarUrl = chatMessage.Author.AvatarUrl,
+ // It isn't sent on chat messages
+ LastActivity = null
+ },
+ Message = chatMessage.Message,
+ MessageId = chatMessage.MessageId,
+ MessageRaw = chatMessage.MessageRaw,
+ RoomId = chatMessage.RoomId
+ };
+
+ if(chatMessage.MessageEditDate == 0)
+ {
+ model.MessageEditDate = null;
+ }
+ else
+ {
+ model.MessageEditDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageEditDate);
+ }
+
+ model.MessageDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageDate);
+
+ messages.Add(model);
+ }
+ _logger.Debug($"Received {messages.Count} chat messages");
+ if (messages.Count == 1)
+ {
+ _logger.Debug($"{JsonConvert.SerializeObject(messages[0], Formatting.Indented)}");
+ }
+ OnMessages?.Invoke(this, messages, data);
+ }
+
+ private void WsChatUsersJoined(ResponseMessage message)
+ {
+ var data = JsonConvert.DeserializeObject(message.Text);
+ var users = new List();
+ foreach (var user in data.Users.Keys)
+ {
+ users.Add(new UserModel
+ {
+ Id = int.Parse(user),
+ Username = data.Users[user].Username,
+ AvatarUrl = data.Users[user].AvatarUrl,
+ LastActivity = DateTimeOffset.FromUnixTimeSeconds(data.Users[user].LastActivity)
+ });
+ }
+ var usersJoined= data.Users.Select(user => int.Parse(user.Key)).ToList();
+ _logger.Debug($"Following users have joined: {string.Join(',', usersJoined)}");
+ OnUsersJoined?.Invoke(this, users, data);
+ }
+
+ private void WsChatUsersParted(ResponseMessage message)
+ {
+ // {"user":{"1337":false}}
+ var data = JsonConvert.DeserializeObject>>(message.Text);
+ var usersParted = data!["user"].Select(user => int.Parse(user.Key)).ToList();
+ _logger.Debug($"Following users have parted: {string.Join(',', usersParted)}");
+ OnUsersParted?.Invoke(this, usersParted);
+ }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj b/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj
new file mode 100644
index 0000000..c6dee9c
--- /dev/null
+++ b/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ default
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs b/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs
new file mode 100644
index 0000000..60cf926
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs
@@ -0,0 +1,12 @@
+namespace KfChatDotNetWsClient.Models;
+
+public class ChatClientConfigModel
+{
+ // XF session token. Sent as a cookie to auth the user
+ public string? XfSessionToken { get; set; }
+ // Currently wss://kiwifarms.net/chat.ws
+ public Uri WsUri { get; set; }
+ public int ReconnectTimeout { get; set; } = 30;
+ public string CookieDomain { get; set; } = "kiwifarms.net";
+ public string? Proxy { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Events/EventHandlers.cs b/KfChatDotNetWsClient/Models/Events/EventHandlers.cs
new file mode 100644
index 0000000..98c938b
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Events/EventHandlers.cs
@@ -0,0 +1,28 @@
+using KfChatDotNetWsClient.Models.Json;
+using Websocket.Client;
+
+namespace KfChatDotNetWsClient.Models.Events;
+
+public class EventHandlers
+{
+ public delegate void OnMessagesEventHandler(object sender, List messages,
+ MessagesJsonModel jsonPayload);
+
+ // When a user first joins the chat, this event will fire with the entire user list (which may be massive)
+ // But when users join in the course of a regular chat, it'll be one at a time
+ public delegate void OnUsersJoinedEventHandler(object sender, List users, UsersJsonModel jsonPayload);
+
+ // Usually only one user parts at a time, but theoretically the model could support more than one at a time
+ public delegate void OnUsersPartedEventHandler(object sender, List userIds);
+
+ public delegate void OnWsReconnectEventHandler(object sender, ReconnectionInfo reconnectionInfo);
+
+ // Usually only one is sent at a time but it is a list hence the pluralization
+ public delegate void OnDeleteMessagesEventHandler(object sender, List messageIds);
+
+ public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo disconnectionInfo);
+
+ public delegate void OnFailedToJoinRoom(object sender, string message);
+
+ public delegate void OnUnknownCommand(object sender, string message);
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Events/MessageModel.cs b/KfChatDotNetWsClient/Models/Events/MessageModel.cs
new file mode 100644
index 0000000..fcbb1f5
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Events/MessageModel.cs
@@ -0,0 +1,12 @@
+namespace KfChatDotNetWsClient.Models.Events;
+
+public class MessageModel
+{
+ public UserModel Author { get; set; }
+ public string Message { get; set; }
+ public int MessageId { get; set; }
+ public DateTimeOffset? MessageEditDate { get; set; }
+ public DateTimeOffset MessageDate { get; set; }
+ public string MessageRaw { get; set; }
+ public int RoomId { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Events/UserModel.cs b/KfChatDotNetWsClient/Models/Events/UserModel.cs
new file mode 100644
index 0000000..ad69fc5
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Events/UserModel.cs
@@ -0,0 +1,10 @@
+namespace KfChatDotNetWsClient.Models.Events;
+
+public class UserModel
+{
+ public int Id { get; set; }
+ public string Username { get; set; }
+ public Uri AvatarUrl { get; set; }
+ // Unset if it's related to a chat message
+ public DateTimeOffset? LastActivity { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs b/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs
new file mode 100644
index 0000000..30aa5bd
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs
@@ -0,0 +1,9 @@
+using Newtonsoft.Json;
+
+namespace KfChatDotNetWsClient.Models.Json;
+
+public class DeleteMessagesJsonModel
+{
+ [JsonProperty("delete")]
+ public List MessageIdsToDelete { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs b/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs
new file mode 100644
index 0000000..7539ad8
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs
@@ -0,0 +1,12 @@
+using Newtonsoft.Json;
+
+namespace KfChatDotNetWsClient.Models.Json;
+
+public class EditMessageJsonModel
+{
+ [JsonProperty("id")]
+ public int Id { get; set; }
+
+ [JsonProperty("message")]
+ public string Message { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs b/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs
new file mode 100644
index 0000000..bd7c501
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs
@@ -0,0 +1,59 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace KfChatDotNetWsClient.Models.Json;
+
+// {
+// "messages": [
+// {
+// "author": {
+// "id": 110635,
+// "username": "felted",
+// "avatar_url": "https://kiwifarms.net/data/avatars/m/110/110635.jpg?1657300618"
+// },
+// "message": "Nigger.",
+// "message_id": 4390866,
+// "message_edit_date": 0,
+// "message_date": 1657317093,
+// "message_raw": "Nigger.",
+// "room_id": 10
+// }
+// ]
+// }
+
+// message_raw contains the original bbcode for the message
+// message is the HTML-version the web client renders with emotes transformed into images, etc.
+
+public class MessagesJsonModel
+{
+ public class AuthorModel
+ {
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ [JsonProperty("username")]
+ public string Username { get; set; }
+ [JsonProperty("avatar_url")]
+ public Uri AvatarUrl { get; set; }
+ }
+
+ public class MessageModel
+ {
+ [JsonProperty("author")]
+ public AuthorModel Author { get; set; }
+ [JsonProperty("message")]
+ public string Message { get; set; }
+ [JsonProperty("message_id")]
+ public int MessageId { get; set; }
+ [JsonProperty("message_edit_date")]
+ public int MessageEditDate { get; set; }
+ [JsonProperty("message_date")]
+ public int MessageDate { get; set; }
+ [JsonProperty("message_raw")]
+ public string MessageRaw { get; set; }
+ [JsonProperty("room_id")]
+ public int RoomId { get; set; }
+ }
+
+ [JsonProperty("messages")]
+ public List Messages { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs b/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs
new file mode 100644
index 0000000..e787a3a
--- /dev/null
+++ b/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs
@@ -0,0 +1,32 @@
+using Newtonsoft.Json;
+
+namespace KfChatDotNetWsClient.Models.Json;
+
+// {
+// "users": {
+// "1337": {
+// "id": 1337,
+// "username": "Example User",
+// "avatar_url": "https://kiwifarms.net/data/avatars/m/13/1337.jpg?1648885311",
+// "last_activity": 1657316000
+// }
+// }
+// }
+
+public class UsersJsonModel
+{
+ public class UserModel
+ {
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ [JsonProperty("username")]
+ public string Username { get; set; }
+ [JsonProperty("avatar_url")]
+ public Uri AvatarUrl { get; set; }
+ [JsonProperty("last_activity")]
+ public int LastActivity { get; set; }
+ }
+
+ [JsonProperty("users")]
+ public Dictionary Users { get; set; }
+}
\ No newline at end of file
diff --git a/KfChatDotNetWsClient/NLog.config b/KfChatDotNetWsClient/NLog.config
new file mode 100644
index 0000000..ab4e010
--- /dev/null
+++ b/KfChatDotNetWsClient/NLog.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/KfChatDotNetWsClient/NLog.xsd b/KfChatDotNetWsClient/NLog.xsd
new file mode 100644
index 0000000..e2b7858
--- /dev/null
+++ b/KfChatDotNetWsClient/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/KickWsClient/KickWsClient.cs b/KickWsClient/KickWsClient.cs
new file mode 100644
index 0000000..a6c23da
--- /dev/null
+++ b/KickWsClient/KickWsClient.cs
@@ -0,0 +1,267 @@
+using System.Net;
+using System.Net.WebSockets;
+using KickWsClient.Models;
+using Newtonsoft.Json;
+using Websocket.Client;
+using NLog;
+
+
+namespace KickWsClient;
+
+public class KickWsClient
+{
+ public event EventHandlers.OnPusherConnectionEstablishedEventHandler OnPusherConnectionEstablished;
+ public event EventHandlers.OnPusherSubscriptionSucceededEventHandler OnPusherSubscriptionSucceeded;
+ public event EventHandlers.OnPusherPongEventHandler OnPusherPong;
+ public event EventHandlers.OnFollowersUpdatedEventHandler OnFollowersUpdated;
+ public event EventHandlers.OnChatMessageEventHandler OnChatMessage;
+ public event EventHandlers.OnChannelSubscriptionEventHandler OnChannelSubscription;
+ public event EventHandlers.OnSubscriptionEventHandler OnSubscription;
+ public event EventHandlers.OnMessageDeletedEventHandler OnMessageDeleted;
+ public event EventHandlers.OnUserBannedEventHandler OnUserBanned;
+ public event EventHandlers.OnUserUnbannedEventHandler OnUserUnbanned;
+ public event EventHandlers.OnUpdatedLiveStreamEventHandler OnUpdatedLiveStream;
+ public event EventHandlers.OnStopStreamBroadcastEventHandler OnStopStreamBroadcast;
+ public event EventHandlers.OnStreamerIsLiveEventHandler OnStreamerIsLive;
+ public event EventHandlers.OnWsDisconnectionEventHandler OnWsDisconnection;
+ public event EventHandlers.OnWsReconnectEventHandler OnWsReconnect;
+ // You really shouldn't use this unless you're extending the functionality of the library, e.g. adding support for
+ // not yet implemented message types.
+ public event EventHandlers.OnWsMessageReceivedEventHandler OnWsMessageReceived;
+ public event EventHandlers.OnPollUpdateEventHandler OnPollUpdate;
+
+ private WebsocketClient _wsClient;
+ private readonly Logger _logger = LogManager.GetCurrentClassLogger();
+ private Uri _kickPusherUri;
+ private int _reconnectTimeout;
+ private string? _proxy;
+
+ public KickWsClient(
+ string kickPusherUri =
+ "wss://ws-us2.pusher.com/app/eb1d5f283081a78b932c?protocol=7&client=js&version=7.6.0&flash=false",
+ string? proxy = null, int reconnectTimeout = 30)
+ {
+ _kickPusherUri = new Uri(kickPusherUri);
+ _proxy = proxy;
+ _reconnectTimeout = reconnectTimeout;
+ }
+
+ public async Task StartWsClient()
+ {
+ _logger.Debug("StartWsClient() called, creating client");
+ _wsClient = await CreateWsClient();
+ }
+
+ public void Disconnect()
+ {
+ _logger.Debug("Disconnect() called, closing Websocket");
+ _wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket").Wait();
+ }
+
+ 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(_kickPusherUri, factory)
+ {
+ ReconnectTimeout = TimeSpan.FromSeconds(_reconnectTimeout)
+ };
+
+ 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!");
+ return client;
+ }
+
+ 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);
+ }
+
+ private void WsReconnection(ReconnectionInfo reconnectionInfo)
+ {
+ _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}");
+ if (reconnectionInfo.Type == ReconnectionType.Initial)
+ {
+ _logger.Error("Not firing the reconnection event as this is the initial event");
+ return;
+ }
+ OnWsReconnect?.Invoke(this, reconnectionInfo);
+ }
+
+ ///
+ /// Send a generic Pusher packet
+ ///
+ /// Event name
+ /// Event data
+ public void SendPusherPacket(string eventName, object data)
+ {
+ var pkt = new PusherModels.BasePusherRequestModel { Event = eventName, Data = data};
+ var json = JsonConvert.SerializeObject(pkt);
+ _logger.Debug("Sending message to Pusher");
+ _logger.Debug(json);
+ _wsClient.Send(json);
+ }
+
+ ///
+ /// Send a ping packet. You should expect a pong response immediately after
+ ///
+ public void SendPusherPing()
+ {
+ SendPusherPacket("pusher:ping", new object());
+ }
+
+ ///
+ /// Send a pusher subscribe packet to subscribe to a channel or chatroom. You should receive a subscription succeeded packet
+ ///
+ /// Channel string e.g. channel.2515504
+ /// Optional authentication string. Empty string means guest
+ public void SendPusherSubscribe(string channel, string auth = "")
+ {
+ var subPacket = new PusherModels.PusherSubscribeRequestModel { Auth = auth, Channel = channel };
+ SendPusherPacket("pusher:subscribe", subPacket);
+ }
+
+ ///
+ /// Send pusher unsubscribe packet to unsub from a channel or chatroom. Expect no response
+ ///
+ /// Channel string e.g. channel.2515504
+ public void SendPusherUnsubscribe(string channel)
+ {
+ var unsubPacket = new PusherModels.PusherUnsubscribeRequestModel { Channel = channel };
+ SendPusherPacket("pusher:unsubscribe", unsubPacket);
+ }
+
+ private void WsMessageReceived(ResponseMessage message)
+ {
+ OnWsMessageReceived?.Invoke(this, message);
+
+ if (message.Text == null)
+ {
+ _logger.Info("Websocket message was null, ignoring packet");
+ return;
+ }
+
+ PusherModels.BasePusherEventModel pusherMsg;
+ try
+ {
+ pusherMsg = JsonConvert.DeserializeObject(message.Text) ??
+ throw new InvalidOperationException();
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Failed to parse Pusher message. Exception follows:");
+ _logger.Error(e);
+ _logger.Error("--- Message from Pusher follows ---");
+ _logger.Error(message.Text);
+ _logger.Error("--- /end of message ---");
+ return;
+ }
+
+ _logger.Debug($"Pusher event receievd: {pusherMsg.Event}");
+
+ switch (pusherMsg.Event)
+ {
+ case "pusher:connection_established":
+ {
+ var data =
+ JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnPusherConnectionEstablished?.Invoke(this, data);
+ return;
+ }
+ case "pusher_internal:subscription_succeeded":
+ OnPusherSubscriptionSucceeded?.Invoke(this, pusherMsg);
+ return;
+ case "pusher:pong":
+ OnPusherPong?.Invoke(this, pusherMsg);
+ return;
+ case @"App\Events\FollowersUpdated":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnFollowersUpdated?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\ChatMessageEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnChatMessage?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\ChannelSubscriptionEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnChannelSubscription?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\SubscriptionEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnSubscription?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\MessageDeletedEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnMessageDeleted?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\UserBannedEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnUserBanned?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\UserUnbannedEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnUserUnbanned?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\LiveStream\UpdatedLiveStreamEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnUpdatedLiveStream?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\StopStreamBroadcast":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnStopStreamBroadcast?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\StreamerIsLive":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnStreamerIsLive?.Invoke(this, data);
+ return;
+ }
+ case @"App\Events\PollUpdateEvent":
+ {
+ var data = JsonConvert.DeserializeObject(pusherMsg.Data);
+ OnPollUpdate?.Invoke(this, data);
+ return;
+ }
+ default:
+ _logger.Info("Event unhandled. JOSN payload follows");
+ _logger.Info(message.Text);
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/KickWsClient/KickWsClient.csproj b/KickWsClient/KickWsClient.csproj
new file mode 100644
index 0000000..c5c5b08
--- /dev/null
+++ b/KickWsClient/KickWsClient.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/KickWsClient/Models/EventHandlers.cs b/KickWsClient/Models/EventHandlers.cs
new file mode 100644
index 0000000..d53c203
--- /dev/null
+++ b/KickWsClient/Models/EventHandlers.cs
@@ -0,0 +1,41 @@
+using Websocket.Client;
+
+namespace KickWsClient.Models;
+
+public class EventHandlers
+{
+ public delegate void OnPusherConnectionEstablishedEventHandler(object sender,
+ PusherModels.PusherConnectionEstablishedEventModel? e);
+
+ public delegate void OnPusherSubscriptionSucceededEventHandler(object sender, PusherModels.BasePusherEventModel e);
+
+ public delegate void OnPusherPongEventHandler(object sender, PusherModels.BasePusherEventModel e);
+
+ public delegate void OnFollowersUpdatedEventHandler(object sender, KickModels.FollowersUpdatedEventModel? e);
+
+ public delegate void OnChatMessageEventHandler(object sender, KickModels.ChatMessageEventModel? e);
+
+ public delegate void OnChannelSubscriptionEventHandler(object sender, KickModels.ChannelSubscriptionEventModel? e);
+
+ public delegate void OnSubscriptionEventHandler(object sender, KickModels.SubscriptionEventModel? e);
+
+ public delegate void OnMessageDeletedEventHandler(object sender, KickModels.MessageDeletedEventModel? e);
+
+ public delegate void OnUserBannedEventHandler(object sender, KickModels.UserBannedEventModel? e);
+
+ public delegate void OnUserUnbannedEventHandler(object sender, KickModels.UserUnbannedEventModel? e);
+
+ public delegate void OnUpdatedLiveStreamEventHandler(object sender, KickModels.UpdatedLiveStreamEventModel? e);
+
+ public delegate void OnStopStreamBroadcastEventHandler(object sender, KickModels.StopStreamBroadcastEventModel? e);
+
+ public delegate void OnStreamerIsLiveEventHandler(object sender, KickModels.StreamerIsLiveEventModel? e);
+
+ public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e);
+
+ public delegate void OnWsReconnectEventHandler(object sender, ReconnectionInfo e);
+
+ public delegate void OnWsMessageReceivedEventHandler(object sender, ResponseMessage e);
+
+ public delegate void OnPollUpdateEventHandler(object sender, KickModels.PollUpdateEventModel? e);
+}
\ No newline at end of file
diff --git a/KickWsClient/Models/KickModels.cs b/KickWsClient/Models/KickModels.cs
new file mode 100644
index 0000000..a225724
--- /dev/null
+++ b/KickWsClient/Models/KickModels.cs
@@ -0,0 +1,519 @@
+using Newtonsoft.Json;
+
+namespace KickWsClient.Models;
+
+public class KickModels
+{
+ public class ChatMessageSenderIdentityBadgeModel
+ {
+ ///
+ /// Internal type for badge e.g. moderator
+ ///
+ [JsonProperty("type")]
+ public required string Type { get; set; }
+ ///
+ /// Friendly name for badge e.g. Moderator
+ ///
+ [JsonProperty("text")]
+ public required string Text { get; set; }
+ ///
+ /// Count (if applicable) for badge (e.g. sub count for gifted subs)
+ ///
+ [JsonProperty("count")]
+ public int? Count { get; set; }
+ }
+
+ public class ChatMessageSenderIdentityModel
+ {
+ ///
+ /// User's hex color
+ ///
+ [JsonProperty("color")]
+ public required string Color { get; set; }
+
+ ///
+ /// Badges a user has
+ ///
+ [JsonProperty("badges")]
+ public List Badges = [];
+ }
+
+ public class ChatMessageSenderModel
+ {
+ ///
+ /// Kick internal user ID
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Kick display name
+ ///
+ [JsonProperty("username")]
+ public required string Username { get; set; }
+ ///
+ /// Kick slug (for URLs)
+ ///
+ [JsonProperty("slug")]
+ public required string Slug { get; set; }
+ ///
+ /// Identity info for display color and badges
+ ///
+ [JsonProperty("identity")]
+ public required ChatMessageSenderIdentityModel Identity { get; set; }
+ }
+
+ public class ChatMessageMetadataOriginalSenderModel
+ {
+ ///
+ /// Original sender's user ID
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Original sender's username
+ ///
+ [JsonProperty("username")]
+ public required string Username { get; set; }
+ }
+
+ public class ChatMessageMetadataOriginalMessageModel
+ {
+ ///
+ /// ID (GUID) of the original message
+ ///
+ [JsonProperty("id")]
+ public required string Id { get; set; }
+ ///
+ /// Content of the original message
+ ///
+ [JsonProperty("content")]
+ public required string Content { get; set; }
+ }
+
+ public class ChatMessageMetadataModel
+ {
+ ///
+ /// Sender of the message that this message is in reply to
+ ///
+ [JsonProperty("original_sender")]
+ public required ChatMessageMetadataOriginalSenderModel OriginalSender { get; set; }
+ ///
+ /// Content of the message that this message is in reply to
+ ///
+ [JsonProperty("original_message")]
+ public required ChatMessageMetadataOriginalMessageModel OriginalMessage { get; set; }
+ }
+
+ public class FollowersUpdatedEventModel
+ {
+ ///
+ /// Channel follower count
+ ///
+ [JsonProperty("followersCount")]
+ public int FollowersCount { get; set; }
+ ///
+ /// ID to identify what chatroom this event belongs to
+ ///
+ [JsonProperty("chatroom_id")]
+ public int ChatroomId { get; set; }
+ ///
+ /// Maybe returns your username if you're auth'd? No idea. Just returned null for me
+ ///
+ [JsonProperty("username")]
+ public string? Username { get; set; }
+ ///
+ /// Epoch value that signifies ???
+ ///
+ [JsonProperty("created_at")]
+ public int? CreatedAtEpoch { get; set; }
+ // It returned true even though I'm not signed in which makes no sense, so I'll assume there's a chance it'll
+ // suddenly appear and mark as nullable as it's not really a useful property anyway.
+ ///
+ /// Does it mean we're following? Who knows, returns true even if you're a guest
+ ///
+ [JsonProperty("followed")]
+ public bool? Followed { get; set; }
+ }
+
+ public class ChatMessageEventModel
+ {
+ ///
+ /// Message unique GUID that's referenced for replies and deletions
+ ///
+ [JsonProperty("id")]
+ public required string Id { get; set; }
+ ///
+ /// Chatroom ID you can use to differentiate this from other rooms if you sub to multiple at a time
+ ///
+ [JsonProperty("chatroom_id")]
+ public int ChatroomId { get; set; }
+ ///
+ /// Content of the message. Emotes are encoded like [emote:161238:russW] which translates to -> https://files.kick.com/emotes/161238/fullsize
+ ///
+ [JsonProperty("content")]
+ public required string Content { get; set; }
+ ///
+ /// Regular message is 'message', replies are 'reply'
+ ///
+ [JsonProperty("type")]
+ public required string Type { get; set; }
+ // Why created at is an epoch for followers updated but ISO8601 for chat messages is just a mystery
+ ///
+ /// Time message was sent
+ ///
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+ ///
+ /// Sender of the message
+ ///
+ [JsonProperty("sender")]
+ public required ChatMessageSenderModel Sender { get; set; }
+ ///
+ /// Message metadata which is set for replies only
+ ///
+ [JsonProperty("metadata")]
+ public ChatMessageMetadataModel? Metadata { get; set; }
+ }
+
+ public class ChannelSubscriptionEventModel
+ {
+ ///
+ /// User IDs of subscription recipients
+ ///
+ [JsonProperty("user_ids")]
+ public List UserIds { get; set; } = [];
+ ///
+ /// Username of the person who subbed / gifted
+ ///
+ [JsonProperty("username")]
+ public required string Username { get; set; }
+ ///
+ /// Channel ID where the sub event occurred
+ ///
+ [JsonProperty("channel_id")]
+ public int ChannelId { get; set; }
+ }
+
+ public class SubscriptionEventModel
+ {
+ ///
+ /// ID of channel where the subscription event occurred
+ ///
+ [JsonProperty("chatroom_id")]
+ public int ChatroomId { get; set; }
+ ///
+ /// Username of the person who bought a sub
+ ///
+ [JsonProperty("username")]
+ public required string Username { get; set; }
+ ///
+ /// Number of months they've subbed now (e.g. 2 if they bought their 2nd month)
+ ///
+ [JsonProperty("months")]
+ public int Months { get; set; }
+ }
+
+ public class MessageDeletedMessageModel
+ {
+ ///
+ /// ID of the message that was deleted
+ ///
+ [JsonProperty("id")]
+ public required string Id { get; set; }
+ }
+
+ public class MessageDeletedEventModel
+ {
+ ///
+ /// ID of this event (NOT the message to be removed!)
+ ///
+ [JsonProperty("id")]
+ public required string Id { get; set; }
+ ///
+ /// Message that was deleted
+ ///
+ [JsonProperty("message")]
+ public required MessageDeletedMessageModel Message { get; set; }
+ }
+
+ public class UserBannedUserModel
+ {
+ ///
+ /// ID of the user. Note it'll be 0 for the janny
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// User's username
+ ///
+ [JsonProperty("username")]
+ public required string Username { get; set; }
+ ///
+ /// Slug suitable for URLs
+ ///
+ [JsonProperty("slug")]
+ public required string Slug { get; set; }
+ }
+
+ public class UserBannedEventModel
+ {
+ ///
+ /// GUID of the event
+ ///
+ [JsonProperty("id")]
+ public required string Id { get; set; }
+ ///
+ /// User who was banished
+ ///
+ [JsonProperty("user")]
+ public required UserBannedUserModel User { get; set; }
+ ///
+ /// Janny who did the sweeping
+ ///
+ [JsonProperty("banned_by")]
+ public required UserBannedUserModel BannedBy { get; set; }
+ ///
+ /// Datetime that the ban expires. Null for permabans
+ ///
+ [JsonProperty("expires_at")]
+ public DateTimeOffset? ExpiresAt { get; set; }
+ }
+
+ public class UserUnbannedEventModel
+ {
+ ///
+ /// GUID of the event
+ ///
+ [JsonProperty("id")]
+ public required string Id { get; set; }
+ ///
+ /// User who was unbanned
+ ///
+ [JsonProperty("user")]
+ public required UserBannedUserModel User { get; set; }
+ ///
+ /// Janny who unbanned
+ ///
+ [JsonProperty("unbanned_by")]
+ public required UserBannedUserModel UnbannedBy { get; set; }
+ }
+
+ public class UpdatedLiveStreamCategoryParentModel
+ {
+ ///
+ /// ID representing the category
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Slug representing the category
+ ///
+ [JsonProperty("slug")]
+ public required string Slug { get; set; }
+ }
+
+ public class UpdatedLiveStreamCategoryModel
+ {
+ ///
+ /// ID of the category
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Friendly name of the category
+ ///
+ [JsonProperty("name")]
+ public required string Name { get; set; }
+ ///
+ /// Category's slug for forming URls etc.
+ ///
+ [JsonProperty("slug")]
+ public required string Slug { get; set; }
+ ///
+ /// Tags for the category
+ ///
+ [JsonProperty("tags")]
+ public List Tags { get; set; } = [];
+ ///
+ /// Parent category, if one is present. I think there usually is one, but made it nullable just in case
+ ///
+ [JsonProperty("parent_category")]
+ public UpdatedLiveStreamCategoryParentModel? ParentCategory { get; set; }
+ }
+
+ public class UpdatedLiveStreamEventModel
+ {
+ ///
+ /// ID of the livestream (numeric)
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Livestream slug
+ ///
+ [JsonProperty("slug")]
+ public required string Slug { get; set; }
+ ///
+ /// Livestream title
+ ///
+ [JsonProperty("session_title")]
+ public required string SessionTitle { get; set; }
+ ///
+ /// Livestream start time
+ ///
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+ ///
+ /// Language of the livestream (e.g. English)
+ ///
+ [JsonProperty("language")]
+ public string? Language { get; set; }
+ ///
+ /// Whether the stream is marked as for a mature audience
+ ///
+ [JsonProperty("is_mature")]
+ public bool IsMature { get; set; }
+ ///
+ /// Number of viewers presently watching
+ ///
+ [JsonProperty("viewers")]
+ public int Viewers { get; set; }
+ ///
+ /// Category of the livestream. I believe this is always required but marked it as nullable just in case
+ ///
+ [JsonProperty("category")]
+ public UpdatedLiveStreamCategoryModel? Category { get; set; }
+ }
+
+ public class StopStreamBroadcastLiveStreamChannelModel
+ {
+ ///
+ /// ID of the channel
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Whether the streamer was sent to ban world
+ ///
+ [JsonProperty("is_banned")]
+ public bool IsBanned { get; set; }
+ }
+
+ public class StopStreamBroadcastLiveStreamModel
+ {
+ ///
+ /// Livestream event ID
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Channel that stopped streaming
+ ///
+ [JsonProperty("channel")]
+ public required StopStreamBroadcastLiveStreamChannelModel Channel { get; set; }
+ }
+
+ public class StopStreamBroadcastEventModel
+ {
+ ///
+ /// Object containing information related to the livestream that stopped
+ ///
+ [JsonProperty("livestream")]
+ public required StopStreamBroadcastLiveStreamModel Livestream { get; set; }
+ }
+
+ public class StreamerIsLiveLiveStreamModel
+ {
+ ///
+ /// ID of the livestream
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// ID of the channel
+ ///
+ [JsonProperty("channel_id")]
+ public int ChannelId { get; set; }
+ ///
+ /// Title of the stream
+ ///
+ [JsonProperty("session_title")]
+ public required string SessionTitle { get; set; }
+ ///
+ /// No idea, just null on my end
+ ///
+ [JsonProperty("source")]
+ public string? Source { get; set; }
+ ///
+ /// Time stream started
+ ///
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+ }
+
+ public class StreamerIsLiveEventModel
+ {
+ ///
+ /// Object containing information related to the livestream that has just started
+ ///
+ [JsonProperty("livestream")]
+ public required StreamerIsLiveLiveStreamModel Livestream { get; set; }
+ }
+
+ public class PollUpdatePollOptionModel
+ {
+ ///
+ /// ID of the poll option
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// Label of the poll option
+ ///
+ [JsonProperty("label")]
+ public required string Label { get; set; }
+ ///
+ /// Number of votes the poll option has gotten
+ ///
+ [JsonProperty("votes")]
+ public int Votes { get; set; }
+ }
+
+ public class PollUpdatePollModel
+ {
+ ///
+ /// Title of the poll
+ ///
+ [JsonProperty("title")]
+ public required string Title { get; set; }
+ ///
+ /// Poll options
+ ///
+ [JsonProperty("options")]
+ public List Options { get; set; } = [];
+ ///
+ /// Duration of the poll in seconds
+ ///
+ [JsonProperty("duration")]
+ public int Duration { get; set; }
+ ///
+ /// Remaining time in seconds
+ ///
+ [JsonProperty("remaining")]
+ public int Remaining { get; set; }
+ ///
+ /// Time in seconds to display the results after completion?
+ ///
+ [JsonProperty("result_display_duration")]
+ public int ResultDisplayDuration { get; set; }
+ }
+
+ public class PollUpdateEventModel
+ {
+ ///
+ /// Poll data
+ ///
+ [JsonProperty("poll")]
+ public required PollUpdatePollModel Poll { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/KickWsClient/Models/PusherModels.cs b/KickWsClient/Models/PusherModels.cs
new file mode 100644
index 0000000..aaa0d03
--- /dev/null
+++ b/KickWsClient/Models/PusherModels.cs
@@ -0,0 +1,76 @@
+using Newtonsoft.Json;
+
+namespace KickWsClient.Models;
+
+public class PusherModels
+{
+ public class BasePusherEventModel
+ {
+ ///
+ /// Name of the event
+ ///
+ [JsonProperty("event")]
+ public required string Event { get; set; }
+ ///
+ /// Stringified JSON payload
+ ///
+ [JsonProperty("data")]
+ public required string Data { get; set; }
+ ///
+ /// Channel where event originates. Only included events where a channel is applicable
+ ///
+ [JsonProperty("channel")]
+ public string? Channel { get; set; }
+ }
+
+ public class BasePusherRequestModel
+ {
+ ///
+ /// Name of the event
+ ///
+ [JsonProperty("event")]
+ public required string Event { get; set; }
+ ///
+ /// Data as object. It's only stringified for responses
+ ///
+ [JsonProperty("data")]
+ public required object Data { get; set; }
+ }
+
+ public class PusherConnectionEstablishedEventModel
+ {
+ ///
+ /// Internal socket ID
+ ///
+ [JsonProperty("socket_id")]
+ public required string SocketId { get; set; }
+ ///
+ /// Timeout on no activity in seconds
+ ///
+ [JsonProperty("activity_timeout")]
+ public int ActivityTimeout { get; set; }
+ }
+
+ public class PusherSubscribeRequestModel
+ {
+ ///
+ /// Token to authenticate with, use an empty string for guest.
+ ///
+ [JsonProperty("auth")]
+ public string Auth { get; set; } = "";
+ ///
+ /// Channel you wish to subscribe to. 'channel.2515504' for stream events. 'chatrooms.2515504.v2' for chat where 2515504 is the channel ID
+ ///
+ [JsonProperty("channel")]
+ public required string Channel { get; set; }
+ }
+
+ public class PusherUnsubscribeRequestModel
+ {
+ ///
+ /// Channel you wish to unsubscribe from, e.g. 'channel.2515504'
+ ///
+ [JsonProperty("channel")]
+ public required string Channel { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..dad2db5
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "8.0.0",
+ "rollForward": "latestMajor",
+ "allowPrerelease": true
+ }
+}
\ No newline at end of file