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