diff --git a/.editorconfig b/.editorconfig index 5095fc7e..5f88eaed 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,5 @@ +# EditorConfig is awesome: https://EditorConfig.org + root = true [*] @@ -7,6 +9,8 @@ charset = utf-8 [*.cs] indent_style = tab +indent_size = tab +tab_width = 4 trim_trailing_whitespace = true [*.{tt,ttinclude}] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..de3fcc60 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**To Reproduce** +Steps to reproduce the behavior (if possible) + +**Version** +Which commit did you build or download? (mono TS3AudiBot.exe -V or !version in chat) + +**Platform** +Which platform(-version) are you running on? (ubuntu 16.04, arch, windows,...) +Which runtime(-version) are you using? (mono: mono -V, dotnet: dotnet --info) + +**Log** +``` +Paste the important log parts from the ts3audiobot.log into this code block here. +Try not to paste too little. +At best from the first to last interaction from you which reproduces this problem. +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..e36eff9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/ISSUE_TEMPLATE/setup_help.md b/.github/ISSUE_TEMPLATE/setup_help.md new file mode 100644 index 00000000..8265e0d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/setup_help.md @@ -0,0 +1,28 @@ +--- +name: Setup help +about: Problems setting up or running the bot + +--- + +**Describe the problem** +A clear and concise description of what the problem is. + +**To Reproduce** +Steps to reproduce the behavior + +**System Information** +- **Platform**: (Ubuntu 16.04, Debian 8, Arch, etc) + +- **Mono version**: (`mono -V`) + +- **Which commit did you download**: (or on prebuilt: `mono TS3AudioBot.exe -V`) + +(If all fields in the TS3AudioBot log header are correctly filled you can +alternatively just post the header here.) + +**Additional Logs, Exceptions, etc** +When applicable try to add relevant log excerpts here. + +``` +Put logs or code in triple backticks like here to properly format it. +``` diff --git a/.gitignore b/.gitignore index 1d84c0d7..54f58294 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,7 @@ ipch/ *.psess *.vsp *.vspx +*.diagsession # TFS 2012 Local Workspace $tf/ diff --git a/.travis.yml b/.travis.yml index a29f2ecf..41142e20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,13 @@ sudo: false language: csharp +matrix: + include: + - dotnet: 2.1.4 + mono: none + env: DOTNETCORE=1 + - mono: latest + notifications: email: false @@ -20,30 +27,48 @@ addons: git: depth: 9999999 +# TODO: add test runner for dotnet core too install: - - nuget restore TS3AudioBot.sln - - nuget install NUnit.Runners -OutputDirectory nunit + - | + if [[ $DOTNETCORE = 1 ]]; then + echo "dotnet core" + dotnet restore TS3AudioBot.sln + else + echo "mono" + nuget restore TS3AudioBot.sln + nuget install NUnit.Runners -OutputDirectory nunit + fi script: - - cfg="/p:Configuration=Release TS3AudioBot.sln" - - if command -v msbuild; then - msbuild $cfg; - elif command -v xbuild; then - xbuild $cfg; + - | + if [[ $DOTNETCORE = 1 ]]; then + dotnet build --framework netcoreapp2.0 --configuration Release TS3AudioBot else - echo "No mono build tool found!"; - false; + if command -v msbuild; then + buildtool="msbuild" + elif command -v xbuild; then + buildtool="xbuild" + else + echo "No mono build tool found!" + false + fi + "${buildtool}" /p:Configuration=Release /p:TargetFramework=net46 TS3AudioBot.sln + mono ./nunit/NUnit.ConsoleRunner.*.*.*/tools/nunit3-console.exe ./TS3ABotUnitTests/bin/Release/net46/TS3ABotUnitTests.dll fi - - mono ./nunit/NUnit.ConsoleRunner.*.*.*/tools/nunit3-console.exe ./TS3ABotUnitTests/bin/Release/TS3ABotUnitTests.dll after_success: - - export MAIN_DIR=`pwd` - - cd ./TS3AudioBot/bin/Release - - ls - - zip TS3AudioBot.zip NLog.config *.exe *.dll x64/* x86/* - - 'export version=`mono TS3AudioBot.exe --version | grep "Version: "`' - - "curl -I -H \"Content-Type: application/zip\" -X PUT \"https://splamy.de/api/nightly/ts3ab/${TRAVIS_BRANCH}?token=${uploadkey}&filename=TS3AudioBot.zip&commit=${TRAVIS_COMMIT}&version=${version:9}\" --upload-file ./TS3AudioBot.zip" - - cd "$MAIN_DIR" + - | + if [[ $DOTNETCORE = 1 ]]; then + echo "No Task!" + else + export MAIN_DIR=`pwd` + cd ./TS3AudioBot/bin/Release/net46 + ls + zip TS3AudioBot.zip NLog.config *.exe *.dll x64/* x86/* + export version=`mono TS3AudioBot.exe --version | grep "Version: "` + curl -I -H "Content-Type: application/zip" -X PUT "https://splamy.de/api/nightly/ts3ab/${TRAVIS_BRANCH}?token=${uploadkey}&filename=TS3AudioBot.zip&commit=${TRAVIS_COMMIT}&version=${version:9}" --upload-file ./TS3AudioBot.zip + cd "$MAIN_DIR" + fi after_script: - chmod u+x ts3notify.sh diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 65a5e5e7..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug", - "type": "mono", - "request": "launch", - "program": "TS3AudioBot/bin/Debug/TS3AudioBot.exe", - "args": [], - "cwd": ".", - "runtimeExecutable": null, - "env": {} - }, - { - "name": "Attach", - "type": "mono", - "request": "attach", - "address": "localhost", - "port": 5858 - } - ] -} \ No newline at end of file diff --git a/InstallOpus.sh b/InstallOpus.sh index c700a3bc..4c888213 100644 --- a/InstallOpus.sh +++ b/InstallOpus.sh @@ -2,7 +2,7 @@ baseDir=`pwd` -OpusBaseName="opus-1.1.3" +OpusBaseName="opus-1.2.1" OpusFileName="$OpusBaseName.tar.gz" # Download the Opus library @@ -21,15 +21,13 @@ tar -vxf "$OpusFileName" cd "$OpusBaseName" # Build the library -./configure && make +./configure && make && sudo make install -# Go back -cd "$baseDir" - -# Copy the required libopus.so to the local folder -cp "$OpusBaseName/.libs/libopus.so" "./" - -# Copy the libopus.so to the folder we need -cp "./libopus.so" "$baseDir/TS3AudioBot/bin/Release/libopus.so" +# Move to global folder +if [ ! -f /usr/lib/libopus.so ]; then + sudo cp ".libs/libopus.so" "/usr/lib/" +else + echo "'/urs/lib/libopus.so' already exists, will not be overwritten" +fi -echo "Done" \ No newline at end of file +echo "Done" diff --git a/README.md b/README.md index 430e431e..b5957a51 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,110 @@ # TS3AudioBot -|master|develop| -|:--:|:--:| -|[![Build Status](https://travis-ci.org/Splamy/TS3AudioBot.svg?branch=master)](https://travis-ci.org/Splamy/TS3AudioBot)|[![Build Status](https://travis-ci.org/Splamy/TS3AudioBot.svg?branch=develop)](https://travis-ci.org/Splamy/TS3AudioBot)| -Nightly builds are available here: http://splamy.de/Nightly#ts3ab +|master|develop|Questions/Discussions|License| +|:--:|:--:|:--:|:--:| +|[![Build Status](https://travis-ci.org/Splamy/TS3AudioBot.svg?branch=master)](https://travis-ci.org/Splamy/TS3AudioBot)|[![Build Status](https://travis-ci.org/Splamy/TS3AudioBot.svg?branch=develop)](https://travis-ci.org/Splamy/TS3AudioBot)|[![Join Gitter Chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/TS3AudioBot/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link)|[![License: OSL-3.0](https://img.shields.io/badge/License-OSL%203.0-blue.svg)](https://opensource.org/licenses/OSL-3.0)| ## About -This is our open-source TeamSpeak 3 audiobot project since +This is our open-source TeamSpeak 3 audio bot project since we haven't found any other open-source one so far. The bot has come a long way is pretty stable by now, though sometimes he hangs up or needs some other maintenance. -For now I'd only recommend this bot on small servers since it doesn't cover any more complex right systems and relies on discipline. - -## How our Bot works -The TS3AudioBot connects with at least 1 TeamSpeak3 Client instance which allows you to: - * issue commands to that instance. - * play music for your channel. - * tell him to stream to different Channels and/or Users simultaneously with TeamSpeak's whisper feature. - -We use a self written TeamSpeak3 Client which gives us very low memory and cpu usage. -About _65MB_ Ram with 1700+ songs in history indexed -And _4-6% CPU_ usage on a single shared vCore from a _Intel Xeon E5-1650 v2 @ 3.50GHz_ - -## Features & Plannings -Done: -* Extract Youtube and Soundcloud songs as well as stream Twitch -* Extensive history manager, including features like: - - getting the last x played songs - - get last x songs played by a certain user - - start any once played song again via id - - search in title from played songs - - (planned) combined search expressions -* (un)subscribe to the Bob to hear music in any channel -* (un)subscribe the Bob to certain channels + +## Features +* Play Youtube and Soundcloud songs as well as stream Twitch (extensible with plugins) +* Song history +* Various voice subscription modes; including to clients, channels and whisper groups * Playlist management for all users -* Advanced permission configuration -* Extensive plugin support +* Powerful permission configuration +* Plugin support * Web API - -In progress: -* Own web-interface -* (Improved) Rights system * Multi-instance +* Localization +* Low CPU and memory with our self-written headless ts3 client -In planning: -*See issues* +To see what's planned and in progress take a look into our [Roadmap](https://github.com/Splamy/TS3AudioBot/projects/2). ## Bot Commands -All in all, the bot is fully operable only via chat (and actually only via chat). -Commands are invoked with !command. -Some commands have restrictions, like they can only be used in a private chat, only in public chat, or need admin rights. +The bot is fully operable via chat. +Commands can be invoked with `!command`. -For the full command list and tutorials see [here in the wiki](https://github.com/Splamy/TS3AudioBot/wiki/CommandSystem) +For the full command list and tutorials see [here in the wiki](https://github.com/Splamy/TS3AudioBot/wiki/CommandSystem). -If the bot can't play some youtube videos it might be due to some embedding restrictions, which are blocking this. -You can add a [youtube-dl](https://github.com/rg3/youtube-dl/) binary or source folder and specify the path in the config to try to bypass this. +## Download +You can download the latest builds precompiled from our [nightly server](https://splamy.de/Nightly#ts3ab): +- [![Download](https://img.shields.io/badge/Download-master-green.svg)](https://splamy.de/api/nightly/ts3ab/master/download) + versions are mostly considered stable but won't get bigger features as fast. +- [![Download](https://img.shields.io/badge/Download-develop-green.svg)](https://splamy.de/api/nightly/ts3ab/develop/download) + will always have the latest and greatest but might not be fully stable or have broken features. + +Continue with downloading the dependencies. + +### Dependencies +You will need to download a few things for the bot to run: -## How to set up the bot +#### Linux +1. Mono: Get the latest version by following [this tutorial](https://www.mono-project.com/download/stable/#download-lin) and install `mono-complete` or `mono-devel` +1. Other dependencies: +* on **Ubuntu**: +Run `sudo apt-get install libopus-dev ffmpeg` +* on **Arch Linux**: +Run `sudo pacman -S opus ffmpeg` +* **manually**: + 1. Make sure you have a C compiler installed + 1. Make the Opus script runnable with `chmod u+x InstallOpus.sh` and run it with `./InstallOpus.sh` + 1. Get the ffmpeg [32bit](https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-32bit-static.tar.xz) or [64bit](https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-64bit-static.tar.xz) binary. + 1. Extract the ffmpeg archive with `tar -vxf ffmpeg-git-XXbit-static.tar.xz` + 1. Get the ffmpeg binary from `ffmpeg-git-*DATE*-64bit-static\ffmpeg` and copy it to `TS3AudioBot/bin/Release/` -If you dont want to compile the AudioBot yourself you can always download the -latest version from our nightly server (linked at the top) and jump straight to -"Getting the dependencies" for your platform. +#### Windows +1. Get the ffmpeg [32bit](https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-latest-win32-static.zip) or [64bit](https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-latest-win64-static.zip) binary. +1. Open the archive and copy the ffmpeg binary from `ffmpeg-latest-winXX-static\bin\ffmpeg.exe` to `TS3AudioBot\bin\Release\net46` -### Prerequisites -* Any C# Compiler (`Visual Studio` with `.NET 4.6` or `mono 5.10.0+` with `msbuild`) -* (Linux only) A C Compiler for Opus +### Optional Dependencies +If the bot can't play some youtube videos it might be due to some embedding restrictions which are blocking this. +You can add a [youtube-dl](https://github.com/rg3/youtube-dl/) binary or source folder and specify the path in the config to try to bypass this. + +## Suggested first time setup +1. The first time you'll need to run `mono TS3AudioBot.exe` without parameter and +it will ask you a few questions. +1. Close the bot again and configure your `rights.toml` to your desires. +You can use the template rules and assign your admin as suggested in the automatically generated file, +or dive into the rights syntax [here](https://github.com/Splamy/TS3AudioBot/wiki/Rights). +1. Start the bot again. +1. This step is optional but highly recommended for everything to work properly. + - Create a privilege key for the ServerAdmin group (or a group which has equivalent rights). + - Send the bot in a private message `!bot setup `. +1. Congratz, you're done! Enjoy listening to your favourite music, experimenting with the crazy command system or do whatever you whish to do ;). +For further reading check out the [CommandSystem](https://github.com/Splamy/TS3AudioBot/wiki/CommandSystem). -### Compilation -Before we start: _If you know what you are doing_ you can alternatively compile each dependency referenced here from source/git by yourself, but I won't add a tutorial for that. +## Building manually +### Download Download the git repository with `git clone --recurse-submodules https://github.com/Splamy/TS3AudioBot.git`. #### Linux -1. Get the latest mono version by following [this tutorial](http://www.mono-project.com/download/stable/#download-lin) and install `mono-devel` -1. See if you have NuGet by just executing `nuget`. If not, get `nuget.exe` with `wget https://dist.nuget.org/win-x86-commandline/latest/nuget.exe` +1. Get the latest mono version by following [this tutorial](https://www.mono-project.com/download/stable/#download-lin) and install `mono-devel` +1. See if you have NuGet by just executing `nuget`. + If not, get it with `sudo apt install nuget msbuild` (or the packet manager or your distribution), + or manually with `wget https://dist.nuget.org/win-x86-commandline/latest/nuget.exe` 1. Go into the directory of the repository with `cd TS3AudioBot` 1. Execute `nuget restore` or `mono ../nuget.exe restore` to download all dependencies -1. Execute `msbuild /p:Configuration=Release TS3AudioBot.sln` to build the AudioBot -1. Getting the dependencies - * on **Ubuntu**: - Run `sudo apt-get install libopus-dev ffmpeg` - * on **Arch Linux**: - Run `sudo pacman -S opus ffmpeg` - * **manually**: - 1. Make the Opus script runnable with `chmod u+x InstallOpus.sh` and run it with `./InstallOpus.sh` - 1. Get the ffmpeg [32bit](https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-32bit-static.tar.xz) or [64bit](https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-64bit-static.tar.xz) binary. - 1. Extract the ffmpeg archive with `tar -vxf ffmpeg-git-XXbit-static.tar.xz` - 1. Get the ffmpeg binary from `ffmpeg-git-*DATE*-64bit-static\ffmpeg` and copy it to `TS3AudioBot/bin/Release/` +1. Execute `msbuild /p:Configuration=Release /p:TargetFramework=net46 TS3AudioBot.sln` to build the AudioBot #### Windows -1. Make sure you have installed `.NET Framework 4.6` +1. Make sure you have installed `Visual Studio` and `.NET Framework 4.6` 1. Build the AudioBot with Visual Studio. -1. Getting the dependencies - 1. Get the ffmpeg [32bit](https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-latest-win32-static.zip) or [64bit](https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-latest-win64-static.zip) binary. - 1. Open the archive and copy the ffmpeg binary from `ffmpeg-latest-winXX-static\bin\ffmpeg.exe` to `TS3AudioBot\bin\Release\` - -### Installation -1. Create a group for the AudioBotAdmin with no requirements (just ensure a high enough `i_group_needed_member_add_power`). -1. Create a privilege key for the ServerAdmin group (or a group which has equivalent rights). -1. The first time you'll need to run `mono TS3AudioBot.exe` without parameter and -it will ask you a few questions. -1. Close the bot again and configure your `rights.toml` in `TS3AudioBot\bin\Release\` to your desires. -You can use the template rules and assign your admin as suggested in the automatically generated file, -or dive into the Rights syntax [here](https://github.com/Splamy/TS3AudioBot/wiki/Rights). -1. Start the bot again. -1. Send the bot in a private message `!bot setup ` where `` is the privilege key from a previous step. -1. Now you can move the process to the background or close the bot with `!quit` in teamspeak and run it in the background. -1. (optional) You can configure the logging levels and outputs in the `NLog.config` file, read [here](https://github.com/NLog/NLog/wiki/Configuration-file) to learn more. -1. Congratz, you're done! Enjoy listening to your favourite music, experimenting with the crazy command system or do whatever you whish to do ;). -For further reading check out the [CommandSystem](https://github.com/Splamy/TS3AudioBot/wiki/CommandSystem) -### Testing and Fuzzying +### Testing and Fuzzing 1. Run the *TS3ABotUnitTests* project in Visual Studio or Monodevelop. -# License +## License This project is licensed under OSL-3.0. Why OSL-3.0: - OSL allows you to link to our libraries without needing to disclose your own project, which might be useful if you want to use the TS3Client as a library. - If you create plugins you do not have to make them public like in GPL. (Although we would be happier if you shared them :) - With OSL we want to allow you providing the TS3AB as a service (even commercially). We do not want the software to be sold but the service. We want this software to be free for everyone. +- TL; DR? https://tldrlegal.com/license/open-software-licence-3.0 -# Badges +--- [![forthebadge](http://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/built-by-developers.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/contains-cat-gifs.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/made-with-c-sharp.svg)](http://forthebadge.com) diff --git a/TS3ABotUnitTests/BotCommandTests.cs b/TS3ABotUnitTests/BotCommandTests.cs index 3b993cfd..8ac03853 100644 --- a/TS3ABotUnitTests/BotCommandTests.cs +++ b/TS3ABotUnitTests/BotCommandTests.cs @@ -1,25 +1,26 @@ // TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2016 TS3AudioBot contributors -// +// Copyright (C) 2017 TS3AudioBot contributors +// // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . namespace TS3ABotUnitTests { using NUnit.Framework; - + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Threading; using TS3AudioBot; + using TS3AudioBot.Algorithm; using TS3AudioBot.CommandSystem; + using TS3AudioBot.CommandSystem.CommandResults; + using TS3AudioBot.CommandSystem.Commands; [TestFixture] public class BotCommandTests @@ -29,7 +30,7 @@ public class BotCommandTests public BotCommandTests() { cmdMgr = new CommandManager(); - cmdMgr.RegisterMain(); + cmdMgr.RegisterCollection(MainCommands.Bag); Utils.ExecInfo.AddDynamicObject(cmdMgr); } @@ -88,6 +89,109 @@ public void BotCommandTest() Assert.AreEqual("text", CallCommand("!if a == a text (!)")); Assert.Throws(() => CallCommand("!if a == b text (!)")); } + + [Test] + public void XCommandSystemFilterTest() + { + var filterList = new Dictionary + { + { "help", null }, + { "quit", null }, + { "play", null }, + { "ply", null } + }; + + var filter = Filter.GetFilterByName("ic3").Unwrap(); + + // Exact match + var result = filter.Filter(filterList, "help"); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("help", result.First().Key); + + // The first occurence of y + result = filter.Filter(filterList, "y"); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("ply", result.First().Key); + + // The smallest word + result = filter.Filter(filterList, "zorn"); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("ply", result.First().Key); + + // First letter match + result = filter.Filter(filterList, "q"); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("quit", result.First().Key); + + // Ignore other letters + result = filter.Filter(filterList, "palyndrom"); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("play", result.First().Key); + + filterList.Add("pla", null); + + // Ambiguous command + result = filter.Filter(filterList, "p"); + Assert.AreEqual(2, result.Count()); + Assert.IsTrue(result.Any(r => r.Key == "ply")); + Assert.IsTrue(result.Any(r => r.Key == "pla")); + } + + private static string OptionalFunc(string s = null) => s == null ? "NULL" : "NOT NULL"; + + [Test] + public void XCommandSystemTest() + { + var commandSystem = new XCommandSystem(); + var group = commandSystem.RootCommand; + group.AddCommand("one", new FunctionCommand(() => "ONE")); + group.AddCommand("two", new FunctionCommand(() => "TWO")); + group.AddCommand("echo", new FunctionCommand(s => s)); + group.AddCommand("optional", new FunctionCommand(GetType().GetMethod(nameof(OptionalFunc), BindingFlags.NonPublic | BindingFlags.Static))); + + // Basic tests + Assert.AreEqual("ONE", ((StringCommandResult)commandSystem.Execute(Utils.ExecInfo, + new ICommand[] { new StringCommand("one") })).Content); + Assert.AreEqual("ONE", commandSystem.ExecuteCommand(Utils.ExecInfo, "!one")); + Assert.AreEqual("TWO", commandSystem.ExecuteCommand(Utils.ExecInfo, "!t")); + Assert.AreEqual("TEST", commandSystem.ExecuteCommand(Utils.ExecInfo, "!e TEST")); + Assert.AreEqual("ONE", commandSystem.ExecuteCommand(Utils.ExecInfo, "!o")); + + // Optional parameters + Assert.Throws(() => commandSystem.ExecuteCommand(Utils.ExecInfo, "!e")); + Assert.AreEqual("NULL", commandSystem.ExecuteCommand(Utils.ExecInfo, "!op")); + Assert.AreEqual("NOT NULL", commandSystem.ExecuteCommand(Utils.ExecInfo, "!op 1")); + + // Command chaining + Assert.AreEqual("TEST", commandSystem.ExecuteCommand(Utils.ExecInfo, "!e (!e TEST)")); + Assert.AreEqual("TWO", commandSystem.ExecuteCommand(Utils.ExecInfo, "!e (!t)")); + Assert.AreEqual("NOT NULL", commandSystem.ExecuteCommand(Utils.ExecInfo, "!op (!e TEST)")); + Assert.AreEqual("ONE", commandSystem.ExecuteCommand(Utils.ExecInfo, "!(!e on)")); + + // Command overloading + var intCom = new Func(_ => "INT"); + var strCom = new Func(_ => "STRING"); + group.AddCommand("overlord", new OverloadedFunctionCommand(new[] { + new FunctionCommand(intCom.Method, intCom.Target), + new FunctionCommand(strCom.Method, strCom.Target) + })); + + Assert.AreEqual("INT", commandSystem.ExecuteCommand(Utils.ExecInfo, "!overlord 1")); + Assert.AreEqual("STRING", commandSystem.ExecuteCommand(Utils.ExecInfo, "!overlord a")); + Assert.Throws(() => commandSystem.ExecuteCommand(Utils.ExecInfo, "!overlord")); + } + + [Test] + public void EnsureAllCommandsHaveEnglishDocumentationEntry() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en"); + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en"); + + foreach (var cmd in cmdMgr.AllCommands) + { + Assert.IsFalse(string.IsNullOrEmpty(cmd.Description), $"Command {cmd.FullQualifiedName} has no documentation"); + } + } } static class Utils diff --git a/TS3ABotUnitTests/M3uParserTests.cs b/TS3ABotUnitTests/M3uParserTests.cs new file mode 100644 index 00000000..8d0e6fcf --- /dev/null +++ b/TS3ABotUnitTests/M3uParserTests.cs @@ -0,0 +1,74 @@ +namespace TS3ABotUnitTests +{ + using NUnit.Framework; + using System.IO; + using System.Text; + using TS3AudioBot.ResourceFactories.AudioTags; + + [TestFixture] + class M3uParserTests + { + [Test] + public void SimpleListTest() + { + var result = M3uReader.TryGetData(new MemoryStream(Encoding.UTF8.GetBytes( +@"#EXTINF:197,Delain - Delain - We Are The Others +/opt/music/bad/Delain.mp3 +#EXTINF:314,MONO - MONO - The Hand That Holds the Truth +/opt/music/bad/MONO.mp3 +#EXTINF:223,Deathstars - Deathstars - Opium +/opt/music/bad/Opium.mp3" + ))); + Assert.That(result.Ok); + Assert.AreEqual(3, result.Value.Count); + + Assert.AreEqual("Delain - Delain - We Are The Others", result.Value[0].DisplayString); + Assert.AreEqual("MONO - MONO - The Hand That Holds the Truth", result.Value[1].DisplayString); + Assert.AreEqual("Deathstars - Deathstars - Opium", result.Value[2].DisplayString); + + Assert.AreEqual("/opt/music/bad/Delain.mp3", result.Value[0].Resource.ResourceId); + Assert.AreEqual("/opt/music/bad/MONO.mp3", result.Value[1].Resource.ResourceId); + Assert.AreEqual("/opt/music/bad/Opium.mp3", result.Value[2].Resource.ResourceId); + } + + [Test] + public void ListWithM3uHeaderTest() + { + var result = M3uReader.TryGetData(new MemoryStream(Encoding.UTF8.GetBytes( +@"#EXTM3U +#EXTINF:1337,Never gonna give you up +C:\Windows\System32\firewall32.cpl +#EXTINF:1337,Never gonna let you down +C:\Windows\System32\firewall64.cpl" + ))); + Assert.That(result.Ok); + Assert.AreEqual(2, result.Value.Count); + + Assert.AreEqual("Never gonna give you up", result.Value[0].DisplayString); + Assert.AreEqual("Never gonna let you down", result.Value[1].DisplayString); + + Assert.AreEqual(@"C:\Windows\System32\firewall32.cpl", result.Value[0].Resource.ResourceId); + Assert.AreEqual(@"C:\Windows\System32\firewall64.cpl", result.Value[1].Resource.ResourceId); + } + + + [Test] + public void ListWithoutMetaTagsTest() + { + var result = M3uReader.TryGetData(new MemoryStream(Encoding.UTF8.GetBytes( +@" +C:\PepeHands.jpg +./do/I/look/like/I/know/what/a/Jaypeg/is +" + ))); + Assert.That(result.Ok); + Assert.AreEqual(2, result.Value.Count); + + Assert.AreEqual(@"C:\PepeHands.jpg", result.Value[0].DisplayString); + Assert.AreEqual(@"./do/I/look/like/I/know/what/a/Jaypeg/is", result.Value[1].DisplayString); + + Assert.AreEqual(@"C:\PepeHands.jpg", result.Value[0].Resource.ResourceId); + Assert.AreEqual(@"./do/I/look/like/I/know/what/a/Jaypeg/is", result.Value[1].Resource.ResourceId); + } + } +} diff --git a/TS3ABotUnitTests/Properties/AssemblyInfo.cs b/TS3ABotUnitTests/Properties/AssemblyInfo.cs deleted file mode 100644 index 1e51ea33..00000000 --- a/TS3ABotUnitTests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TS3ABotUnitTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TS3ABotUnitTests")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("55ebc9b7-3a9d-4312-9602-8d6d9808ddf5")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TS3ABotUnitTests/RingQueueTest.cs b/TS3ABotUnitTests/RingQueueTest.cs new file mode 100644 index 00000000..9162e4d4 --- /dev/null +++ b/TS3ABotUnitTests/RingQueueTest.cs @@ -0,0 +1,130 @@ +namespace TS3ABotUnitTests +{ + using NUnit.Framework; + using System; + using TS3Client.Full; + + [TestFixture] + class RingQueueTest + { + + [Test] + public void RingQueueTest1() + { + var q = new RingQueue(3, 5); + + q.Set(0, 42); + + Assert.True(q.TryPeekStart(0, out int ov)); + Assert.AreEqual(ov, 42); + + q.Set(1, 43); + + // already set + Assert.Throws(() => q.Set(1, 99)); + + Assert.True(q.TryPeekStart(0, out ov)); + Assert.AreEqual(ov, 42); + Assert.True(q.TryPeekStart(1, out ov)); + Assert.AreEqual(ov, 43); + + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 42); + + Assert.True(q.TryPeekStart(0, out ov)); + Assert.AreEqual(ov, 43); + Assert.False(q.TryPeekStart(1, out ov)); + + q.Set(3, 45); + q.Set(2, 44); + + // buffer overfull + Assert.Throws(() => q.Set(4, 99)); + + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 43); + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 44); + + q.Set(4, 46); + + // out of mod range + Assert.Throws(() => q.Set(5, 99)); + + q.Set(0, 47); + + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 45); + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 46); + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 47); + + q.Set(2, 49); + + Assert.False(q.TryDequeue(out ov)); + + q.Set(1, 48); + + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 48); + Assert.True(q.TryDequeue(out ov)); + Assert.AreEqual(ov, 49); + } + + [Test] + public void RingQueueTest2() + { + var q = new RingQueue(50, ushort.MaxValue + 1); + + for (int i = 0; i < ushort.MaxValue - 10; i++) + { + q.Set(i, i); + Assert.True(q.TryDequeue(out var iCheck)); + Assert.AreEqual(iCheck, i); + } + + var setStatus = q.IsSet(ushort.MaxValue - 20); + Assert.True(setStatus.HasFlag(ItemSetStatus.Set)); + + for (int i = ushort.MaxValue - 10; i < ushort.MaxValue + 10; i++) + { + q.Set(i % (ushort.MaxValue + 1), 42); + } + } + + [Test] + public void RingQueueTest3() + { + var q = new RingQueue(100, ushort.MaxValue + 1); + + int iSet = 0; + for (int blockSize = 1; blockSize < 100; blockSize++) + { + for (int i = 0; i < blockSize; i++) + { + q.Set(iSet++, i); + } + for (int i = 0; i < blockSize; i++) + { + Assert.True(q.TryDequeue(out var iCheck)); + Assert.AreEqual(i, iCheck); + } + } + + for (int blockSize = 1; blockSize < 100; blockSize++) + { + q = new RingQueue(100, ushort.MaxValue + 1); + for (int i = 0; i < blockSize; i++) + { + q.Set(i, i); + } + for (int i = 0; i < blockSize; i++) + { + Assert.True(q.TryDequeue(out var iCheck)); + Assert.AreEqual(i, iCheck); + } + } + } + } +} diff --git a/TS3ABotUnitTests/TS3ABotUnitTests.csproj b/TS3ABotUnitTests/TS3ABotUnitTests.csproj index 034ff853..3cbc36a4 100644 --- a/TS3ABotUnitTests/TS3ABotUnitTests.csproj +++ b/TS3ABotUnitTests/TS3ABotUnitTests.csproj @@ -1,101 +1,21 @@ - - + + - Debug - AnyCPU - {20B6F767-5396-41D9-83D8-98B5730C6E2E} Library - Properties - TS3ABotUnitTests - TS3ABotUnitTests - v4.6 - 512 - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - 5b0cf4cb - - - - true - bin\Debug\ - DEBUG;TRACE - full - AnyCPU - prompt - - - - bin\Release\ - TRACE - true - pdbonly + net46;netcoreapp2.0 + + 7.2 AnyCPU - prompt - + false + - - ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll - - - - - - - - - - - - - - - - - - - - - - {0ecc38f3-de6e-4d7f-81eb-58b15f584635} - TS3AudioBot - - - {0eb99e9d-87e5-4534-a100-55d231c2b6a6} - TS3Client - + + - + + - - - - - False - - - False - - - False - - - False - - - - - - - - \ No newline at end of file + + diff --git a/TS3ABotUnitTests/TS3MessageParserTests.cs b/TS3ABotUnitTests/TS3MessageParserTests.cs new file mode 100644 index 00000000..f26e2937 --- /dev/null +++ b/TS3ABotUnitTests/TS3MessageParserTests.cs @@ -0,0 +1,136 @@ +namespace TS3ABotUnitTests +{ + using NUnit.Framework; + using System.Collections; + using System.Reflection; + using System.Text; + using TS3Client; + using TS3Client.Messages; + + [TestFixture] + public class TS3MessageParserTests + { + [Test] + public void Deserializer1Test() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("cid=6"), NotificationType.ChannelChanged); + Assert.True(notif.Ok); + var notifv = notif.Value; + Assert.AreEqual(notifv.Length, 1); + var notifs = notifv[0]; + AssertEx.PropertyValuesAreEquals(notifs, new ChannelChanged() { ChannelId = 6 }); + } + + [Test] + public void Deserializer2Test() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("clid=42 cluid=asdfe\\/rvt=="), NotificationType.ClientChatComposing); + Assert.True(notif.Ok); + var notifv = notif.Value; + Assert.AreEqual(notifv.Length, 1); + var notifs = notifv[0]; + AssertEx.PropertyValuesAreEquals(notifs, new ClientChatComposing() { ClientId = 42, ClientUid = "asdfe/rvt==" }); + } + + [Test] + public void Deserializer3Test() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("cid=5 | cid=4"), NotificationType.ChannelChanged); + Assert.True(notif.Ok); + var notifv = notif.Value; + Assert.AreEqual(notifv.Length, 2); + AssertEx.PropertyValuesAreEquals(notifv[0], new ChannelChanged() { ChannelId = 5 }); + AssertEx.PropertyValuesAreEquals(notifv[1], new ChannelChanged() { ChannelId = 4 }); + } + + [Test] + public void Deserializer4Test() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("cluid=asdfe\\/rvt== clid=42 | clid=1337"), NotificationType.ClientChatComposing); + Assert.True(notif.Ok); + var notifv = notif.Value; + Assert.AreEqual(notifv.Length, 2); + AssertEx.PropertyValuesAreEquals(notifv[0], new ClientChatComposing() { ClientId = 42, ClientUid = "asdfe/rvt==" }); + AssertEx.PropertyValuesAreEquals(notifv[1], new ClientChatComposing() { ClientId = 1337, ClientUid = "asdfe/rvt==" }); + } + + [Test] + public void Deserializer5Test() + { + var notif = Deserializer.GenerateResponse(Encoding.UTF8.GetBytes( + "clid=1 cid=1 client_database_id=2 client_nickname=TestBob1 client_type=0 client_unique_identifier=u\\/dFMOFFipxS9fJ8HKv0KH6WVzA=" + + "|clid=2 cid=4 client_database_id=2 client_nickname=TestBob client_type=0 client_unique_identifier=u\\/dFMOFFipxS9fJ8HKv0KH6WVzA=" + + "|clid=3 cid=4 client_database_id=6 client_nickname=Splamy client_type=0 client_unique_identifier=uA0U7t4PBxdJ5TLnarsOHQh4\\/tY=" + + "|clid=4 cid=4 client_database_id=7 client_nickname=AudioBud client_type=0 client_unique_identifier=b+P0CqXms5I0C+A66HZ4Sbu\\/PNw=" + )); + Assert.True(notif.Ok); + var notifv = notif.Value; + Assert.AreEqual(notifv.Length, 4); + AssertEx.PropertyValuesAreEquals(notifv[0], new ClientData() { ClientId = 1, ChannelId = 1, DatabaseId = 2, Name = "TestBob1", ClientType = ClientType.Full, Uid = "u/dFMOFFipxS9fJ8HKv0KH6WVzA=" }); + AssertEx.PropertyValuesAreEquals(notifv[1], new ClientData() { ClientId = 2, ChannelId = 4, DatabaseId = 2, Name = "TestBob", ClientType = ClientType.Full, Uid = "u/dFMOFFipxS9fJ8HKv0KH6WVzA=" }); + AssertEx.PropertyValuesAreEquals(notifv[2], new ClientData() { ClientId = 3, ChannelId = 4, DatabaseId = 6, Name = "Splamy", ClientType = ClientType.Full, Uid = "uA0U7t4PBxdJ5TLnarsOHQh4/tY=" }); + AssertEx.PropertyValuesAreEquals(notifv[3], new ClientData() { ClientId = 4, ChannelId = 4, DatabaseId = 7, Name = "AudioBud", ClientType = ClientType.Full, Uid = "b+P0CqXms5I0C+A66HZ4Sbu/PNw=" }); + } + + [Test] + public void Deserializer6DOnlyTest() + { + var notif = Deserializer.GenerateResponse(Encoding.UTF8.GetBytes("cmd a=1 c=3 b=2|b=4|b=5")); + Assert.True(notif.Ok); + var notifv = notif.Value; + Assert.AreEqual(notifv.Length, 3); + } + + [Test] + public void Deserializer7DOnlyTest() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("clientinitiv alpha=41Te9Ar7hMPx+A== omega=MEwDAgcAAgEgAiEAq2iCMfcijKDZ5tn2tuZcH+\\/GF+dmdxlXjDSFXLPGadACIHzUnbsPQ0FDt34Su4UXF46VFI0+4wjMDNszdoDYocu0 ip"), NotificationType.ClientInitIv); + Assert.True(notif.Ok); + var notifv = notif.Value; + } + + [Test] + public void Deserializer8DOnlyTest() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("initserver virtualserver_name=Server\\sder\\sVerplanten virtualserver_welcomemessage=This\\sis\\sSplamys\\sWorld virtualserver_platform=Linux virtualserver_version=3.0.13.8\\s[Build:\\s1500452811] virtualserver_maxclients=32 virtualserver_created=0 virtualserver_nodec_encryption_mode=1 virtualserver_hostmessage=L\xc3\xa9\\sServer\\sde\\sSplamy virtualserver_name=Server_mode=0 virtualserver_default_server group=8 virtualserver_default_channel_group=8 virtualserver_hostbanner_url virtualserver_hostmessagegfx_url virtualserver_hostmessagegfx_interval=2000 virtualserver_priority_speaker_dimm_modificat"), NotificationType.InitServer); + Assert.True(notif.Ok); + var notifv = notif.Value; + } + + [Test] + public void Deserializer9DOnlyTest() + { + var notif = Deserializer.GenerateNotification(Encoding.UTF8.GetBytes("channellist cid=2 cpid=0 channel_name=Trusted\\sChannel channel_topic channel_codec=0 channel_codec_quality=0 channel_maxclients=0 channel_maxfamilyclients=-1 channel_order=1 channel_flag_permanent=1 channel_flag_semi_permanent=0 channel_flag_default=0 channel_flag_password=0 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 channel_delete_delay=0 channel_flag_maxclients_unlimited=0 channel_flag_maxfamilyclients_unlimited=0 channel_flag_maxfamilyclients_inherited=1 channel_needed_talk_power=0 channel_forced_silence=0 channel_name_phonetic channel_icon_id=0 channel_flag_private=0|cid=4 cpid=2 channel_name=Ding\\s•\\s1\\s\\p\\sSplamy´s\\sBett channel_topic channel_codec=4 channel_codec_quality=7 channel_maxclients=-1 channel_maxfamilyclients=-1 channel_order=0 channel_flag_permanent=1 channel_flag_semi_permanent=0 channel_flag_default=0 channel_flag_password=0 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 channel_delete_delay=0 channel_flag_maxclients_unlimited=1 channel_flag_maxfamilyclients_unlimited=0 channel_flag_maxfamilyclients_inherited=1 channel_needed_talk_power=0 channel_forced_silence=0 channel_name_phonetic=Neo\\sSeebi\\sEvangelion channel_icon_id=0 channel_flag_private=0"), NotificationType.ChannelList); + Assert.True(notif.Ok); + var notifv = notif.Value; + } + } + + public static class AssertEx + { + public static void PropertyValuesAreEquals(object actual, object expected) + { + PropertyInfo[] properties = expected.GetType().GetProperties(); + foreach (PropertyInfo property in properties) + { + object expectedValue = property.GetValue(expected, null); + object actualValue = property.GetValue(actual, null); + + if (actualValue is IList) + AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue); + else if (!Equals(expectedValue, actualValue)) + Assert.Fail("Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue); + } + } + + private static void AssertListsAreEquals(PropertyInfo property, IList actualList, IList expectedList) + { + if (actualList.Count != expectedList.Count) + Assert.Fail("Property {0}.{1} does not match. Expected IList containing {2} elements but was IList containing {3} elements", property.PropertyType.Name, property.Name, expectedList.Count, actualList.Count); + + for (int i = 0; i < actualList.Count; i++) + if (!Equals(actualList[i], expectedList[i])) + Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} equals to {2} but was IList with element {1} equals to {3}", property.PropertyType.Name, property.Name, expectedList[i], actualList[i]); + } + } +} diff --git a/TS3ABotUnitTests/UnitTests.cs b/TS3ABotUnitTests/UnitTests.cs index 215cf89b..14bac2ce 100644 --- a/TS3ABotUnitTests/UnitTests.cs +++ b/TS3ABotUnitTests/UnitTests.cs @@ -1,18 +1,11 @@ // TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2016 TS3AudioBot contributors -// +// Copyright (C) 2017 TS3AudioBot contributors +// // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . namespace TS3ABotUnitTests { @@ -22,13 +15,10 @@ namespace TS3ABotUnitTests using System.Collections.Generic; using System.IO; using System.Linq; - using System.Reflection; using System.Text.RegularExpressions; using TS3AudioBot; using TS3AudioBot.Algorithm; - using TS3AudioBot.CommandSystem; - using TS3AudioBot.CommandSystem.CommandResults; - using TS3AudioBot.CommandSystem.Commands; + using TS3AudioBot.Config; using TS3AudioBot.Helper; using TS3AudioBot.History; using TS3AudioBot.ResourceFactories; @@ -48,30 +38,29 @@ public void HistoryFileIntergrityTest() string testFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "history.test"); if (File.Exists(testFile)) File.Delete(testFile); - - var inv1 = new ClientData { ClientId = 10, DatabaseId = 101, Name = "Invoker1" }; - var inv2 = new ClientData { ClientId = 20, DatabaseId = 102, Name = "Invoker2" }; + var inv1 = new ClientData { ClientId = 10, Uid = "Uid1", Name = "Invoker1" }; + var inv2 = new ClientData { ClientId = 20, Uid = "Uid2", Name = "Invoker2" }; var ar1 = new AudioResource("asdf", "sc_ar1", "soundcloud"); var ar2 = new AudioResource("./File.mp3", "me_ar2", "media"); var ar3 = new AudioResource("kitty", "tw_ar3", "twitch"); - var data1 = new HistorySaveData(ar1, inv1.DatabaseId); - var data2 = new HistorySaveData(ar2, inv2.DatabaseId); - var data3 = new HistorySaveData(ar3, 103); + var data1 = new HistorySaveData(ar1, inv1.Uid); + var data2 = new HistorySaveData(ar2, inv2.Uid); + var data3 = new HistorySaveData(ar3, "Uid3"); - var memcfg = ConfigFile.CreateDummy(); - var hmf = memcfg.GetDataStruct("HistoryManager", true); - hmf.HistoryFile = testFile; - hmf.FillDeletedIds = false; + var confHistory = ConfigTable.CreateRoot(); + confHistory.FillDeletedIds.Value = false; + var confDb = ConfigTable.CreateRoot(); + confDb.Path.Value = testFile; DbStore db; HistoryManager hf; void CreateDbStore() { - db = new DbStore(hmf); - hf = new HistoryManager(hmf) { Database = db }; + db = new DbStore(confDb); + hf = new HistoryManager(confHistory) { Database = db }; hf.Initialize(); } @@ -111,7 +100,7 @@ void CreateDbStore() var ale1 = hf.FindEntryByResource(ar1); hf.RenameEntry(ale1, "sc_ar1X"); - hf.LogAudioResource(new HistorySaveData(ale1.AudioResource, 42)); + hf.LogAudioResource(new HistorySaveData(ale1.AudioResource, "Uid4")); db.Dispose(); @@ -125,14 +114,14 @@ void CreateDbStore() var ale2 = hf.FindEntryByResource(ar2); hf.RenameEntry(ale2, "me_ar2_loong1"); - hf.LogAudioResource(new HistorySaveData(ale2.AudioResource, 42)); + hf.LogAudioResource(new HistorySaveData(ale2.AudioResource, "Uid4")); ale1 = hf.FindEntryByResource(ar1); hf.RenameEntry(ale1, "sc_ar1X_loong1"); - hf.LogAudioResource(new HistorySaveData(ale1.AudioResource, 42)); + hf.LogAudioResource(new HistorySaveData(ale1.AudioResource, "Uid4")); hf.RenameEntry(ale2, "me_ar2_exxxxxtra_loong1"); - hf.LogAudioResource(new HistorySaveData(ale2.AudioResource, 42)); + hf.LogAudioResource(new HistorySaveData(ale2.AudioResource, "Uid4")); db.Dispose(); @@ -191,97 +180,6 @@ public void UtilSeedTest() /* ====================== Algorithm Tests =========================*/ - [Test] - public void XCommandSystemFilterTest() - { - var filterList = new Dictionary - { - { "help", null }, - { "quit", null }, - { "play", null }, - { "ply", null } - }; - - var filter = Filter.GetFilterByName("ic3").Unwrap(); - - // Exact match - var result = filter.Filter(filterList, "help"); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("help", result.First().Key); - - // The first occurence of y - result = filter.Filter(filterList, "y"); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("ply", result.First().Key); - - // The smallest word - result = filter.Filter(filterList, "zorn"); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("ply", result.First().Key); - - // First letter match - result = filter.Filter(filterList, "q"); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("quit", result.First().Key); - - // Ignore other letters - result = filter.Filter(filterList, "palyndrom"); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("play", result.First().Key); - - filterList.Add("pla", null); - - // Ambiguous command - result = filter.Filter(filterList, "p"); - Assert.AreEqual(2, result.Count()); - Assert.IsTrue(result.Any(r => r.Key == "ply")); - Assert.IsTrue(result.Any(r => r.Key == "pla")); - } - - private static string OptionalFunc(string s = null) => s == null ? "NULL" : "NOT NULL"; - - [Test] - public void XCommandSystemTest() - { - var commandSystem = new XCommandSystem(); - var group = commandSystem.RootCommand; - group.AddCommand("one", new FunctionCommand(() => "ONE")); - group.AddCommand("two", new FunctionCommand(() => "TWO")); - group.AddCommand("echo", new FunctionCommand(s => s)); - group.AddCommand("optional", new FunctionCommand(typeof(UnitTests).GetMethod(nameof(OptionalFunc), BindingFlags.NonPublic | BindingFlags.Static))); - - // Basic tests - Assert.AreEqual("ONE", ((StringCommandResult)commandSystem.Execute(Utils.ExecInfo, - new ICommand[] { new StringCommand("one") })).Content); - Assert.AreEqual("ONE", commandSystem.ExecuteCommand(Utils.ExecInfo, "!one")); - Assert.AreEqual("TWO", commandSystem.ExecuteCommand(Utils.ExecInfo, "!t")); - Assert.AreEqual("TEST", commandSystem.ExecuteCommand(Utils.ExecInfo, "!e TEST")); - Assert.AreEqual("ONE", commandSystem.ExecuteCommand(Utils.ExecInfo, "!o")); - - // Optional parameters - Assert.Throws(() => commandSystem.ExecuteCommand(Utils.ExecInfo, "!e")); - Assert.AreEqual("NULL", commandSystem.ExecuteCommand(Utils.ExecInfo, "!op")); - Assert.AreEqual("NOT NULL", commandSystem.ExecuteCommand(Utils.ExecInfo, "!op 1")); - - // Command chaining - Assert.AreEqual("TEST", commandSystem.ExecuteCommand(Utils.ExecInfo, "!e (!e TEST)")); - Assert.AreEqual("TWO", commandSystem.ExecuteCommand(Utils.ExecInfo, "!e (!t)")); - Assert.AreEqual("NOT NULL", commandSystem.ExecuteCommand(Utils.ExecInfo, "!op (!e TEST)")); - Assert.AreEqual("ONE", commandSystem.ExecuteCommand(Utils.ExecInfo, "!(!e on)")); - - // Command overloading - var intCom = new Func(i => "INT"); - var strCom = new Func(s => "STRING"); - group.AddCommand("overlord", new OverloadedFunctionCommand(new[] { - new FunctionCommand(intCom.Method, intCom.Target), - new FunctionCommand(strCom.Method, strCom.Target) - })); - - Assert.AreEqual("INT", commandSystem.ExecuteCommand(Utils.ExecInfo, "!overlord 1")); - Assert.AreEqual("STRING", commandSystem.ExecuteCommand(Utils.ExecInfo, "!overlord a")); - Assert.Throws(() => commandSystem.ExecuteCommand(Utils.ExecInfo, "!overlord")); - } - [Test] public void ListedShuffleTest() { @@ -319,7 +217,7 @@ private static void TestShuffleAlgorithm(IShuffleAlgorithm algo) [Test] public void Factory_YoutubeFactoryTest() { - using (IResourceFactory rfac = new YoutubeFactory(new YoutubeFactoryData())) + using (IResourceFactory rfac = new YoutubeFactory()) { // matching links Assert.AreEqual(rfac.MatchResource(@"https://www.youtube.com/watch?v=robqdGEhQWo"), MatchCertainty.Always); @@ -334,125 +232,6 @@ public void Factory_YoutubeFactoryTest() /* ======================= TS3Client Tests ========================*/ - [Test] - public void Ts3Client_RingQueueTest() - { - var q = new RingQueue(3, 5); - - q.Set(0, 42); - - Assert.True(q.TryPeekStart(0, out int ov)); - Assert.AreEqual(ov, 42); - - q.Set(1, 43); - - // already set - Assert.Throws(() => q.Set(1, 99)); - - Assert.True(q.TryPeekStart(0, out ov)); - Assert.AreEqual(ov, 42); - Assert.True(q.TryPeekStart(1, out ov)); - Assert.AreEqual(ov, 43); - - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 42); - - Assert.True(q.TryPeekStart(0, out ov)); - Assert.AreEqual(ov, 43); - Assert.False(q.TryPeekStart(1, out ov)); - - q.Set(3, 45); - q.Set(2, 44); - - // buffer overfull - Assert.Throws(() => q.Set(4, 99)); - - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 43); - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 44); - - q.Set(4, 46); - - // out of mod range - Assert.Throws(() => q.Set(5, 99)); - - q.Set(0, 47); - - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 45); - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 46); - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 47); - - q.Set(2, 49); - - Assert.False(q.TryDequeue(out ov)); - - q.Set(1, 48); - - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 48); - Assert.True(q.TryDequeue(out ov)); - Assert.AreEqual(ov, 49); - } - - [Test] - public void Ts3Client_RingQueueTest2() - { - var q = new RingQueue(50, ushort.MaxValue + 1); - - for (int i = 0; i < ushort.MaxValue - 10; i++) - { - q.Set(i, i); - Assert.True(q.TryDequeue(out var iCheck)); - Assert.AreEqual(iCheck, i); - } - - var setStatus = q.IsSet(ushort.MaxValue - 20); - Assert.True(setStatus.HasFlag(ItemSetStatus.Set)); - - for (int i = ushort.MaxValue - 10; i < ushort.MaxValue + 10; i++) - { - q.Set(i % (ushort.MaxValue + 1), 42); - } - } - - [Test] - public void Ts3Client_RingQueueTest3() - { - var q = new RingQueue(100, ushort.MaxValue + 1); - - int iSet = 0; - for (int blockSize = 1; blockSize < 100; blockSize++) - { - for (int i = 0; i < blockSize; i++) - { - q.Set(iSet++, i); - } - for (int i = 0; i < blockSize; i++) - { - Assert.True(q.TryDequeue(out var iCheck)); - Assert.AreEqual(i, iCheck); - } - } - - for (int blockSize = 1; blockSize < 100; blockSize++) - { - q = new RingQueue(100, ushort.MaxValue + 1); - for (int i = 0; i < blockSize; i++) - { - q.Set(i, i); - } - for (int i = 0; i < blockSize; i++) - { - Assert.True(q.TryDequeue(out var iCheck)); - Assert.AreEqual(i, iCheck); - } - } - } - [Test] public void VersionSelfCheck() { @@ -460,7 +239,7 @@ public void VersionSelfCheck() } } - static class Extensions + internal static class Extensions { public static IEnumerable GetLastXEntrys(this HistoryManager hf, int num) { diff --git a/TS3ABotUnitTests/packages.config b/TS3ABotUnitTests/packages.config deleted file mode 100644 index 3ca55d45..00000000 --- a/TS3ABotUnitTests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/TS3Client/TS3Client.ruleset b/TS3AudioBot.ruleset similarity index 99% rename from TS3Client/TS3Client.ruleset rename to TS3AudioBot.ruleset index 06677ca3..d9f639b1 100644 --- a/TS3Client/TS3Client.ruleset +++ b/TS3AudioBot.ruleset @@ -91,5 +91,6 @@ + \ No newline at end of file diff --git a/TS3AudioBot.sln b/TS3AudioBot.sln index 111570d9..c6b0a3f9 100644 --- a/TS3AudioBot.sln +++ b/TS3AudioBot.sln @@ -2,20 +2,21 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2036 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TS3AudioBot", "TS3AudioBot\TS3AudioBot.csproj", "{0ECC38F3-DE6E-4D7F-81EB-58B15F584635}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3AudioBot", "TS3AudioBot\TS3AudioBot.csproj", "{0ECC38F3-DE6E-4D7F-81EB-58B15F584635}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TS3ABotUnitTests", "TS3ABotUnitTests\TS3ABotUnitTests.csproj", "{20B6F767-5396-41D9-83D8-98B5730C6E2E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3ABotUnitTests", "TS3ABotUnitTests\TS3ABotUnitTests.csproj", "{20B6F767-5396-41D9-83D8-98B5730C6E2E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TS3Client", "TS3Client\TS3Client.csproj", "{0EB99E9D-87E5-4534-A100-55D231C2B6A6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3Client", "TS3Client\TS3Client.csproj", "{0EB99E9D-87E5-4534-A100-55D231C2B6A6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{ADAA5A65-0CE1-45FA-91F4-3D8D39073AB0}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .travis.yml = .travis.yml README.md = README.md + TS3AudioBot.ruleset = TS3AudioBot.ruleset EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ts3ClientTests", "Ts3ClientTests\Ts3ClientTests.csproj", "{3F6F11F0-C0DE-4C24-B39F-4A5B5B150376}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ts3ClientTests", "Ts3ClientTests\Ts3ClientTests.csproj", "{3F6F11F0-C0DE-4C24-B39F-4A5B5B150376}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,4 +47,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {15C2EA96-9126-41B1-A7A1-B02663F50EE3} EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/TS3AudioBot/Algorithm/IFilterAlgorithm.cs b/TS3AudioBot/Algorithm/IFilterAlgorithm.cs index 4498b504..98c5e5e5 100644 --- a/TS3AudioBot/Algorithm/IFilterAlgorithm.cs +++ b/TS3AudioBot/Algorithm/IFilterAlgorithm.cs @@ -9,8 +9,9 @@ namespace TS3AudioBot.Algorithm { - using System.Linq; + using System; using System.Collections.Generic; + using System.Linq; public interface IFilterAlgorithm { @@ -26,13 +27,14 @@ public sealed class Filter public static R GetFilterByName(string filter) { + R ToR(IFilterAlgorithm obj) => R.OkR(obj); switch (filter) { - case "exact": return ExactFilter.Instance.ToR(); - case "substring": return SubstringFilter.Instance.ToR(); - case "ic3": return Ic3Filter.Instance.ToR(); - case "hamming": return HammingFilter.Instance.ToR(); - default: return "Unkown filter type"; + case "exact": return ToR(ExactFilter.Instance); + case "substring": return ToR(SubstringFilter.Instance); + case "ic3": return ToR(Ic3Filter.Instance); + case "hamming": return ToR(HammingFilter.Instance); + default: return R.Err; } } } @@ -100,7 +102,17 @@ private SubstringFilter() { } IEnumerable> IFilterAlgorithm.Filter(IEnumerable> list, string filter) { - return list.Where(x => x.Key.StartsWith(filter)); + var result = list.Where(x => x.Key.StartsWith(filter)); + using (var enu = result.GetEnumerator()) + { + if (!enu.MoveNext()) + yield break; + yield return enu.Current; + if (enu.Current.Key == filter) + yield break; + while (enu.MoveNext()) + yield return enu.Current; + } } } } diff --git a/TS3AudioBot/Audio/CustomTargetPipe.cs b/TS3AudioBot/Audio/CustomTargetPipe.cs index 3008323c..80ab00c2 100644 --- a/TS3AudioBot/Audio/CustomTargetPipe.cs +++ b/TS3AudioBot/Audio/CustomTargetPipe.cs @@ -17,7 +17,7 @@ namespace TS3AudioBot.Audio using TS3Client.Audio; using TS3Client.Full; - internal class CustomTargetPipe : ITargetManager, IAudioPassiveConsumer + internal class CustomTargetPipe : IVoiceTarget, IAudioPassiveConsumer { public TargetSendMode SendMode { get; set; } = TargetSendMode.None; public ulong GroupWhisperTargetId { get; private set; } diff --git a/TS3AudioBot/Audio/FfmpegProducer.cs b/TS3AudioBot/Audio/FfmpegProducer.cs index a79eccf0..f61ba861 100644 --- a/TS3AudioBot/Audio/FfmpegProducer.cs +++ b/TS3AudioBot/Audio/FfmpegProducer.cs @@ -9,12 +9,14 @@ namespace TS3AudioBot.Audio { + using Config; using Helper; using System; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; + using System.Threading; using TS3Client.Audio; public class FfmpegProducer : IAudioPassiveProducer, ISampleInfo, IDisposable @@ -24,52 +26,37 @@ public class FfmpegProducer : IAudioPassiveProducer, ISampleInfo, IDisposable private const string PreLinkConf = "-hide_banner -nostats -i \""; private const string PostLinkConf = "\" -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1"; private readonly TimeSpan retryOnDropBeforeEnd = TimeSpan.FromSeconds(10); - private readonly object ffmpegLock = new object(); - private readonly Ts3FullClientData ts3FullClientData; + private readonly ConfToolsFfmpeg config; public event EventHandler OnSongEnd; - private readonly PreciseAudioTimer audioTimer; private string lastLink; - private Process ffmpegProcess; - private TimeSpan? parsedSongLength; - private bool hasTriedToReconnectAudio; + private ActiveFfmpegInstance ffmpegInstance; public int SampleRate { get; } = 48000; public int Channels { get; } = 2; public int BitsPerSample { get; } = 16; - public FfmpegProducer(Ts3FullClientData tfcd) + public FfmpegProducer(ConfToolsFfmpeg config) { - ts3FullClientData = tfcd; - audioTimer = new PreciseAudioTimer(this); + this.config = config; } - public R AudioStart(string url) => StartFfmpegProcess(url); + public E AudioStart(string url) => StartFfmpegProcess(url, TimeSpan.Zero); - public R AudioStop() + public E AudioStop() { - audioTimer.Stop(); StopFfmpegProcess(); - return R.OkR; + return R.Ok; } public TimeSpan Length => GetCurrentSongLength(); public TimeSpan Position { - get => audioTimer.SongPosition; - set - { - if (value < TimeSpan.Zero || value > Length) - throw new ArgumentOutOfRangeException(nameof(value)); - AudioStop(); - StartFfmpegProcess(lastLink, - $"-ss {value.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}", - $"-ss {value.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}"); - audioTimer.SongPositionOffset = value; - } + get => ffmpegInstance?.AudioTimer.SongPosition ?? TimeSpan.Zero; + set => SetPosition(value); } public int Read(byte[] buffer, int offset, int length, out Meta meta) @@ -78,39 +65,48 @@ public int Read(byte[] buffer, int offset, int length, out Meta meta) bool triggerEndSafe = false; int read; - lock (ffmpegLock) - { - if (ffmpegProcess == null) - return 0; + var instance = ffmpegInstance; + + if (instance == null) + return 0; + + read = instance.FfmpegProcess.StandardOutput.BaseStream.Read(buffer, 0, length); - read = ffmpegProcess.StandardOutput.BaseStream.Read(buffer, 0, length); - if (read == 0) + if (read == 0) + { + // check for premature connection drop + if (instance.FfmpegProcess.HasExited && !instance.hasTriedToReconnectAudio) { - // check for premature connection drop - if (ffmpegProcess.HasExited && !hasTriedToReconnectAudio) + var expectedStopLength = GetCurrentSongLength(); + Log.Trace("Expected song length {0}", expectedStopLength); + if (expectedStopLength != TimeSpan.Zero) { - var expectedStopLength = GetCurrentSongLength(); - Log.Trace("Expected song length {0}", expectedStopLength); - if (expectedStopLength != TimeSpan.Zero) + var actualStopPosition = instance.AudioTimer.SongPosition; + Log.Trace("Actual song position {0}", actualStopPosition); + if (actualStopPosition + retryOnDropBeforeEnd < expectedStopLength) { - var actualStopPosition = audioTimer.SongPosition; - Log.Trace("Actual song position {0}", actualStopPosition); - if (actualStopPosition + retryOnDropBeforeEnd < expectedStopLength) + Log.Debug("Connection to song lost, retrying at {0}", actualStopPosition); + instance.hasTriedToReconnectAudio = true; + var newInstance = SetPosition(actualStopPosition); + if (newInstance.Ok) { - Log.Debug("Connection to song lost, retrying at {0}", actualStopPosition); - hasTriedToReconnectAudio = true; - Position = actualStopPosition; + newInstance.Value.hasTriedToReconnectAudio = true; return 0; } + else + { + Log.Debug("Retry failed {0}", newInstance.Error); + triggerEndSafe = true; + } } } + } - if (ffmpegProcess.HasExited) - { - Log.Trace("Ffmpeg has exited with {0}", ffmpegProcess.ExitCode); - AudioStop(); - triggerEndSafe = true; - } + if (instance.FfmpegProcess.HasExited) + { + Log.Trace("Ffmpeg has exited with {0}", instance.FfmpegProcess.ExitCode); + AudioStop(); + triggerEndSafe = true; } } @@ -120,25 +116,34 @@ public int Read(byte[] buffer, int offset, int length, out Meta meta) return 0; } - hasTriedToReconnectAudio = false; - audioTimer.PushBytes(read); + instance.hasTriedToReconnectAudio = false; + instance.AudioTimer.PushBytes(read); return read; } - public R StartFfmpegProcess(string url, string extraPreParam = null, string extraPostParam = null) + private R SetPosition(TimeSpan value) + { + if (value < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(value)); + return StartFfmpegProcess(lastLink, value, + $"-ss {value.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}", + $"-ss {value.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}"); + } + + private R StartFfmpegProcess(string url, TimeSpan offset, string extraPreParam = null, string extraPostParam = null) { Log.Trace("Start request {0}", url); try { - lock (ffmpegLock) - { - StopFfmpegProcess(); + StopFfmpegProcess(); - ffmpegProcess = new Process + var newInstance = new ActiveFfmpegInstance() + { + FfmpegProcess = new Process { StartInfo = new ProcessStartInfo { - FileName = ts3FullClientData.FfmpegPath, + FileName = config.Path.Value, Arguments = string.Concat(extraPreParam, " ", PreLinkConf, url, PostLinkConf, " ", extraPostParam), RedirectStandardOutput = true, RedirectStandardInput = true, @@ -147,86 +152,107 @@ public R StartFfmpegProcess(string url, string extraPreParam = null, string extr CreateNoWindow = true, }, EnableRaisingEvents = true, - }; - Log.Trace("Starting with {0}", ffmpegProcess.StartInfo.Arguments); - ffmpegProcess.ErrorDataReceived += FfmpegProcess_ErrorDataReceived; - ffmpegProcess.Start(); - ffmpegProcess.BeginErrorReadLine(); - - lastLink = url; - parsedSongLength = null; - - audioTimer.SongPositionOffset = TimeSpan.Zero; - audioTimer.Start(); - return R.OkR; - } - } - catch (Win32Exception ex) { return $"Ffmpeg could not be found ({ex.Message})"; } - catch (Exception ex) { return $"Unable to create stream ({ex.Message})"; } - } + }, + AudioTimer = new PreciseAudioTimer(this) + { + SongPositionOffset = offset, + } + }; - private void FfmpegProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e) - { - if (e.Data == null) - return; + Log.Trace("Starting with {0}", newInstance.FfmpegProcess.StartInfo.Arguments); + newInstance.FfmpegProcess.ErrorDataReceived += newInstance.FfmpegProcess_ErrorDataReceived; + newInstance.FfmpegProcess.Start(); + newInstance.FfmpegProcess.BeginErrorReadLine(); - lock (ffmpegLock) - { - if (parsedSongLength.HasValue) - return; + lastLink = url; - var match = FindDurationMatch.Match(e.Data); - if (!match.Success) - return; + newInstance.AudioTimer.Start(); - if (sender != ffmpegProcess) - return; + var oldInstance = Interlocked.Exchange(ref ffmpegInstance, newInstance); + oldInstance?.Close(); - int hours = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - int minutes = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); - int seconds = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); - int millisec = int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture) * 10; - parsedSongLength = new TimeSpan(0, hours, minutes, seconds, millisec); + return newInstance; + } + catch (Win32Exception ex) + { + var error = $"Ffmpeg could not be found ({ex.Message})"; + Log.Warn(ex, error); + return error; + } + catch (Exception ex) + { + var error = $"Unable to create stream ({ex.Message})"; + Log.Warn(ex, error); + return error; } } private void StopFfmpegProcess() { - // TODO somehow bypass lock - lock (ffmpegLock) - { - if (ffmpegProcess == null) - return; + var oldInstance = Interlocked.Exchange(ref ffmpegInstance, null); + oldInstance?.Close(); + } + + private TimeSpan GetCurrentSongLength() + { + var instance = ffmpegInstance; + if (instance == null) + return TimeSpan.Zero; + + return instance.ParsedSongLength ?? TimeSpan.Zero; + } + + public void Dispose() + { + StopFfmpegProcess(); + } + + private class ActiveFfmpegInstance + { + public Process FfmpegProcess { get; set; } + public bool HasIcyTag { get; private set; } = false; + public bool hasTriedToReconnectAudio; + public PreciseAudioTimer AudioTimer { get; set; } + public TimeSpan? ParsedSongLength { get; set; } = null; + public void Close() + { try { - if (!ffmpegProcess.HasExited) - ffmpegProcess.Kill(); + if (!FfmpegProcess.HasExited) + FfmpegProcess.Kill(); else - ffmpegProcess.Close(); + FfmpegProcess.Close(); } catch (InvalidOperationException) { } - ffmpegProcess = null; } - } - private TimeSpan GetCurrentSongLength() - { - lock (ffmpegLock) + public void FfmpegProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e) { - if (ffmpegProcess == null) - return TimeSpan.Zero; + if (e.Data == null) + return; - if (parsedSongLength.HasValue) - return parsedSongLength.Value; + if (sender != FfmpegProcess) + throw new InvalidOperationException("Wrong process associated to event"); - return TimeSpan.Zero; - } - } + if (!ParsedSongLength.HasValue) + { + var match = FindDurationMatch.Match(e.Data); + if (!match.Success) + return; + + int hours = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + int minutes = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + int seconds = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); + int millisec = int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture) * 10; + ParsedSongLength = new TimeSpan(0, hours, minutes, seconds, millisec); + } - public void Dispose() - { - // TODO close ffmpeg if open + if (!HasIcyTag && e.Data.AsSpan().TrimStart().StartsWith("icy-".AsSpan())) + { + HasIcyTag = true; + } + } } } } diff --git a/TS3AudioBot/Bot.cs b/TS3AudioBot/Bot.cs index 411f7a94..e8237fea 100644 --- a/TS3AudioBot/Bot.cs +++ b/TS3AudioBot/Bot.cs @@ -12,16 +12,18 @@ namespace TS3AudioBot using Algorithm; using CommandSystem; using CommandSystem.CommandResults; + using Config; using Dependency; using Helper; using History; + using Localization; + using Playlists; using Plugins; using Sessions; using System; - using System.IO; using System.Threading; + using System.Threading.Tasks; using TS3Client; - using TS3Client.Full; using TS3Client.Messages; /// Core class managing all bots and utility modules. @@ -29,76 +31,89 @@ public sealed class Bot : IDisposable { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - private MainBotData mainBotData; + private readonly ConfBot config; internal object SyncRoot { get; } = new object(); internal bool IsDisposed { get; private set; } internal BotInjector Injector { get; set; } public int Id { get; internal set; } + /// This is the template name. Can be null. + public string Name { get; internal set; } public bool QuizMode { get; set; } public string BadgesString { get; set; } // Injected dependencies - public ConfigFile Config { get; set; } + public ConfRoot CoreConfig { get; set; } public ResourceFactories.ResourceFactoryManager FactoryManager { get; set; } public CommandManager CommandManager { get; set; } public BotManager BotManager { get; set; } public PluginManager PluginManager { get; set; } - // Onw modules + // Own modules /// Connection object for the current client. - public TeamspeakControl QueryConnection { get; set; } + public Ts3Client ClientConnection { get; set; } public SessionManager SessionManager { get; set; } public PlayManager PlayManager { get; set; } - public ITargetManager TargetManager { get; private set; } + public IVoiceTarget TargetManager { get; private set; } public IPlayerConnection PlayerConnection { get; private set; } public Filter Filter { get; private set; } - public R InitializeBot() + public Bot(ConfBot config) { - Log.Info("Bot connecting..."); + this.config = config; + } - // Read Config File - var afd = Config.GetDataStruct("AudioFramework", true); - var tfcd = Config.GetDataStruct("QueryConnection", true); - var hmd = Config.GetDataStruct("HistoryManager", true); - var pld = Config.GetDataStruct("PlaylistManager", true); - mainBotData = Config.GetDataStruct("MainBot", true); - mainBotData.PropertyChanged += OnConfigUpdate; + public E InitializeBot() + { + Log.Info("Bot ({0}) connecting to \"{1}\"", Id, config.Connect.Address); - AudioValues.audioFrameworkData = afd; + // Registering config changes + config.CommandMatcher.Changed += (s, e) => + { + var newMatcher = Filter.GetFilterByName(e.NewValue); + if (newMatcher.Ok) + Filter.Current = newMatcher.Value; + }; + config.Language.Changed += (s, e) => + { + var langResult = LocalizationManager.LoadLanguage(e.NewValue); + if (!langResult.Ok) + Log.Error("Failed to load language file ({0})", langResult.Error); + }; Injector.RegisterType(); + Injector.RegisterType(); Injector.RegisterType(); Injector.RegisterType(); - Injector.RegisterType(); + Injector.RegisterType(); Injector.RegisterType(); Injector.RegisterType(); Injector.RegisterType(); Injector.RegisterType(); - Injector.RegisterType(); + Injector.RegisterType(); Injector.RegisterType(); Injector.RegisterType(); Injector.RegisterModule(this); + Injector.RegisterModule(config); Injector.RegisterModule(Injector); - Injector.RegisterModule(new PlaylistManager(pld)); - var teamspeakClient = new Ts3Full(tfcd); + Injector.RegisterModule(new PlaylistManager(config.Playlists)); + var teamspeakClient = new Ts3Client(config); Injector.RegisterModule(teamspeakClient); - Injector.RegisterModule(teamspeakClient.GetLowLibrary()); + Injector.RegisterModule(teamspeakClient.TsFullClient); Injector.RegisterModule(new SessionManager()); HistoryManager historyManager = null; - if (hmd.EnableHistory) - Injector.RegisterModule(historyManager = new HistoryManager(hmd), x => x.Initialize()); + if (config.History.Enabled) + Injector.RegisterModule(historyManager = new HistoryManager(config.History), x => x.Initialize()); Injector.RegisterModule(new PlayManager()); Injector.RegisterModule(teamspeakClient.TargetPipe); - var filter = Filter.GetFilterByName(mainBotData.CommandMatching); + var filter = Filter.GetFilterByName(config.CommandMatcher); Injector.RegisterModule(new Filter { Current = filter.OkOr(Filter.DefaultAlgorithm) }); - if (!filter.Ok) Log.Warn("Unknown CommandMatching config. Using default."); + if (!filter.Ok) Log.Warn("Unknown command_matcher config. Using default."); if (!Injector.AllResolved()) { @@ -117,101 +132,98 @@ public R InitializeBot() PlayManager.AfterResourceStarted += LoggedUpdateBotStatus; PlayManager.AfterResourceStopped += LoggedUpdateBotStatus; // Log our resource in the history - if (hmd.EnableHistory) - PlayManager.AfterResourceStarted += (s, e) => historyManager.LogAudioResource(new HistorySaveData(e.PlayResource.BaseData, e.Owner)); + if (historyManager != null) + PlayManager.AfterResourceStarted += (s, e) => historyManager.LogAudioResource(new HistorySaveData(e.PlayResource.BaseData, e.Invoker.ClientUid)); // Update our thumbnail PlayManager.AfterResourceStarted += GenerateStatusImage; PlayManager.AfterResourceStopped += GenerateStatusImage; // Register callback for all messages happening - QueryConnection.OnMessageReceived += TextCallback; + ClientConnection.OnMessageReceived += TextCallback; // Register callback to remove open private sessions, when user disconnects - QueryConnection.OnClientDisconnect += OnClientDisconnect; - QueryConnection.OnBotDisconnect += (s, e) => Dispose(); - QueryConnection.OnBotConnected += OnBotConnected; - BadgesString = tfcd.ClientBadges; + ClientConnection.OnClientDisconnect += OnClientDisconnect; + ClientConnection.OnBotConnected += OnBotConnected; + ClientConnection.OnBotDisconnect += OnBotDisconnect; + BadgesString = config.Connect.Badges; // Connect the query after everyting is set up - try { QueryConnection.Connect(); } - catch (Ts3Exception qcex) - { - Log.Info(qcex, "There is either a problem with your connection configuration, or the query has not all permissions it needs."); - return "Query error"; - } - return R.OkR; + return ClientConnection.Connect(); } private void OnBotConnected(object sender, EventArgs e) { - Log.Info("Bot connected."); - QueryConnection.ChangeBadges(BadgesString); + Log.Info("Bot ({0}) connected.", Id); + if (!string.IsNullOrEmpty(BadgesString)) + ClientConnection?.ChangeBadges(BadgesString); + } + + private void OnBotDisconnect(object sender, DisconnectEventArgs e) + { + Dispose(); } private void TextCallback(object sender, TextMessage textMessage) { - Log.Debug("Got message from {0}: {1}", textMessage.InvokerName, textMessage.Message); + var langResult = LocalizationManager.LoadLanguage(config.Language); + if (!langResult.Ok) + Log.Error("Failed to load language file ({0})", langResult.Error); textMessage.Message = textMessage.Message.TrimStart(' '); if (!textMessage.Message.StartsWith("!", StringComparison.Ordinal)) return; - var refreshResult = QueryConnection.RefreshClientBuffer(true); - if (!refreshResult.Ok) - Log.Warn("Bot is not correctly set up. Some commands might not work or are slower. ({0})", refreshResult.Error); + Log.Info("User {0} requested: {1}", textMessage.InvokerName, textMessage.Message); - var clientResult = QueryConnection.GetClientById(textMessage.InvokerId); + ClientConnection.InvalidateClientBuffer(); - // get the current session - UserSession session = null; - var result = SessionManager.GetSession(textMessage.InvokerId); - if (result.Ok) + ulong? channelId = null, databaseId = null; + ulong[] channelGroups = null; + var clientResult = ClientConnection.GetCachedClientById(textMessage.InvokerId); + if (clientResult.Ok) { - session = result.Value; + channelId = clientResult.Value.ChannelId; + databaseId = clientResult.Value.DatabaseId; } else { - if (clientResult.Ok) - session = SessionManager.CreateSession(clientResult.Value); + var clientInfoResult = ClientConnection.GetClientInfoById(textMessage.InvokerId); + if (clientInfoResult.Ok) + { + channelId = clientInfoResult.Value.ChannelId; + databaseId = clientInfoResult.Value.DatabaseId; + channelGroups = clientInfoResult.Value.ServerGroups; + } else - Log.Warn("Could not create session with user, some commands might not work ({0})", clientResult.Error); + { + Log.Warn("Bot is not correctly set up. Some commands might not work or are slower (clientlist:{0}, clientinfo:{1}).", + clientResult.Error.Str, clientInfoResult.Error.Str); + } } - var invoker = new InvokerData(textMessage.InvokerUid) - { - ClientId = textMessage.InvokerId, - Visibiliy = textMessage.Target, - NickName = textMessage.InvokerName, - }; - if (clientResult.Ok) - { - invoker.ChannelId = clientResult.Value.ChannelId; - invoker.DatabaseId = clientResult.Value.DatabaseId; - } + var invoker = new InvokerData(textMessage.InvokerUid, + clientId: textMessage.InvokerId, + visibiliy: textMessage.Target, + nickName: textMessage.InvokerName, + channelId: channelId, + databaseId: databaseId) + { ServerGroups = channelGroups }; + var session = SessionManager.GetOrCreateSession(textMessage.InvokerId); var info = CreateExecInfo(invoker, session); - UserSession.SessionToken sessionLock = null; - try + using (session.GetLock()) { - if (session != null) + // check if the user has an open request + if (session.ResponseProcessor != null) { - sessionLock = session.GetLock(); - // check if the user has an open request - if (session.ResponseProcessor != null) - { - var msg = session.ResponseProcessor(textMessage.Message); - session.ClearResponse(); - if (!string.IsNullOrEmpty(msg)) - info.Write(msg).UnwrapThrow(); - return; - } + var msg = session.ResponseProcessor(textMessage.Message); + session.ClearResponse(); + if (!string.IsNullOrEmpty(msg)) + info.Write(msg).UnwrapThrow(); + return; } CallScript(info, textMessage.Message, true, false); } - finally - { - sessionLock?.Dispose(); - } } private void OnClientDisconnect(object sender, ClientLeftView eventArgs) @@ -224,10 +236,10 @@ private void LoggedUpdateBotStatus(object sender, EventArgs e) { var result = UpdateBotStatus(); if (!result) - Log.Warn(result.Error); + Log.Warn(result.Error.Str); } - public R UpdateBotStatus(string overrideStr = null) + public E UpdateBotStatus(string overrideStr = null) { lock (SyncRoot) { @@ -239,45 +251,46 @@ public R UpdateBotStatus(string overrideStr = null) else if (PlayManager.IsPlaying) { setString = QuizMode - ? "" - : PlayManager.CurrentPlayData.ResourceData.ResourceTitle; + ? strings.info_botstatus_quiztime + : (PlayManager.CurrentPlayData.ResourceData.ResourceTitle); } else { - setString = ""; + setString = strings.info_botstatus_sleeping; } - return QueryConnection.ChangeDescription(setString); + return ClientConnection.ChangeDescription(setString ?? ""); } } private void GenerateStatusImage(object sender, EventArgs e) { - if (!mainBotData.GenerateStatusAvatar) + if (!config.GenerateStatusAvatar) return; if (e is PlayInfoEventArgs startEvent) { - var thumresult = FactoryManager.GetThumbnail(startEvent.PlayResource); - if (!thumresult.Ok) - return; - - using (var bmp = ImageUtil.BuildStringImage("Now playing: " + startEvent.ResourceData.ResourceTitle, thumresult.Value)) + Task.Run(() => { - using (var mem = new MemoryStream()) + var thumresult = FactoryManager.GetThumbnail(startEvent.PlayResource); + if (!thumresult.Ok) + return; + + using (var image = ImageUtil.ResizeImage(thumresult.Value)) { - bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg); - var result = QueryConnection.UploadAvatar(mem); + if (image == null) + return; + var result = ClientConnection.UploadAvatar(image); if (!result.Ok) Log.Warn("Could not save avatar: {0}", result.Error); } - } + }); } else { using (var sleepPic = Util.GetEmbeddedFile("TS3AudioBot.Media.SleepingKitty.png")) { - var result = QueryConnection.UploadAvatar(sleepPic); + var result = ClientConnection.UploadAvatar(sleepPic); if (!result.Ok) Log.Warn("Could not save avatar: {0}", result.Error); } @@ -289,7 +302,7 @@ private void BeforeResourceStarted(object sender, PlayInfoEventArgs e) const string DefaultVoiceScript = "!whisper off"; const string DefaultWhisperScript = "!xecute (!whisper subscription) (!unsubscribe temporary) (!subscribe channeltemp (!getmy channel))"; - var mode = AudioValues.audioFrameworkData.AudioMode; + var mode = config.Audio.SendMode.Value; string script; if (mode.StartsWith("!", StringComparison.Ordinal)) script = mode; @@ -358,21 +371,21 @@ private ExecutionInformation CreateExecInfo(InvokerData invoker = null, UserSess public BotLock GetBotLock() { Monitor.Enter(SyncRoot); - return new BotLock(!IsDisposed, this); - } - - public BotInfo GetInfo() => new BotInfo { Id = Id, NickName = QueryConnection.GetSelf().OkOr(null)?.Name }; - - private void OnConfigUpdate(object sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MainBotData.CommandMatching)) + if (IsDisposed) { - var newMatcher = Filter.GetFilterByName(mainBotData.CommandMatching); - if (newMatcher.Ok) - Filter.Current = newMatcher.Value; + Monitor.Exit(SyncRoot); + return null; } + return new BotLock(this); } + public BotInfo GetInfo() => new BotInfo + { + Id = Id, + Name = Name, + Server = config.Connect.Address, + }; + public void Dispose() { BotManager.RemoveBot(this); @@ -381,18 +394,18 @@ public void Dispose() { if (!IsDisposed) IsDisposed = true; else return; - Log.Info("Bot disconnecting."); + Log.Info("Bot ({0}) disconnecting.", Id); PluginManager.StopPlugins(this); PlayManager.Stop(); PlayManager = null; - PlayerConnection.Dispose(); // before: logStream, + PlayerConnection.Dispose(); PlayerConnection = null; - QueryConnection.Dispose(); // before: logStream, - QueryConnection = null; + ClientConnection.Dispose(); + ClientConnection = null; } } } @@ -400,22 +413,9 @@ public void Dispose() public class BotInfo { public int Id { get; set; } - public string NickName { get; set; } + public string Name { get; set; } public string Server { get; set; } - public override string ToString() => $"Id: {Id} Name: {NickName} Server: {Server}"; - } - -#pragma warning disable CS0649 - internal class MainBotData : ConfigData - { - [Info("Teamspeak group id giving the Bot enough power to do his job", "0")] - public ulong BotGroupId { get; set; } - [Info("Generate fancy status images as avatar", "true")] - public bool GenerateStatusAvatar { get; set; } - [Info("Defines how the bot tries to match your !commands.\n" + - "# Possible types: exact, substring, ic3, hamming", "ic3")] - public string CommandMatching { get; set; } + public override string ToString() => $"Id: {Id} Name: {Name} Server: {Server}"; // LOC: TODO } -#pragma warning restore CS0649 } diff --git a/TS3AudioBot/BotManager.cs b/TS3AudioBot/BotManager.cs index 74ab581a..513fc42a 100644 --- a/TS3AudioBot/BotManager.cs +++ b/TS3AudioBot/BotManager.cs @@ -9,6 +9,7 @@ namespace TS3AudioBot { + using Config; using Dependency; using Helper; using System; @@ -20,100 +21,194 @@ public class BotManager : IDisposable { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - private bool isRunning; private List activeBots; private readonly object lockObj = new object(); + public ConfRoot Config { get; set; } public CoreInjector CoreInjector { get; set; } public BotManager() { - isRunning = true; Util.Init(out activeBots); } - public void WatchBots() + public void RunBots(bool interactive) { - while (isRunning) + var templates = Config.ListAllBots().ToArray(); + + if (templates.Length == 0) { - bool createBot; - lock (lockObj) + if (!interactive) { - createBot = activeBots.Count == 0; + Log.Warn("No bots are configured in the load list."); + return; } - if (createBot && CreateBot() != null) + Log.Info("It seems like there are no bots configured."); + Log.Info("Fill out this quick setup to get started."); + + var newBot = CreateNewBot(); + string address; + while (true) { - Thread.Sleep(1000); + Console.WriteLine("Please enter the ip, domain or nickname (with port; default: 9987) where to connect to:"); + address = Console.ReadLine(); + if (TS3Client.TsDnsResolver.TryResolve(address, out var _)) + break; + Console.WriteLine("The address seems invalid or could not be resolved, continue anyway? [y/N]"); + var cont = Console.ReadLine(); + if (string.Equals(cont, "y", StringComparison.InvariantCultureIgnoreCase)) + break; } + newBot.Connect.Address.Value = address; + Console.WriteLine("Please enter the server password (or leave empty for none):"); + newBot.Connect.ServerPassword.Password.Value = Console.ReadLine(); + + const string defaultBotName = "default"; + + if (!newBot.SaveNew(defaultBotName)) + { + Log.Error("Could not save new bot. Ensure that the bot has access to the directory."); + return; + } + + var botMetaConfig = Config.Bots.GetOrCreateItem(defaultBotName); + botMetaConfig.Run.Value = true; - CleanStrayBots(); - Thread.Sleep(1000); + if (!Config.Save()) + Log.Error("Could not save root config. The bot won't start by default."); + + var runResult = RunBot(newBot); + if (!runResult.Ok) + Log.Error("Could not run bot ({0})", runResult.Error); + return; } - } - private void CleanStrayBots() - { - List strayList = null; - lock (lockObj) + foreach (var template in Config.Bots.GetAllItems().Where(t => t.Run)) { - foreach (var bot in activeBots) + var result = RunBotTemplate(template.Key); + if (!result.Ok) { - var botFull = bot.QueryConnection as Ts3Full; - if (!botFull.HasConnection) - { - Log.Warn("Cleaning up stray bot."); - strayList = strayList ?? new List(); - strayList.Add(bot); - } + Log.Error("Could not instantiate bot: {0}", result.Error); } } + } - if (strayList != null) - foreach (var bot in strayList) - StopBot(bot); + public ConfBot CreateNewBot() => Config.CreateBot(); + + public R CreateAndRunNewBot() + { + var botConf = CreateNewBot(); + return RunBot(botConf); } - public BotInfo CreateBot(/*Ts3FullClientData bot*/) + public R RunBotTemplate(string name) { - bool removeBot = false; - var bot = new Bot { Injector = CoreInjector.CloneRealm() }; + var config = Config.GetBotTemplate(name); + if (!config.Ok) + return config.Error.Message; + var botInfo = RunBot(config.Value, name); + if (!botInfo.Ok) + return botInfo.Error; + return botInfo.Value; + } + + public R RunBot(ConfBot config, string name = null) + { + var bot = new Bot(config) { Injector = CoreInjector.CloneRealm(), Name = name }; if (!CoreInjector.TryInject(bot)) Log.Warn("Partial bot dependency loaded only"); lock (bot.SyncRoot) { - if (bot.InitializeBot()) + var initializeResult = bot.InitializeBot(); + var removeBot = false; + if (initializeResult.Ok) { lock (lockObj) { - activeBots.Add(bot); - bot.Id = activeBots.Count - 1; - removeBot = !isRunning; + if (!InsertIntoFreeId(bot)) + removeBot = true; } } + else + { + return $"Bot failed to connect ({initializeResult.Error})"; + } if (removeBot) { StopBot(bot); - return null; + return "BotManager is shutting down"; } } return bot.GetInfo(); } + // !! This method must be called with a lock on lockObj + private bool InsertIntoFreeId(Bot bot) + { + if (activeBots == null) + return false; + + for (int i = 0; i < activeBots.Count; i++) + { + if (activeBots[i] == null) + { + activeBots[i] = bot; + bot.Id = i; + return true; + } + } + + // All slots are full, get a new slot + activeBots.Add(bot); + bot.Id = activeBots.Count - 1; + return true; + } + + // !! This method must be called with a lock on lockObj + private Bot GetBotSave(int id) + { + if (activeBots == null || id < 0 || id >= activeBots.Count) + return null; + return activeBots[id]; + } + + // !! This method must be called with a lock on lockObj + private Bot GetBotSave(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (activeBots == null) + return null; + return activeBots.Find(x => x?.Name == name); + } + public BotLock GetBotLock(int id) { Bot bot; lock (lockObj) { - if (!isRunning) + bot = GetBotSave(id); + if (bot == null) return null; - bot = id >= 0 && id < activeBots.Count - ? activeBots[id] - : null; + if (bot.Id != id) + throw new Exception("Got not matching bot id"); + } + return bot.GetBotLock(); + } + + public BotLock GetBotLock(string name) + { + Bot bot; + lock (lockObj) + { + bot = GetBotSave(name); if (bot == null) - return new BotLock(false, null); + return null; + if (bot.Name != name) + throw new Exception("Got not matching bot name"); } return bot.GetBotLock(); } @@ -128,7 +223,13 @@ internal void RemoveBot(Bot bot) { lock (lockObj) { - activeBots.Remove(bot); + Bot botInList; + if (activeBots != null + && (botInList = GetBotSave(bot.Id)) != null + && botInList == bot) + { + activeBots[bot.Id] = null; + } } } @@ -136,7 +237,9 @@ public BotInfo[] GetBotInfolist() { lock (lockObj) { - return activeBots.Select(x => x.GetInfo()).ToArray(); + if (activeBots == null) + return Array.Empty(); + return activeBots.Where(x => x != null).Select(x => x.GetInfo()).ToArray(); } } @@ -145,12 +248,14 @@ public void Dispose() List disposeBots; lock (lockObj) { - isRunning = false; + if (activeBots == null) + return; + disposeBots = activeBots; - activeBots = new List(); + activeBots = null; } - foreach (var bot in disposeBots) + foreach (var bot in disposeBots.Where(x => x != null)) { StopBot(bot); } @@ -159,23 +264,16 @@ public void Dispose() public class BotLock : IDisposable { - private readonly Bot bot; - public bool IsValid { get; private set; } - public Bot Bot => IsValid ? bot : throw new InvalidOperationException("The bot lock is not valid."); + public Bot Bot { get; } - internal BotLock(bool isValid, Bot bot) + internal BotLock(Bot bot) { - IsValid = isValid; - this.bot = bot; + Bot = bot; } public void Dispose() { - if (IsValid) - { - IsValid = false; - Monitor.Exit(bot.SyncRoot); - } + Monitor.Exit(Bot.SyncRoot); } } } diff --git a/TS3AudioBot/CommandSystem/Ast/AstValue.cs b/TS3AudioBot/CommandSystem/Ast/AstValue.cs index 4e782ae7..3ec5b6fa 100644 --- a/TS3AudioBot/CommandSystem/Ast/AstValue.cs +++ b/TS3AudioBot/CommandSystem/Ast/AstValue.cs @@ -16,6 +16,11 @@ internal class AstValue : AstNode public override AstType Type => AstType.Value; public string Value { get; set; } + public void BuildValue() + { + Value = FullRequest.Substring(Position, Length); + } + public override void Write(StringBuilder strb, int depth) => strb.Space(depth).Append(Value); } } diff --git a/TS3AudioBot/CommandSystem/BotCommand.cs b/TS3AudioBot/CommandSystem/BotCommand.cs index 4094a1a9..988b1d51 100644 --- a/TS3AudioBot/CommandSystem/BotCommand.cs +++ b/TS3AudioBot/CommandSystem/BotCommand.cs @@ -11,22 +11,25 @@ namespace TS3AudioBot.CommandSystem { using CommandResults; using Commands; + using Localization; using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; + [DebuggerDisplay("{DebuggerDisplay, nq}")] public class BotCommand : FunctionCommand { - private string cachedHelp; + private readonly string helpLookupName; private string cachedFullQualifiedName; private object cachedAsJsonObj; public string InvokeName { get; } private readonly string[] requiredRights; public string RequiredRight => requiredRights[0]; - public string Description { get; } + public string Description => LocalizationManager.GetString(helpLookupName); public UsageAttribute[] UsageList { get; } public string FullQualifiedName { @@ -45,6 +48,19 @@ public string FullQualifiedName } } + public string DebuggerDisplay + { + get + { + var strb = new StringBuilder(); + strb.Append('!').Append(InvokeName); + strb.Append(" : "); + foreach (var param in UsageList) + strb.Append(param.UsageSyntax).Append('/'); + return strb.ToString(); + } + } + public object AsJsonObj { get @@ -58,46 +74,32 @@ public object AsJsonObj public BotCommand(CommandBuildInfo buildInfo) : base(buildInfo.Method, buildInfo.Parent) { InvokeName = buildInfo.CommandData.CommandNameSpace; - Description = buildInfo.CommandData.CommandHelp; + helpLookupName = buildInfo.CommandData.OverrideHelpName ?? ("cmd_" + InvokeName.Replace(" ", "_") + "_help"); requiredRights = new[] { "cmd." + string.Join(".", InvokeName.Split(' ')) }; UsageList = buildInfo.UsageList?.ToArray() ?? Array.Empty(); } - public string GetHelp() - { - if (cachedHelp == null) - { - var strb = new StringBuilder(); - if (!string.IsNullOrEmpty(Description)) - strb.Append("\n!").Append(InvokeName).Append(": ").Append(Description); - - if (UsageList.Length > 0) - { - int longest = UsageList.Max(p => p.UsageSyntax.Length) + 1; - foreach (var para in UsageList) - strb.Append("\n!").Append(InvokeName).Append(" ").Append(para.UsageSyntax) - .Append(' ', longest - para.UsageSyntax.Length).Append(para.UsageHelp); - } - cachedHelp = strb.ToString(); - } - return cachedHelp; - } - public override string ToString() { var strb = new StringBuilder(); - strb.Append('!').Append(InvokeName); - strb.Append(" : "); - foreach (var param in UsageList) - strb.Append(param.UsageSyntax).Append('/'); + strb.Append("\n!").Append(InvokeName).Append(": ").Append(Description ?? strings.error_no_help ?? ""); + + if (UsageList.Length > 0) + { + int longest = UsageList.Max(p => p.UsageSyntax.Length) + 1; + foreach (var para in UsageList) + strb.Append("\n!").Append(InvokeName).Append(" ").Append(para.UsageSyntax) + .Append(' ', longest - para.UsageSyntax.Length).Append(para.UsageHelp); + } return strb.ToString(); } public override ICommandResult Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes) { if (!info.HasRights(requiredRights)) - throw new CommandException($"You cannot execute \"{InvokeName}\". You are missing the \"{RequiredRight}\" right.!", + throw new CommandException(string.Format(strings.error_missing_right, InvokeName, RequiredRight), CommandExceptionReason.MissingRights); + return base.Execute(info, arguments, returnTypes); } @@ -123,6 +125,8 @@ from x in botCmd.CommandParameter select x.type.Name + (x.optional ? "?" : "")).ToArray(); Return = UnwrapReturnType(botCmd.CommandReturn).Name; } + + public override string ToString() => botCmd.ToString(); } } diff --git a/TS3AudioBot/CommandSystem/CommandAttribute.cs b/TS3AudioBot/CommandSystem/CommandAttribute.cs index e376c2da..a2120e79 100644 --- a/TS3AudioBot/CommandSystem/CommandAttribute.cs +++ b/TS3AudioBot/CommandSystem/CommandAttribute.cs @@ -18,14 +18,14 @@ namespace TS3AudioBot.CommandSystem [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class CommandAttribute : Attribute { - public CommandAttribute(string commandNameSpace, string help = null) + public CommandAttribute(string commandNameSpace, string overrideHelpName = null) { CommandNameSpace = commandNameSpace; - CommandHelp = help; + OverrideHelpName = overrideHelpName; } public string CommandNameSpace { get; } - public string CommandHelp { get; } + public string OverrideHelpName { get; } } /// diff --git a/TS3AudioBot/CommandSystem/CommandManager.cs b/TS3AudioBot/CommandSystem/CommandManager.cs index a2e30a2c..43539f0e 100644 --- a/TS3AudioBot/CommandSystem/CommandManager.cs +++ b/TS3AudioBot/CommandSystem/CommandManager.cs @@ -22,101 +22,67 @@ public class CommandManager { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); private static readonly Regex CommandNamespaceValidator = - new Regex(@"^[a-z]+( [a-z]+)*$", Util.DefaultRegexConfig & ~RegexOptions.IgnoreCase); + new Regex("^[a-z]+( [a-z]+)*$", Util.DefaultRegexConfig & ~RegexOptions.IgnoreCase); - private readonly List baseCommands; private readonly HashSet commandPaths; - private readonly List dynamicCommands; - private readonly Dictionary> pluginCommands; + private readonly HashSet baggedCommands; + + public Rights.RightsManager RightsManager { get; set; } public CommandManager() { CommandSystem = new XCommandSystem(); - Util.Init(out baseCommands); Util.Init(out commandPaths); - Util.Init(out dynamicCommands); - Util.Init(out pluginCommands); + Util.Init(out baggedCommands); } public void Initialize() { - RegisterMain(); + RegisterCollection(MainCommands.Bag); } public XCommandSystem CommandSystem { get; } - public IEnumerable AllCommands - { - get - { - foreach (var com in baseCommands) - yield return com; - foreach (var com in dynamicCommands) - yield return com; - foreach (var comArr in pluginCommands.Values) - foreach (var com in comArr) - yield return com; - // todo alias - } - } - - public IEnumerable AllRights => AllCommands.Select(x => x.RequiredRight); + public IEnumerable AllCommands => baggedCommands.SelectMany(x => x.BagCommands); - public void RegisterMain() - { - if (baseCommands.Count > 0) - throw new InvalidOperationException("Operation can only be executed once."); - - foreach (var com in GetBotCommands(GetCommandMethods(null, typeof(MainCommands)))) - { - LoadCommand(com); - baseCommands.Add(com); - } - } + public IEnumerable AllRights => AllCommands.Select(x => x.RequiredRight).Concat(baggedCommands.SelectMany(x => x.AdditionalRights)); - public void RegisterCommand(BotCommand command) + public void RegisterCollection(ICommandBag bag) { - LoadCommand(command); - dynamicCommands.Add(command); - } - - internal void RegisterCollection(ICommandBag bag) - { - if (pluginCommands.ContainsKey(bag)) + if (baggedCommands.Contains(bag)) throw new InvalidOperationException("This bag is already loaded."); - var comList = bag.ExposedCommands.ToList(); - - CheckDistinct(comList); - - pluginCommands.Add(bag, comList.AsReadOnly()); + CheckDistinct(bag.BagCommands); + baggedCommands.Add(bag); - int loaded = 0; try { - for (; loaded < comList.Count; loaded++) - LoadCommand(comList[loaded]); + foreach (var command in bag.BagCommands) + LoadCommand(command); + RightsManager?.SetRightsList(AllRights); } - catch // TODO test + catch (Exception ex) { - for (int i = 0; i <= loaded && i < comList.Count; i++) - UnloadCommand(comList[i]); + Log.Error(ex, "Failed to load command bag."); + UnregisterCollection(bag); throw; } } - internal void UnregisterCollection(ICommandBag bag) + public void UnregisterCollection(ICommandBag bag) { - if (pluginCommands.TryGetValue(bag, out var commands)) + if (baggedCommands.Remove(bag)) { - pluginCommands.Remove(bag); - foreach (var com in commands) + foreach (var com in bag.BagCommands) { UnloadCommand(com); } + RightsManager?.SetRightsList(AllRights); } } + public static IEnumerable GetBotCommands(object obj, Type type = null) => GetBotCommands(GetCommandMethods(obj, type)); + public static IEnumerable GetBotCommands(IEnumerable methods) { foreach (var botData in methods) @@ -130,26 +96,30 @@ public static IEnumerable GetCommandMethods(object obj, Type t { if (obj == null && type == null) throw new ArgumentNullException(nameof(type), "No type information given."); - var objType = type ?? obj.GetType(); - - foreach (var method in objType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) + return GetCommandMethodsIterator(); + IEnumerable GetCommandMethodsIterator() { - var comAtt = method.GetCustomAttribute(); - if (comAtt == null) continue; - if (obj == null && !method.IsStatic) + var objType = type ?? obj.GetType(); + + foreach (var method in objType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) { - Log.Warn("Method '{0}' needs an instance, but no instance was provided. It will be ignored.", method.Name); - continue; + var comAtt = method.GetCustomAttribute(); + if (comAtt == null) continue; + if (obj == null && !method.IsStatic) + { + Log.Warn("Method '{0}' needs an instance, but no instance was provided. It will be ignored.", method.Name); + continue; + } + yield return new CommandBuildInfo(obj, method, comAtt); } - yield return new CommandBuildInfo(obj, method, comAtt); } } - private static void CheckDistinct(ICollection list) // TODO test + private static void CheckDistinct(IReadOnlyCollection list) { - if (list.Select(c => c.InvokeName).Distinct().Count() < list.Count) + if (list.Select(c => c.FullQualifiedName).Distinct().Count() < list.Count) { - var duplicates = list.GroupBy(c => c.InvokeName).Where(g => g.Count() > 1).Select(g => g.Key); + var duplicates = list.GroupBy(c => c.FullQualifiedName).Where(g => g.Count() > 1).Select(g => g.Key); throw new InvalidOperationException("The object contains duplicates: " + string.Join(", ", duplicates)); } } @@ -164,8 +134,8 @@ private void LoadCommand(BotCommand com) // TODO test throw new InvalidOperationException("BotCommand has an invalid invoke name: " + com.InvokeName); if (commandPaths.Contains(com.FullQualifiedName)) throw new InvalidOperationException("Command already exists: " + com.InvokeName); - commandPaths.Add(com.FullQualifiedName); + commandPaths.Add(com.FullQualifiedName); LoadICommand(com, com.InvokeName); } @@ -182,7 +152,7 @@ private void LoadICommand(ICommand com, string path) GenerateError(result.Error, com as BotCommand); } - private R BuildAndGet(IEnumerable comPath) + private R BuildAndGet(IEnumerable comPath) { CommandGroup group = CommandSystem.RootCommand; // this for loop iterates through the seperate names of @@ -222,7 +192,7 @@ private R BuildAndGet(IEnumerable comPath) return group; } - private static R InsertInto(CommandGroup group, ICommand com, string name) + private static E InsertInto(CommandGroup group, ICommand com, string name) { var subCommand = group.GetCommand(name); @@ -232,7 +202,7 @@ private static R InsertInto(CommandGroup group, ICommand com, string name) // the group we are trying to insert has no element with the current // name, so just insert it group.AddCommand(name, com); - return R.OkR; + return R.Ok; case CommandGroup insertCommand: // to add a command to CommandGroup will have to treat it as a subcommand @@ -243,7 +213,7 @@ private static R InsertInto(CommandGroup group, ICommand com, string name) insertCommand.AddCommand(string.Empty, com); if (com is BotCommand botCom && botCom.NormalParameters > 0) Log.Warn("\"{0}\" has at least one parameter and won't be reachable due to an overloading function.", botCom.FullQualifiedName); - return R.OkR; + return R.Ok; } else return "An empty named function under a group cannot be overloaded."; @@ -273,7 +243,7 @@ private static R InsertInto(CommandGroup group, ICommand com, string name) return "Unknown node to insert to."; } - return R.OkR; + return R.Ok; } private static void GenerateError(string msg, BotCommand involvedCom) @@ -286,9 +256,8 @@ private static void GenerateError(string msg, BotCommand involvedCom) private void UnloadCommand(BotCommand com) { - if (!commandPaths.Contains(com.FullQualifiedName)) + if (!commandPaths.Remove(com.FullQualifiedName)) return; - commandPaths.Remove(com.FullQualifiedName); var comPath = com.InvokeName.Split(' '); diff --git a/TS3AudioBot/CommandSystem/CommandParser.cs b/TS3AudioBot/CommandSystem/CommandParser.cs index 830817ad..7805ac05 100644 --- a/TS3AudioBot/CommandSystem/CommandParser.cs +++ b/TS3AudioBot/CommandSystem/CommandParser.cs @@ -57,7 +57,9 @@ public static AstNode ParseCommandRequest(string request, char commandChar = Def strPtr.SkipChar(delimeterChar); if (strPtr.End) + { build = BuildStatus.End; + } else { switch (strPtr.Char) @@ -66,20 +68,28 @@ public static AstNode ParseCommandRequest(string request, char commandChar = Def build = BuildStatus.ParseQuotedString; //goto case BuildStatus.ParseQuotedString; break; + case '(': if (!strPtr.HasNext) + { build = BuildStatus.ParseFreeString; + } else if (strPtr.IsNext(commandChar)) { strPtr.Next('('); build = BuildStatus.ParseCommand; } else + { build = BuildStatus.ParseFreeString; + } break; + case ')': if (comAst.Count <= 0) + { build = BuildStatus.End; + } else { comAst.Pop(); @@ -88,6 +98,7 @@ public static AstNode ParseCommandRequest(string request, char commandChar = Def } strPtr.Next(); break; + default: build = BuildStatus.ParseFreeString; break; @@ -96,8 +107,6 @@ public static AstNode ParseCommandRequest(string request, char commandChar = Def break; case BuildStatus.ParseFreeString: - strb.Clear(); - var valFreeAst = new AstValue(); using (strPtr.TrackNode(valFreeAst)) { @@ -106,11 +115,12 @@ public static AstNode ParseCommandRequest(string request, char commandChar = Def if ((strPtr.Char == '(' && strPtr.HasNext && strPtr.IsNext(commandChar)) || strPtr.Char == ')' || strPtr.Char == delimeterChar) + { break; - strb.Append(strPtr.Char); + } } } - valFreeAst.Value = strb.ToString(); + valFreeAst.BuildValue(); buildCom = comAst.Peek(); buildCom.Parameter.Add(valFreeAst); build = BuildStatus.SelectParam; @@ -127,14 +137,21 @@ public static AstNode ParseCommandRequest(string request, char commandChar = Def bool escaped = false; for (; !strPtr.End; strPtr.Next()) { - if (strPtr.Char == '\\') escaped = true; + if (strPtr.Char == '\\') + { + escaped = true; + } else if (strPtr.Char == '"') { - if (escaped) strb.Length--; + if (escaped) { strb.Length--; } else { strPtr.Next(); break; } escaped = false; } - else escaped = false; + else + { + escaped = false; + } + strb.Append(strPtr.Char); } } @@ -208,7 +225,7 @@ public NodeTracker TrackNode(AstNode node) astnode.Position = index; astnode.Length = 0; } - return (curTrack = new NodeTracker(this)); + return curTrack = new NodeTracker(this); } private void UntrackNode() diff --git a/TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs b/TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs index ec1ce992..e4560f51 100644 --- a/TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs +++ b/TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs @@ -9,8 +9,12 @@ namespace TS3AudioBot.CommandSystem.CommandResults { - public class EmptyCommandResult : ICommandResult + public sealed class EmptyCommandResult : ICommandResult { + public static EmptyCommandResult Instance { get; } = new EmptyCommandResult(); + + private EmptyCommandResult() { } + public CommandResultType ResultType => CommandResultType.Empty; public override string ToString() => string.Empty; } diff --git a/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs b/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs index 6774b6dc..e8dfd484 100644 --- a/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs +++ b/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs @@ -52,7 +52,6 @@ public virtual ICommandResult Execute(ExecutionInformation info, IReadOnlyListAll parameter types, including special types. - public (Type type, ParamKind kind, bool optional)[] CommandParameter { get; } + public ParamInfo[] CommandParameter { get; } /// Return type of method. public Type CommandReturn { get; } /// Count of parameter, without special types. @@ -39,7 +39,7 @@ public class FunctionCommand : ICommand public FunctionCommand(MethodInfo command, object obj = null, int? requiredParameters = null) { internCommand = command; - CommandParameter = command.GetParameters().Select(p => (p.ParameterType, ParamKind.Unknown, p.IsOptional || p.GetCustomAttribute() != null)).ToArray(); + CommandParameter = command.GetParameters().Select(p => new ParamInfo(p, ParamKind.Unknown, p.IsOptional || p.GetCustomAttribute() != null)).ToArray(); PrecomputeTypes(); CommandReturn = command.ReturnType; @@ -81,7 +81,7 @@ protected virtual object ExecuteFunction(object[] parameters) public R FitArguments(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes, out int takenArguments) { var parameters = new object[CommandParameter.Length]; - var filterLazy = new Lazy(() =>info.TryGet(out var filter) ? filter : Algorithm.Filter.DefaultFilter, false); + var filterLazy = new Lazy(() => info.TryGet(out var filter) ? filter : Algorithm.Filter.DefaultFilter, false); // takenArguments: Index through arguments which have been moved into a parameter // p: Iterate through parameters @@ -152,11 +152,11 @@ public R FitArguments(ExecutionInformation info, IRe // Check if we were able to set enough arguments if (takenArguments < Math.Min(parameters.Length, RequiredParameters) && !returnTypes.Contains(CommandResultType.Command)) - throw new CommandException("Not enough arguments for function " + internCommand.Name, CommandExceptionReason.MissingParameter); + return new CommandException("Not enough arguments for function " + internCommand.Name, CommandExceptionReason.MissingParameter); return parameters; } - + public virtual ICommandResult Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes) { // Make arguments lazy, we only want to execute them once @@ -196,7 +196,7 @@ public virtual ICommandResult Execute(ExecutionInformation info, IReadOnlyList)) - CommandParameter[i].kind = ParamKind.SpecialArguments; + paramInfo.kind = ParamKind.SpecialArguments; else if (arg == typeof(IReadOnlyList)) - CommandParameter[i].kind = ParamKind.SpecialReturns; + paramInfo.kind = ParamKind.SpecialReturns; else if (arg == typeof(ICommand)) - CommandParameter[i].kind = ParamKind.NormalCommand; + paramInfo.kind = ParamKind.NormalCommand; else if (arg.IsArray) - CommandParameter[i].kind = ParamKind.NormalArray; + paramInfo.kind = ParamKind.NormalArray; else if (arg.IsEnum || XCommandSystem.BasicTypes.Contains(arg) || XCommandSystem.BasicTypes.Contains(UnwrapParamType(arg))) - CommandParameter[i].kind = ParamKind.NormalParam; + paramInfo.kind = ParamKind.NormalParam; else - CommandParameter[i].kind = ParamKind.Dependency; + paramInfo.kind = ParamKind.Dependency; } } @@ -270,6 +271,8 @@ public static Type UnwrapReturnType(Type type) return type.GenericTypeArguments[0]; if (genDef == typeof(JsonValue<>)) return type.GenericTypeArguments[0]; + if (genDef == typeof(JsonArray<>)) + return type.GenericTypeArguments[0].MakeArrayType(); } return type; } @@ -318,6 +321,21 @@ public enum ParamKind NormalArray, } + public struct ParamInfo + { + public ParameterInfo param; + public Type type => param.ParameterType; + public ParamKind kind; + public bool optional; + + public ParamInfo(ParameterInfo param, ParamKind kind, bool optional) + { + this.param = param; + this.kind = kind; + this.optional = optional; + } + } + public static class FunctionCommandExtensions { public static bool IsNormal(this ParamKind kind) => kind == ParamKind.NormalParam || kind == ParamKind.NormalArray || kind == ParamKind.NormalCommand; diff --git a/TS3AudioBot/CommandSystem/ICommandBag.cs b/TS3AudioBot/CommandSystem/ICommandBag.cs index a6ab2810..5fb85479 100644 --- a/TS3AudioBot/CommandSystem/ICommandBag.cs +++ b/TS3AudioBot/CommandSystem/ICommandBag.cs @@ -11,9 +11,9 @@ namespace TS3AudioBot.CommandSystem { using System.Collections.Generic; - internal interface ICommandBag + public interface ICommandBag { - IEnumerable ExposedCommands { get; } - IEnumerable ExposedRights { get; } + IReadOnlyCollection BagCommands { get; } + IReadOnlyCollection AdditionalRights { get; } } } diff --git a/TS3AudioBot/CommandSystem/XCommandSystem.cs b/TS3AudioBot/CommandSystem/XCommandSystem.cs index b4267731..cab1b5e1 100644 --- a/TS3AudioBot/CommandSystem/XCommandSystem.cs +++ b/TS3AudioBot/CommandSystem/XCommandSystem.cs @@ -12,9 +12,9 @@ namespace TS3AudioBot.CommandSystem using Ast; using CommandResults; using Commands; + using Helper; using System; using System.Collections.Generic; - using System.Linq; public class XCommandSystem { @@ -23,6 +23,7 @@ public class XCommandSystem public static readonly CommandResultType[] ReturnString = { CommandResultType.String }; public static readonly CommandResultType[] ReturnStringOrNothing = { CommandResultType.String, CommandResultType.Empty }; public static readonly CommandResultType[] ReturnCommandOrString = { CommandResultType.Command, CommandResultType.String }; + public static readonly CommandResultType[] ReturnAnyPreferNothing = { CommandResultType.Empty, CommandResultType.String, CommandResultType.Json, CommandResultType.Command }; /// /// The order of types, the first item has the highest priority, items not in the list have lower priority. @@ -44,7 +45,7 @@ public XCommandSystem() { RootCommand = new RootCommand(); } - + internal ICommand AstToCommandResult(AstNode node) { switch (node.Type) @@ -60,7 +61,7 @@ internal ICommand AstToCommandResult(AstNode node) case AstType.Value: return new StringCommand(((AstValue)node).Value); default: - throw new NotSupportedException("Seems like there's a new NodeType, this code should not be reached"); + throw Util.UnhandledDefault(node.Type); } } diff --git a/TS3AudioBot/Config/Config.cs b/TS3AudioBot/Config/Config.cs new file mode 100644 index 00000000..7317a689 --- /dev/null +++ b/TS3AudioBot/Config/Config.cs @@ -0,0 +1,175 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Helper; + using Localization; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + public partial class ConfRoot + { + private string fileName; + + public static R Open(string file) + { + var loadResult = Load(file); + if (!loadResult.Ok) + { + Log.Error(loadResult.Error, "Could not load core config."); + return R.Err; + } + + if (!loadResult.Value.CheckAndSet(file)) + return R.Err; + return loadResult.Value; + } + + public static R Create(string file) + { + var newFile = CreateRoot(); + if (!newFile.CheckAndSet(file)) + return newFile; + var saveResult = newFile.Save(file, true); + if (!saveResult.Ok) + { + Log.Error(saveResult.Error, "Failed to save config file '{0}'.", file); + return R.Err; + } + return newFile; + } + + public static R OpenOrCreate(string file) => File.Exists(file) ? Open(file) : Create(file); + + private bool CheckAndSet(string file) + { + fileName = file; + if (!CheckPaths()) + return false; + // further checks... + return true; + } + + private bool CheckPaths() + { + try + { + if (!Directory.Exists(Configs.BotsPath.Value)) + Directory.CreateDirectory(Configs.BotsPath.Value); + } + catch (Exception ex) + { + Log.Error(ex, "Could not create bot config subdirectory."); + return false; + } + return true; + } + + public bool Save() => Save(fileName, false); + + // apply root_path to input path + public string GetFilePath(string path) + { + throw new NotImplementedException(); + } + + internal R NameToPath(string name) + { + var nameResult = Util.IsSafeFileName(name); + if (!nameResult.Ok) + return nameResult.Error; + return Path.Combine(Configs.BotsPath.Value, $"bot_{name}.toml"); + } + + public ConfBot CreateBot() + { + var config = CreateRoot(); + return InitializeBotConfig(config); + } + + public IEnumerable ListAllBots() + { + try + { + return Directory.EnumerateFiles(Configs.BotsPath.Value, "bot_*.toml", SearchOption.TopDirectoryOnly) + .Select(file => + { + var fi = new FileInfo(file); + return fi.Name; + }); + } + catch (Exception ex) + { + Log.Error(ex, "Could not access bot config subdirectory."); + return Array.Empty(); + } + } + + public R GetBotTemplate(string name) + { + string botFile = NameToPath(name).UnwrapThrow(); + var botConfResult = Load(botFile); + if (!botConfResult.Ok) + return botConfResult.Error; + var botConf = InitializeBotConfig(botConfResult.Value); + botConf.Name = name; + return botConf; + } + + private ConfBot InitializeBotConfig(ConfBot config) + { + Bot.Derive(config); + config.Parent = this; + return config; + } + } + + public partial class ConfBot + { + public string Name { get; set; } + + public E SaveNew(string name) + { + var file = GetParent().NameToPath(name).UnwrapThrow(); + if (File.Exists(file)) + return new LocalStr("The file already exists."); // LOC: TODO + var result = SaveInternal(file); + if (result.Ok) + Name = name; + return result; + } + + public E SaveWhenExists() + { + if (string.IsNullOrEmpty(Name)) + return R.Ok; + + var file = GetParent().NameToPath(Name); + if (!file.Ok) + return file.Error; + return SaveInternal(file.Value); + } + + private E SaveInternal(string file) + { + var result = Save(file, false); + if (!result.Ok) + { + Log.Error(result.Error, "An error occoured saving the bot config."); + return new LocalStr(string.Format("An error occoured saving the bot config.")); // LOC: TODO + } + return R.Ok; + } + + public ConfRoot GetParent() => Parent as ConfRoot; + } +} diff --git a/TS3AudioBot/Config/ConfigArray.cs b/TS3AudioBot/Config/ConfigArray.cs new file mode 100644 index 00000000..4107f7cf --- /dev/null +++ b/TS3AudioBot/Config/ConfigArray.cs @@ -0,0 +1,68 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Nett; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + using Helper; + + public class ConfigArray : ConfigValue> + { + public ConfigArray(string key, IReadOnlyList defaultVal, string doc = "") : base(key, defaultVal, doc) { } + + public override void FromToml(TomlObject tomlObject) + { + if (tomlObject != null) + { + var array = tomlObject.TryGetValueArray(); + if (array != null) + { + Value = array; + } + } + } + + public override void ToJson(JsonWriter writer) + { + writer.WriteStartArray(); + foreach (var item in Value) + { + writer.WriteValue(item); + } + writer.WriteEndArray(); + } + + public override E FromJson(JsonReader reader) + { + try + { + if (reader.Read() + && (reader.TokenType == JsonToken.StartArray)) + { + var list = new List(); + while (reader.TryReadValue(out var value)) + { + list.Add(value); + } + + if (reader.TokenType != JsonToken.EndArray) + return $"Expected end of array but found {reader.TokenType}"; + + Value = list; + return R.Ok; + } + return $"Wrong type, expected {typeof(T).Name}, got {reader.TokenType}"; + } + catch (JsonReaderException ex) { return $"Could not read value: {ex.Message}"; } + } + } +} diff --git a/TS3AudioBot/Config/ConfigDynamicTable.cs b/TS3AudioBot/Config/ConfigDynamicTable.cs new file mode 100644 index 00000000..8b91d4b0 --- /dev/null +++ b/TS3AudioBot/Config/ConfigDynamicTable.cs @@ -0,0 +1,76 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Nett; + using System; + using System.Collections.Generic; + using System.Diagnostics; + using Helper; + + [DebuggerDisplay("dyntable:{Key}")] + public class ConfigDynamicTable : ConfigEnumerable, IDynamicTable where T : ConfigEnumerable, new() + { + private readonly Dictionary dynamicTables; + + public ConfigDynamicTable() + { + Util.Init(out dynamicTables); + } + + public override void FromToml(TomlObject tomlObject) + { + base.FromToml(tomlObject); + + dynamicTables.Clear(); + + if (tomlObject != null) + { + if (!(tomlObject is TomlTable tomlTable)) + throw new InvalidCastException(); + + foreach (var child in tomlTable.Rows) + { + var childConfig = Create(child.Key, this, child.Value); + dynamicTables.Add(child.Key, childConfig); + } + } + } + + public override IEnumerable GetAllChildren() => GetAllItems(); + + public override ConfigPart GetChild(string key) => GetItem(key); + + public ConfigPart GetOrCreateChild(string key) => GetOrCreateItem(key); + + public override void Derive(ConfigPart derived) + { + // TODO + } + + public T GetItem(string key) => dynamicTables.TryGetValue(key, out var item) ? item : null; + + public IEnumerable GetAllItems() => dynamicTables.Values; + + public T CreateItem(string key) + { + var childConfig = Create(key, this, null); + dynamicTables.Add(key, childConfig); + return childConfig; + } + + public T GetOrCreateItem(string key) => GetItem(key) ?? CreateItem(key); + } + + public interface IDynamicTable + { + ConfigPart GetOrCreateChild(string key); + } +} diff --git a/TS3AudioBot/Config/ConfigEnumerable.cs b/TS3AudioBot/Config/ConfigEnumerable.cs new file mode 100644 index 00000000..3a6211d8 --- /dev/null +++ b/TS3AudioBot/Config/ConfigEnumerable.cs @@ -0,0 +1,138 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Nett; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + + public abstract class ConfigEnumerable : ConfigPart + { + private static readonly object EmptyObject = new object(); + + protected virtual TomlTable.TableTypes TableType { get => TomlTable.TableTypes.Default; } + public TomlTable TomlObject { get; set; } + public override bool ExpectsString => false; + + public override void FromToml(TomlObject tomlObject) + { + if (tomlObject == null) + { + if (Parent == null) + TomlObject = Toml.Create(); + else + TomlObject = Parent.TomlObject.Add(Key, EmptyObject, TableType); + } + else + { + if (tomlObject is TomlTable tomlTable) + TomlObject = tomlTable; + else + throw new InvalidCastException(); + } + } + + public override void ToToml(bool writeDefaults, bool writeDocumentation) + { + if (writeDocumentation) + CreateDocumentation(TomlObject); + foreach (var part in GetAllChildren()) + { + part.ToToml(writeDefaults, writeDocumentation); + } + } + + public override void ToJson(JsonWriter writer) + { + writer.WriteStartObject(); + foreach (var item in GetAllChildren()) + { + writer.WritePropertyName(item.Key); + item.ToJson(writer); + } + writer.WriteEndObject(); + } + + public override E FromJson(JsonReader reader) + { + try + { + if (!reader.Read() || (reader.TokenType != JsonToken.StartObject)) + return $"Wrong type, expected start of object but found {reader.TokenType}"; + + while (reader.Read() + && (reader.TokenType == JsonToken.PropertyName)) + { + var childName = (string)reader.Value; + var child = GetChild(childName); + if (child == null) + { + if (this is IDynamicTable dynTable) + child = dynTable.GetOrCreateChild(childName); + else + return "No child found"; + } + + child.FromJson(reader); + } + + if (reader.TokenType != JsonToken.EndObject) + return $"Expected end of array but found {reader.TokenType}"; + + return R.Ok; + } + catch (JsonReaderException ex) { return $"Could not read value: {ex.Message}"; } + } + + // Virtual table methods + + public abstract ConfigPart GetChild(string key); + + public abstract IEnumerable GetAllChildren(); + + // Static factory methods + + protected static T Create(string key, string doc = "") where T : ConfigEnumerable, new() + { + return new T + { + Key = key, + Documentation = doc, + }; + } + + protected static T Create(string key, ConfigEnumerable parent, TomlObject fromToml, string doc = "") where T : ConfigEnumerable, new() + { + var table = Create(key, doc); + table.Parent = parent; + table.FromToml(fromToml); + return table; + } + + public static T CreateRoot() where T : ConfigEnumerable, new() => Create(null, null, null, ""); + + public static R Load(string path) where T : ConfigEnumerable, new() + { + TomlTable rootToml; + try { rootToml = Toml.ReadFile(path); } + catch (Exception ex) { return ex; } + return Create(null, null, rootToml); + } + + public E Save(string path, bool writeDefaults, bool writeDocumentation = true) + { + ToToml(writeDefaults, writeDocumentation); + try { Toml.WriteFile(TomlObject, path); } + catch (Exception ex) { return ex; } + return R.Ok; + } + } +} diff --git a/TS3AudioBot/Config/ConfigHelper.cs b/TS3AudioBot/Config/ConfigHelper.cs new file mode 100644 index 00000000..d4ef93c4 --- /dev/null +++ b/TS3AudioBot/Config/ConfigHelper.cs @@ -0,0 +1,47 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using CommandSystem; + using Newtonsoft.Json; + using System; + using System.Linq; + + public static class ConfigHelper + { + public static ConfigPart[] ByPathAsArray(this ConfigPart config, string path) + { + try + { + return config.ByPath(path).ToArray(); + } + catch (Exception ex) + { + throw new CommandException("Invalid TomlPath expression", ex, CommandExceptionReason.CommandError); + } + } + + public static bool TryReadValue(this JsonReader reader, out T value) + { + if (reader.Read() + && (reader.TokenType == JsonToken.Boolean + || reader.TokenType == JsonToken.Date + || reader.TokenType == JsonToken.Float + || reader.TokenType == JsonToken.Integer + || reader.TokenType == JsonToken.String)) + { + value = (T)Convert.ChangeType(reader.Value, typeof(T)); + return true; + } + value = default; + return false; + } + } +} diff --git a/TS3AudioBot/Config/ConfigPart.cs b/TS3AudioBot/Config/ConfigPart.cs new file mode 100644 index 00000000..92e5894a --- /dev/null +++ b/TS3AudioBot/Config/ConfigPart.cs @@ -0,0 +1,236 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Helper; + using Nett; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using static Helper.TomlTools; + + [DebuggerDisplay("unknown:{Key}")] + public abstract class ConfigPart : IJsonSerializable + { + protected static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + + public string Documentation { get; protected set; } + public string Key { get; protected set; } + // must be a field otherwise it will be found as a child for ConfigTable + public ConfigEnumerable Parent; + + protected ConfigPart() { } + protected ConfigPart(string key) + { + Key = key; + } + + public abstract bool ExpectsString { get; } + public abstract void FromToml(TomlObject tomlObject); + public abstract void ToToml(bool writeDefaults, bool writeDocumentation); + public abstract void Derive(ConfigPart derived); + public abstract E FromJson(JsonReader reader); + public abstract void ToJson(JsonWriter writer); + + protected void CreateDocumentation(TomlObject tomlObject) + { + var docs = tomlObject.Comments.Where(x => x.Text.StartsWith("#")).ToArray(); + tomlObject.ClearComments(); + if (!string.IsNullOrEmpty(Documentation)) + tomlObject.AddComment(Documentation); + if (docs.Length > 0) + tomlObject.AddComments(docs); + } + + public override string ToString() => this.ToJson(); + + // *** Path accessor *** + + public IEnumerable ByPath(string path) + { + var pathM = path.AsMemory(); + return ProcessIdentifier(pathM); + } + + private IEnumerable ProcessIdentifier(ReadOnlyMemory pathM) + { + if (pathM.IsEmpty) + return Enumerable.Empty(); + + var path = pathM.Span; + switch (path[0]) + { + case '*': + { + var rest = pathM.Slice(1); + if (rest.IsEmpty) + return GetAllSubItems(); + + if (IsArray(rest.Span)) + return GetAllSubItems().SelectMany(x => x.ProcessArray(rest)); + else if (IsDot(rest.Span)) + return GetAllSubItems().SelectMany(x => x.ProcessDot(rest)); + else + throw new ArgumentException("Invalid expression after wildcard", nameof(path)); + } + + case '[': + throw new ArgumentException("Invalid array open bracket", nameof(path)); + case ']': + throw new ArgumentException("Invalid array close bracket", nameof(path)); + case '.': + throw new ArgumentException("Invalid dot", nameof(path)); + + default: + { + var subItemName = path; + var rest = ReadOnlyMemory.Empty; + bool cont = false; + for (int i = 0; i < path.Length; i++) + { + // todo allow in future + if (path[i] == '*') + throw new ArgumentException("Invalid wildcard position", nameof(path)); + + var currentSub = path.Slice(i); + if (!IsIdentifier(currentSub)) // if (!IsName) + { + cont = true; + subItemName = path.Slice(0, i); + rest = pathM.Slice(i); + break; + } + } + var item = GetSubItemByName(subItemName); + if (item == null) + return Enumerable.Empty(); + + if (cont) + { + if (IsArray(rest.Span)) + return item.ProcessArray(rest); + else if (IsDot(rest.Span)) + return item.ProcessDot(rest); + else + throw new ArgumentException("Invalid expression name identifier", nameof(path)); + } + return new[] { item }; + } + } + } + + private IEnumerable ProcessArray(ReadOnlyMemory pathM) + { + var path = pathM.Span; + if (path[0] != '[') + throw new ArgumentException("Expected array open breacket", nameof(path)); + for (int i = 1; i < path.Length; i++) + { + if (path[i] == ']') + { + if (i == 0) + throw new ArgumentException("Empty array indexer", nameof(path)); + var indexer = path.Slice(1, i - 1); + var rest = pathM.Slice(i + 1); + bool cont = rest.Length > 0; + + // select + if (indexer.Length == 1 && indexer[0] == '*') + { + var ret = GetAllArrayItems(); + if (cont) + { + if (IsArray(rest.Span)) + return ret.SelectMany(x => x.ProcessArray(rest)); + else if (IsDot(rest.Span)) + return ret.SelectMany(x => x.ProcessDot(rest)); + else + throw new ArgumentException("Invalid expression after array indexer", nameof(path)); + } + + return ret; + } + else + { + var ret = GetArrayItemByIndex(indexer); + if (ret == null) + return Enumerable.Empty(); + + if (cont) + { + if (IsArray(rest.Span)) + return ret.ProcessArray(rest); + else if (IsDot(rest.Span)) + return ret.ProcessDot(rest); + else + throw new ArgumentException("Invalid expression after array indexer", nameof(path)); + } + return new[] { ret }; + } + } + } + throw new ArgumentException("Missing array close bracket", nameof(path)); + } + + private IEnumerable ProcessDot(ReadOnlyMemory pathM) + { + var path = pathM.Span; + if (!IsDot(path)) + throw new ArgumentException("Expected dot", nameof(path)); + + var rest = pathM.Slice(1); + if (!IsIdentifier(rest.Span)) + throw new ArgumentException("Expected identifier after dot", nameof(path)); + + return ProcessIdentifier(rest); + } + + private ConfigPart GetArrayItemByIndex(ReadOnlySpan index) + { + var indexNum = new string(index.ToArray()); + + //if (!System.Buffers.Text.Utf8Parser.TryParse(index, out int indexNum, out int bytesConsumed)) + //throw new ArgumentException("Invalid array indexer"); + if (this is ConfigEnumerable table) + { + return table.GetChild(indexNum); + } + /*else if (this is ConfigValue<[]ARRAY> array) + { + // TODO + }*/ + return null; + } + + private IEnumerable GetAllArrayItems() + { + if (this is ConfigEnumerable table) + return table.GetAllChildren(); + return Enumerable.Empty(); + } + + private ConfigPart GetSubItemByName(ReadOnlySpan name) + { + var indexNum = new string(name.ToArray()); + if (this is ConfigEnumerable table) + return table.GetChild(indexNum); + return null; + } + + private IEnumerable GetAllSubItems() + { + if (this is ConfigEnumerable table) + return table.GetAllChildren(); + return Enumerable.Empty(); + } + } +} diff --git a/TS3AudioBot/Config/ConfigStructs.cs b/TS3AudioBot/Config/ConfigStructs.cs new file mode 100644 index 00000000..8de91ae1 --- /dev/null +++ b/TS3AudioBot/Config/ConfigStructs.cs @@ -0,0 +1,263 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Nett; + + public partial class ConfRoot : ConfigTable + { + public ConfBot Bot { get; } = Create("bot", + "! IMPORTANT !\n" + + "All config tables here starting with 'bot.*' will only be used as default values for each bot.\n" + + "To make bot-instance specific changes go to the 'Bots' folder (configs.bots_path) and set your configuration values in the desired bot config."); + public ConfBots Bots { get; } = Create("bots", + "You can create new subtables matching the bot config name to configure meta-settings for each bot.\n" + + "Current layout: { run:bool }"); + public ConfConfigs Configs { get; } = Create("configs"); + public ConfDb Db { get; } = Create("db"); + public ConfFactories Factories { get; } = Create("factories"); + public ConfTools Tools { get; } = Create("tools"); + public ConfRights Rights { get; } = Create("rights"); + public ConfPlugins Plugins { get; } = Create("plugins"); + public ConfWeb Web { get; } = Create("web"); + + //public ConfigValue ActiveDocumentation { get; } = new ConfigValue("_active_doc", true); + } + + public class ConfBots : ConfigDynamicTable { } + + public class BotTemplate : ConfigTable + { + protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Inline; + + public ConfigValue Run { get; } = new ConfigValue("run", false); + } + + public class ConfConfigs : ConfigTable + { + //public ConfigValue RootPath { get; } = new ConfigValue("root_path", "."); // TODO enable when done + public ConfigValue BotsPath { get; } = new ConfigValue("bots_path", "Bots", + "Path to a folder where the configuration files for each bot template will be stored."); + } + + public class ConfDb : ConfigTable + { + public ConfigValue Path { get; } = new ConfigValue("path", "ts3audiobot.db", + "The path to the database file for persistent data."); + } + + public class ConfFactories : ConfigTable + { + public ConfPath Media { get; } = Create("media", + "The default path to look for local resources."); + } + + public class ConfTools : ConfigTable + { + // youtube-dl can be empty by default as we make some thorough lookups. + public ConfPath YoutubeDl { get; } = Create("youtube-dl", + "Path to the youtube-dl binary or local git repository."); + public ConfToolsFfmpeg Ffmpeg { get; } = Create("ffmpeg", + "The path to ffmpeg."); + //public ConfPath Ffprobe { get; } = Create("ffprobe"); + } + + public class ConfToolsFfmpeg : ConfigTable + { + public ConfigValue Path { get; } = new ConfigValue("path", "ffmpeg"); + } + + public class ConfRights : ConfigTable + { + public ConfigValue Path { get; } = new ConfigValue("path", "rights.toml", + "Path to the permission file. The file will be generated if it doesn't exist."); + } + + public class ConfPlugins : ConfigTable + { + public ConfigValue Path { get; } = new ConfigValue("path", "Plugins", + "The path to the plugins folder."); + public ConfigValue WriteStatusFiles { get; } = new ConfigValue("write_status_files", false, + "Write to .status files to store a plugin enable status persistently and restart them on launch."); // TODO deprecate + + public ConfPluginsLoad Load { get; } = Create("load"); + } + + public class ConfPluginsLoad : ConfigTable + { + // TODO: dynamic table + } + + public class ConfWeb : ConfigTable + { + public ConfigArray Hosts { get; } = new ConfigArray("hosts", new[] { "localhost", "127.0.0.1" }, + "An array of all urls the web api should be possible to be accessed with."); + public ConfigValue Port { get; } = new ConfigValue("port", 8180, + "The port for the web server."); + + public ConfWebApi Api { get; } = Create("api"); + public ConfWebInterface Interface { get; } = Create("interface"); + } + + public class ConfWebApi : ConfigTable + { + public ConfigValue Enabled { get; } = new ConfigValue("enabled", true, + "If you want to enable the web api."); + } + + public class ConfWebInterface : ConfigTable + { + public ConfigValue Enabled { get; } = new ConfigValue("enabled", true, + "If you want to enable the webinterface."); + public ConfigValue Path { get; } = new ConfigValue("path", "", + "The webinterface folder to host. Leave empty to let the bot look for default locations."); + } + + public partial class ConfBot : ConfigTable + { + public ConfigValue BotGroupId { get; } = new ConfigValue("bot_group_id", 0, + "This field will be automatically set when you call '!bot setup'.\n" + + "The bot will use the specified group to set/update the required permissions and add himself into it.\n" + + "You can set this field manually if you already have a preexisting group the bot should add himself to."); + public ConfigValue GenerateStatusAvatar { get; } = new ConfigValue("generate_status_avatar", true, + "Tries to fetch a cover image when playing."); + public ConfigValue Language { get; } = new ConfigValue("language", "en", + "The language the bot should use to respond to users. (Make sure you have added the required language packs)"); + public ConfigValue CommandMatcher { get; } = new ConfigValue("command_matcher", "ic3", + "Defines how the bot tries to match your !commands. Possible types:\n" + + " - exact : Only when the command matches exactly.\n" + + " - substring : The shortest command starting with the given prefix.\n" + + " - ic3 : 'interleaved continuous character chain' A fuzzy algorithm similar to hamming distance but preferring characters at the start." + /* "hamming : " */); + + public ConfConnect Connect { get; } = Create("connect"); + public ConfAudio Audio { get; } = Create("audio"); + public ConfPlaylists Playlists { get; } = Create("playlists"); + public ConfHistory History { get; } = Create("history"); + } + + public class ConfConnect : ConfigTable + { + public ConfigValue Address { get; } = new ConfigValue("address", "", + "The address, ip or nickname (and port; default: 9987) of the TeamSpeak3 server"); + public ConfigValue Channel { get; } = new ConfigValue("channel", "", + "Default channel when connecting. Use a channel path or '/'.\n" + + "Examples: 'Home/Lobby', '/5', 'Home/Afk \\/ Not Here'."); + public ConfigValue Badges { get; } = new ConfigValue("badges", "", + "The client badges. You can set a comma seperated string with max three GUID's. Here is a list: http://yat.qa/ressourcen/abzeichen-badges/"); + public ConfigValue Name { get; } = new ConfigValue("name", + "TS3AudioBot", "Client nickname when connecting."); + + public ConfPassword ServerPassword { get; } = Create("server_password", + "The server password. Leave empty for none."); + public ConfPassword ChannelPassword { get; } = Create("channel_password", + "The default channel password. Leave empty for none."); + public ConfTsVersion ClientVersion { get; } = Create("client_version", + "Overrides the displayed version for the ts3 client. Leave empty for default."); + public ConfIdentity Identity { get; } = Create("identity"); + } + + public class ConfIdentity : ConfigTable + { + new public ConfigValue Key { get; } = new ConfigValue("key", "", + "||| DO NOT MAKE THIS KEY PUBLIC ||| The client identity. You can import a teamspeak3 identity here too."); + public ConfigValue Offset { get; } = new ConfigValue("offset", 0, + "The client identity offset determining the security level."); + public ConfigValue Level { get; } = new ConfigValue("level", -1, + "The client identity security level which should be calculated before connecting\n" + + "or -1 to generate on demand when connecting."); + } + + public class ConfAudio : ConfigTable + { + public ConfAudioVolume Volume { get; } = Create("volume", + "When a new song starts the volume will be trimmed to between min and max.\n" + + "When the current volume already is between min and max nothing will happen.\n" + + "To completely or partially disable this feature, set min to 0 and/or max to 100."); + public ConfigValue MaxUserVolume { get; } = new ConfigValue("max_user_volume", 30, + "The maximum volume a normal user can request. Only user with the 'ts3ab.admin.volume' permission can request higher volumes."); + public ConfigValue Bitrate { get; } = new ConfigValue("bitrate", 48, + "Specifies the bitrate (in kbps) for sending audio.\n" + + "Values between 8 and 98 are supported, more or less can work but without guarantees.\n" + + "Reference values: 16 - poor (~3KiB/s), 24 - okay (~4KiB/s), 32 - good (~5KiB/s), 48 - very good (~7KiB/s), 64 - not noticeably better than 48, stop wasting your bandwith, go back (~9KiB/s)"); + public ConfigValue SendMode { get; } = new ConfigValue("send_mode", "voice", + "How the bot should play music. Options are:\n" + + " - whisper : Whispers to the channel where the request came from. Other users can join with '!subscribe'.\n" + + " - voice : Sends via normal voice to the current channel. '!subscribe' will not work in this mode.\n" + + " - !... : A custom command. Use '!xecute (!a) (!b)' for example to execute multiple commands."); + } + + public class ConfAudioVolume : ConfigTable + { + protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Default; // TODO inline when Nett has fixed the inline bug. + + public ConfigValue Default { get; } = new ConfigValue("default", 10); + public ConfigValue Min { get; } = new ConfigValue("min", 10); + public ConfigValue Max { get; } = new ConfigValue("max", 50); + } + + public class ConfPlaylists : ConfigTable + { + protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Default; // TODO inline when Nett has fixed the inline bug. + + public ConfigValue Path { get; } = new ConfigValue("path", "Playlists", + "Path to the folder where playlist files will be saved."); + } + + public class ConfHistory : ConfigTable + { + public ConfigValue Enabled { get; } = new ConfigValue("enabled", true, + "Enable or disable history features completely to save resources."); + public ConfigValue FillDeletedIds { get; } = new ConfigValue("fill_deleted_ids", true, + "Whether or not deleted history ids should be filled up with new songs."); + } + + // Utility config structs + + public class ConfPath : ConfigTable + { + protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Default; // TODO inline when Nett has fixed the inline bug. + + public ConfigValue Path { get; } = new ConfigValue("path", string.Empty); + } + + public class ConfPassword : ConfigTable + { + protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Default; // TODO inline when Nett has fixed the inline bug. + + public ConfigValue Password { get; } = new ConfigValue("pw", string.Empty); + public ConfigValue Hashed { get; } = new ConfigValue("hashed", false); + public ConfigValue AutoHash { get; } = new ConfigValue("autohash", false); + + public TS3Client.Password Get() + { + if (string.IsNullOrEmpty(Password)) + return TS3Client.Password.Empty; + var pass = Hashed + ? TS3Client.Password.FromHash(Password) + : TS3Client.Password.FromPlain(Password); + if (AutoHash && !Hashed) + { + Password.Value = pass.HashedPassword; + Hashed.Value = true; + } + return pass; + } + } + + public class ConfTsVersion : ConfigTable + { + protected override TomlTable.TableTypes TableType => TomlTable.TableTypes.Default; // TODO inline when Nett has fixed the inline bug. + + public ConfigValue Build { get; } = new ConfigValue("build", string.Empty); + public ConfigValue Platform { get; } = new ConfigValue("platform", string.Empty); + public ConfigValue Sign { get; } = new ConfigValue("sign", string.Empty); + } +} diff --git a/TS3AudioBot/Config/ConfigTable.cs b/TS3AudioBot/Config/ConfigTable.cs new file mode 100644 index 00000000..debbc2de --- /dev/null +++ b/TS3AudioBot/Config/ConfigTable.cs @@ -0,0 +1,67 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Nett; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + + [DebuggerDisplay("table:{Key}")] + public abstract class ConfigTable : ConfigEnumerable + { + protected List Properties { get; } = new List(); + + protected ConfigTable() + { + GetMember(); + foreach (var configPart in Properties) + configPart.Parent = this; + } + + private IEnumerable GetConfigPartProperties() + { + return GetType() + .GetProperties() + .Where(x => typeof(ConfigPart).IsAssignableFrom(x.PropertyType)); + } + + private void GetMember() + { + Properties.Clear(); + Properties.AddRange(GetConfigPartProperties().Select(x => (ConfigPart)x.GetValue(this))); + } + + public override void FromToml(TomlObject tomlObject) + { + base.FromToml(tomlObject); + + foreach (var part in Properties) + { + var child = TomlObject.TryGetValue(part.Key); + part.FromToml(child); + } + } + + public override void Derive(ConfigPart derived) + { + foreach (var prop in GetConfigPartProperties()) + { + var self = (ConfigPart)prop.GetValue(this); + var other = (ConfigPart)prop.GetValue(derived); + self.Derive(other); + } + } + + public override ConfigPart GetChild(string key) => Properties.FirstOrDefault(x => x.Key == key); + + public override IEnumerable GetAllChildren() => Properties; + } +} diff --git a/TS3AudioBot/Config/ConfigValue.cs b/TS3AudioBot/Config/ConfigValue.cs new file mode 100644 index 00000000..eec3f2ed --- /dev/null +++ b/TS3AudioBot/Config/ConfigValue.cs @@ -0,0 +1,135 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config +{ + using Nett; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + using System.Diagnostics; + using Helper; + + [DebuggerDisplay("{Key}:{Value}")] + public class ConfigValue : ConfigPart + { + public override bool ExpectsString => typeof(T) == typeof(string); + private ConfigValue backingValue; + private bool hasValue = false; + public T Default { get; } + private T value; + public T Value + { + get + { + if (hasValue) + return value; + if (backingValue != null) + return backingValue.Value; + return Default; + } + set + { + hasValue = true; + if (EqualityComparer.Default.Equals(this.value, value)) + return; + this.value = value; + if (Changed != null) + { + var args = new ConfigChangedEventArgs(value); + Changed?.Invoke(this, args); + } + } + } + + public event EventHandler> Changed; + + public ConfigValue(string key, T defaultVal, string doc = "") : base(key) + { + Documentation = doc; + Default = defaultVal; + } + + private void InvokeChange(object sender, ConfigChangedEventArgs args) => Changed?.Invoke(sender, args); + + public override void FromToml(TomlObject tomlObject) + { + if (tomlObject != null) + { + if (tomlObject.TryGetValue(out var tomlValue)) + Value = tomlValue; + else + Log.Warn("Failed to read '{0}', got {1} with {2}", Key, tomlObject.ReadableTypeName, tomlObject.DumpToJson()); + } + } + + public override void ToToml(bool writeDefaults, bool writeDocumentation) + { + // Keys with underscore are read-only + if (Key.StartsWith("_")) + return; + + // Set field if either + // - this value is set + // - or we explicitely want to write out default values + var selfToml = Parent.TomlObject.TryGetValue(Key); + if (hasValue || (writeDefaults && selfToml == null)) // TODO optimize: check if existing value is same as Own.Value + { + selfToml = Parent.TomlObject.Set(Key, Value); + } + if (writeDocumentation && selfToml != null) + { + CreateDocumentation(selfToml); + } + } + + public override void Derive(ConfigPart derived) + { + if (derived is ConfigValue derivedValue) + { + derivedValue.backingValue = this; + Changed -= derivedValue.InvokeChange; + Changed += derivedValue.InvokeChange; + } + } + + public override void ToJson(JsonWriter writer) + { + writer.WriteValue(Value); + } + + public override E FromJson(JsonReader reader) + { + try + { + if (reader.TryReadValue(out var tomlValue)) + { + Value = tomlValue; + return R.Ok; + } + return $"Wrong type, expected {typeof(T).Name}, got {reader.TokenType}"; + } + catch (JsonReaderException ex) { return $"Could not read value: {ex.Message}"; } + } + + public override string ToString() => Value.ToString(); + + public static implicit operator T(ConfigValue conf) => conf.Value; + } + + public class ConfigChangedEventArgs : EventArgs + { + public T NewValue { get; } + + public ConfigChangedEventArgs(T newV) + { + NewValue = newV; + } + } +} diff --git a/TS3AudioBot/Helper/ConfigFile.cs b/TS3AudioBot/Config/Deprecated/ConfigFile.cs similarity index 87% rename from TS3AudioBot/Helper/ConfigFile.cs rename to TS3AudioBot/Config/Deprecated/ConfigFile.cs index 9874203c..6f1ce010 100644 --- a/TS3AudioBot/Helper/ConfigFile.cs +++ b/TS3AudioBot/Config/Deprecated/ConfigFile.cs @@ -7,16 +7,17 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -namespace TS3AudioBot.Helper +namespace TS3AudioBot.Config.Deprecated { - using PropertyChanged; + using Helper; + using Localization; using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; - using System.Reflection; using System.Linq; + using System.Reflection; public abstract class ConfigFile { @@ -117,14 +118,14 @@ protected virtual void RegisterConfigObj(ConfigData obj) confObjects.Add(obj.AssociatedClass, obj); } - public R SetSetting(string key, string value) + public E SetSetting(string key, string value) { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); string[] keyParam = key.Split(new[] { NameSeperator }, StringSplitOptions.None); if (!confObjects.TryGetValue(keyParam[0], out var co)) - return "No active entries found for this key"; + return new LocalStr(strings.error_config_no_key_found); object convertedValue; PropertyInfo prop = co.GetType().GetProperty(keyParam[1], BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); @@ -134,11 +135,11 @@ public R SetSetting(string key, string value) } catch (Exception ex) when (ex is FormatException || ex is OverflowException) { - return "The value could not be parsed"; + return new LocalStr(strings.error_config_value_parse_error); } - prop.SetValue(co, convertedValue); + co.SetUnsafe(prop.Name, convertedValue); WriteValueToConfig(key, convertedValue); - return R.OkR; + return R.Ok; } protected void WriteValueToConfig(string entryName, object value) @@ -256,8 +257,10 @@ private void ConfigDataPropertyChanged(object sender, PropertyChangedEventArgs e return; string key = cd.AssociatedClass + NameSeperator + e.PropertyName; - var property = cd.GetType().GetProperty(e.PropertyName); - WriteValueToConfig(key, property.GetValue(cd)); + if (cd.TryGet(e.PropertyName, out var propValue)) + WriteValueToConfig(key, propValue); + else + throw new InvalidOperationException($"No config entry with this value found '{e.PropertyName}'"); } public override void Close() @@ -343,13 +346,31 @@ public override void Close() { } } } - // disable 'event is never used' since fody generates the usage for us -#pragma warning disable CS0067 - public class ConfigData : INotifyPropertyChanged + public class ConfigData { - [DoNotNotify] internal string AssociatedClass { get; set; } - public event PropertyChangedEventHandler PropertyChanged; + private readonly Dictionary Values = new Dictionary(); + internal event PropertyChangedEventHandler PropertyChanged; + + protected T Get([System.Runtime.CompilerServices.CallerMemberName] string memberName = "") + { + if (Values.TryGetValue(memberName.ToUpperInvariant(), out var data)) + return (T)data; + return default; + } + + internal bool TryGet(string memberName, out object value) => Values.TryGetValue(memberName.ToUpperInvariant(), out value); + + protected void Set(T value, + [System.Runtime.CompilerServices.CallerMemberName] string memberName = "") + { + Values[memberName.ToUpperInvariant()] = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memberName)); + } + + internal void SetUnsafe(string memberName, object value) + { + Values[memberName.ToUpperInvariant()] = value; + } } -#pragma warning restore CS0067 } diff --git a/TS3AudioBot/Helper/InfoAttribute.cs b/TS3AudioBot/Config/Deprecated/InfoAttribute.cs similarity index 86% rename from TS3AudioBot/Helper/InfoAttribute.cs rename to TS3AudioBot/Config/Deprecated/InfoAttribute.cs index 946ce206..69033e2d 100644 --- a/TS3AudioBot/Helper/InfoAttribute.cs +++ b/TS3AudioBot/Config/Deprecated/InfoAttribute.cs @@ -7,7 +7,7 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -namespace TS3AudioBot.Helper +namespace TS3AudioBot.Config.Deprecated { using System; @@ -15,8 +15,8 @@ namespace TS3AudioBot.Helper internal sealed class InfoAttribute : Attribute { public bool HasDefault => DefaultValue != null; - public string Description { get; private set; } - public string DefaultValue { get; private set; } + public string Description { get; } + public string DefaultValue { get; } public InfoAttribute(string description) { diff --git a/TS3AudioBot/Config/Deprecated/OldConfigStructs.cs b/TS3AudioBot/Config/Deprecated/OldConfigStructs.cs new file mode 100644 index 00000000..daa4cb08 --- /dev/null +++ b/TS3AudioBot/Config/Deprecated/OldConfigStructs.cs @@ -0,0 +1,132 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config.Deprecated +{ + public class WebData : ConfigData + { + [Info("A space seperated list of all urls the web api should be possible to be accessed with", "")] + public string HostAddress { get => Get(); set => Set(value); } + + [Info("The port for the api server", "8180")] + public ushort Port { get => Get(); set => Set(value); } + + [Info("If you want to start the web api server.", "false")] + public bool EnableApi { get => Get(); set => Set(value); } + + [Info("If you want to start the webinterface server", "false")] + public bool EnableWebinterface { get => Get(); set => Set(value); } + + [Info("The folder to host. Leave empty to let the bot look for default locations.", "")] + public string WebinterfaceHostPath { get => Get(); set => Set(value); } + } + + public class RightsManagerData : ConfigData + { + [Info("Path to the config file", "rights.toml")] + public string RightsFile { get => Get(); set => Set(value); } + } + + public class YoutubeFactoryData : ConfigData + { + [Info("Path to the youtube-dl binary or local git repository", "")] + public string YoutubedlPath { get => Get(); set => Set(value); } + } + + public class PluginManagerData : ConfigData + { + [Info("The absolute or relative path to the plugins folder", "Plugins")] + public string PluginPath { get => Get(); set => Set(value); } + + [Info("Write to .status files to store a plugin enable status persistently and restart them on launch.", "false")] + public bool WriteStatusFiles { get => Get(); set => Set(value); } + } + + public class MediaFactoryData : ConfigData + { + [Info("The default path to look for local resources.", "")] + public string DefaultPath { get => Get(); set => Set(value); } + } + + internal class MainBotData : ConfigData + { + [Info("The language the bot should use to respond to users. (Make sure you have added the required language packs)", "en")] + public string Language { get => Get(); set => Set(value); } + [Info("Teamspeak group id giving the Bot enough power to do his job", "0")] + public ulong BotGroupId { get => Get(); set => Set(value); } + [Info("Generate fancy status images as avatar", "true")] + public bool GenerateStatusAvatar { get => Get(); set => Set(value); } + [Info("Defines how the bot tries to match your !commands.\n" + + "# Possible types: exact, substring, ic3, hamming", "ic3")] + public string CommandMatching { get => Get(); set => Set(value); } + } + + public class HistoryManagerData : ConfigData + { + [Info("Allows to enable or disable history features completely to save resources.", "true")] + public bool EnableHistory { get => Get(); set => Set(value); } + [Info("The Path to the history database file", "history.db")] + public string HistoryFile { get => Get(); set => Set(value); } + [Info("Whether or not deleted history ids should be filled up with new songs", "true")] + public bool FillDeletedIds { get => Get(); set => Set(value); } + } + + public class PlaylistManagerData : ConfigData + { + [Info("Path the playlist folder", "Playlists")] + public string PlaylistPath { get => Get(); set => Set(value); } + } + + public class AudioFrameworkData : ConfigData + { + [Info("The default volume a song should start with", "10")] + public float DefaultVolume { get => Get(); set => Set(value); } + [Info("The maximum volume a normal user can request", "30")] + public float MaxUserVolume { get => Get(); set => Set(value); } + [Info("How the bot should play music. Options are: whisper, voice, (!...)", "whisper")] + public string AudioMode { get => Get(); set => Set(value); } + } + + public class Ts3FullClientData : ConfigData + { + [Info("The address (and port, default: 9987) of the TeamSpeak3 server")] + public string Address { get => Get(); set => Set(value); } + [Info("| DO NOT MAKE THIS KEY PUBLIC | The client identity", "")] + public string Identity { get => Get(); set => Set(value); } + [Info("The client identity security offset", "0")] + public ulong IdentityOffset { get => Get(); set => Set(value); } + [Info("The client identity security level which should be calculated before connecting, or \"auto\" to generate on demand.", "auto")] + public string IdentityLevel { get => Get(); set => Set(value); } + [Info("The server password. Leave empty for none.")] + public string ServerPassword { get => Get(); set => Set(value); } + [Info("Set this to true, if the server password is hashed.", "false")] + public bool ServerPasswordIsHashed { get => Get(); set => Set(value); } + [Info("Enable this to automatically hash and store unhashed passwords.\n" + + "# (Be careful since this will overwrite the 'ServerPassword' field with the hashed value once computed)", "false")] + public bool ServerPasswordAutoHash { get => Get(); set => Set(value); } + [Info("The path to ffmpeg", "ffmpeg")] + public string FfmpegPath { get => Get(); set => Set(value); } + [Info("Specifies the bitrate (in kbps) for sending audio.\n" + + "# Values between 8 and 98 are supported, more or less can work but without guarantees.\n" + + "# Reference values: 32 - ok (~5KiB/s), 48 - good (~7KiB/s), 64 - very good (~9KiB/s)", "48")] + public int AudioBitrate { get => Get(); set => Set(value); } + [Info("Version for the client in the form of ||\n" + + "# Leave empty for default.", "")] + public string ClientVersion { get => Get(); set => Set(value); } + [Info("Default Nickname when connecting", "AudioBot")] + public string DefaultNickname { get => Get(); set => Set(value); } + [Info("Default Channel when connectiong\n" + + "# Use a channel path or '/', examples: 'Home/Lobby', '/5', 'Home/Afk \\/ Not Here'", "")] + public string DefaultChannel { get => Get(); set => Set(value); } + [Info("The password for the default channel. Leave empty for none. Not required with permission b_channel_join_ignore_password", "")] + public string DefaultChannelPassword { get => Get(); set => Set(value); } + [Info("The client badges. You can set a comma seperated string with max three GUID's. Here is a list: http://yat.qa/ressourcen/abzeichen-badges/", "overwolf=0:badges=")] + public string ClientBadges { get => Get(); set => Set(value); } + } +} diff --git a/TS3AudioBot/Config/Deprecated/UpgradeScript.cs b/TS3AudioBot/Config/Deprecated/UpgradeScript.cs new file mode 100644 index 00000000..f0d7485e --- /dev/null +++ b/TS3AudioBot/Config/Deprecated/UpgradeScript.cs @@ -0,0 +1,124 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Config.Deprecated +{ + using System; + using System.IO; + + internal static class UpgradeScript + { + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + + // false on error, true if successful + public static void CheckAndUpgrade(ConfRoot coreConfig, string oldFilename = "configTS3AudioBot.cfg") + { + if (!File.Exists(oldFilename)) + return; + var oldConfig = ConfigFile.OpenOrCreate(oldFilename); + if (oldConfig == null) + { + Log.Error("Old config file '{0}' found but could not be read", oldFilename); + return; + } + + try + { + Upgrade(oldConfig, coreConfig); + } + catch (Exception ex) + { + Log.Error(ex, "Error while upgrading from old config"); + return; + } + + try + { + File.Move(oldFilename, oldFilename + ".old"); + } + catch (Exception ex) + { + Log.Error(ex, "Couldn't move the old config file. Remove it manually to prevent this upgrade step and message."); + } + } + + // false on error, true if successful + private static void Upgrade(ConfigFile from, ConfRoot to) + { + // Read old data + var web = from.GetDataStruct("WebData", true); + var rmd = from.GetDataStruct("RightsManager", true); + var ytd = from.GetDataStruct("YoutubeFactory", true); + var pmd = from.GetDataStruct("PluginManager", true); + var mfd = from.GetDataStruct("MediaFactory", true); + var mbd = from.GetDataStruct("MainBot", true); + var hmd = from.GetDataStruct("HistoryManager", true); + var pld = from.GetDataStruct("PlaylistManager", true); + var afd = from.GetDataStruct("AudioFramework", true); + var qcd = from.GetDataStruct("QueryConnection", true); + + // Get all root stuff and save it + + to.Web.Hosts.Value = web.HostAddress.Split(' '); + to.Web.Port.Value = web.Port; + to.Web.Api.Enabled.Value = web.EnableApi; + to.Web.Interface.Enabled.Value = web.EnableWebinterface; + to.Web.Interface.Path.Value = web.WebinterfaceHostPath; + to.Rights.Path.Value = rmd.RightsFile; + to.Tools.YoutubeDl.Path.Value = ytd.YoutubedlPath; + to.Tools.Ffmpeg.Path.Value = qcd.FfmpegPath; + to.Plugins.Path.Value = pmd.PluginPath; + to.Plugins.WriteStatusFiles.Value = pmd.WriteStatusFiles; + to.Factories.Media.Path.Value = mfd.DefaultPath; + to.Db.Path.Value = hmd.HistoryFile; + + const string defaultBotName = "default"; + var botMetaConfig = to.Bots.GetOrCreateItem(defaultBotName); + botMetaConfig.Run.Value = true; + + to.Save(); + + // Create a default client for all bot instance relate stuff and save + + var bot = to.CreateBot(); + + bot.Language.Value = mbd.Language; + bot.BotGroupId.Value = mbd.BotGroupId; + bot.GenerateStatusAvatar.Value = mbd.GenerateStatusAvatar; + bot.CommandMatcher.Value = mbd.CommandMatching; + bot.History.Enabled.Value = hmd.EnableHistory; + bot.History.FillDeletedIds.Value = hmd.FillDeletedIds; + bot.Playlists.Path.Value = pld.PlaylistPath; + bot.Audio.Volume.Default.Value = afd.DefaultVolume; + bot.Audio.MaxUserVolume.Value = afd.MaxUserVolume; + bot.Audio.SendMode.Value = afd.AudioMode; + bot.Audio.Bitrate.Value = qcd.AudioBitrate; + bot.Connect.Address.Value = qcd.Address; + bot.Connect.Identity.Key.Value = qcd.Identity; + bot.Connect.Identity.Level.Value = qcd.IdentityLevel == "auto" ? -1 : int.Parse(qcd.IdentityLevel); + bot.Connect.Identity.Offset.Value = qcd.IdentityOffset; + bot.Connect.ServerPassword.Password.Value = qcd.ServerPassword; + bot.Connect.ServerPassword.AutoHash.Value = qcd.ServerPasswordAutoHash; + bot.Connect.ServerPassword.Hashed.Value = qcd.ServerPasswordIsHashed; + if (!string.IsNullOrEmpty(qcd.ClientVersion)) + { + var clientVersion = qcd.ClientVersion.Split('|'); + bot.Connect.ClientVersion.Build.Value = clientVersion[0]; + bot.Connect.ClientVersion.Platform.Value = clientVersion[1]; + bot.Connect.ClientVersion.Sign.Value = clientVersion[2]; + } + bot.Connect.Name.Value = qcd.DefaultNickname; + bot.Connect.Channel.Value = qcd.DefaultChannel; + bot.Connect.ChannelPassword.Password.Value = qcd.DefaultChannelPassword; + bot.Connect.Badges.Value = qcd.ClientBadges == "overwolf=0:badges=" ? "" : qcd.ClientBadges; + + bot.SaveNew(defaultBotName); + } + } +} diff --git a/TS3AudioBot/Core.cs b/TS3AudioBot/Core.cs index 67803893..435093a5 100644 --- a/TS3AudioBot/Core.cs +++ b/TS3AudioBot/Core.cs @@ -10,10 +10,9 @@ namespace TS3AudioBot { using CommandSystem; + using Config; using Dependency; using Helper; - using Helper.Environment; - using History; using NLog; using Plugins; using ResourceFactories; @@ -26,199 +25,106 @@ namespace TS3AudioBot public sealed class Core : IDisposable { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private string configFilePath; + private const string DefaultConfigFileName = "ts3audiobot.toml"; + private readonly string configFilePath; private bool forceNextExit; internal static void Main(string[] args) { Thread.CurrentThread.Name = "TAB Main"; - if (LogManager.Configuration == null || LogManager.Configuration.AllTargets.Count == 0) - { - Console.WriteLine("No or empty NLog config found.\n" + - "You can copy the default config from TS3AudioBot/NLog.config.\n" + - "Please refer to https://github.com/NLog/NLog/wiki/Configuration-file " + - "to learn more how to set up your own logging configuration."); + var setup = Setup.ReadParameter(args); - if (LogManager.Configuration == null) - { - Console.WriteLine("Create a default config to prevent this step."); - Console.WriteLine("Do you want to continue? [Y/N]"); - while (true) - { - var key = Console.ReadKey().Key; - if (key == ConsoleKey.N) - return; - if (key == ConsoleKey.Y) - break; - } - } - } + if (setup.Exit == ExitType.Immediately) + return; - var core = new Core(); + if (!setup.SkipVerifications && !Setup.VerifyAll()) + return; + + if (!setup.HideBanner) + Setup.LogHeader(); + + // Initialize the actual core + var core = new Core(setup.ConfigFile); AppDomain.CurrentDomain.UnhandledException += core.ExceptionHandler; Console.CancelKeyPress += core.ConsoleInterruptHandler; + TS3Client.Messages.Deserializer.OnError += (s, e) => Log.Error(e.Exception, "{0}", e); - if (!core.ReadParameter(args)) - return; - - var initResult = core.InitializeCore(); + var initResult = core.Run(!setup.NonInteractive); if (!initResult) { Log.Error("Core initialization failed: {0}", initResult.Error); core.Dispose(); - return; } - - core.Run(); } /// General purpose persistant storage for internal modules. internal DbStore Database { get; set; } /// Manages plugins, provides various loading and unloading mechanisms. internal PluginManager PluginManager { get; set; } - /// Manages a dependency hierachy and injects required modules at runtime. - internal CoreInjector Injector { get; set; } /// Manages factories which can load resources. public ResourceFactoryManager FactoryManager { get; set; } /// Minimalistic webserver hosting the api and web-interface. - public WebManager WebManager { get; set; } - /// Minimalistic config store for automatically serialized classes. - public ConfigFile ConfigManager { get; set; } + public WebServer WebManager { get; set; } /// Management of conntected Bots. public BotManager Bots { get; set; } - public Core() + public Core(string configFilePath = null) { // setting defaults - configFilePath = "configTS3AudioBot.cfg"; - } - - private bool ReadParameter(string[] args) - { - for (int i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "-h": - case "--help": - Console.WriteLine(" --quiet -q Deactivates all output to stdout."); - Console.WriteLine(" --config -c Specifies the path to the config file."); - Console.WriteLine(" --version -V Gets the bot version."); - Console.WriteLine(" --help -h Prints this help...."); - return false; - - case "-c": - case "--config": - if (i >= args.Length - 1) - { - Console.WriteLine("No config file specified after \"{0}\"", args[i]); - return false; - } - configFilePath = args[++i]; - break; - - case "-V": - case "--version": - Console.WriteLine(SystemData.AssemblyData.ToLongString()); - return false; - - default: - Console.WriteLine("Unrecognized parameter: {0}", args[i]); - return false; - } - } - return true; + this.configFilePath = configFilePath ?? DefaultConfigFileName; } - private R InitializeCore() + private E Run(bool interactive = false) { - ConfigManager = ConfigFile.OpenOrCreate(configFilePath) ?? ConfigFile.CreateDummy(); - - // TODO: DUMMY REQUESTS - var webd = ConfigManager.GetDataStruct("WebData", true); - var rmd = ConfigManager.GetDataStruct("RightsManager", true); - ConfigManager.GetDataStruct("MainBot", true); - YoutubeDlHelper.DataObj = ConfigManager.GetDataStruct("YoutubeFactory", true); - var pmd = ConfigManager.GetDataStruct("PluginManager", true); - ConfigManager.GetDataStruct("MediaFactory", true); - var hmd = ConfigManager.GetDataStruct("HistoryManager", true); - ConfigManager.GetDataStruct("AudioFramework", true); - ConfigManager.GetDataStruct("QueryConnection", true); - ConfigManager.GetDataStruct("PlaylistManager", true); - // END TODO - ConfigManager.Close(); - - Log.Info("[============ TS3AudioBot started =============]"); - Log.Info("[=== Date/Time: {0} {1}", DateTime.Now.ToLongDateString(), DateTime.Now.ToLongTimeString()); - Log.Info("[=== Version: {0}", SystemData.AssemblyData); - Log.Info("[=== Platform: {0}", SystemData.PlattformData); - Log.Info("[=== Runtime: {0}", SystemData.RuntimeData.FullName); - Log.Info("[=== Opus: {0}", TS3Client.Audio.Opus.NativeMethods.Info); - Log.Info("[==============================================]"); - if (SystemData.RuntimeData.Runtime == Runtime.Mono) - { - if (SystemData.RuntimeData.SemVer == null) - { - Log.Warn("Could not find your running mono version!"); - Log.Warn("This version might not work properly."); - Log.Warn("If you encounter any problems, try installing the latest mono version by following http://www.mono-project.com/download/"); - } - else if (SystemData.RuntimeData.SemVer.Major < 5) - { - Log.Error("You are running a mono version below 5.0.0!"); - Log.Error("This version is not supported and will not work properly."); - Log.Error("Install the latest mono version by following http://www.mono-project.com/download/"); - } - } - - Log.Info("[============ Initializing Modules ============]"); - TS3Client.Messages.Deserializer.OnError += (s, e) => Log.Error(e.ToString()); - - Injector = new CoreInjector(); - - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - Injector.RegisterType(); - - Injector.RegisterModule(this); - Injector.RegisterModule(ConfigManager); - Injector.RegisterModule(Injector); - Injector.RegisterModule(new DbStore(hmd)); - Injector.RegisterModule(new PluginManager(pmd)); - Injector.RegisterModule(new CommandManager(), x => x.Initialize()); - Injector.RegisterModule(new ResourceFactoryManager(), x => x.Initialize()); - Injector.RegisterModule(new WebManager(webd), x => x.Initialize()); - Injector.RegisterModule(new RightsManager(rmd), x => x.Initialize()); - Injector.RegisterModule(new BotManager()); - Injector.RegisterModule(new TokenManager(), x => x.Initialize()); - - if (!Injector.AllResolved()) + var configResult = ConfRoot.OpenOrCreate(configFilePath); + if (!configResult.Ok) + return "Could not create config"; + ConfRoot config = configResult.Value; + Config.Deprecated.UpgradeScript.CheckAndUpgrade(config); + + var injector = new CoreInjector(); + + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + injector.RegisterType(); + + injector.RegisterModule(this); + injector.RegisterModule(config); + injector.RegisterModule(injector); + injector.RegisterModule(new DbStore(config.Db)); + injector.RegisterModule(new PluginManager(config.Plugins)); + injector.RegisterModule(new CommandManager(), x => x.Initialize()); + injector.RegisterModule(new ResourceFactoryManager(config.Factories), x => x.Initialize()); + injector.RegisterModule(new WebServer(config.Web), x => x.Initialize()); + injector.RegisterModule(new RightsManager(config.Rights)); + injector.RegisterModule(new BotManager()); + injector.RegisterModule(new TokenManager(), x => x.Initialize()); + + if (!injector.AllResolved()) { Log.Debug("Cyclic core module dependency"); - Injector.ForceCyclicResolve(); - if (!Injector.AllResolved()) + injector.ForceCyclicResolve(); + if (!injector.AllResolved()) { Log.Error("Missing core module dependency"); return "Could not load all core modules"; } } - Log.Info("[==================== Done ====================]"); - return R.OkR; - } + YoutubeDlHelper.DataObj = config.Tools.YoutubeDl; - private void Run() - { - Bots.WatchBots(); + Bots.RunBots(interactive); + + return R.Ok; } public void ExceptionHandler(object sender, UnhandledExceptionEventArgs e) @@ -233,12 +139,14 @@ public void ConsoleInterruptHandler(object sender, ConsoleCancelEventArgs e) { if (!forceNextExit) { + Log.Info("Got interrupt signal, trying to soft-exit."); e.Cancel = true; forceNextExit = true; Dispose(); } else { + Log.Info("Got multiple interrupt signals, trying to force-exit."); Environment.Exit(0); } } @@ -251,13 +159,13 @@ public void Dispose() Bots?.Dispose(); Bots = null; - PluginManager?.Dispose(); // before: SessionManager, logStream, + PluginManager?.Dispose(); // before: SessionManager, PluginManager = null; - WebManager?.Dispose(); // before: logStream, + WebManager?.Dispose(); // before: WebManager = null; - Database?.Dispose(); // before: logStream, + Database?.Dispose(); // before: Database = null; FactoryManager?.Dispose(); // before: diff --git a/TS3AudioBot/DbStore.cs b/TS3AudioBot/DbStore.cs index 5906ba45..3765b456 100644 --- a/TS3AudioBot/DbStore.cs +++ b/TS3AudioBot/DbStore.cs @@ -9,7 +9,7 @@ namespace TS3AudioBot { - using History; + using Config; using LiteDB; using System; using System.IO; @@ -21,10 +21,10 @@ public class DbStore : IDisposable private readonly LiteDatabase database; private readonly LiteCollection metaTable; - public DbStore(HistoryManagerData hmd) + public DbStore(ConfDb config) { - var historyFile = new FileInfo(hmd.HistoryFile); - database = new LiteDatabase(historyFile.FullName); + var historyFile = Path.GetFullPath(config.Path); + database = new LiteDatabase(historyFile); metaTable = database.GetCollection(DbMetaInformationTable); } diff --git a/TS3AudioBot/DocGen.cs b/TS3AudioBot/DocGen.cs deleted file mode 100644 index aaf0190e..00000000 --- a/TS3AudioBot/DocGen.cs +++ /dev/null @@ -1,56 +0,0 @@ -// TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2017 TS3AudioBot contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3AudioBot -{ - using CommandSystem; - using System; - using System.Collections.Generic; - using System.IO; - - /// - /// Used to generate the command overview for GitHub. - /// - internal static class DocGen - { - private static void Main(string[] args) - { - const bool writeSubCommands = false; - - var cmdMgr = new CommandManager(); - cmdMgr.RegisterMain(); - - var lines = new List(); - - foreach (var com in cmdMgr.AllCommands) - { - if (!writeSubCommands && com.InvokeName.Contains(" ")) - continue; - - string description; - if (string.IsNullOrEmpty(com.Description)) - description = " - no description yet -"; - else - description = com.Description; - lines.Add($"* *{com.InvokeName}*: {description}"); - } - - // adding commands which only have subcommands - lines.Add("* *history*: Shows recently played songs."); - - lines.Sort(); - - string final = string.Join("\n", lines); - Console.WriteLine(final); - File.WriteAllText("docs.txt", final); - - Console.ReadLine(); - } - } -} diff --git a/TS3AudioBot/FodyWeavers.xml b/TS3AudioBot/FodyWeavers.xml deleted file mode 100644 index 73699281..00000000 --- a/TS3AudioBot/FodyWeavers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/TS3AudioBot/Helper/AudioTags/M3uReader.cs b/TS3AudioBot/Helper/AudioTags/M3uReader.cs deleted file mode 100644 index d4ab976f..00000000 --- a/TS3AudioBot/Helper/AudioTags/M3uReader.cs +++ /dev/null @@ -1,99 +0,0 @@ -// TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2017 TS3AudioBot contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3AudioBot.Helper.AudioTags -{ - using ResourceFactories; - using System; - using System.Collections.Generic; - using System.IO; - - internal class M3uReader - { - const int MaxLineLength = 4096; - - public static R> TryGetData(Stream stream) - { - var br = new BinaryReader(stream); - int read = 1; - int bufferLen = 0; - var buffer = new char[MaxLineLength]; - var data = new List(); - string trackTitle = null; - bool extm3u = false; - - try - { - while (true) - { - if (read > 0) - { - read = br.Read(buffer, bufferLen, MaxLineLength - bufferLen); - bufferLen += read; - } - - if (bufferLen <= 0) - break; - - // find linebreak index - int index = Array.IndexOf(buffer, '\n'); - int lb = 1; - if (index == -1) - index = Array.IndexOf(buffer, '\r'); - else if (index > 0 && buffer[index - 1] == '\r') - { - index--; - lb = 2; - } - - string line; - if (index == -1) - { - if (bufferLen == MaxLineLength) - return "Max read buffer exceeded"; - line = new string(buffer, 0, bufferLen); - bufferLen = 0; - } - else - { - line = new string(buffer, 0, index); - index += lb; - Array.Copy(buffer, index, buffer, 0, MaxLineLength - index); - bufferLen -= index; - } - - if (line.StartsWith("#")) - { - if (extm3u && line.StartsWith("#EXTINF:")) - { - var trackInfo = line.Substring(8).Split(new[] { ',' }, 2); - if (trackInfo.Length == 2) - trackTitle = trackInfo[1]; - } - else if (line.StartsWith("#EXTM3U")) - { - extm3u = true; - } - // else: unsupported m3u tag - } - else - { - data.Add(new PlaylistItem(new AudioResource(line, trackTitle ?? line, "media"))); - trackTitle = null; - } - - if (index == -1) - return data; - } - } - catch { } - return "Unexpected m3u parsing error"; - } - } -} diff --git a/TS3AudioBot/Helper/Environment/SystemData.cs b/TS3AudioBot/Helper/Environment/SystemData.cs index f20845ac..cf37fb7e 100644 --- a/TS3AudioBot/Helper/Environment/SystemData.cs +++ b/TS3AudioBot/Helper/Environment/SystemData.cs @@ -10,15 +10,17 @@ namespace TS3AudioBot.Helper.Environment { using System; + using System.Collections.Generic; using System.Diagnostics; + using System.IO; using System.Reflection; using System.Text.RegularExpressions; - using Version = System.ValueTuple; + using PlatformVersion = System.ValueTuple; public static class SystemData { - private static readonly Regex PlattformRegex = new Regex(@"(\w+)=(.*)", Util.DefaultRegexConfig | RegexOptions.Multiline); - private static readonly Regex SemVerRegex = new Regex(@"(\d+)(?:\.(\d+)){2,3}", Util.DefaultRegexConfig | RegexOptions.Multiline); + private static readonly Regex PlattformRegex = new Regex(@"(\w+)=(.*)", RegexOptions.IgnoreCase | RegexOptions.ECMAScript | RegexOptions.Multiline); + private static readonly Regex SemVerRegex = new Regex(@"(\d+)(?:\.(\d+)){1,3}", RegexOptions.IgnoreCase | RegexOptions.ECMAScript | RegexOptions.Multiline); public static bool IsLinux { get; } = Environment.OSVersion.Platform == PlatformID.Unix @@ -44,51 +46,56 @@ private static BuildData GenAssemblyData() private static string GenPlattformDat() { string plattform = null; - string version = ""; + string version = null; string bitness = Environment.Is64BitProcess ? "64bit" : "32bit"; if (IsLinux) { - try - { - var p = new Process() - { - StartInfo = new ProcessStartInfo() - { - FileName = "bash", - Arguments = "-c \"cat /etc/*[_-]release\"", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardOutput = true, - } - }; - p.Start(); - p.WaitForExit(100); + var values = new Dictionary(); - while (p.StandardOutput.Peek() > -1) + RunBash("cat /etc/*[_-][Rr]elease", x => + { + var lines = x.ReadToEnd().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - var infoLine = p.StandardOutput.ReadLine(); - if (string.IsNullOrEmpty(infoLine)) - continue; - var match = PlattformRegex.Match(infoLine); + var match = PlattformRegex.Match(line); if (!match.Success) continue; - switch (match.Groups[1].Value.ToUpper()) + values[match.Groups[1].Value.ToUpper()] = TextUtil.StripQuotes(match.Groups[2].Value); + } + + if (values.Count > 0) + { + string value; + plattform = values.TryGetValue("NAME", out value) ? value + : values.TryGetValue("ID", out value) ? value + : values.TryGetValue("DISTRIB_ID", out value) ? value + : values.TryGetValue("PRETTY_NAME", out value) ? value + : null; + + version = values.TryGetValue("VERSION", out value) ? value + : values.TryGetValue("VERSION_ID", out value) ? value + : values.TryGetValue("DISTRIB_RELEASE", out value) ? value + : null; + } + + if (plattform == null && version == null) + { + foreach (var line in lines) { - case "DISTRIB_ID": - plattform = match.Groups[2].Value; - break; - case "DISTRIB_RELEASE": - version = match.Groups[2].Value; - break; + var match = SemVerRegex.Match(line); + if (match.Success) + { + version = line; + break; + } } } - } - catch (Exception) { } - if (plattform == null) - plattform = "Linux"; + plattform = plattform ?? "Linux"; + version = version ?? ""; + }); } else { @@ -99,8 +106,33 @@ private static string GenPlattformDat() return $"{plattform} {version} ({bitness})"; } - public static (Runtime Runtime, string FullName, SemVer SemVer) RuntimeData { get; } = GenRuntimeData(); - private static Version GenRuntimeData() + private static void RunBash(string param, Action action) + { + try + { + using (var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "bash", + Arguments = $"-c \"{param}\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + } + }) + { + p.Start(); + p.WaitForExit(200); + + action.Invoke(p.StandardOutput); + } + } + catch { } + } + + public static (Runtime Runtime, string FullName, Version SemVer) RuntimeData { get; } = GenRuntimeData(); + private static PlatformVersion GenRuntimeData() { var ver = GetNetCoreVersion(); if (ver.HasValue) @@ -117,7 +149,7 @@ private static Version GenRuntimeData() return (Runtime.Unknown, "? (?)", null); } - private static Version? GetNetCoreVersion() + private static PlatformVersion? GetNetCoreVersion() { var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; var assemblyPath = assembly.CodeBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); @@ -129,7 +161,7 @@ private static Version GenRuntimeData() return (Runtime.Core, $".NET Core ({version})", semVer); } - private static Version? GetMonoVersion() + private static PlatformVersion? GetMonoVersion() { var type = Type.GetType("Mono.Runtime"); if (type == null) @@ -142,27 +174,26 @@ private static Version GenRuntimeData() return (Runtime.Mono, $"Mono ({version})", semVer); } - private static Version? GetNetFrameworkVersion() + private static PlatformVersion? GetNetFrameworkVersion() { var version = Environment.Version.ToString(); var semVer = ParseToSemVer(version); return (Runtime.Net, $".NET Framework {version}", semVer); } - private static SemVer ParseToSemVer(string version) + private static Version ParseToSemVer(string version) { var semMatch = SemVerRegex.Match(version); if (!semMatch.Success) return null; - var semVer = new SemVer(); - if (int.TryParse(semMatch.Groups[1].Value, out var major)) semVer.Major = major; - if (int.TryParse(semMatch.Groups[2].Captures[0].Value, out var minor)) semVer.Minor = minor; - if (int.TryParse(semMatch.Groups[2].Captures[1].Value, out var patch)) semVer.Patch = patch; - if (semMatch.Groups[2].Captures.Count > 2 && - int.TryParse(semMatch.Groups[2].Captures[2].Value, out var revision)) semVer.Revision = revision; - else semVer.Revision = null; - return semVer; + if (!int.TryParse(semMatch.Groups[1].Value, out var major)) major = 0; + if (!int.TryParse(semMatch.Groups[2].Captures[0].Value, out var minor)) minor = 0; + if (semMatch.Groups[2].Captures.Count <= 1 + || !int.TryParse(semMatch.Groups[2].Captures[1].Value, out var patch)) patch = 0; + if (semMatch.Groups[2].Captures.Count <= 2 + || int.TryParse(semMatch.Groups[2].Captures[2].Value, out var revision)) revision = 0; + return new Version(major, minor, patch, revision); } } @@ -184,15 +215,8 @@ public class BuildData public override string ToString() => $"{Version}/{Branch}/{(CommitSha.Length > 8 ? CommitSha.Substring(0, 8) : CommitSha)}"; } - public class SemVer + public static class SemVerExtension { - public int Major { get; set; } - public int Minor { get; set; } - public int Patch { get; set; } - - // Not used in SemVer - public int? Revision { get; set; } - - public override string ToString() => $"{Major}.{Minor}.{Patch}" + (Revision.HasValue ? $".{Revision}" : null); + public static string AsSemVer(this Version version) => $"{version.Major}.{version.Minor}.{version.Build}" + (version.Revision != 0 ? $".{version.Revision}" : null); } } diff --git a/TS3AudioBot/Helper/IJsonConfig.cs b/TS3AudioBot/Helper/IJsonConfig.cs new file mode 100644 index 00000000..1ff08df8 --- /dev/null +++ b/TS3AudioBot/Helper/IJsonConfig.cs @@ -0,0 +1,50 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Helper +{ + using Newtonsoft.Json; + using System; + using System.IO; + using System.Text; + + public interface IJsonSerializable + { + bool ExpectsString { get; } + void ToJson(JsonWriter writer); + E FromJson(JsonReader reader); + } + + public static class JsonSerializableExtensions + { + public static E FromJson(this IJsonSerializable jsonConfig, string json) + { + if (jsonConfig.ExpectsString) + json = JsonConvert.SerializeObject(json); + + var sr = new StringReader(json); + using (var reader = new JsonTextReader(sr)) + { + return jsonConfig.FromJson(reader); + } + } + + public static string ToJson(this IJsonSerializable jsonConfig) + { + var sb = new StringBuilder(); + var sw = new StringWriter(sb); + using (var writer = new JsonTextWriter(sw)) + { + writer.Formatting = Formatting.Indented; + jsonConfig.ToJson(writer); + } + return sb.ToString(); + } + } +} diff --git a/TS3AudioBot/Helper/ImageUtil.cs b/TS3AudioBot/Helper/ImageUtil.cs index c9a5f1f6..7130cffb 100644 --- a/TS3AudioBot/Helper/ImageUtil.cs +++ b/TS3AudioBot/Helper/ImageUtil.cs @@ -9,141 +9,49 @@ namespace TS3AudioBot.Helper { - using Environment; + using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; using System; - using System.Collections.Generic; - using System.Drawing; - using System.Drawing.Drawing2D; - using System.Drawing.Text; + using System.IO; internal static class ImageUtil { public const int ResizeMaxWidthDefault = 320; - private static readonly StringFormat AvatarTextFormat = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Near }; - - public static Image BuildStringImage(string str, Image img, int resizeMaxWidth = ResizeMaxWidthDefault) + public static Stream ResizeImage(Stream imgStream, int resizeMaxWidth = ResizeMaxWidthDefault) { - img = AutoResize(img, resizeMaxWidth); - - var imgRect = new RectangleF(0, 0, img.Width, img.Height); - - using (var graphics = Graphics.FromImage(img)) + try { - if (SystemData.IsLinux) - { - BuildStringImageLinux(str, graphics, imgRect); - } - else + using (var img = Image.Load(imgStream)) { - using (var gp = new GraphicsPath()) - { - gp.AddString(str, FontFamily.GenericSansSerif, 0, 15, imgRect, AvatarTextFormat); + if (img.Width <= resizeMaxWidth) + return SaveAdaptive(img); - graphics.InterpolationMode = InterpolationMode.High; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.TextRenderingHint = TextRenderingHint.AntiAlias; - graphics.CompositingQuality = CompositingQuality.HighQuality; + float ratio = img.Width / (float)img.Height; + img.Mutate(x => x.Resize(resizeMaxWidth, (int)(resizeMaxWidth / ratio))); - using (Pen avatarTextOutline = new Pen(Color.Black, 4) { LineJoin = LineJoin.Round }) - { - graphics.DrawPath(avatarTextOutline, gp); - } - graphics.FillPath(Brushes.White, gp); - } + return SaveAdaptive(img); } } - - return img; - } - - private static Image AutoResize(Image img, int resizeMaxWidth) - { - if (img.Width <= resizeMaxWidth) - return img; - - using (img) + catch (NotSupportedException) { - float ratio = img.Width / (float)img.Height; - var destImage = new Bitmap(resizeMaxWidth, (int)(resizeMaxWidth / ratio)); - - using (var graphics = Graphics.FromImage(destImage)) - { - graphics.CompositingMode = CompositingMode.SourceCopy; - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.DrawImage(img, new Rectangle(0, 0, destImage.Width, destImage.Height), 0, 0, img.Width, img.Height, GraphicsUnit.Pixel); - } - - return destImage; + return null; } } - private static void BuildStringImageLinux(string str, Graphics target, RectangleF rect) + private static MemoryStream SaveAdaptive(Image img) { - const int maxMonoBugWidth = 150; - - using (var gp = new GraphicsPath()) - using (var builder = new Bitmap(maxMonoBugWidth, 100)) - using (var bg = Graphics.FromImage(builder)) + var mem = new MemoryStream(); + if (img.Frames.Count > 1) { - gp.AddString("X", FontFamily.GenericMonospace, 0, 15, rect, AvatarTextFormat); - var bounds = gp.GetBounds(); - var charW = bounds.Width; - var charH = bounds.Height * 2; - if (charW < 0.1e-6) - return; - - var buildRect = new RectangleF(0, 0, maxMonoBugWidth, charH); - - var sep = new List(); - - bg.InterpolationMode = InterpolationMode.High; - bg.SmoothingMode = SmoothingMode.HighQuality; - bg.TextRenderingHint = TextRenderingHint.AntiAlias; - bg.CompositingQuality = CompositingQuality.HighQuality; - target.CompositingQuality = CompositingQuality.HighQuality; - - int lastBreak = 0; - int lastBreakOption = 0; - for (int i = 0; i < str.Length; i++) - { - if (!char.IsLetterOrDigit(str[i])) - { - lastBreakOption = i; - } - - if ((i - lastBreak) * charW >= rect.Width && lastBreak != lastBreakOption) - { - sep.Add(str.Substring(lastBreak, lastBreakOption - lastBreak)); - lastBreak = lastBreakOption; - } - } - sep.Add(str.Substring(lastBreak)); - - var step = (int)(maxMonoBugWidth / charW) - 1; - for (int i = 0; i < sep.Count; i++) - { - var line = sep[i]; - for (int j = 0; j * step < line.Length; j++) - { - var part = line.Substring(j * step, Math.Min(step, line.Length - j * step)); - gp.Reset(); - gp.AddString(part, FontFamily.GenericMonospace, 0, 15, buildRect, AvatarTextFormat); - - bg.Clear(Color.Transparent); - using (Pen avatarTextOutline = new Pen(Color.Black, 4) { LineJoin = LineJoin.Round }) - { - bg.DrawPath(avatarTextOutline, gp); - } - bg.FillPath(Brushes.White, gp); - - target.DrawImageUnscaled(builder, (int)(rect.X + j * (maxMonoBugWidth - 5)), (int)(rect.Y + i * charH)); - } - } + img.Save(mem, ImageFormats.Gif); + } + else + { + img.Save(mem, ImageFormats.Jpeg); } + return mem; } } } diff --git a/TS3AudioBot/Helper/TextUtil.cs b/TS3AudioBot/Helper/TextUtil.cs index 83327ce3..864d8fd9 100644 --- a/TS3AudioBot/Helper/TextUtil.cs +++ b/TS3AudioBot/Helper/TextUtil.cs @@ -49,12 +49,17 @@ public static string ExtractUrlFromBb(string ts3Link) return ts3Link; } - public static string StripQuotes(string quotedString) + public static string StripQuotes(string quotedString, bool throwWhenIncorrect = false) { - if (quotedString.Length <= 1 || - !quotedString.StartsWith("\"", StringComparison.Ordinal) || - !quotedString.EndsWith("\"", StringComparison.Ordinal)) - throw new ArgumentException("The string is not properly quoted"); + if (quotedString.Length <= 1 + || !quotedString.StartsWith("\"", StringComparison.Ordinal) + || !quotedString.EndsWith("\"", StringComparison.Ordinal)) + { + if (throwWhenIncorrect) + throw new ArgumentException("The string is not properly quoted"); + else + return quotedString; + } return quotedString.Substring(1, quotedString.Length - 2); } diff --git a/TS3AudioBot/Helper/TickPool.cs b/TS3AudioBot/Helper/TickPool.cs index 884007cc..9c8c8d34 100644 --- a/TS3AudioBot/Helper/TickPool.cs +++ b/TS3AudioBot/Helper/TickPool.cs @@ -29,12 +29,16 @@ static TickPool() { run = false; Util.Init(out workList); - tickThread = new Thread(Tick) {Name = "TickPool"}; + tickThread = new Thread(Tick) { Name = "TickPool" }; } - public static void RegisterTickOnce(Action method) + public static TickWorker RegisterTickOnce(Action method, TimeSpan? delay = null) { - AddWorker(new TickWorker(method, TimeSpan.Zero) { Active = true, TickOnce = true }); + if (method == null) throw new ArgumentNullException(nameof(method)); + if (delay.HasValue && delay.Value <= TimeSpan.Zero) throw new ArgumentException("The parameter must be at least '1'", nameof(delay)); + var worker = new TickWorker(method, delay ?? TimeSpan.Zero) { Active = true, TickOnce = true }; + AddWorker(worker); + return worker; } public static TickWorker RegisterTick(Action method, TimeSpan interval, bool active) @@ -104,7 +108,8 @@ private static void Tick() } } - tickLoopPulse.WaitOne(curSleep); + if (curSleep >= TimeSpan.Zero) + tickLoopPulse.WaitOne(curSleep); } } diff --git a/TS3AudioBot/Helper/TomlTools.cs b/TS3AudioBot/Helper/TomlTools.cs new file mode 100644 index 00000000..78cac714 --- /dev/null +++ b/TS3AudioBot/Helper/TomlTools.cs @@ -0,0 +1,432 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Helper +{ + using Nett; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + + public static class TomlTools + { + // *** Convenience method for getting values out of a toml object. *** + + public static T[] TryGetValueArray(this TomlObject tomlObj) + { + if (tomlObj.TomlType == TomlObjectType.Array) + { + var tomlArray = (TomlArray)tomlObj; + var retArr = new T[tomlArray.Length]; + for (int i = 0; i < tomlArray.Length; i++) + { + if (!tomlArray.Items[i].TryGetValue(out retArr[i])) + { + return null; + } + } + return retArr; + } + else if (tomlObj.TryGetValue(out T retSingleVal)) + { + return new[] { retSingleVal }; + } + return null; + } + + public static bool TryGetValue(this TomlObject tomlObj, out T value) + { + switch (tomlObj.TomlType) + { + case TomlObjectType.Int: + if (typeof(T) == typeof(long)) + { + // The base storage type for TomlInt is long, so we can simply return it. + value = (T)(object)((TomlInt)tomlObj).Value; + return true; + } + else if (typeof(T) == typeof(ulong)) + { + // ulong is the only type which needs to be casted so we can use it. + // This might not be the greatest solution, but we can express ulong.MaxValue with -1 for example. + value = (T)(object)(ulong)((TomlInt)tomlObj).Value; + return true; + } + else if (typeof(T) == typeof(uint) || typeof(T) == typeof(int) + || typeof(T) == typeof(ushort) || typeof(T) == typeof(short) + || typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte) + || typeof(T) == typeof(float) || typeof(T) == typeof(double)) + { + // All other types will be converted to catch overflow issues. + try + { + value = (T)Convert.ChangeType(((TomlInt)tomlObj).Value, typeof(T)); + return true; + } + catch (OverflowException) { } + } + break; + + case TomlObjectType.Float: + if (typeof(T) == typeof(double)) + { + // Same here, double is the base type for TomlFloat. + value = (T)(object)((TomlFloat)tomlObj).Value; + return true; + } + else if (typeof(T) == typeof(float)) + { + // double -> float cast works as we expect it. + value = (T)(object)(float)((TomlFloat)tomlObj).Value; + return true; + } + break; + + case TomlObjectType.Bool: + case TomlObjectType.DateTime: + case TomlObjectType.TimeSpan: + if (tomlObj is TomlValue tomlValue && typeof(T) == tomlValue.Value.GetType()) + { + value = tomlValue.Value; + return true; + } + break; + + case TomlObjectType.String: + if (typeof(T).IsEnum) + { + try + { + value = (T)Enum.Parse(typeof(T), ((TomlString)tomlObj).Value, true); + return true; + } + catch (ArgumentException) { } + catch (OverflowException) { } + } + else if (typeof(T) == typeof(string)) + { + value = ((TomlValue)tomlObj).Value; + return true; + } + break; + } + value = default; + return false; + } + + // *** Convenience method for setting values to a toml object. *** + + public static TomlObject Set(this TomlTable tomlTable, string key, T value) + { + if (tomlTable == null) throw new ArgumentNullException(nameof(tomlTable)); + if (key == null) throw new ArgumentNullException(nameof(key)); + + // I literally have no idea how to write it better with this toml library. + + TomlObject retobj = tomlTable.TryGetValue(key); + if (retobj == null) + { + if (typeof(T) == typeof(bool)) return tomlTable.Add(key, (bool)(object)value); + else if (typeof(T) == typeof(string)) return tomlTable.Add(key, (string)(object)value); + else if (typeof(T) == typeof(double)) return tomlTable.Add(key, (double)(object)value); + else if (typeof(T) == typeof(float)) return tomlTable.Add(key, (float)(object)value); + else if (typeof(T) == typeof(ushort)) return tomlTable.Add(key, /*auto*/(ushort)(object)value); + else if (typeof(T) == typeof(int)) return tomlTable.Add(key, (int)(object)value); + else if (typeof(T) == typeof(long)) return tomlTable.Add(key, (long)(object)value); + else if (typeof(T) == typeof(ulong)) return tomlTable.Add(key, (long)(ulong)(object)value); + else if (typeof(T) == typeof(TimeSpan)) return tomlTable.Add(key, (TimeSpan)(object)value); + else if (typeof(T) == typeof(DateTime)) return tomlTable.Add(key, (DateTime)(object)value); + else if (value is IEnumerable enubool) return tomlTable.Add(key, enubool); + else if (value is IEnumerable enustring) return tomlTable.Add(key, enustring); + else if (value is IEnumerable enudouble) return tomlTable.Add(key, enudouble); + else if (value is IEnumerable enufloat) return tomlTable.Add(key, enufloat); + else if (value is IEnumerable enuushort) return tomlTable.Add(key, enuushort.Select(x => (int)x)); + else if (value is IEnumerable enuint) return tomlTable.Add(key, enuint); + else if (value is IEnumerable enulong) return tomlTable.Add(key, enulong); + else if (value is IEnumerable enuulong) return tomlTable.Add(key, enuulong.Select(x => (long)x)); + else if (value is IEnumerable enuTimeSpan) return tomlTable.Add(key, enuTimeSpan); + else if (value is IEnumerable enuDateTime) return tomlTable.Add(key, enuDateTime); + } + else + { + TomlComment[] docs = null; + if (retobj.Comments.Any()) + docs = retobj.Comments.ToArray(); + if (typeof(T) == typeof(bool)) retobj = tomlTable.Update(key, (bool)(object)value); + else if (typeof(T) == typeof(string)) retobj = tomlTable.Update(key, (string)(object)value); + else if (typeof(T) == typeof(double)) retobj = tomlTable.Update(key, (double)(object)value); + else if (typeof(T) == typeof(float)) retobj = tomlTable.Update(key, (float)(object)value); + else if (typeof(T) == typeof(ushort)) return tomlTable.Update(key, /*auto*/(ushort)(object)value); + else if (typeof(T) == typeof(int)) retobj = tomlTable.Update(key, /*auto*/(int)(object)value); + else if (typeof(T) == typeof(long)) retobj = tomlTable.Update(key, (long)(object)value); + else if (typeof(T) == typeof(ulong)) return tomlTable.Update(key, (long)(ulong)(object)value); + else if (typeof(T) == typeof(TimeSpan)) retobj = tomlTable.Update(key, (TimeSpan)(object)value); + else if (typeof(T) == typeof(DateTime)) retobj = tomlTable.Update(key, (DateTime)(object)value); + else if (value is IEnumerable enubool) return tomlTable.Update(key, enubool); + else if (value is IEnumerable enustring) return tomlTable.Update(key, enustring); + else if (value is IEnumerable enudouble) return tomlTable.Update(key, enudouble); + else if (value is IEnumerable enufloat) return tomlTable.Update(key, enufloat); + else if (value is IEnumerable enuushort) return tomlTable.Update(key, enuushort.Select(x => (int)x)); + else if (value is IEnumerable enuint) return tomlTable.Update(key, enuint); + else if (value is IEnumerable enulong) return tomlTable.Update(key, enulong); + else if (value is IEnumerable enuulong) return tomlTable.Update(key, enuulong.Select(x => (long)x)); + else if (value is IEnumerable enuTimeSpan) return tomlTable.Update(key, enuTimeSpan); + else if (value is IEnumerable enuDateTime) return tomlTable.Update(key, enuDateTime); + else throw new NotSupportedException("The type is not supported"); + if (docs != null) + retobj.AddComments(docs); + return retobj; + } + throw new NotSupportedException("The type is not supported"); + } + + // *** TomlPath engine *** + + public static IEnumerable ByPath(this TomlObject obj, string path) + { + var pathM = path.AsMemory(); + return ProcessIdentifier(obj, pathM); + } + + private static IEnumerable ProcessIdentifier(TomlObject obj, ReadOnlyMemory pathM) + { + if (pathM.IsEmpty) + return Enumerable.Empty(); + + var path = pathM.Span; + switch (path[0]) + { + case '*': + { + var rest = pathM.Slice(1); + if (rest.IsEmpty) + return obj.GetAllSubItems(); + + if (IsArray(rest.Span)) + return obj.GetAllSubItems().SelectMany(x => ProcessArray(x, rest)); + else if (IsDot(rest.Span)) + return obj.GetAllSubItems().SelectMany(x => ProcessDot(x, rest)); + else + throw new ArgumentException(nameof(path), "Invalid expression after wildcard"); + } + + case '[': + throw new ArgumentException(nameof(path), "Invalid array open bracket"); + case ']': + throw new ArgumentException(nameof(path), "Invalid array close bracket"); + case '.': + throw new ArgumentException(nameof(path), "Invalid dot"); + + default: + { + var subItemName = path; + var rest = ReadOnlyMemory.Empty; + bool cont = false; + for (int i = 0; i < path.Length; i++) + { + // todo allow in future + if (path[i] == '*') + throw new ArgumentException(nameof(path), "Invalid wildcard position"); + + var currentSub = path.Slice(i); + if (!IsIdentifier(currentSub)) // if (!IsName) + { + cont = true; + subItemName = path.Slice(0, i); + rest = pathM.Slice(i); + break; + } + } + var item = obj.GetSubItemByName(subItemName); + if (item == null) + return Enumerable.Empty(); + + if (cont) + { + if (IsArray(rest.Span)) + return ProcessArray(item, rest); + else if (IsDot(rest.Span)) + return ProcessDot(item, rest); + else + throw new ArgumentException(nameof(path), "Invalid expression name identifier"); + } + return new[] { item }; + } + } + } + + private static IEnumerable ProcessArray(TomlObject obj, ReadOnlyMemory pathM) + { + var path = pathM.Span; + if (path[0] != '[') + throw new ArgumentException(nameof(path), "Expected array open breacket"); + for (int i = 1; i < path.Length; i++) + { + if (path[i] == ']') + { + if (i == 0) + throw new ArgumentException(nameof(path), "Empty array indexer"); + var indexer = path.Slice(1, i - 1); + var rest = pathM.Slice(i + 1); + bool cont = rest.Length > 0; + + // select + if (indexer.Length == 1 && indexer[0] == '*') + { + var ret = obj.GetAllArrayItems(); + if (cont) + { + if (IsArray(rest.Span)) + return ret.SelectMany(x => ProcessArray(x, rest)); + else if (IsDot(rest.Span)) + return ret.SelectMany(x => ProcessDot(x, rest)); + else + throw new ArgumentException(nameof(path), "Invalid expression after array indexer"); + } + + return ret; + } + else + { + var ret = obj.GetArrayItemByIndex(indexer); + if (ret == null) + return Enumerable.Empty(); + + if (cont) + { + if (IsArray(rest.Span)) + return ProcessArray(ret, rest); + else if (IsDot(rest.Span)) + return ProcessDot(ret, rest); + else + throw new ArgumentException(nameof(path), "Invalid expression after array indexer"); + } + return new[] { ret }; + } + } + } + throw new ArgumentException(nameof(path), "Missing array close bracket"); + } + + private static IEnumerable ProcessDot(TomlObject obj, ReadOnlyMemory pathM) + { + var path = pathM.Span; + if (!IsDot(path)) + throw new ArgumentException(nameof(path), "Expected dot"); + + var rest = pathM.Slice(1); + if (!IsIdentifier(rest.Span)) + throw new ArgumentException(nameof(path), "Expected identifier after dot"); + + return ProcessIdentifier(obj, rest); + } + + internal static bool IsArray(ReadOnlySpan name) + => name.Length >= 1 && (name[0] == '['); + + internal static bool IsIdentifier(ReadOnlySpan name) + => name.Length >= 1 && (name[0] != '[' && name[0] != ']' && name[0] != '.'); + + internal static bool IsDot(ReadOnlySpan name) + => name.Length >= 1 && (name[0] == '.'); + + private static TomlObject GetArrayItemByIndex(this TomlObject obj, ReadOnlySpan index) + { + int indexNum = int.Parse(new string(index.ToArray())); + if (indexNum < 0) + return null; + //if (!System.Buffers.Text.Utf8Parser.TryParse(index, out int indexNum, out int bytesConsumed)) + //throw new ArgumentException("Invalid array indexer"); + if (obj.TomlType == TomlObjectType.Array) + { + var tomlTable = (TomlArray)obj; + if (indexNum < tomlTable.Length) + return tomlTable[indexNum]; + } + else if (obj.TomlType == TomlObjectType.ArrayOfTables) + { + var tomlTableArray = (TomlTableArray)obj; + if (indexNum >= tomlTableArray.Count) + return tomlTableArray[indexNum]; + } + return null; + } + + private static IEnumerable GetAllArrayItems(this TomlObject obj) + { + if (obj.TomlType == TomlObjectType.Array) + return ((TomlArray)obj).Items; + else if (obj.TomlType == TomlObjectType.ArrayOfTables) + return ((TomlTableArray)obj).Items; + return Enumerable.Empty(); + } + + private static TomlObject GetSubItemByName(this TomlObject obj, ReadOnlySpan name) + { + if (obj.TomlType == TomlObjectType.Table) + return ((TomlTable)obj).TryGetValue(new string(name.ToArray())); + return null; + } + + private static IEnumerable GetAllSubItems(this TomlObject obj) + { + if (obj.TomlType == TomlObjectType.Table) + return ((TomlTable)obj).Values; + return Enumerable.Empty(); + } + + // *** Toml Serializer *** + + public static string DumpToJson(this TomlObject obj) + { + var sb = new StringBuilder(); + var sw = new StringWriter(sb); + using (var writer = new JsonTextWriter(sw)) + { + writer.Formatting = Formatting.Indented; + DumpToJson(obj, writer); + } + return sb.ToString(); + } + + public static void DumpToJson(this TomlObject obj, JsonWriter writer) + { + switch (obj.TomlType) + { + case TomlObjectType.Bool: writer.WriteValue(((TomlBool)obj).Value); break; + case TomlObjectType.Int: writer.WriteValue(((TomlInt)obj).Value); break; + case TomlObjectType.Float: writer.WriteValue(((TomlFloat)obj).Value); break; + case TomlObjectType.String: writer.WriteValue(((TomlString)obj).Value); break; + case TomlObjectType.DateTime: writer.WriteValue(((TomlDateTime)obj).Value); break; + case TomlObjectType.TimeSpan: writer.WriteValue(((TomlDuration)obj).Value); break; + case TomlObjectType.Array: + case TomlObjectType.ArrayOfTables: + writer.WriteStartArray(); + IEnumerable list; + if (obj.TomlType == TomlObjectType.Array) list = ((TomlArray)obj).Items; else list = ((TomlTableArray)obj).Items; + foreach (var item in list) + DumpToJson(item, writer); + writer.WriteEndArray(); + break; + case TomlObjectType.Table: + writer.WriteStartObject(); + foreach (var kvp in (TomlTable)obj) + { + writer.WritePropertyName(kvp.Key); + DumpToJson(kvp.Value, writer); + } + writer.WriteEndObject(); + break; + } + } + } +} diff --git a/TS3AudioBot/Helper/Util.cs b/TS3AudioBot/Helper/Util.cs index 36f24ba9..c158309f 100644 --- a/TS3AudioBot/Helper/Util.cs +++ b/TS3AudioBot/Helper/Util.cs @@ -10,35 +10,36 @@ namespace TS3AudioBot.Helper { using CommandSystem; + using Localization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.IO; using System.Reflection; - using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; using System.Threading; - [Serializable] public static class Util { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); public const RegexOptions DefaultRegexConfig = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ECMAScript; + private static readonly Regex ValidPlistName = new Regex(@"^[\w-]+$", DefaultRegexConfig); + /// Blocks the thread while the predicate returns false or until the timeout runs out. /// Check function that will be called every millisecond. /// Timeout in milliseconds. public static void WaitOrTimeout(Func predicate, TimeSpan timeout) { - int msTimeout = (int)timeout.TotalSeconds; + var msTimeout = (int)timeout.TotalMilliseconds; while (!predicate() && msTimeout-- > 0) Thread.Sleep(1); } public static void WaitForThreadEnd(Thread thread, TimeSpan timeout) { - if (thread != null && thread.IsAlive) + if (thread?.IsAlive == true) { WaitOrTimeout(() => thread.IsAlive, timeout); if (thread.IsAlive) @@ -60,12 +61,13 @@ public static bool IsAdmin { get { +#if NET46 try { - using (var user = WindowsIdentity.GetCurrent()) + using (var user = System.Security.Principal.WindowsIdentity.GetCurrent()) { - var principal = new WindowsPrincipal(user); - return principal.IsInRole(WindowsBuiltInRole.Administrator); + var principal = new System.Security.Principal.WindowsPrincipal(user); + return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator); } } catch (UnauthorizedAccessException) { return false; } @@ -74,6 +76,9 @@ public static bool IsAdmin Log.Warn("Uncatched admin check."); return false; } +#else + return false; +#endif } } @@ -94,7 +99,7 @@ private static long Pow(long b, int pow) public static string FromSeed(int seed) { - char[] seedstr = new char[7]; + var seedstr = new char[7]; uint plainseed = unchecked((uint)seed); for (int i = 0; i < 7; i++) { @@ -102,12 +107,13 @@ public static string FromSeed(int seed) { plainseed--; var remainder = plainseed % 26; - char digit = (char)(remainder + 'a'); - seedstr[i] = digit; + seedstr[i] = (char)(remainder + 'a'); plainseed = (plainseed - remainder) / 26; } else + { seedstr[i] = '\0'; + } } return new string(seedstr).TrimEnd('\0'); } @@ -122,22 +128,22 @@ public static int ToSeed(string seed) finalValue += powVal; finalValue %= ((long)uint.MaxValue + 1); } - uint uval = (uint)finalValue; + var uval = (uint)finalValue; return unchecked((int)uval); } - public static void UnwrapThrow(this R r) + public static void UnwrapThrow(this E r) { if (!r.Ok) - throw new CommandException(r.Error, CommandExceptionReason.CommandError); + throw new CommandException(r.Error.Str, CommandExceptionReason.CommandError); } - public static T UnwrapThrow(this R r) + public static T UnwrapThrow(this R r) { if (r.Ok) return r.Value; else - throw new CommandException(r.Error, CommandExceptionReason.CommandError); + throw new CommandException(r.Error.Str, CommandExceptionReason.CommandError); } public static string UnrollException(this Exception ex) @@ -162,12 +168,23 @@ public static Stream GetEmbeddedFile(string name) public static R TryCast(this JToken token, string key) { if (token == null) - return "No json token"; + return R.Err; var value = token.SelectToken(key); if (value == null) - return "Key not found"; + return R.Err; try { return value.ToObject(); } - catch (JsonReaderException) { return "Invalid type"; } + catch (JsonReaderException) { return R.Err; } + } + + public static E IsSafeFileName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return new LocalStr(strings.error_playlist_name_invalid_empty); // TODO change to more generic error + if (name.Length >= 64) + return new LocalStr(strings.error_playlist_name_invalid_too_long); + if (!ValidPlistName.IsMatch(name)) + return new LocalStr(strings.error_playlist_name_invalid_character); + return R.Ok; } } diff --git a/TS3AudioBot/Helper/WebWrapper.cs b/TS3AudioBot/Helper/WebWrapper.cs index e2cf08a6..875c9d48 100644 --- a/TS3AudioBot/Helper/WebWrapper.cs +++ b/TS3AudioBot/Helper/WebWrapper.cs @@ -9,6 +9,7 @@ namespace TS3AudioBot.Helper { + using Localization; using System; using System.IO; using System.Net; @@ -18,7 +19,7 @@ public static class WebWrapper private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(3); - public static bool DownloadString(out string site, Uri link, params (string name, string value)[] optionalHeaders) + public static E DownloadString(out string site, Uri link, params (string name, string value)[] optionalHeaders) { var request = WebRequest.Create(link); foreach (var (name, value) in optionalHeaders) @@ -32,30 +33,31 @@ public static bool DownloadString(out string site, Uri link, params (string name if (stream == null) { site = null; - return false; + return new LocalStr(strings.error_net_empty_response); } using (var reader = new StreamReader(stream)) { site = reader.ReadToEnd(); - return true; + return R.Ok; } } } - catch (WebException) + catch (WebException webEx) { site = null; - return false; + return ToLoggedError(webEx); } } - //if (request.Method == "GET") - // request.Method = "HEAD"; - public static ValidateCode GetResponse(Uri link) => GetResponse(link, null); - public static ValidateCode GetResponse(Uri link, TimeSpan timeout) => GetResponse(link, null, timeout); - public static ValidateCode GetResponse(Uri link, Action body) => GetResponse(link, body, DefaultTimeout); - public static ValidateCode GetResponse(Uri link, Action body, TimeSpan timeout) + public static E GetResponse(Uri link) => GetResponse(link, null); + public static E GetResponse(Uri link, TimeSpan timeout) => GetResponse(link, null, timeout); + public static E GetResponse(Uri link, Action body) => GetResponse(link, body, DefaultTimeout); + public static E GetResponse(Uri link, Action body, TimeSpan timeout) { - var request = WebRequest.Create(link); + WebRequest request; + try { request = WebRequest.Create(link); } + catch (NotSupportedException) { return new LocalStr(strings.error_media_invalid_uri); } + try { request.Timeout = (int)timeout.TotalMilliseconds; @@ -63,57 +65,52 @@ public static ValidateCode GetResponse(Uri link, Action body, TimeS { body?.Invoke(response); } - return ValidateCode.Ok; + return R.Ok; } catch (WebException webEx) { - HttpWebResponse errorResponse; - if (webEx.Status == WebExceptionStatus.Timeout) - { - Log.Warn("Request timed out"); - return ValidateCode.Timeout; - } - else if ((errorResponse = webEx.Response as HttpWebResponse) != null) - { - Log.Warn("Web error: [{0}] {1}", (int)errorResponse.StatusCode, errorResponse.StatusCode); - return ValidateCode.Restricted; - } - else - { - Log.Warn("Unknown request error: {0}", webEx); - return ValidateCode.UnknownError; - } + return ToLoggedError(webEx); } } - internal static R GetResponseUnsafe(Uri link) => GetResponseUnsafe(link, DefaultTimeout); - internal static R GetResponseUnsafe(Uri link, TimeSpan timeout) + internal static R GetResponseUnsafe(Uri link) => GetResponseUnsafe(link, DefaultTimeout); + internal static R GetResponseUnsafe(Uri link, TimeSpan timeout) { - var request = WebRequest.Create(link); + WebRequest request; + try { request = WebRequest.Create(link); } + catch (NotSupportedException) { return new LocalStr(strings.error_media_invalid_uri); } + try { request.Timeout = (int)timeout.TotalMilliseconds; var stream = request.GetResponse().GetResponseStream(); if (stream == null) - return "WEB No content"; + return new LocalStr(strings.error_net_empty_response); return stream; } catch (WebException webEx) { - if (webEx.Status == WebExceptionStatus.Timeout) - return "WEB Request timed out"; - if (webEx.Response is HttpWebResponse errorResponse) - return $"WEB error: [{(int)errorResponse.StatusCode}] {errorResponse.StatusCode}"; - return $"WEB Unknown request error: {webEx}"; + return ToLoggedError(webEx); } } - } - public enum ValidateCode - { - Ok, - UnknownError, - Restricted, - Timeout, + private static LocalStr ToLoggedError(WebException webEx) + { + if (webEx.Status == WebExceptionStatus.Timeout) + { + Log.Warn("Request timed out"); + return new LocalStr(strings.error_net_timeout); + } + else if (webEx.Response is HttpWebResponse errorResponse) + { + Log.Warn("Web error: [{0}] {1}", (int)errorResponse.StatusCode, errorResponse.StatusCode); + return new LocalStr($"{strings.error_net_error_status_code} [{(int)errorResponse.StatusCode}] {errorResponse.StatusCode}"); + } + else + { + Log.Warn("Unknown request error: {0}", webEx); + return new LocalStr(strings.error_net_unknown); + } + } } } diff --git a/TS3AudioBot/History/AudioLogEntry.cs b/TS3AudioBot/History/AudioLogEntry.cs index aa8cd110..f578fb6c 100644 --- a/TS3AudioBot/History/AudioLogEntry.cs +++ b/TS3AudioBot/History/AudioLogEntry.cs @@ -17,8 +17,11 @@ public class AudioLogEntry { /// A unique id for each , given by the history system. public int Id { get; set; } - /// The dbid of the teamspeak user, who played this song first. - public uint UserInvokeId { get; set; } + /// Left for legacy reasons. The dbid of the teamspeak user, who played this song first. + [Obsolete] + public uint? UserInvokeId { get; set; } + /// The Uid of the teamspeak user, who played this song first. + public string UserUid { get; set; } /// How often the song has been played. public uint PlayCount { get; set; } /// The last time this song has been played. @@ -44,7 +47,7 @@ public void SetName(string newName) public override string ToString() { - return string.Format(CultureInfo.InvariantCulture, "[{0}] @ {1} by {2}: {3}, ({4})", Id, Timestamp, UserInvokeId, AudioResource.ResourceTitle, AudioResource); + return string.Format(CultureInfo.InvariantCulture, "[{0}] @ {1} by {2}: {3}, ({4})", Id, Timestamp, UserUid, AudioResource.ResourceTitle, AudioResource); } } } diff --git a/TS3AudioBot/History/HistoryManager.cs b/TS3AudioBot/History/HistoryManager.cs index ab19c79f..d3a9b1a9 100644 --- a/TS3AudioBot/History/HistoryManager.cs +++ b/TS3AudioBot/History/HistoryManager.cs @@ -9,8 +9,11 @@ namespace TS3AudioBot.History { + using Config; using Helper; using LiteDB; + using Localization; + using Playlists; using ResourceFactories; using System; using System.Collections.Generic; @@ -25,9 +28,9 @@ public sealed class HistoryManager private const string ResourceTitleQueryColumn = "lowTitle"; private LiteCollection audioLogEntries; - private readonly HistoryManagerData historyManagerData; private readonly LinkedList unusedIds; private readonly object dbLock = new object(); + private readonly ConfHistory config; public IHistoryFormatter Formatter { get; private set; } public uint HighestId => (uint)audioLogEntries.Max().AsInt32; @@ -42,30 +45,43 @@ static HistoryManager() .Id(x => x.Id); } - public HistoryManager(HistoryManagerData hmd) + public HistoryManager(ConfHistory config) { Formatter = new SmartHistoryFormatter(); - historyManagerData = hmd; Util.Init(out unusedIds); + this.config = config; } public void Initialize() { + var meta = Database.GetMetaData(AudioLogEntriesTable); + + if (meta.Version > CurrentHistoryVersion) + { + Log.Error("Database table \"{0}\" is higher than the current version. (table:{1}, app:{2}). " + + "Please download the latest TS3AudioBot to read the history.", AudioLogEntriesTable, meta.Version, CurrentHistoryVersion); + return; + } + audioLogEntries = Database.GetCollection(AudioLogEntriesTable); audioLogEntries.EnsureIndex(x => x.AudioResource.UniqueId, true); audioLogEntries.EnsureIndex(x => x.Timestamp); audioLogEntries.EnsureIndex(ResourceTitleQueryColumn, $"LOWER($.{nameof(AudioLogEntry.AudioResource)}.{nameof(AudioResource.ResourceTitle)})"); - RestoreFromFile(); - // Content upgrade + if (meta.Version == CurrentHistoryVersion) + return; - var meta = Database.GetMetaData(AudioLogEntriesTable); - if (meta.Version >= CurrentHistoryVersion) + if (audioLogEntries.Count() == 0) + { + meta.Version = CurrentHistoryVersion; + Database.UpdateMetaData(meta); return; + } + // Content upgrade switch (meta.Version) { case 0: @@ -97,13 +113,6 @@ private void RestoreFromFile() } public R LogAudioResource(HistorySaveData saveData) - { - var entry = Store(saveData); - if (entry != null) return entry; - else return "Entry could not be stored"; - } - - private AudioLogEntry Store(HistorySaveData saveData) { if (saveData == null) throw new ArgumentNullException(nameof(saveData)); @@ -113,9 +122,13 @@ private AudioLogEntry Store(HistorySaveData saveData) var ale = FindByUniqueId(saveData.Resource.UniqueId); if (ale == null) { - ale = CreateLogEntry(saveData); - if (ale == null) - Log.Error("AudioLogEntry could not be created!"); + var createResult = CreateLogEntry(saveData); + if (!createResult.Ok) + { + Log.Warn("AudioLogEntry could not be created! ({0})", createResult.Error); + return R.Err; + } + ale = createResult.Value; } else { @@ -141,13 +154,13 @@ private void LogEntryPlay(AudioLogEntry ale) audioLogEntries.Update(ale); } - private AudioLogEntry CreateLogEntry(HistorySaveData saveData) + private R CreateLogEntry(HistorySaveData saveData) { if (string.IsNullOrWhiteSpace(saveData.Resource.ResourceTitle)) - return null; + return "Track name is empty"; int nextHid; - if (historyManagerData.FillDeletedIds && unusedIds.Count > 0) + if (config.FillDeletedIds && unusedIds.Count > 0) { nextHid = unusedIds.First.Value; unusedIds.RemoveFirst(); @@ -159,7 +172,7 @@ private AudioLogEntry CreateLogEntry(HistorySaveData saveData) var ale = new AudioLogEntry(nextHid, saveData.Resource) { - UserInvokeId = (uint)(saveData.OwnerDbId ?? 0), + UserUid = saveData.InvokerUid, Timestamp = Util.GetNow(), PlayCount = 1, }; @@ -191,8 +204,8 @@ public IEnumerable Search(SeachQuery search) Query.Where(ResourceTitleQueryColumn, val => val.AsString.Contains(titleLower))); } - if (search.UserId.HasValue) - query = Query.And(query, Query.EQ(nameof(AudioLogEntry.UserInvokeId), (long)search.UserId.Value)); + if (search.UserUid != null) + query = Query.And(query, Query.EQ(nameof(AudioLogEntry.UserUid), search.UserUid)); if (search.LastInvokedAfter.HasValue) query = Query.And(query, Query.GTE(nameof(AudioLogEntry.Timestamp), search.LastInvokedAfter.Value)); @@ -216,11 +229,11 @@ public AudioLogEntry FindEntryByResource(AudioResource resource) /// Gets an by its history id or null if not exising. /// The id of the AudioLogEntry - public R GetEntryById(uint id) + public R GetEntryById(uint id) { var entry = audioLogEntries.FindById((long)id); if (entry != null) return entry; - else return "Could not find track with this id"; + else return new LocalStr(strings.error_history_could_not_find_entry); } /// Removes the from the Database. @@ -276,7 +289,7 @@ public void RemoveBrokenLinks() /// A new list with all working items. private List FilterList(IReadOnlyCollection list) { - int userNotityCnt = 0; + int userNotifyCnt = 0; var nextIter = new List(list.Count); foreach (var entry in list) { @@ -287,20 +300,53 @@ private List FilterList(IReadOnlyCollection list) nextIter.Add(entry); } - if (++userNotityCnt % 100 == 0) - Log.Debug("Clean in progress {0}", new string('.', userNotityCnt / 100 % 10)); + if (++userNotifyCnt % 100 == 0) + Log.Debug("Clean in progress {0}", new string('.', userNotifyCnt / 100 % 10)); } return nextIter; } - } - public class HistoryManagerData : ConfigData - { - [Info("Allows to enable or disable history features completely to save resources.", "true")] - public bool EnableHistory { get; set; } - [Info("The Path to the history database file", "history.db")] - public string HistoryFile { get; set; } - [Info("Whether or not deleted history ids should be filled up with new songs", "true")] - public bool FillDeletedIds { get; set; } + public void UpdadeDbIdToUid(Ts3Client ts3Client) + { + var upgradedEntries = new List(); + var dbIdCache = new Dictionary(); + + foreach (var audioLogEntry in audioLogEntries.FindAll()) + { +#pragma warning disable CS0612 + if (!audioLogEntry.UserInvokeId.HasValue) + continue; + + if (audioLogEntry.UserUid != null || audioLogEntry.UserInvokeId.Value == 0) + { + audioLogEntry.UserInvokeId = null; + upgradedEntries.Add(audioLogEntry); + continue; + } + + if (!dbIdCache.TryGetValue(audioLogEntry.UserInvokeId.Value, out var data)) + { + var result = ts3Client.GetDbClientByDbId(audioLogEntry.UserInvokeId.Value); + data.uid = (data.valid = result.Ok) ? result.Value.Uid : null; + if (!data.valid) + { + Log.Warn("Client DbId {0} could not be found.", audioLogEntry.UserInvokeId.Value); + } + dbIdCache.Add(audioLogEntry.UserInvokeId.Value, data); + } + + if (!data.valid) + continue; + + audioLogEntry.UserInvokeId = null; + audioLogEntry.UserUid = data.uid; + upgradedEntries.Add(audioLogEntry); +#pragma warning restore CS0612 + } + + if (upgradedEntries.Count > 0) + audioLogEntries.Update(upgradedEntries); + Log.Info("Upgraded {0} entries.", upgradedEntries.Count); + } } } diff --git a/TS3AudioBot/History/HistorySaveData.cs b/TS3AudioBot/History/HistorySaveData.cs index a8bb2570..58bf1326 100644 --- a/TS3AudioBot/History/HistorySaveData.cs +++ b/TS3AudioBot/History/HistorySaveData.cs @@ -15,12 +15,12 @@ namespace TS3AudioBot.History public class HistorySaveData { public AudioResource Resource { get; } - public ulong? OwnerDbId { get; } + public string InvokerUid { get; } - public HistorySaveData(AudioResource resource, ulong? ownerDbId) + public HistorySaveData(AudioResource resource, string invokerUid) { Resource = resource ?? throw new ArgumentNullException(nameof(resource)); - OwnerDbId = ownerDbId; + InvokerUid = invokerUid; } } } diff --git a/TS3AudioBot/History/SearchQuery.cs b/TS3AudioBot/History/SearchQuery.cs index 1d6b0286..efcd5d8f 100644 --- a/TS3AudioBot/History/SearchQuery.cs +++ b/TS3AudioBot/History/SearchQuery.cs @@ -14,14 +14,14 @@ namespace TS3AudioBot.History public class SeachQuery { public string TitlePart { get; set; } - public uint? UserId { get; set; } + public string UserUid { get; set; } public DateTime? LastInvokedAfter { get; set; } public int MaxResults { get; set; } public SeachQuery() { TitlePart = null; - UserId = null; + UserUid = null; LastInvokedAfter = null; MaxResults = 10; } diff --git a/TS3AudioBot/History/SmartHistoryFormatter.cs b/TS3AudioBot/History/SmartHistoryFormatter.cs index 377f1661..01846170 100644 --- a/TS3AudioBot/History/SmartHistoryFormatter.cs +++ b/TS3AudioBot/History/SmartHistoryFormatter.cs @@ -129,7 +129,7 @@ public string ProcessQuery(IEnumerable entries, Func string.Format("{0} ({2}): {1}", e.Id, e.AudioResource.ResourceTitle, e.UserInvokeId, e.PlayCount, e.Timestamp); + => string.Format("{0} ({2}): {1}", e.Id, e.AudioResource.ResourceTitle, e.UserUid, e.PlayCount, e.Timestamp); /// Trims a string to have the given token count at max. /// The string to substring from the left side. diff --git a/TS3AudioBot/IPlayerConnection.cs b/TS3AudioBot/IPlayerConnection.cs index 0bb31a61..50af9c5c 100644 --- a/TS3AudioBot/IPlayerConnection.cs +++ b/TS3AudioBot/IPlayerConnection.cs @@ -23,7 +23,7 @@ public interface IPlayerConnection : IDisposable TimeSpan Length { get; } bool Playing { get; } - R AudioStart(string url); - R AudioStop(); + E AudioStart(string url); + E AudioStop(); } } diff --git a/TS3AudioBot/ITargetManager.cs b/TS3AudioBot/IVoiceTarget.cs similarity index 98% rename from TS3AudioBot/ITargetManager.cs rename to TS3AudioBot/IVoiceTarget.cs index 8e76cc6c..e5760875 100644 --- a/TS3AudioBot/ITargetManager.cs +++ b/TS3AudioBot/IVoiceTarget.cs @@ -14,7 +14,7 @@ namespace TS3AudioBot using TS3Client.Audio; /// Used to specify playing mode and active targets to send to. - public interface ITargetManager + public interface IVoiceTarget { TargetSendMode SendMode { get; set; } ulong GroupWhisperTargetId { get; } diff --git a/TS3AudioBot/ResourceFactories/RResultCode.cs b/TS3AudioBot/Localization/LocalStr.cs similarity index 53% rename from TS3AudioBot/ResourceFactories/RResultCode.cs rename to TS3AudioBot/Localization/LocalStr.cs index 0ed2d55e..b582461e 100644 --- a/TS3AudioBot/ResourceFactories/RResultCode.cs +++ b/TS3AudioBot/Localization/LocalStr.cs @@ -7,23 +7,22 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -namespace TS3AudioBot.ResourceFactories +namespace TS3AudioBot.Localization { - public enum RResultCode // Resource Result Code + /// + /// Represents a localizable string + /// + public struct LocalStr { - UnknowError, - Success, - MediaInvalidUri, - MediaUnknownUri, - MediaNoWebResponse, - MediaFileNotFound, - ScInvalidLink, - TwitchInvalidUrl, - TwitchMalformedM3u8File, - TwitchNoStreamsExtracted, + public static readonly LocalStr Empty = new LocalStr(string.Empty); - // general errors - NoConnection, - AccessDenied, + public string Str { get; } + + public LocalStr(string str) + { + Str = str; + } + + public override string ToString() => Str; } } diff --git a/TS3AudioBot/Localization/LocalizationManager.cs b/TS3AudioBot/Localization/LocalizationManager.cs new file mode 100644 index 00000000..0a31c986 --- /dev/null +++ b/TS3AudioBot/Localization/LocalizationManager.cs @@ -0,0 +1,92 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Localization +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Reflection; + using System.Threading; + + public static class LocalizationManager + { + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + private static readonly HashSet loadedLanguage = new HashSet(); + + static LocalizationManager() + { + loadedLanguage.Add("en"); + } + + public static E LoadLanguage(string lang) + { + CultureInfo culture; + try { culture = new CultureInfo(lang); } + catch (CultureNotFoundException) { return "Language not found"; } + + if (!loadedLanguage.Contains(culture.Name)) + { + var result = LoadLanguageAssembly(culture); + if (!result.Ok) + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + return result.Error; + } + loadedLanguage.Add(culture.Name); + } + + Thread.CurrentThread.CurrentUICulture = culture; + return R.Ok; + } + + private static E LoadLanguageAssembly(CultureInfo culture) + { + if (strings.ResourceManager.GetResourceSet(culture, true, false) != null) + return R.Ok; + + CultureInfo currentResolveCulture = culture; + while (currentResolveCulture != CultureInfo.InvariantCulture) + { + if (strings.ResourceManager.GetResourceSet(currentResolveCulture, true, false) != null) + return R.Ok; + currentResolveCulture = currentResolveCulture.Parent; + } + + currentResolveCulture = culture; + bool loadOk = false; + while (currentResolveCulture != CultureInfo.InvariantCulture) + { + string tryPath = Path.Combine(currentResolveCulture.Name, "TS3AudioBot.resources.dll"); + try + { + Assembly.LoadFrom(tryPath); + loadOk = true; + break; + } + catch (Exception ex) + { + Log.Trace(ex, "Failed trying to load language from '{0}'", tryPath); + currentResolveCulture = currentResolveCulture.Parent; + } + } + + if (loadOk) + return R.Ok; + else + return "Could not find language file"; + } + + public static string GetString(string name) + { + return strings.ResourceManager.GetString(name); + } + } +} diff --git a/TS3AudioBot/Localization/strings.Designer.cs b/TS3AudioBot/Localization/strings.Designer.cs new file mode 100644 index 00000000..b2192e95 --- /dev/null +++ b/TS3AudioBot/Localization/strings.Designer.cs @@ -0,0 +1,2406 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TS3AudioBot.Localization { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TS3AudioBot.Localization.strings", typeof(strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to This feature is not documented.. + /// + internal static string _undocumented { + get { + return ResourceManager.GetString("_undocumented", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Adds a new song to the queue.. + /// + internal static string cmd_add_help { + get { + return ResourceManager.GetString("cmd_add_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generates an api nonce.. + /// + internal static string cmd_api_nonce_help { + get { + return ResourceManager.GetString("cmd_api_nonce_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generates an api token.. + /// + internal static string cmd_api_token_help { + get { + return ResourceManager.GetString("cmd_api_token_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets an avatar for the bot. + /// + internal static string cmd_bot_avatar_help { + get { + return ResourceManager.GetString("cmd_bot_avatar_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set your bot a badge. The badges string starts with 'overwolf=0:badges='. + /// + internal static string cmd_bot_badges_help { + get { + return ResourceManager.GetString("cmd_bot_badges_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Moves the bot to your channel.. + /// + internal static string cmd_bot_come_help { + get { + return ResourceManager.GetString("cmd_bot_come_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the status of the channel commander mode.. + /// + internal static string cmd_bot_commander_help { + get { + return ResourceManager.GetString("cmd_bot_commander_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disables channel commander.. + /// + internal static string cmd_bot_commander_off_help { + get { + return ResourceManager.GetString("cmd_bot_commander_off_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enables channel commander.. + /// + internal static string cmd_bot_commander_on_help { + get { + return ResourceManager.GetString("cmd_bot_commander_on_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connects a new bot with the settings from the template name.. + /// + internal static string cmd_bot_connect_template_help { + get { + return ResourceManager.GetString("cmd_bot_connect_template_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connects a new bot to the given address.. + /// + internal static string cmd_bot_connect_to_help { + get { + return ResourceManager.GetString("cmd_bot_connect_to_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop this bot instance.. + /// + internal static string cmd_bot_disconnect_help { + get { + return ResourceManager.GetString("cmd_bot_disconnect_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets various information about the bot.. + /// + internal static string cmd_bot_info_help { + get { + return ResourceManager.GetString("cmd_bot_info_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the id of the current bot.. + /// + internal static string cmd_bot_info_id_help { + get { + return ResourceManager.GetString("cmd_bot_info_id_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets a list of all active bots.. + /// + internal static string cmd_bot_list_help { + get { + return ResourceManager.GetString("cmd_bot_list_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Moves the bot to you or a specified channel.. + /// + internal static string cmd_bot_move_help { + get { + return ResourceManager.GetString("cmd_bot_move_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gives the bot a new name.. + /// + internal static string cmd_bot_name_help { + get { + return ResourceManager.GetString("cmd_bot_name_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Saves the configuration for the newly connected bot.. + /// + internal static string cmd_bot_save_help { + get { + return ResourceManager.GetString("cmd_bot_save_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bot setup failed. See logs for more details.. + /// + internal static string cmd_bot_setup_error { + get { + return ResourceManager.GetString("cmd_bot_setup_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets all teamspeak rights for the bot to be fully functional.. + /// + internal static string cmd_bot_setup_help { + get { + return ResourceManager.GetString("cmd_bot_setup_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switches the conetext to the requested bot.. + /// + internal static string cmd_bot_use_help { + get { + return ResourceManager.GetString("cmd_bot_use_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Removes all songs from the current playlist.. + /// + internal static string cmd_clear_help { + get { + return ResourceManager.GetString("cmd_clear_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Executes a given command or string. + /// + internal static string cmd_eval_help { + get { + return ResourceManager.GetString("cmd_eval_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets all information about you.. + /// + internal static string cmd_getmy_all_help { + get { + return ResourceManager.GetString("cmd_getmy_all_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets your channel id you are currently in.. + /// + internal static string cmd_getmy_channel_help { + get { + return ResourceManager.GetString("cmd_getmy_channel_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets your database id.. + /// + internal static string cmd_getmy_dbid_help { + get { + return ResourceManager.GetString("cmd_getmy_dbid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets your id.. + /// + internal static string cmd_getmy_id_help { + get { + return ResourceManager.GetString("cmd_getmy_id_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets your nickname.. + /// + internal static string cmd_getmy_name_help { + get { + return ResourceManager.GetString("cmd_getmy_name_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets your unique id.. + /// + internal static string cmd_getmy_uid_help { + get { + return ResourceManager.GetString("cmd_getmy_uid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets all information about a user, searching with his id.. + /// + internal static string cmd_getuser_all_byid_help { + get { + return ResourceManager.GetString("cmd_getuser_all_byid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets all information of a user, searching with his name.. + /// + internal static string cmd_getuser_all_byname_help { + get { + return ResourceManager.GetString("cmd_getuser_all_byname_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the channel id a user is currently in, searching with his id.. + /// + internal static string cmd_getuser_channel_byid_help { + get { + return ResourceManager.GetString("cmd_getuser_channel_byid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the database id of a user, searching with his id.. + /// + internal static string cmd_getuser_dbid_byid_help { + get { + return ResourceManager.GetString("cmd_getuser_dbid_byid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the id of a user, searching with his name.. + /// + internal static string cmd_getuser_id_byname_help { + get { + return ResourceManager.GetString("cmd_getuser_id_byname_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the user name by dbid, searching with his database id.. + /// + internal static string cmd_getuser_name_bydbid_help { + get { + return ResourceManager.GetString("cmd_getuser_name_bydbid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the nickname of a user, searching with his id.. + /// + internal static string cmd_getuser_name_byid_help { + get { + return ResourceManager.GetString("cmd_getuser_name_byid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the unique id of a user, searching with his database id.. + /// + internal static string cmd_getuser_uid_bydbid_help { + get { + return ResourceManager.GetString("cmd_getuser_uid_bydbid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the unique id of a user, searching with his id.. + /// + internal static string cmd_getuser_uid_byid_help { + get { + return ResourceManager.GetString("cmd_getuser_uid_byid_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Requested command is ambiguous between: {0}. + /// + internal static string cmd_help_error_ambiguous_command { + get { + return ResourceManager.GetString("cmd_help_error_ambiguous_command", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command has no further subfunctions after {0}. + /// + internal static string cmd_help_error_no_further_subfunctions { + get { + return ResourceManager.GetString("cmd_help_error_no_further_subfunctions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No matching command found! Try !help to get a list of all commands.. + /// + internal static string cmd_help_error_no_matching_command { + get { + return ResourceManager.GetString("cmd_help_error_no_matching_command", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Seems like something went wrong. No help can be shown for this command path.. + /// + internal static string cmd_help_error_unknown_error { + get { + return ResourceManager.GetString("cmd_help_error_unknown_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ========= Welcome to the TS3AudioBot ========= + ///If you need any help with a special command use !help <commandName>. + ///Here are all possible commands:. + /// + internal static string cmd_help_header { + get { + return ResourceManager.GetString("cmd_help_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows all commands or detailed help about a specific command.. + /// + internal static string cmd_help_help { + get { + return ResourceManager.GetString("cmd_help_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command contains the following subfunctions: {0}. + /// + internal static string cmd_help_info_contains_subfunctions { + get { + return ResourceManager.GetString("cmd_help_info_contains_subfunctions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <id> Adds the song with <id> to the queue. + /// + internal static string cmd_history_add_help { + get { + return ResourceManager.GetString("cmd_history_add_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do want to clean the history file now? . + /// + internal static string cmd_history_clean_confirm_clean { + get { + return ResourceManager.GetString("cmd_history_clean_confirm_clean", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cleans up the history file for better startup performance.. + /// + internal static string cmd_history_clean_help { + get { + return ResourceManager.GetString("cmd_history_clean_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do want to remove all defective links file now?. + /// + internal static string cmd_history_clean_removedefective_confirm_clean { + get { + return ResourceManager.GetString("cmd_history_clean_removedefective_confirm_clean", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Checks for all links in the history which cannot be opened anymore and removes them.. + /// + internal static string cmd_history_clean_removedefective_help { + get { + return ResourceManager.GetString("cmd_history_clean_removedefective_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do you really want to delete the entry "{0}" + ///with the id {1}?. + /// + internal static string cmd_history_delete_confirm { + get { + return ResourceManager.GetString("cmd_history_delete_confirm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <id> Removes the entry with <id> from the history. + /// + internal static string cmd_history_delete_help { + get { + return ResourceManager.GetString("cmd_history_delete_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the last <count> songs from the user with the given <user-dbid>. + /// + internal static string cmd_history_from_help { + get { + return ResourceManager.GetString("cmd_history_from_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is the currently highest song id.. + /// + internal static string cmd_history_id_last { + get { + return ResourceManager.GetString("cmd_history_id_last", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} will be the next song id.. + /// + internal static string cmd_history_id_next { + get { + return ResourceManager.GetString("cmd_history_id_next", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to (last|next) Gets the highest|next song id. + /// + internal static string cmd_history_id_string_help { + get { + return ResourceManager.GetString("cmd_history_id_string_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <id> Displays all saved informations about the song with <id>. + /// + internal static string cmd_history_id_uint_help { + get { + return ResourceManager.GetString("cmd_history_id_uint_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plays the last song again. + /// + internal static string cmd_history_last_help { + get { + return ResourceManager.GetString("cmd_history_last_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <count> Gets the last <count> played songs.. + /// + internal static string cmd_history_last_int_help { + get { + return ResourceManager.GetString("cmd_history_last_int_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is no song in the history.. + /// + internal static string cmd_history_last_is_empty { + get { + return ResourceManager.GetString("cmd_history_last_is_empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <id> Playes the song with <id>. + /// + internal static string cmd_history_play_help { + get { + return ResourceManager.GetString("cmd_history_play_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <id> <name> Sets the name of the song with <id> to <name>. + /// + internal static string cmd_history_rename_help { + get { + return ResourceManager.GetString("cmd_history_rename_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The new name must not be empty or only whitespaces.. + /// + internal static string cmd_history_rename_invalid_name { + get { + return ResourceManager.GetString("cmd_history_rename_invalid_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <date> Gets all songs played until <date>.. + /// + internal static string cmd_history_till_DateTime_help { + get { + return ResourceManager.GetString("cmd_history_till_DateTime_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <name> Any of those desciptors: (hour|today|yesterday|week). + /// + internal static string cmd_history_till_string_help { + get { + return ResourceManager.GetString("cmd_history_till_string_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets all songs which title contains <string>. + /// + internal static string cmd_history_title_help { + get { + return ResourceManager.GetString("cmd_history_title_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Conditionally executes subcommands.. + /// + internal static string cmd_if_help { + get { + return ResourceManager.GetString("cmd_if_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown comparison operator.. + /// + internal static string cmd_if_unknown_operator { + get { + return ResourceManager.GetString("cmd_if_unknown_operator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows you to combine multiple JsonResults into one. + /// + internal static string cmd_json_merge_help { + get { + return ResourceManager.GetString("cmd_json_merge_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Guess what?. + /// + internal static string cmd_kickme_help { + get { + return ResourceManager.GetString("cmd_kickme_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I'm not strong enough, master!. + /// + internal static string cmd_kickme_missing_permission { + get { + return ResourceManager.GetString("cmd_kickme_missing_permission", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets a link to the origin of the current song.. + /// + internal static string cmd_link_help { + get { + return ResourceManager.GetString("cmd_link_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <id> Adds a link to your private playlist from the history by <id>.. + /// + internal static string cmd_list_add_help { + get { + return ResourceManager.GetString("cmd_list_add_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clears your private playlist.. + /// + internal static string cmd_list_clear_help { + get { + return ResourceManager.GetString("cmd_list_clear_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are not allowed to delete others playlists.. + /// + internal static string cmd_list_delete_cannot_delete_others_playlist { + get { + return ResourceManager.GetString("cmd_list_delete_cannot_delete_others_playlist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do you really want to delete the playlist "{0}". + /// + internal static string cmd_list_delete_confirm { + get { + return ResourceManager.GetString("cmd_list_delete_confirm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <name> Deletes the playlist with the name <name>. You can only delete playlists which you also have created. Admins can delete every playlist.. + /// + internal static string cmd_list_delete_help { + get { + return ResourceManager.GetString("cmd_list_delete_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <link> Imports a playlist form an other plattform like youtube etc.. + /// + internal static string cmd_list_get_help { + get { + return ResourceManager.GetString("cmd_list_get_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <index> Removes the item at <index>.. + /// + internal static string cmd_list_item_delete_help { + get { + return ResourceManager.GetString("cmd_list_item_delete_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <from> <to> Moves a item in a playlist <from> <to> position.. + /// + internal static string cmd_list_item_move_help { + get { + return ResourceManager.GetString("cmd_list_item_move_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displays all available playlists from all users.. + /// + internal static string cmd_list_list_help { + get { + return ResourceManager.GetString("cmd_list_list_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opens a playlist to be editable for you. This replaces your current worklist with the opened playlist.. + /// + internal static string cmd_list_load_help { + get { + return ResourceManager.GetString("cmd_list_load_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loaded "{0}" with {1} songs.. + /// + internal static string cmd_list_load_response { + get { + return ResourceManager.GetString("cmd_list_load_response", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Appends another playlist to yours.. + /// + internal static string cmd_list_merge_help { + get { + return ResourceManager.GetString("cmd_list_merge_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displays the name of the playlist you are currently working on.. + /// + internal static string cmd_list_name_help { + get { + return ResourceManager.GetString("cmd_list_name_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replaces the current freelist with your workinglist and plays from the beginning.. + /// + internal static string cmd_list_play_help { + get { + return ResourceManager.GetString("cmd_list_play_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Appends your playlist to the freelist.. + /// + internal static string cmd_list_queue_help { + get { + return ResourceManager.GetString("cmd_list_queue_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stores your current workinglist to disk.. + /// + internal static string cmd_list_save_help { + get { + return ResourceManager.GetString("cmd_list_save_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Playlist: "{0}" with {1} songs.. + /// + internal static string cmd_list_show_header { + get { + return ResourceManager.GetString("cmd_list_show_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <name> Displays all songs currently in the playlists with the name <name>. + /// + internal static string cmd_list_show_help { + get { + return ResourceManager.GetString("cmd_list_show_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets whether or not to loop the entire playlist.. + /// + internal static string cmd_loop_help { + get { + return ResourceManager.GetString("cmd_loop_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disables looping the entire playlist.. + /// + internal static string cmd_loop_off_help { + get { + return ResourceManager.GetString("cmd_loop_off_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enables looping the entire playlist.. + /// + internal static string cmd_loop_on_help { + get { + return ResourceManager.GetString("cmd_loop_on_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plays the next song in the playlist.. + /// + internal static string cmd_next_help { + get { + return ResourceManager.GetString("cmd_next_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displays the AST of the requested command.. + /// + internal static string cmd_parse_command_help { + get { + return ResourceManager.GetString("cmd_parse_command_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Well, pauses the song. Undo with !play.. + /// + internal static string cmd_pause_help { + get { + return ResourceManager.GetString("cmd_pause_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically tries to decide whether the link is a special resource (like youtube) or a direct resource (like ./hello.mp3) and starts it.. + /// + internal static string cmd_play_help { + get { + return ResourceManager.GetString("cmd_play_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lists all found plugins.. + /// + internal static string cmd_plugin_list_help { + get { + return ResourceManager.GetString("cmd_plugin_list_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unloads a plugin.. + /// + internal static string cmd_plugin_load_help { + get { + return ResourceManager.GetString("cmd_plugin_load_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unloads a plugin.. + /// + internal static string cmd_plugin_unload_help { + get { + return ResourceManager.GetString("cmd_plugin_unload_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Requests a private session with the ServerBot so you can be intimate.. + /// + internal static string cmd_pm_help { + get { + return ResourceManager.GetString("cmd_pm_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hi {0}. + /// + internal static string cmd_pm_hi { + get { + return ResourceManager.GetString("cmd_pm_hi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plays the previous song in the playlist.. + /// + internal static string cmd_previous_help { + get { + return ResourceManager.GetString("cmd_previous_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lets you format multiple parameter to one.. + /// + internal static string cmd_print_help { + get { + return ResourceManager.GetString("cmd_print_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do you really want to quit?. + /// + internal static string cmd_quit_confirm { + get { + return ResourceManager.GetString("cmd_quit_confirm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closes the TS3AudioBot application.. + /// + internal static string cmd_quit_help { + get { + return ResourceManager.GetString("cmd_quit_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows the quizmode status.. + /// + internal static string cmd_quiz_help { + get { + return ResourceManager.GetString("cmd_quiz_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disable to show the songnames again.. + /// + internal static string cmd_quiz_off_help { + get { + return ResourceManager.GetString("cmd_quiz_off_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No cheating! Everybody has to see it!. + /// + internal static string cmd_quiz_off_no_cheating { + get { + return ResourceManager.GetString("cmd_quiz_off_no_cheating", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable to hide the songnames and let your friends guess the title.. + /// + internal static string cmd_quiz_on_help { + get { + return ResourceManager.GetString("cmd_quiz_on_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets whether or not to play playlists in random order.. + /// + internal static string cmd_random_help { + get { + return ResourceManager.GetString("cmd_random_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disables random playlist playback. + /// + internal static string cmd_random_off_help { + get { + return ResourceManager.GetString("cmd_random_off_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enables random playlist playback. + /// + internal static string cmd_random_on_help { + get { + return ResourceManager.GetString("cmd_random_on_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the unique seed for a certain playback order. + /// + internal static string cmd_random_seed_help { + get { + return ResourceManager.GetString("cmd_random_seed_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets the unique seed for a certain playback order. + /// + internal static string cmd_random_seed_int_help { + get { + return ResourceManager.GetString("cmd_random_seed_int_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only letters allowed.. + /// + internal static string cmd_random_seed_only_letters_allowed { + get { + return ResourceManager.GetString("cmd_random_seed_only_letters_allowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets the unique seed for a certain playback order. + /// + internal static string cmd_random_seed_string_help { + get { + return ResourceManager.GetString("cmd_random_seed_string_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets whether or not to loop a single song.. + /// + internal static string cmd_repeat_help { + get { + return ResourceManager.GetString("cmd_repeat_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disables single song repeat.. + /// + internal static string cmd_repeat_off_help { + get { + return ResourceManager.GetString("cmd_repeat_off_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enables single song repeat.. + /// + internal static string cmd_repeat_on_help { + get { + return ResourceManager.GetString("cmd_repeat_on_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returns the subset of allowed commands the caller (you) can execute.. + /// + internal static string cmd_rights_can_help { + get { + return ResourceManager.GetString("cmd_rights_can_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error while parsing file, see log for more details.. + /// + internal static string cmd_rights_reload_error_parsing_file { + get { + return ResourceManager.GetString("cmd_rights_reload_error_parsing_file", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reloads the rights configuration from file.. + /// + internal static string cmd_rights_reload_help { + get { + return ResourceManager.GetString("cmd_rights_reload_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets a random number.. + /// + internal static string cmd_rng_help { + get { + return ResourceManager.GetString("cmd_rng_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be 0 or positive.. + /// + internal static string cmd_rng_value_must_be_positive { + get { + return ResourceManager.GetString("cmd_rng_value_must_be_positive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Jumps to a timemark within the current song.. + /// + internal static string cmd_seek_help { + get { + return ResourceManager.GetString("cmd_seek_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The time was not in a correct format, see !help seek for more information.. + /// + internal static string cmd_seek_invalid_format { + get { + return ResourceManager.GetString("cmd_seek_invalid_format", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The point of time is not within the song length.. + /// + internal static string cmd_seek_out_of_range { + get { + return ResourceManager.GetString("cmd_seek_out_of_range", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please specify a key. E.g {0}. + /// + internal static string cmd_settings_empty_usage { + get { + return ResourceManager.GetString("cmd_settings_empty_usage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets a value from the bot settings.. + /// + internal static string cmd_settings_get_help { + get { + return ResourceManager.GetString("cmd_settings_get_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets a value from the core settings.. + /// + internal static string cmd_settings_global_get_help { + get { + return ResourceManager.GetString("cmd_settings_global_get_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set a value from the core settings.. + /// + internal static string cmd_settings_global_set_help { + get { + return ResourceManager.GetString("cmd_settings_global_set_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Provides get/set methods to change the settings.. + /// + internal static string cmd_settings_help { + get { + return ResourceManager.GetString("cmd_settings_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get the desciption for a setting.. + /// + internal static string cmd_settings_help_help { + get { + return ResourceManager.GetString("cmd_settings_help_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets a value from the bot settings.. + /// + internal static string cmd_settings_set_help { + get { + return ResourceManager.GetString("cmd_settings_set_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tells you the name of the current song.. + /// + internal static string cmd_song_help { + get { + return ResourceManager.GetString("cmd_song_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the length and position of the current track.. + /// + internal static string cmd_song_position_help { + get { + return ResourceManager.GetString("cmd_song_position_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stops the current song.. + /// + internal static string cmd_stop_help { + get { + return ResourceManager.GetString("cmd_stop_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Adds your current channel to the music playback.. + /// + internal static string cmd_subscribe_channel_help { + get { + return ResourceManager.GetString("cmd_subscribe_channel_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lets you hear the music independent from the channel you are in.. + /// + internal static string cmd_subscribe_help { + get { + return ResourceManager.GetString("cmd_subscribe_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Adds your current channel to the music playback.. + /// + internal static string cmd_subscribe_tempchannel_help { + get { + return ResourceManager.GetString("cmd_subscribe_tempchannel_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Take a substring from a string.. + /// + internal static string cmd_take_help { + get { + return ResourceManager.GetString("cmd_take_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not enough arguments to take.. + /// + internal static string cmd_take_not_enough_arguements { + get { + return ResourceManager.GetString("cmd_take_not_enough_arguements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Removes your current channel from the music playback.. + /// + internal static string cmd_unsubscribe_channel_help { + get { + return ResourceManager.GetString("cmd_unsubscribe_channel_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only lets you hear the music in active channels again.. + /// + internal static string cmd_unsubscribe_help { + get { + return ResourceManager.GetString("cmd_unsubscribe_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clears all temporary targets.. + /// + internal static string cmd_unsubscribe_temporary_help { + get { + return ResourceManager.GetString("cmd_unsubscribe_temporary_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gets the current build version.. + /// + internal static string cmd_version_help { + get { + return ResourceManager.GetString("cmd_version_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current volume: {0}. + /// + internal static string cmd_volume_current { + get { + return ResourceManager.GetString("cmd_volume_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets the volume level of the music.. + /// + internal static string cmd_volume_help { + get { + return ResourceManager.GetString("cmd_volume_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Careful you are requesting a very high volume! Do you want to apply this?. + /// + internal static string cmd_volume_high_volume_confirm { + get { + return ResourceManager.GetString("cmd_volume_high_volume_confirm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The volume level must be between {0} and {1}. + /// + internal static string cmd_volume_is_limited { + get { + return ResourceManager.GetString("cmd_volume_is_limited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are not allowed to set higher volumes.. + /// + internal static string cmd_volume_missing_high_volume_permission { + get { + return ResourceManager.GetString("cmd_volume_missing_high_volume_permission", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The new volume could not be parsed. + /// + internal static string cmd_volume_parse_error { + get { + return ResourceManager.GetString("cmd_volume_parse_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set how to send music.. + /// + internal static string cmd_whisper_all_help { + get { + return ResourceManager.GetString("cmd_whisper_all_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set a specific teamspeak whisper group.. + /// + internal static string cmd_whisper_group_help { + get { + return ResourceManager.GetString("cmd_whisper_group_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This type requires an additional target.. + /// + internal static string cmd_whisper_group_missing_target { + get { + return ResourceManager.GetString("cmd_whisper_group_missing_target", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This type does not take an additional target.. + /// + internal static string cmd_whisper_group_superfluous_target { + get { + return ResourceManager.GetString("cmd_whisper_group_superfluous_target", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Currently targeting:. + /// + internal static string cmd_whisper_list_header { + get { + return ResourceManager.GetString("cmd_whisper_list_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set how to send music.. + /// + internal static string cmd_whisper_list_help { + get { + return ResourceManager.GetString("cmd_whisper_list_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nowhere!. + /// + internal static string cmd_whisper_list_target_none { + get { + return ResourceManager.GetString("cmd_whisper_list_target_none", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This channel via voice!. + /// + internal static string cmd_whisper_list_target_voice { + get { + return ResourceManager.GetString("cmd_whisper_list_target_voice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel. + /// + internal static string cmd_whisper_list_target_whisper_channel { + get { + return ResourceManager.GetString("cmd_whisper_list_target_whisper_channel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clients. + /// + internal static string cmd_whisper_list_target_whisper_clients { + get { + return ResourceManager.GetString("cmd_whisper_list_target_whisper_clients", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A whisper group {0} {1} ({2})!. + /// + internal static string cmd_whisper_list_target_whispergroup { + get { + return ResourceManager.GetString("cmd_whisper_list_target_whispergroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enables normal voice mode.. + /// + internal static string cmd_whisper_off_help { + get { + return ResourceManager.GetString("cmd_whisper_off_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enables default whisper subscription mode.. + /// + internal static string cmd_whisper_subscription_help { + get { + return ResourceManager.GetString("cmd_whisper_subscription_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Evaluates all parameter.. + /// + internal static string cmd_xecute_help { + get { + return ResourceManager.GetString("cmd_xecute_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This bot does not exists.. + /// + internal static string error_bot_does_not_exist { + get { + return ResourceManager.GetString("error_bot_does_not_exist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error: {0}. + /// + internal static string error_call_error { + get { + return ResourceManager.GetString("error_call_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An unexpected error occured: {0}. + /// + internal static string error_call_unexpected_error { + get { + return ResourceManager.GetString("error_call_unexpected_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Need at least four arguments to evaluate.. + /// + internal static string error_cmd_at_least_four_argument { + get { + return ResourceManager.GetString("error_cmd_at_least_four_argument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Need at least one argument to evaluate.. + /// + internal static string error_cmd_at_least_one_argument { + get { + return ResourceManager.GetString("error_cmd_at_least_one_argument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Need at least two arguments to evaluate.. + /// + internal static string error_cmd_at_least_two_argument { + get { + return ResourceManager.GetString("error_cmd_at_least_two_argument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Found more than one matching key: {0}. + /// + internal static string error_config_multiple_keys_found { + get { + return ResourceManager.GetString("error_config_multiple_keys_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No config key matching the pattern found.. + /// + internal static string error_config_no_key_found { + get { + return ResourceManager.GetString("error_config_no_key_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value could not be parsed.. + /// + internal static string error_config_value_parse_error { + get { + return ResourceManager.GetString("error_config_value_parse_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not create new instance.. + /// + internal static string error_could_not_create_bot { + get { + return ResourceManager.GetString("error_could_not_create_bot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This feature is currently unavailable.. + /// + internal static string error_feature_unavailable { + get { + return ResourceManager.GetString("error_feature_unavailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Entry could not be stored.. + /// + internal static string error_history_could_not_create_entry { + get { + return ResourceManager.GetString("error_history_could_not_create_entry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find entry with this id.. + /// + internal static string error_history_could_not_find_entry { + get { + return ResourceManager.GetString("error_history_could_not_find_entry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid token-valid duration.. + /// + internal static string error_invalid_token_duration { + get { + return ResourceManager.GetString("error_invalid_token_duration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invoker casted Ghost Walk.. + /// + internal static string error_invoker_not_visible { + get { + return ResourceManager.GetString("error_invoker_not_visible", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The file still in use.. + /// + internal static string error_io_in_use { + get { + return ResourceManager.GetString("error_io_in_use", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The bot does not have the required system permission.. + /// + internal static string error_io_missing_permission { + get { + return ResourceManager.GetString("error_io_missing_permission", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An unknown error occoured.. + /// + internal static string error_io_unknown_error { + get { + return ResourceManager.GetString("error_io_unknown_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The id could not get parsed.. + /// + internal static string error_media_failed_to_parse_id { + get { + return ResourceManager.GetString("error_media_failed_to_parse_id", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The resource could not be found.. + /// + internal static string error_media_file_not_found { + get { + return ResourceManager.GetString("error_media_file_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No image could not be found.. + /// + internal static string error_media_image_not_found { + get { + return ResourceManager.GetString("error_media_image_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid or malformed response parts.. + /// + internal static string error_media_internal_invalid { + get { + return ResourceManager.GetString("error_media_internal_invalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Empty or missing response parts.. + /// + internal static string error_media_internal_missing { + get { + return ResourceManager.GetString("error_media_internal_missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The image is invalid.. + /// + internal static string error_media_invalid_image { + get { + return ResourceManager.GetString("error_media_invalid_image", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid uri.. + /// + internal static string error_media_invalid_uri { + get { + return ResourceManager.GetString("error_media_invalid_uri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No stream extracted.. + /// + internal static string error_media_no_stream_extracted { + get { + return ResourceManager.GetString("error_media_no_stream_extracted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot execute '{0}'. You are missing the '{1}' right!. + /// + internal static string error_missing_right { + get { + return ResourceManager.GetString("error_missing_right", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing session context.. + /// + internal static string error_missing_session_context { + get { + return ResourceManager.GetString("error_missing_session_context", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request got empty response.. + /// + internal static string error_net_empty_response { + get { + return ResourceManager.GetString("error_net_empty_response", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Got error status code.. + /// + internal static string error_net_error_status_code { + get { + return ResourceManager.GetString("error_net_error_status_code", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No connection could be established.. + /// + internal static string error_net_no_connection { + get { + return ResourceManager.GetString("error_net_no_connection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request timed out.. + /// + internal static string error_net_timeout { + get { + return ResourceManager.GetString("error_net_timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown request error.. + /// + internal static string error_net_unknown { + get { + return ResourceManager.GetString("error_net_unknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No active token found.. + /// + internal static string error_no_active_token { + get { + return ResourceManager.GetString("error_no_active_token", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No help found.. + /// + internal static string error_no_help { + get { + return ResourceManager.GetString("error_no_help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No invoker in context.. + /// + internal static string error_no_invoker_in_context { + get { + return ResourceManager.GetString("error_no_invoker_in_context", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing session context.. + /// + internal static string error_no_session_in_context { + get { + return ResourceManager.GetString("error_no_session_in_context", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No target channel found.. + /// + internal static string error_no_target_channel { + get { + return ResourceManager.GetString("error_no_target_channel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No teamspeak connection in context.. + /// + internal static string error_no_teamspeak_in_context { + get { + return ResourceManager.GetString("error_no_teamspeak_in_context", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No Uid found to register token for.. + /// + internal static string error_no_uid_found { + get { + return ResourceManager.GetString("error_no_uid_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This command is not available as API.. + /// + internal static string error_not_available_from_api { + get { + return ResourceManager.GetString("error_not_available_from_api", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not found.. + /// + internal static string error_not_found { + get { + return ResourceManager.GetString("error_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't find a fitting return type to return.. + /// + internal static string error_nothing_to_return { + get { + return ResourceManager.GetString("error_nothing_to_return", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The playlist file is corrupted. Only admins can modify it until fixed.. + /// + internal static string error_playlist_broken_file { + get { + return ResourceManager.GetString("error_playlist_broken_file", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You cannot access a playlist which you dont own.. + /// + internal static string error_playlist_cannot_access_not_owned { + get { + return ResourceManager.GetString("error_playlist_cannot_access_not_owned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nothing to play.... + /// + internal static string error_playlist_is_empty { + get { + return ResourceManager.GetString("error_playlist_is_empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Index must be within playlist length.. + /// + internal static string error_playlist_item_index_out_of_range { + get { + return ResourceManager.GetString("error_playlist_item_index_out_of_range", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The playlist name contains invalid characters; please only use [a-zA-Z0-9_-].. + /// + internal static string error_playlist_name_invalid_character { + get { + return ResourceManager.GetString("error_playlist_name_invalid_character", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An empty playlist name is not valid.. + /// + internal static string error_playlist_name_invalid_empty { + get { + return ResourceManager.GetString("error_playlist_name_invalid_empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The playlist name length must be <64.. + /// + internal static string error_playlist_name_invalid_too_long { + get { + return ResourceManager.GetString("error_playlist_name_invalid_too_long", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No playlist directory has been set up.. + /// + internal static string error_playlist_no_store_directory { + get { + return ResourceManager.GetString("error_playlist_no_store_directory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The playlist could not be found.. + /// + internal static string error_playlist_not_found { + get { + return ResourceManager.GetString("error_playlist_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Special list not found.. + /// + internal static string error_playlist_special_not_found { + get { + return ResourceManager.GetString("error_playlist_special_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Internal error.. + /// + internal static string error_playmgr_internal_error { + get { + return ResourceManager.GetString("error_playmgr_internal_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A few songs failed to start, use {0} to continue.. + /// + internal static string error_playmgr_many_songs_failed { + get { + return ResourceManager.GetString("error_playmgr_many_songs_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plugin error: {0}. + /// + internal static string error_plugin_error { + get { + return ResourceManager.GetString("error_plugin_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not load.. + /// + internal static string error_resfac_could_not_load { + get { + return ResourceManager.GetString("error_resfac_could_not_load", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Considered multiple factories but all failed.. + /// + internal static string error_resfac_multiple_factories_failed { + get { + return ResourceManager.GetString("error_resfac_multiple_factories_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No registered factory for "{0}" found.. + /// + internal static string error_resfac_no_registered_factory { + get { + return ResourceManager.GetString("error_resfac_no_registered_factory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot move there.. + /// + internal static string error_ts_cannot_move { + get { + return ResourceManager.GetString("error_ts_cannot_move", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot set commander mode.. + /// + internal static string error_ts_cannot_set_commander { + get { + return ResourceManager.GetString("error_ts_cannot_set_commander", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing permissions.. + /// + internal static string error_ts_code_2568 { + get { + return ResourceManager.GetString("error_ts_code_2568", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Teamspeak Error: {0}. + /// + internal static string error_ts_error { + get { + return ResourceManager.GetString("error_ts_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The new name is too long or invalid.. + /// + internal static string error_ts_invalid_name { + get { + return ResourceManager.GetString("error_ts_invalid_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The message to send is longer than the allowed maximum.. + /// + internal static string error_ts_msg_too_long { + get { + return ResourceManager.GetString("error_ts_msg_too_long", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No client found.. + /// + internal static string error_ts_no_client_found { + get { + return ResourceManager.GetString("error_ts_no_client_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown Teamspeak Error.. + /// + internal static string error_ts_unknown_error { + get { + return ResourceManager.GetString("error_ts_unknown_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unrecognized descriptor.. + /// + internal static string error_unrecognized_descriptor { + get { + return ResourceManager.GetString("error_unrecognized_descriptor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please use this command in a private session.. + /// + internal static string error_use_private { + get { + return ResourceManager.GetString("error_use_private", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No youtube-dl response. + /// + internal static string error_ytdl_empty_response { + get { + return ResourceManager.GetString("error_ytdl_empty_response", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to run youtube-dl.. + /// + internal static string error_ytdl_failed_to_run { + get { + return ResourceManager.GetString("error_ytdl_failed_to_run", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Youtube-Dl could not be found. The song/video cannot be played due to restrictions.. + /// + internal static string error_ytdl_not_found { + get { + return ResourceManager.GetString("error_ytdl_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to youtube-dl failed to load the resource.. + /// + internal static string error_ytdl_song_failed_to_load { + get { + return ResourceManager.GetString("error_ytdl_song_failed_to_load", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This might take a while and make the bot unresponsive in meanwhile.. + /// + internal static string info_bot_might_be_unresponsive { + get { + return ResourceManager.GetString("info_bot_might_be_unresponsive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <Quiztime!>. + /// + internal static string info_botstatus_quiztime { + get { + return ResourceManager.GetString("info_botstatus_quiztime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <Sleeping>. + /// + internal static string info_botstatus_sleeping { + get { + return ResourceManager.GetString("info_botstatus_sleeping", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cleanup done!. + /// + internal static string info_cleanup_done { + get { + return ResourceManager.GetString("info_cleanup_done", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is nothing on right now.... + /// + internal static string info_currently_not_playing { + get { + return ResourceManager.GetString("info_currently_not_playing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Now playing: {0}. + /// + internal static string info_currently_playing { + get { + return ResourceManager.GetString("info_currently_playing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <empty>. + /// + internal static string info_empty { + get { + return ResourceManager.GetString("info_empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to off. + /// + internal static string info_off { + get { + return ResourceManager.GetString("info_off", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ok. + /// + internal static string info_ok { + get { + return ResourceManager.GetString("info_ok", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to on. + /// + internal static string info_on { + get { + return ResourceManager.GetString("info_on", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No next song could be played.. + /// + internal static string info_playmgr_no_next_song { + get { + return ResourceManager.GetString("info_playmgr_no_next_song", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No previous song could be played.. + /// + internal static string info_playmgr_no_previous_song { + get { + return ResourceManager.GetString("info_playmgr_no_previous_song", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sorry, you have to guess!. + /// + internal static string info_quizmode_is_active { + get { + return ResourceManager.GetString("info_quizmode_is_active", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Removed "{0}". + /// + internal static string info_removed { + get { + return ResourceManager.GetString("info_removed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Channel commander is {0}. + /// + internal static string info_status_channelcommander { + get { + return ResourceManager.GetString("info_status_channelcommander", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loop is {0}. + /// + internal static string info_status_loop { + get { + return ResourceManager.GetString("info_status_loop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quizmode is {0}. + /// + internal static string info_status_quizmode { + get { + return ResourceManager.GetString("info_status_quizmode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Random is {0}. + /// + internal static string info_status_random { + get { + return ResourceManager.GetString("info_status_random", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Repeat is {0}. + /// + internal static string info_status_repeat { + get { + return ResourceManager.GetString("info_status_repeat", resourceCulture); + } + } + } +} diff --git a/TS3AudioBot/Localization/strings.resx b/TS3AudioBot/Localization/strings.resx new file mode 100644 index 00000000..7f5b48e1 --- /dev/null +++ b/TS3AudioBot/Localization/strings.resx @@ -0,0 +1,903 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Adds a new song to the queue. + + + Generates an api token. + + + Generates an api nonce. + + + Gets the status of the channel commander mode. + + + Enables channel commander. + + + Disables channel commander. + + + Moves the bot to your channel. + + + Gets various information about the bot. + + + Gets the id of the current bot. + + + Gets a list of all active bots. + + + Moves the bot to you or a specified channel. + + + Gives the bot a new name. + + + Set your bot a badge. The badges string starts with 'overwolf=0:badges=' + + + Sets all teamspeak rights for the bot to be fully functional. + + + Switches the conetext to the requested bot. + + + Removes all songs from the current playlist. + + + Stop this bot instance. + + + Executes a given command or string + + + Gets your id. + + + Gets your unique id. + + + Gets your nickname. + + + Gets your database id. + + + Gets your channel id you are currently in. + + + Gets all information about you. + + + Gets the unique id of a user, searching with his id. + + + Gets the nickname of a user, searching with his id. + + + Gets the database id of a user, searching with his id. + + + Gets the channel id a user is currently in, searching with his id. + + + Gets all information about a user, searching with his id. + + + Gets the id of a user, searching with his name. + + + Gets all information of a user, searching with his name. + + + Gets the user name by dbid, searching with his database id. + + + Gets the unique id of a user, searching with his database id. + + + Shows all commands or detailed help about a specific command. + + + <id> Adds the song with <id> to the queue + + + Cleans up the history file for better startup performance. + + + <id> Removes the entry with <id> from the history + + + Gets the last <count> songs from the user with the given <user-dbid> + + + <id> Displays all saved informations about the song with <id> + + + (last|next) Gets the highest|next song id + + + <count> Gets the last <count> played songs. + + + Plays the last song again + + + <id> Playes the song with <id> + + + <id> <name> Sets the name of the song with <id> to <name> + + + <date> Gets all songs played until <date>. + + + <name> Any of those desciptors: (hour|today|yesterday|week) + + + Gets all songs which title contains <string> + + + Allows you to combine multiple JsonResults into one + + + Guess what? + + + Gets a link to the origin of the current song. + + + <id> Adds a link to your private playlist from the history by <id>. + + + Clears your private playlist. + + + <name> Deletes the playlist with the name <name>. You can only delete playlists which you also have created. Admins can delete every playlist. + + + <link> Imports a playlist form an other plattform like youtube etc. + + + <from> <to> Moves a item in a playlist <from> <to> position. + + + <index> Removes the item at <index>. + + + Displays all available playlists from all users. + + + Opens a playlist to be editable for you. This replaces your current worklist with the opened playlist. + + + Appends another playlist to yours. + + + Displays the name of the playlist you are currently working on. + + + Replaces the current freelist with your workinglist and plays from the beginning. + + + Appends your playlist to the freelist. + + + Stores your current workinglist to disk. + + + <name> Displays all songs currently in the playlists with the name <name> + + + Gets whether or not to loop the entire playlist. + + + Enables looping the entire playlist. + + + Disables looping the entire playlist. + + + Plays the next song in the playlist. + + + Requests a private session with the ServerBot so you can be intimate. + + + Displays the AST of the requested command. + + + Well, pauses the song. Undo with !play. + + + Automatically tries to decide whether the link is a special resource (like youtube) or a direct resource (like ./hello.mp3) and starts it. + + + Lists all found plugins. + + + Unloads a plugin. + + + Unloads a plugin. + + + Plays the previous song in the playlist. + + + Lets you format multiple parameter to one. + + + Closes the TS3AudioBot application. + + + Shows the quizmode status. + + + Enable to hide the songnames and let your friends guess the title. + + + Disable to show the songnames again. + + + Gets whether or not to play playlists in random order. + + + Enables random playlist playback + + + Disables random playlist playback + + + Gets the unique seed for a certain playback order + + + Sets the unique seed for a certain playback order + + + Sets the unique seed for a certain playback order + + + Gets whether or not to loop a single song. + + + Enables single song repeat. + + + Disables single song repeat. + + + Returns the subset of allowed commands the caller (you) can execute. + + + Reloads the rights configuration from file. + + + Gets a random number. + + + Jumps to a timemark within the current song. + + + Tells you the name of the current song. + + + Stops the current song. + + + Lets you hear the music independent from the channel you are in. + + + Adds your current channel to the music playback. + + + Adds your current channel to the music playback. + + + Take a substring from a string. + + + Only lets you hear the music in active channels again. + + + Removes your current channel from the music playback. + + + Clears all temporary targets. + + + Gets the current build version. + + + Sets the volume level of the music. + + + Set how to send music. + + + Set a specific teamspeak whisper group. + + + Set how to send music. + + + Enables normal voice mode. + + + Enables default whisper subscription mode. + + + Evaluates all parameter. + + + Requested command is ambiguous between: {0} + + + The command has no further subfunctions after {0} + + + No matching command found! Try !help to get a list of all commands. + + + Seems like something went wrong. No help can be shown for this command path. + + + ========= Welcome to the TS3AudioBot ========= +If you need any help with a special command use !help <commandName>. +Here are all possible commands: + + + The command contains the following subfunctions: {0} + + + Do want to clean the history file now? + + + Do want to remove all defective links file now? + + + Do you really want to delete the entry "{0}" +with the id {1}? + + + {0} is the currently highest song id. + + + {0} will be the next song id. + + + There is no song in the history. + + + The new name must not be empty or only whitespaces. + + + Unknown comparison operator. + + + I'm not strong enough, master! + + + You are not allowed to delete others playlists. + + + Do you really want to delete the playlist "{0}" + + + Loaded "{0}" with {1} songs. + + + Playlist: "{0}" with {1} songs. + + + Hi {0} + + + Do you really want to quit? + + + No cheating! Everybody has to see it! + + + Only letters allowed. + + + Error while parsing file, see log for more details. + + + Value must be 0 or positive. + + + The time was not in a correct format, see !help seek for more information. + + + The point of time is not within the song length. + + + Please specify a key. E.g {0} + + + Found more than one matching key: {0} + + + No config key matching the pattern found. + + + Not enough arguments to take. + + + Current volume: {0} + + + Careful you are requesting a very high volume! Do you want to apply this? + + + The volume level must be between {0} and {1} + + + You are not allowed to set higher volumes. + + + The new volume could not be parsed + + + This type requires an additional target. + + + This type does not take an additional target. + + + Currently targeting: + + + Nowhere! + + + This channel via voice! + + + A whisper group {0} {1} ({2})! + + + Channel + + + Clients + + + This bot does not exists. + + + Error: {0} + + + An unexpected error occured: {0} + + + Need at least four arguments to evaluate. + + + Need at least one argument to evaluate. + + + Need at least two arguments to evaluate. + + + Could not create new instance. + + + Invalid token-valid duration. + + + Invoker casted Ghost Walk. + + + Missing session context. + + + Can't find a fitting return type to return. + + + This command is not available as API. + + + Not found. + + + No active token found. + + + No invoker in context. + + + No target channel found. + + + No teamspeak connection in context. + + + No Uid found to register token for. + + + Nothing to play... + + + Index must be within playlist length. + + + The playlist could not be found. + + + Plugin error: {0} + + + Unrecognized descriptor. + + + Please use this command in a private session. + + + This might take a while and make the bot unresponsive in meanwhile. + + + Channel commander is {0} + + + Cleanup done! + + + There is nothing on right now... + + + Now playing: {0} + + + <empty> + + + Ok + + + Sorry, you have to guess! + + + Removed "{0}" + + + <Quiztime!> + + + <Sleeping> + + + Bot setup failed. See logs for more details. + + + This feature is currently unavailable. + + + Entry could not be stored. + + + Could not find entry with this id. + + + The playlist file is corrupted. Only admins can modify it until fixed. + + + Cannot move there. + + + Cannot set commander mode. + + + Teamspeak Error: {0} + + + The new name is too long or invalid. + + + The message to send is longer than the allowed maximum. + + + No client found. + + + Unknown Teamspeak Error. + + + off + + + on + + + Loop is {0} + + + Quizmode is {0} + + + Random is {0} + + + Repeat is {0} + + + The value could not be parsed. + + + The file still in use. + + + The bot does not have the required system permission. + + + An unknown error occoured. + + + The id could not get parsed. + + + The resource could not be found. + + + No image could not be found. + + + Invalid or malformed response parts. + + + Empty or missing response parts. + + + The image is invalid. + + + Invalid uri. + + + No stream extracted. + + + Request got empty response. + + + Got error status code. + + + No connection could be established. + + + Request timed out. + + + Unknown request error. + + + You cannot access a playlist which you dont own. + + + The playlist name contains invalid characters; please only use [a-zA-Z0-9_-]. + + + An empty playlist name is not valid. + + + The playlist name length must be <64. + + + No playlist directory has been set up. + + + Special list not found. + + + Internal error. + + + A few songs failed to start, use {0} to continue. + + + Could not load. + + + Considered multiple factories but all failed. + + + No registered factory for "{0}" found. + + + No youtube-dl response + + + Failed to run youtube-dl. + + + Youtube-Dl could not be found. The song/video cannot be played due to restrictions. + + + youtube-dl failed to load the resource. + + + No next song could be played. + + + No previous song could be played. + + + No help found. + + + Missing session context. + + + Missing permissions. + + + Connects a new bot to the given address. + + + Connects a new bot with the settings from the template name. + + + Saves the configuration for the newly connected bot. + + + Checks for all links in the history which cannot be opened anymore and removes them. + + + Conditionally executes subcommands. + + + Gets a value from the bot settings. + + + Gets a value from the core settings. + + + Set a value from the core settings. + + + Provides get/set methods to change the settings. + + + Get the desciption for a setting. + + + Sets a value from the bot settings. + + + This feature is not documented. + + + Sets an avatar for the bot + + + Gets the length and position of the current track. + + + You cannot execute '{0}'. You are missing the '{1}' right! + + diff --git a/TS3AudioBot/MainCommands.cs b/TS3AudioBot/MainCommands.cs index fdbeb731..09269be4 100644 --- a/TS3AudioBot/MainCommands.cs +++ b/TS3AudioBot/MainCommands.cs @@ -13,10 +13,14 @@ namespace TS3AudioBot using CommandSystem.Ast; using CommandSystem.CommandResults; using CommandSystem.Commands; + using Config; using Dependency; using Helper; using Helper.Environment; using History; + using Localization; + using Newtonsoft.Json.Linq; + using Playlists; using Plugins; using ResourceFactories; using Rights; @@ -33,28 +37,43 @@ namespace TS3AudioBot public static class MainCommands { + internal static ICommandBag Bag { get; } = new MainCommandsBag(); + + internal class MainCommandsBag : ICommandBag + { + public IReadOnlyCollection BagCommands { get; } + public IReadOnlyCollection AdditionalRights { get; } = new string[] { RightHighVolume, RightDeleteAllPlaylists }; + + public MainCommandsBag() + { + BagCommands = CommandManager.GetBotCommands(null, typeof(MainCommands)).ToArray(); + } + } + public const string RightHighVolume = "ts3ab.admin.volume"; public const string RightDeleteAllPlaylists = "ts3ab.admin.list"; + private const string YesNoOption = " !(yes|no)"; + // [...] = Optional // = Placeholder for a text // [text] = Option for fixed text // (a|b) = either or switch // ReSharper disable UnusedMember.Global - [Command("add", "Adds a new song to the queue.")] + [Command("add")] [Usage("", "Any link that is also recognized by !play")] - public static void CommandAdd(PlayManager playManager, InvokerData invoker, string parameter) - => playManager.Enqueue(invoker, parameter).UnwrapThrow(); + public static void CommandAdd(PlayManager playManager, InvokerData invoker, string url) + => playManager.Enqueue(invoker, url).UnwrapThrow(); - [Command("api token", "Generates an api token.")] + [Command("api token")] [Usage("[]", "Optionally specifies a duration this key is valid in hours.")] public static string CommandApiToken(TokenManager tokenManager, InvokerData invoker, double? validHours = null) { if (invoker.Visibiliy.HasValue && invoker.Visibiliy != TextMessageTargetMode.Private) - throw new CommandException("Please use this command in a private session.", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_use_private, CommandExceptionReason.CommandError); if (invoker.ClientUid == null) - throw new CommandException("No Uid found to register token for.", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_no_uid_found, CommandExceptionReason.CommandError); TimeSpan? validSpan = null; try @@ -64,82 +83,135 @@ public static string CommandApiToken(TokenManager tokenManager, InvokerData invo } catch (OverflowException oex) { - throw new CommandException("Invalid token-valid duration.", oex, CommandExceptionReason.CommandError); + throw new CommandException(strings.error_invalid_token_duration, oex, CommandExceptionReason.CommandError); } - return tokenManager.GenerateToken(invoker.ClientUid, validSpan).UnwrapThrow(); + return tokenManager.GenerateToken(invoker.ClientUid, validSpan); } - [Command("api nonce", "Generates an api nonce.")] + [Command("api nonce")] public static string CommandApiNonce(TokenManager tokenManager, InvokerData invoker) { if (invoker.Visibiliy.HasValue && invoker.Visibiliy != TextMessageTargetMode.Private) - throw new CommandException("Please use this command in a private session.", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_use_private, CommandExceptionReason.CommandError); if (invoker.ClientUid == null) - throw new CommandException("No Uid found to register token for.", CommandExceptionReason.CommandError); - var result = tokenManager.GetToken(invoker.ClientUid); - if (!result.Ok) - throw new CommandException("No active token found.", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_no_uid_found, CommandExceptionReason.CommandError); + var result = tokenManager.GetToken(invoker.ClientUid).UnwrapThrow(); - var nonce = result.Value.CreateNonce(); + var nonce = result.CreateNonce(); return nonce.Value; } - [Command("bot commander", "Gets the status of the channel commander mode.")] - public static JsonValue CommandBotCommander(TeamspeakControl queryConnection) + [Command("bot avatar")] + public static void CommandBotAvatar(Ts3Client ts3Client, string url) + { + url = TextUtil.ExtractUrlFromBb(url); + Uri uri; + try { uri = new Uri(url); } + catch (Exception ex) { throw new CommandException(strings.error_media_invalid_uri, ex, CommandExceptionReason.CommandError); } + + WebWrapper.GetResponse(uri, x => + { + var stream = x.GetResponseStream(); + if (stream == null) + throw new CommandException(strings.error_net_empty_response, CommandExceptionReason.CommandError); + using (var image = ImageUtil.ResizeImage(stream)) + { + if (image == null) + throw new CommandException(strings.error_media_internal_invalid, CommandExceptionReason.CommandError); + ts3Client.UploadAvatar(image).UnwrapThrow(); + } + }); + } + + [Command("bot disconnect")] + public static void CommandBotDisconnect(BotManager bots, Bot bot) => bots.StopBot(bot); + + [Command("bot commander")] + public static JsonValue CommandBotCommander(Ts3Client ts3Client) { - var value = queryConnection.IsChannelCommander().UnwrapThrow(); - return new JsonValue(value, "Channel commander is " + (value ? "on" : "off")); + var value = ts3Client.IsChannelCommander().UnwrapThrow(); + return new JsonValue(value, string.Format(strings.info_status_channelcommander, value ? strings.info_on : strings.info_off)); } - [Command("bot commander on", "Enables channel commander.")] - public static void CommandBotCommanderOn(TeamspeakControl queryConnection) => queryConnection.SetChannelCommander(true).UnwrapThrow(); - [Command("bot commander off", "Disables channel commander.")] - public static void CommandBotCommanderOff(TeamspeakControl queryConnection) => queryConnection.SetChannelCommander(false).UnwrapThrow(); + [Command("bot commander on")] + public static void CommandBotCommanderOn(Ts3Client ts3Client) => ts3Client.SetChannelCommander(true).UnwrapThrow(); + [Command("bot commander off")] + public static void CommandBotCommanderOff(Ts3Client ts3Client) => ts3Client.SetChannelCommander(false).UnwrapThrow(); - [Command("bot come", "Moves the bot to your channel.")] - public static void CommandBotCome(TeamspeakControl queryConnection, InvokerData invoker, string password = null) + [Command("bot come")] + public static void CommandBotCome(Ts3Client ts3Client, InvokerData invoker, string password = null) { var channel = invoker?.ChannelId; if (!channel.HasValue) - throw new CommandException("No target channel found", CommandExceptionReason.CommandError); - CommandBotMove(queryConnection, channel.Value, password); + throw new CommandException(strings.error_no_target_channel, CommandExceptionReason.CommandError); + CommandBotMove(ts3Client, channel.Value, password); } - [Command("bot info", "Gets various information about the bot.")] + [Command("bot connect template")] + public static BotInfo CommandBotConnectTo(BotManager bots, string name) + { + var botInfo = bots.RunBotTemplate(name); + if (!botInfo.Ok) + throw new CommandException(strings.error_could_not_create_bot + $" ({botInfo.Error})", CommandExceptionReason.CommandError); + return botInfo.Value; // TODO check value/object + } + + [Command("bot connect to")] + public static BotInfo CommandBotConnectNew(BotManager bots, string address, string password = null) + { + var botConf = bots.CreateNewBot(); + botConf.Connect.Address.Value = address; + if (!string.IsNullOrEmpty(password)) + botConf.Connect.ServerPassword.Password.Value = password; + var botInfo = bots.RunBot(botConf); + if (!botInfo.Ok) + throw new CommandException(strings.error_could_not_create_bot + $" ({botInfo.Error})", CommandExceptionReason.CommandError); + return botInfo.Value; // TODO check value/object + } + + [Command("bot info")] public static BotInfo CommandBotInfo(Bot bot) => bot.GetInfo(); - [Command("bot info id", "Gets the id of the current bot.")] - public static int CommandBotId(Bot bot) => bot.Id; + [Command("bot info client", "_undocumented")] + public static JsonValue CommandBotInfoClient(Ts3Client ts3Client) + => new JsonValue(ts3Client.GetSelf().UnwrapThrow(), string.Empty); - [Command("bot list", "Gets the id of the current bot.")] + [Command("bot list")] public static JsonArray CommandBotId(BotManager bots) { var botlist = bots.GetBotInfolist(); - return new JsonArray(botlist, string.Join("\n", botlist.Select(x => x.ToString()))); + return new JsonArray(botlist, bl => string.Join("\n", bl.Select(x => x.ToString()))); } - [Command("bot move", "Moves the bot to you or a specified channel.")] - public static void CommandBotMove(TeamspeakControl queryConnection, ulong channel, string password = null) => queryConnection.MoveTo(channel, password).UnwrapThrow(); + [Command("bot move")] + public static void CommandBotMove(Ts3Client ts3Client, ulong channel, string password = null) => ts3Client.MoveTo(channel, password).UnwrapThrow(); + + [Command("bot name")] + public static void CommandBotName(Ts3Client ts3Client, string name) => ts3Client.ChangeName(name).UnwrapThrow(); - [Command("bot name", "Gives the bot a new name.")] - public static void CommandBotName(TeamspeakControl queryConnection, string name) => queryConnection.ChangeName(name).UnwrapThrow(); + [Command("bot badges")] + public static void CommandBotBadges(Ts3Client ts3Client, string badges) => ts3Client.ChangeBadges(badges).UnwrapThrow(); - [Command("bot badges", "Set your bot a badge. The badges string starts with 'overwolf=0:badges='")] - public static void CommandBotBadges(TeamspeakControl queryConnection, string badgesString) => queryConnection.ChangeBadges(badgesString).UnwrapThrow(); + [Command("bot save")] + public static void CommandBotSetup(Bot bot, ConfBot botConfig, string name) + { + botConfig.SaveNew(name).UnwrapThrow(); + bot.Name = name; + } - [Command("bot setup", "Sets all teamspeak rights for the bot to be fully functional.")] - public static void CommandBotSetup(ConfigFile configManager, TeamspeakControl queryConnection, string adminToken = null) + [Command("bot setup")] + public static void CommandBotSetup(Ts3Client ts3Client, string adminToken = null) { - var mbd = configManager.GetDataStruct("MainBot", true); - queryConnection.SetupRights(adminToken, mbd).UnwrapThrow(); + if (!ts3Client.SetupRights(adminToken)) + throw new CommandException(strings.cmd_bot_setup_error, CommandExceptionReason.CommandError); } - [Command("bot use", "Switches the conetext to the requested bot.")] + [Command("bot use")] public static ICommandResult CommandBotUse(ExecutionInformation info, IReadOnlyList returnTypes, BotManager bots, int botId, ICommand cmd) { using (var botLock = bots.GetBotLock(botId)) { - if (!botLock.IsValid) - throw new CommandException("This bot does not exists", CommandExceptionReason.CommandError); + if (botLock == null) + throw new CommandException(strings.error_bot_does_not_exist, CommandExceptionReason.CommandError); var childInfo = new ExecutionInformation(botLock.Bot.Injector.CloneRealm()); if (info.TryGet(out var caller)) @@ -153,29 +225,17 @@ public static ICommandResult CommandBotUse(ExecutionInformation info, IReadOnlyL } } - [Command("clear", "Removes all songs from the current playlist.")] + [Command("clear")] public static void CommandClear(PlaylistManager playlistManager) => playlistManager.ClearFreelist(); - [Command("connect", "Start a new bot instance.")] - public static BotInfo CommandConnect(BotManager bots) - { - var botInfo = bots.CreateBot(); - if (botInfo == null) - throw new CommandException("Could not create new instance", CommandExceptionReason.CommandError); - return botInfo; // TODO check value/object - } - - [Command("disconnect", "Stop this bot instance.")] - public static void CommandDisconnect(BotManager bots, Bot bot) => bots.StopBot(bot); - - [Command("eval", "Executes a given command or string")] + [Command("eval")] [Usage(" ", "Executes the given command on arguments")] [Usage("", "Concat the strings and execute them with the command system")] public static ICommandResult CommandEval(ExecutionInformation info, CommandManager commandManager, IReadOnlyList arguments, IReadOnlyList returnTypes) { // Evaluate the first argument on the rest of the arguments if (arguments.Count == 0) - throw new CommandException("Need at least one argument to evaluate", CommandExceptionReason.MissingParameter); + throw new CommandException(strings.error_cmd_at_least_one_argument, CommandExceptionReason.MissingParameter); var leftArguments = arguments.TrySegment(1); var arg0 = arguments[0].Execute(info, Array.Empty(), XCommandSystem.ReturnCommandOrString); if (arg0.ResultType == CommandResultType.Command) @@ -192,114 +252,116 @@ public static ICommandResult CommandEval(ExecutionInformation info, CommandManag return cmd.Execute(info, leftArguments, returnTypes); } - [Command("getmy id", "Gets your id.")] + [Command("getmy id")] public static ushort CommandGetId(InvokerData invoker) - => invoker.ClientId ?? throw new CommandException("Not found.", CommandExceptionReason.CommandError); - [Command("getmy uid", "Gets your unique id.")] + => invoker.ClientId ?? throw new CommandException(strings.error_not_found, CommandExceptionReason.CommandError); + [Command("getmy uid")] public static string CommandGetUid(InvokerData invoker) - => invoker.ClientUid ?? throw new CommandException("Not found.", CommandExceptionReason.CommandError); - [Command("getmy name", "Gets your nickname.")] + => invoker.ClientUid ?? throw new CommandException(strings.error_not_found, CommandExceptionReason.CommandError); + [Command("getmy name")] public static string CommandGetName(InvokerData invoker) - => invoker.NickName ?? throw new CommandException("Not found.", CommandExceptionReason.CommandError); - [Command("getmy dbid", "Gets your database id.")] + => invoker.NickName ?? throw new CommandException(strings.error_not_found, CommandExceptionReason.CommandError); + [Command("getmy dbid")] public static ulong CommandGetDbId(InvokerData invoker) - => invoker.DatabaseId ?? throw new CommandException("Not found.", CommandExceptionReason.CommandError); - [Command("getmy channel", "Gets your channel id you are currently in.")] + => invoker.DatabaseId ?? throw new CommandException(strings.error_not_found, CommandExceptionReason.CommandError); + [Command("getmy channel")] public static ulong CommandGetChannel(InvokerData invoker) - => invoker.ChannelId ?? throw new CommandException("Not found.", CommandExceptionReason.CommandError); - [Command("getmy all", "Gets all information about you.")] + => invoker.ChannelId ?? throw new CommandException(strings.error_not_found, CommandExceptionReason.CommandError); + [Command("getmy all")] public static JsonValue CommandGetUser(InvokerData invoker) - => new JsonValue(invoker, $"Client: Id:{invoker.ClientId} DbId:{invoker.DatabaseId} ChanId:{invoker.ChannelId} Uid:{invoker.ClientUid}"); - - [Command("getuser uid byid", "Gets the unique id of a user, searching with his id.")] - public static string CommandGetUidById(TeamspeakControl queryConnection, ushort id) => queryConnection.GetClientById(id).UnwrapThrow().Uid; - [Command("getuser name byid", "Gets the nickname of a user, searching with his id.")] - public static string CommandGetNameById(TeamspeakControl queryConnection, ushort id) => queryConnection.GetClientById(id).UnwrapThrow().Name; - [Command("getuser dbid byid", "Gets the database id of a user, searching with his id.")] - public static ulong CommandGetDbIdById(TeamspeakControl queryConnection, ushort id) => queryConnection.GetClientById(id).UnwrapThrow().DatabaseId; - [Command("getuser channel byid", "Gets the channel id a user is currently in, searching with his id.")] - public static ulong CommandGetChannelById(TeamspeakControl queryConnection, ushort id) => queryConnection.GetClientById(id).UnwrapThrow().ChannelId; - [Command("getuser all byid", "Gets all information about a user, searching with his id.")] - public static JsonValue CommandGetUserById(TeamspeakControl queryConnection, ushort id) - { - var client = queryConnection.GetClientById(id).UnwrapThrow(); + => new JsonValue(invoker, $"Client: Id:{invoker.ClientId} DbId:{invoker.DatabaseId} ChanId:{invoker.ChannelId} Uid:{invoker.ClientUid}"); // LOC: TODO + + [Command("getuser uid byid")] + public static string CommandGetUidById(Ts3Client ts3Client, ushort id) => ts3Client.GetFallbackedClientById(id).UnwrapThrow().Uid; + [Command("getuser name byid")] + public static string CommandGetNameById(Ts3Client ts3Client, ushort id) => ts3Client.GetFallbackedClientById(id).UnwrapThrow().Name; + [Command("getuser dbid byid")] + public static ulong CommandGetDbIdById(Ts3Client ts3Client, ushort id) => ts3Client.GetFallbackedClientById(id).UnwrapThrow().DatabaseId; + [Command("getuser channel byid")] + public static ulong CommandGetChannelById(Ts3Client ts3Client, ushort id) => ts3Client.GetFallbackedClientById(id).UnwrapThrow().ChannelId; + [Command("getuser all byid")] + public static JsonValue CommandGetUserById(Ts3Client ts3Client, ushort id) + { + var client = ts3Client.GetFallbackedClientById(id).UnwrapThrow(); return new JsonValue(client, $"Client: Id:{client.ClientId} DbId:{client.DatabaseId} ChanId:{client.ChannelId} Uid:{client.Uid}"); } - [Command("getuser id byname", "Gets the id of a user, searching with his name.")] - public static ushort CommandGetIdByName(TeamspeakControl queryConnection, string username) => queryConnection.GetClientByName(username).UnwrapThrow().ClientId; - [Command("getuser all byname", "Gets all information of a user, searching with his name.")] - public static JsonValue CommandGetUserByName(TeamspeakControl queryConnection, string username) + [Command("getuser id byname")] + public static ushort CommandGetIdByName(Ts3Client ts3Client, string username) => ts3Client.GetClientByName(username).UnwrapThrow().ClientId; + [Command("getuser all byname")] + public static JsonValue CommandGetUserByName(Ts3Client ts3Client, string username) { - var client = queryConnection.GetClientByName(username).UnwrapThrow(); + var client = ts3Client.GetClientByName(username).UnwrapThrow(); return new JsonValue(client, $"Client: Id:{client.ClientId} DbId:{client.DatabaseId} ChanId:{client.ChannelId} Uid:{client.Uid}"); } - [Command("getuser name bydbid", "Gets the user name by dbid, searching with his database id.")] - public static string CommandGetNameByDbId(TeamspeakControl queryConnection, ulong dbId) => queryConnection.GetDbClientByDbId(dbId).UnwrapThrow().Name; - [Command("getuser uid bydbid", "Gets the unique id of a user, searching with his database id.")] - public static string CommandGetUidByDbId(TeamspeakControl queryConnection, ulong dbId) => queryConnection.GetDbClientByDbId(dbId).UnwrapThrow().Uid; + [Command("getuser name bydbid")] + public static string CommandGetNameByDbId(Ts3Client ts3Client, ulong dbId) => ts3Client.GetDbClientByDbId(dbId).UnwrapThrow().Name; + [Command("getuser uid bydbid")] + public static string CommandGetUidByDbId(Ts3Client ts3Client, ulong dbId) => ts3Client.GetDbClientByDbId(dbId).UnwrapThrow().Uid; - [Command("help", "Shows all commands or detailed help about a specific command.")] + [Command("help")] [Usage("[]", "Any currently accepted command")] - public static JsonObject CommandHelp(CommandManager commandManager, CallerInfo caller, Algorithm.Filter filter = null, params string[] parameter) + public static JsonObject CommandHelp(CommandManager commandManager, CallerInfo caller, Algorithm.Filter filter = null, params string[] command) { - if (parameter.Length == 0 && !caller.ApiCall) + if (command.Length == 0 && !caller.ApiCall) { - var strb = new StringBuilder(); - strb.Append("\n========= Welcome to the TS3AudioBot =========" - + "\nIf you need any help with a special command use !help ." - + "\nHere are all possible commands:\n"); var botComList = commandManager.AllCommands.Select(c => c.InvokeName).OrderBy(x => x).GroupBy(n => n.Split(' ')[0]).Select(x => x.Key).ToArray(); - foreach (var botCom in botComList) - strb.Append(botCom).Append(", "); - strb.Length -= 2; - return new JsonArray(botComList, strb.ToString()); + return new JsonArray(botComList, bcl => + { + var strb = new StringBuilder(); + strb.AppendLine(); + strb.AppendLine(strings.cmd_help_header); + foreach (var botCom in bcl) + strb.Append(botCom).Append(", "); + strb.Length -= 2; + return strb.ToString(); + }); } CommandGroup group = commandManager.CommandSystem.RootCommand; ICommand target = group; - for (int i = 0; i < parameter.Length; i++) + for (int i = 0; i < command.Length; i++) { filter = filter ?? Algorithm.Filter.DefaultFilter; - var possibilities = filter.Current.Filter(group.Commands, parameter[i]).ToList(); + var possibilities = filter.Current.Filter(group.Commands, command[i]).ToList(); if (possibilities.Count <= 0) - throw new CommandException("No matching command found! Try !help to get a list of all commands.", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_help_error_no_matching_command, CommandExceptionReason.CommandError); if (possibilities.Count > 1) - throw new CommandException("Requested command is ambiguous between: " + string.Join(", ", possibilities.Select(kvp => kvp.Key)), CommandExceptionReason.CommandError); + throw new CommandException(string.Format(strings.cmd_help_error_ambiguous_command, string.Join(", ", possibilities.Select(kvp => kvp.Key))), CommandExceptionReason.CommandError); target = possibilities[0].Value; - if (i < parameter.Length - 1) + if (i < command.Length - 1) { group = target as CommandGroup; if (group == null) - throw new CommandException("The command has no further subfunctions after " + string.Join(" ", parameter, 0, i), CommandExceptionReason.CommandError); + throw new CommandException(string.Format(strings.cmd_help_error_no_further_subfunctions, string.Join(" ", command, 0, i)), CommandExceptionReason.CommandError); } } switch (target) { case BotCommand targetB: - return new JsonValue(targetB); + return new JsonValue(targetB.AsJsonObj); case CommandGroup targetCg: var subList = targetCg.Commands.Select(g => g.Key).ToArray(); - return new JsonArray(subList, "The command contains the following subfunctions: " + string.Join(", ", subList)); + return new JsonArray(subList, string.Format(strings.cmd_help_info_contains_subfunctions, string.Join(", ", subList))); case OverloadedFunctionCommand targetOfc: var strb = new StringBuilder(); foreach (var botCom in targetOfc.Functions.OfType()) - strb.Append(botCom.GetHelp()); + strb.Append(botCom); return new JsonValue(strb.ToString()); default: - throw new CommandException("Seems like something went wrong. No help can be shown for this command path.", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_help_error_unknown_error, CommandExceptionReason.CommandError); } } - [Command("history add", " Adds the song with to the queue")] + [Command("history add")] public static void CommandHistoryQueue(HistoryManager historyManager, PlayManager playManager, InvokerData invoker, uint hid) { var ale = historyManager.GetEntryById(hid).UnwrapThrow(); playManager.Enqueue(invoker, ale.AudioResource).UnwrapThrow(); } - [Command("history clean", "Cleans up the history file for better startup performance.")] + [Command("history clean")] public static JsonEmpty CommandHistoryClean(DbStore database, CallerInfo caller, UserSession session = null) { if (caller.ApiCall) @@ -313,17 +375,15 @@ string ResponseHistoryClean(string message) if (TextUtil.GetAnswer(message) == Answer.Yes) { database.CleanFile(); - return "Cleanup done!"; + return strings.info_cleanup_done; } return null; } session.SetResponse(ResponseHistoryClean); - return new JsonEmpty("Do want to clean the history file now? " + - "This might take a while and make the bot unresponsive in meanwhile. !(yes|no)"); + return new JsonEmpty($"{strings.cmd_history_clean_confirm_clean} {strings.info_bot_might_be_unresponsive} {YesNoOption}"); } - [Command("history clean removedefective", "Cleans up the history file for better startup performance. " + - "Also checks for all links in the history which cannot be opened anymore")] + [Command("history clean removedefective")] public static JsonEmpty CommandHistoryCleanRemove(HistoryManager historyManager, CallerInfo caller, UserSession session = null) { if (caller.ApiCall) @@ -337,16 +397,21 @@ string ResponseHistoryCleanRemove(string message) if (TextUtil.GetAnswer(message) == Answer.Yes) { historyManager.RemoveBrokenLinks(); - return "Cleanup done!"; + return strings.info_cleanup_done; } return null; } session.SetResponse(ResponseHistoryCleanRemove); - return new JsonEmpty("Do want to remove all defective links file now? " + - "This might(will!) take a while and make the bot unresponsive in meanwhile. !(yes|no)"); + return new JsonEmpty($"{strings.cmd_history_clean_removedefective_confirm_clean} {strings.info_bot_might_be_unresponsive} {YesNoOption}"); } - [Command("history delete", " Removes the entry with from the history")] + [Command("history clean upgrade", "_undocumented")] + public static void CommandHistoryCleanUpgrade(HistoryManager historyManager, Ts3Client ts3Client) + { + historyManager.UpdadeDbIdToUid(ts3Client); + } + + [Command("history delete")] public static JsonEmpty CommandHistoryDelete(HistoryManager historyManager, CallerInfo caller, uint id, UserSession session = null) { var ale = historyManager.GetEntryById(id).UnwrapThrow(); @@ -371,82 +436,82 @@ string ResponseHistoryDelete(string message) string name = ale.AudioResource.ResourceTitle; if (name.Length > 100) name = name.Substring(100) + "..."; - return new JsonEmpty($"Do you really want to delete the entry \"{name}\"\nwith the id {id}? !(yes|no)"); + return new JsonEmpty(string.Format(strings.cmd_history_delete_confirm + YesNoOption, name, id)); } - [Command("history from", "Gets the last songs from the user with the given ")] - public static JsonArray CommandHistoryFrom(HistoryManager historyManager, uint userDbId, int? amount = null) + [Command("history from")] + public static JsonArray CommandHistoryFrom(HistoryManager historyManager, string userUid, int? amount = null) { - var query = new SeachQuery { UserId = userDbId }; + var query = new SeachQuery { UserUid = userUid }; if (amount.HasValue) query.MaxResults = amount.Value; var results = historyManager.Search(query).ToArray(); - return new JsonArray(results, historyManager.Format(results)); + return new JsonArray(results, historyManager.Format); } - [Command("history id", " Displays all saved informations about the song with ")] + [Command("history id", "cmd_history_id_uint_help")] public static JsonValue CommandHistoryId(HistoryManager historyManager, uint id) { var result = historyManager.GetEntryById(id).UnwrapThrow(); return new JsonValue(result, historyManager.Format(result)); } - [Command("history id", "(last|next) Gets the highest|next song id")] + [Command("history id", "cmd_history_id_string_help")] public static JsonValue CommandHistoryId(HistoryManager historyManager, string special) { if (special == "last") - return new JsonValue(historyManager.HighestId, $"{historyManager.HighestId} is the currently highest song id."); + return new JsonValue(historyManager.HighestId, string.Format(strings.cmd_history_id_last, historyManager.HighestId)); else if (special == "next") - return new JsonValue(historyManager.HighestId + 1, $"{historyManager.HighestId + 1} will be the next song id."); + return new JsonValue(historyManager.HighestId + 1, string.Format(strings.cmd_history_id_next, historyManager.HighestId + 1)); else throw new CommandException("Unrecognized name descriptor", CommandExceptionReason.CommandError); } - [Command("history last", " Gets the last played songs.")] + [Command("history last", "cmd_history_last_int_help")] public static JsonArray CommandHistoryLast(HistoryManager historyManager, int amount) { var query = new SeachQuery { MaxResults = amount }; var results = historyManager.Search(query).ToArray(); - return new JsonArray(results, historyManager.Format(results)); + return new JsonArray(results, historyManager.Format); } - [Command("history last", "Plays the last song again")] + [Command("history last", "cmd_history_last_help")] public static void CommandHistoryLast(HistoryManager historyManager, PlayManager playManager, InvokerData invoker) { var ale = historyManager.Search(new SeachQuery { MaxResults = 1 }).FirstOrDefault(); if (ale == null) - throw new CommandException("There is no song in the history", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_history_last_is_empty, CommandExceptionReason.CommandError); playManager.Play(invoker, ale.AudioResource).UnwrapThrow(); } - [Command("history play", " Playes the song with ")] + [Command("history play")] public static void CommandHistoryPlay(HistoryManager historyManager, PlayManager playManager, InvokerData invoker, uint hid) { var ale = historyManager.GetEntryById(hid).UnwrapThrow(); playManager.Play(invoker, ale.AudioResource).UnwrapThrow(); } - [Command("history rename", " Sets the name of the song with to ")] + [Command("history rename")] public static void CommandHistoryRename(HistoryManager historyManager, uint id, string newName) { var ale = historyManager.GetEntryById(id).UnwrapThrow(); if (string.IsNullOrWhiteSpace(newName)) - throw new CommandException("The new name must not be empty or only whitespaces", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_history_rename_invalid_name, CommandExceptionReason.CommandError); historyManager.RenameEntry(ale, newName); } - [Command("history till", " Gets all songs played until .")] + [Command("history till", "cmd_history_till_DateTime_help")] public static JsonArray CommandHistoryTill(HistoryManager historyManager, DateTime time) { var query = new SeachQuery { LastInvokedAfter = time }; var results = historyManager.Search(query).ToArray(); - return new JsonArray(results, historyManager.Format(results)); + return new JsonArray(results, historyManager.Format); } - [Command("history till", " Any of those desciptors: (hour|today|yesterday|week)")] + [Command("history till", "cmd_history_till_string_help")] public static JsonArray CommandHistoryTill(HistoryManager historyManager, string time) { DateTime tillTime; @@ -456,19 +521,19 @@ public static JsonArray CommandHistoryTill(HistoryManager history case "today": tillTime = DateTime.Today; break; case "yesterday": tillTime = DateTime.Today.AddDays(-1); break; case "week": tillTime = DateTime.Today.AddDays(-7); break; - default: throw new CommandException("Not recognized time desciption.", CommandExceptionReason.CommandError); + default: throw new CommandException(strings.error_unrecognized_descriptor, CommandExceptionReason.CommandError); } var query = new SeachQuery { LastInvokedAfter = tillTime }; var results = historyManager.Search(query).ToArray(); - return new JsonArray(results, historyManager.Format(results)); + return new JsonArray(results, historyManager.Format); } - [Command("history title", "Gets all songs which title contains ")] + [Command("history title")] public static JsonArray CommandHistoryTitle(HistoryManager historyManager, string part) { var query = new SeachQuery { TitlePart = part }; var results = historyManager.Search(query).ToArray(); - return new JsonArray(results, historyManager.Format(results)); + return new JsonArray(results, historyManager.Format); } [Command("if")] @@ -477,7 +542,7 @@ public static JsonArray CommandHistoryTitle(HistoryManager histor public static ICommandResult CommandIf(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes) { if (arguments.Count < 4) - throw new CommandException("Expected at least 4 arguments", CommandExceptionReason.MissingParameter); + throw new CommandException(strings.error_cmd_at_least_four_argument, CommandExceptionReason.MissingParameter); var arg0 = ((StringCommandResult)arguments[0].Execute(info, Array.Empty(), XCommandSystem.ReturnString)).Content; var cmp = ((StringCommandResult)arguments[1].Execute(info, Array.Empty(), XCommandSystem.ReturnString)).Content; var arg1 = ((StringCommandResult)arguments[2].Execute(info, Array.Empty(), XCommandSystem.ReturnString)).Content; @@ -491,16 +556,20 @@ public static ICommandResult CommandIf(ExecutionInformation info, IReadOnlyList< case ">=": comparer = (a, b) => a >= b; break; case "==": comparer = (a, b) => Math.Abs(a - b) < 1e-6; break; case "!=": comparer = (a, b) => Math.Abs(a - b) > 1e-6; break; - default: throw new CommandException("Unknown comparison operator", CommandExceptionReason.CommandError); + default: throw new CommandException(strings.cmd_if_unknown_operator, CommandExceptionReason.CommandError); } bool cmpResult; // Try to parse arguments into doubles if (double.TryParse(arg0, NumberStyles.Number, CultureInfo.InvariantCulture, out var d0) && double.TryParse(arg1, NumberStyles.Number, CultureInfo.InvariantCulture, out var d1)) + { cmpResult = comparer(d0, d1); + } else + { cmpResult = comparer(string.CompareOrdinal(arg0, arg1), 0); + } // If branch if (cmpResult) @@ -511,81 +580,95 @@ public static ICommandResult CommandIf(ExecutionInformation info, IReadOnlyList< // Try to return nothing if (returnTypes.Contains(CommandResultType.Empty)) - return new EmptyCommandResult(); - throw new CommandException("If found nothing to return", CommandExceptionReason.MissingParameter); + return EmptyCommandResult.Instance; + throw new CommandException(strings.error_nothing_to_return, CommandExceptionReason.NoReturnMatch); } - [Command("json merge", "Allows you to combine multiple JsonResults into one")] + [Command("json merge")] public static JsonArray CommandJsonMerge(ExecutionInformation info, IReadOnlyList arguments) { if (arguments.Count == 0) return new JsonArray(Array.Empty(), string.Empty); var jsonArr = arguments - .Select(arg => arg.Execute(info, Array.Empty(), XCommandSystem.ReturnJson)) - .Where(arg => arg.ResultType == CommandResultType.Json) - .OfType() - .Select(arg => arg.JsonObject.GetSerializeObject()) + .Select(arg => + { + ICommandResult res; + try { res = arg.Execute(info, Array.Empty(), XCommandSystem.ReturnJson); } + catch (CommandException) { return null; } + if (res.ResultType == CommandResultType.Json) + return ((JsonCommandResult)res).JsonObject.GetSerializeObject(); + else + throw new CommandException(strings.error_nothing_to_return, CommandExceptionReason.NoReturnMatch); + }) .ToArray(); return new JsonArray(jsonArr, string.Empty); } - [Command("kickme", "Guess what?")] + [Command("json api", "_undocumented")] + public static JsonObject CommandJsonApi(CommandManager commandManager, BotManager botManager = null) + { + var bots = botManager?.GetBotInfolist() ?? Array.Empty(); + var api = OpenApiGenerator.Generate(commandManager, bots); + return new JsonValue(api, string.Empty); + } + + [Command("kickme")] [Usage("[far]", "Optional attribute for the extra punch strength")] - public static void CommandKickme(TeamspeakControl queryConnection, InvokerData invoker, CallerInfo caller, string parameter = null) + public static void CommandKickme(Ts3Client ts3Client, InvokerData invoker, CallerInfo caller, string special = null) { if (caller.ApiCall) - throw new CommandException("This command is not available as API", CommandExceptionReason.NotSupported); + throw new CommandException(strings.error_not_available_from_api, CommandExceptionReason.NotSupported); if (invoker.ClientId.HasValue) { - var result = R.OkR; - if (string.IsNullOrEmpty(parameter) || parameter == "near") - result = queryConnection.KickClientFromChannel(invoker.ClientId.Value); - else if (parameter == "far") - result = queryConnection.KickClientFromServer(invoker.ClientId.Value); + E result = R.Ok; + if (string.IsNullOrEmpty(special) || special == "near") + result = ts3Client.KickClientFromChannel(invoker.ClientId.Value); + else if (special == "far") + result = ts3Client.KickClientFromServer(invoker.ClientId.Value); if (!result.Ok) - throw new CommandException("I'm not strong enough, master!", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_kickme_missing_permission, CommandExceptionReason.CommandError); } } - [Command("link", "Gets a link to the origin of the current song.")] + [Command("link")] public static string CommandLink(ResourceFactoryManager factoryManager, PlayManager playManager, Bot bot, CallerInfo caller, InvokerData invoker = null) { if (playManager.CurrentPlayData == null) - throw new CommandException("There is nothing on right now...", CommandExceptionReason.CommandError); + throw new CommandException(strings.info_currently_not_playing, CommandExceptionReason.CommandError); if (bot.QuizMode && !caller.ApiCall && (invoker == null || playManager.CurrentPlayData.Invoker.ClientId != invoker.ClientId)) - throw new CommandException("Sorry, you have to guess!", CommandExceptionReason.CommandError); + throw new CommandException(strings.info_quizmode_is_active, CommandExceptionReason.CommandError); return factoryManager.RestoreLink(playManager.CurrentPlayData.ResourceData); } - [Command("list add", "Adds a link to your private playlist.")] + [Command("list add")] [Usage("", "Any link that is also recognized by !play")] public static void CommandListAdd(ResourceFactoryManager factoryManager, UserSession session, InvokerData invoker, string link) { var plist = AutoGetPlaylist(session, invoker); var playResource = factoryManager.Load(link).UnwrapThrow(); - plist.AddItem(new PlaylistItem(playResource.BaseData, new MetaData { ResourceOwnerDbId = invoker.DatabaseId })); + plist.AddItem(new PlaylistItem(playResource.BaseData, new MetaData { ResourceOwnerUid = invoker.ClientUid })); } - [Command("list add", " Adds a link to your private playlist from the history by .")] + [Command("list add")] public static void CommandListAdd(HistoryManager historyManager, UserSession session, InvokerData invoker, uint hid) { var plist = AutoGetPlaylist(session, invoker); var ale = historyManager.GetEntryById(hid).UnwrapThrow(); - plist.AddItem(new PlaylistItem(ale.AudioResource, new MetaData { ResourceOwnerDbId = invoker.DatabaseId })); + plist.AddItem(new PlaylistItem(ale.AudioResource, new MetaData { ResourceOwnerUid = invoker.ClientUid })); } - [Command("list clear", "Clears your private playlist.")] + [Command("list clear")] public static void CommandListClear(UserSession session, InvokerData invoker) => AutoGetPlaylist(session, invoker).Clear(); - [Command("list delete", " Deletes the playlist with the name . You can only delete playlists which you also have created. Admins can delete every playlist.")] + [Command("list delete")] public static JsonEmpty CommandListDelete(ExecutionInformation info, PlaylistManager playlistManager, CallerInfo caller, InvokerData invoker, string name, UserSession session = null) { if (caller.ApiCall) - playlistManager.DeletePlaylist(name, invoker.DatabaseId ?? 0, info.HasRights(RightDeleteAllPlaylists)).UnwrapThrow(); + playlistManager.DeletePlaylist(name, invoker.ClientUid, info.HasRights(RightDeleteAllPlaylists)).UnwrapThrow(); bool? canDeleteAllPlaylists = null; @@ -593,7 +676,7 @@ string ResponseListDelete(string message) { if (TextUtil.GetAnswer(message) == Answer.Yes) { - playlistManager.DeletePlaylist(name, invoker.DatabaseId ?? 0, canDeleteAllPlaylists ?? info.HasRights(RightDeleteAllPlaylists)).UnwrapThrow(); + playlistManager.DeletePlaylist(name, invoker.ClientUid, canDeleteAllPlaylists ?? info.HasRights(RightDeleteAllPlaylists)).UnwrapThrow(); } return null; } @@ -603,37 +686,39 @@ string ResponseListDelete(string message) { session.SetResponse(ResponseListDelete); // TODO check if return == string => ask, return == empty => just delete - return new JsonEmpty($"Do you really want to delete the playlist \"{name}\" (error:{hresult.Error})"); + return new JsonEmpty(string.Format(strings.cmd_list_delete_confirm, name)); } else { canDeleteAllPlaylists = info.HasRights(RightDeleteAllPlaylists); - if (hresult.Value.CreatorDbId != invoker.DatabaseId && !canDeleteAllPlaylists.Value) - throw new CommandException("You are not allowed to delete others playlists", CommandExceptionReason.MissingRights); + if (hresult.Value.OwnerUid != invoker.ClientUid && !canDeleteAllPlaylists.Value) + throw new CommandException(strings.cmd_list_delete_cannot_delete_others_playlist, CommandExceptionReason.MissingRights); session.SetResponse(ResponseListDelete); - return new JsonEmpty($"Do you really want to delete the playlist \"{name}\""); + return new JsonEmpty(string.Format(strings.cmd_list_delete_confirm, name)); } } - [Command("list get", " Imports a playlist form an other plattform like youtube etc.")] + [Command("list get")] public static JsonEmpty CommandListGet(ResourceFactoryManager factoryManager, UserSession session, InvokerData invoker, string link) { var playlist = factoryManager.LoadPlaylistFrom(link).UnwrapThrow(); - playlist.CreatorDbId = invoker.DatabaseId; + playlist.OwnerUid = invoker.ClientUid; session.Set(playlist); - return new JsonEmpty("Ok"); + return new JsonEmpty(strings.info_ok); } - [Command("list item move", " Moves a item in a playlist position.")] + [Command("list item move")] public static void CommandListMove(UserSession session, InvokerData invoker, int from, int to) { var plist = AutoGetPlaylist(session, invoker); if (from < 0 || from >= plist.Count || to < 0 || to >= plist.Count) - throw new CommandException("Index must be within playlist length", CommandExceptionReason.CommandError); + { + throw new CommandException(strings.error_playlist_item_index_out_of_range, CommandExceptionReason.CommandError); + } if (from == to) return; @@ -643,49 +728,50 @@ public static void CommandListMove(UserSession session, InvokerData invoker, int plist.InsertItem(plitem, Math.Min(to, plist.Count)); } - [Command("list item delete", " Removes the item at .")] + [Command("list item delete")] public static string CommandListRemove(UserSession session, InvokerData invoker, int index) { var plist = AutoGetPlaylist(session, invoker); if (index < 0 || index >= plist.Count) - throw new CommandException("Index must be within playlist length", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_playlist_item_index_out_of_range, CommandExceptionReason.CommandError); var deletedItem = plist.GetResource(index); plist.RemoveItemAt(index); - return "Removed: " + deletedItem.DisplayString; + return string.Format(strings.info_removed, deletedItem.DisplayString); } // add list item rename - [Command("list list", "Displays all available playlists from all users.")] + [Command("list list")] [Usage("", "Filters all lists cantaining the given pattern.")] public static JsonArray CommandListList(PlaylistManager playlistManager, string pattern = null) { var files = playlistManager.GetAvailablePlaylists(pattern).ToArray(); if (files.Length <= 0) - return new JsonArray(files, "No playlists found"); + return new JsonArray(files, strings.error_playlist_not_found); - var strb = new StringBuilder(); - int tokenLen = 0; - foreach (var file in files) + return new JsonArray(files, fi => { - int newTokenLen = tokenLen + TS3Client.Commands.Ts3String.TokenLength(file) + 3; - if (newTokenLen < TS3Client.Commands.Ts3Const.MaxSizeTextMessage) + var strb = new StringBuilder(); + int tokenLen = 0; + foreach (var file in fi) { + int newTokenLen = tokenLen + TS3Client.Commands.Ts3String.TokenLength(file) + 3; + if (newTokenLen >= TS3Client.Commands.Ts3Const.MaxSizeTextMessage) + break; + strb.Append(file).Append(", "); tokenLen = newTokenLen; } - else - break; - } - if (strb.Length > 2) - strb.Length -= 2; - return new JsonArray(files, strb.ToString()); + if (strb.Length > 2) + strb.Length -= 2; + return strb.ToString(); + }); } - [Command("list load", "Opens a playlist to be editable for you. This replaces your current worklist with the opened playlist.")] + [Command("list load")] public static JsonValue CommandListLoad(PlaylistManager playlistManager, UserSession session, InvokerData invoker, string name) { var loadList = AutoGetPlaylist(session, invoker); @@ -695,22 +781,22 @@ public static JsonValue CommandListLoad(PlaylistManager playlistManage loadList.Clear(); loadList.AddRange(playList.AsEnumerable()); loadList.Name = playList.Name; - return new JsonValue(loadList, $"Loaded: \"{name}\" with {loadList.Count} songs"); + return new JsonValue(loadList, string.Format(strings.cmd_list_load_response, name, loadList.Count)); } - [Command("list merge", "Appends another playlist to yours.")] + [Command("list merge")] public static void CommandListMerge(PlaylistManager playlistManager, UserSession session, InvokerData invoker, string name) { var plist = AutoGetPlaylist(session, invoker); var lresult = playlistManager.LoadPlaylist(name); if (!lresult) - throw new CommandException("The other playlist could not be found", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_playlist_not_found, CommandExceptionReason.CommandError); plist.AddRange(lresult.Value.AsEnumerable()); } - [Command("list name", "Displays the name of the playlist you are currently working on.")] + [Command("list name")] [Usage("", "Changes the playlist name to .")] public static string CommandListName(UserSession session, InvokerData invoker, string name) { @@ -719,59 +805,57 @@ public static string CommandListName(UserSession session, InvokerData invoker, s if (string.IsNullOrEmpty(name)) return plist.Name; - PlaylistManager.IsNameValid(name).UnwrapThrow(); + Util.IsSafeFileName(name).UnwrapThrow(); plist.Name = name; return null; } - [Command("list play", "Replaces the current freelist with your workinglist and plays from the beginning.")] + [Command("list play")] [Usage("", "Lets you specify the starting song index.")] public static void CommandListPlay(PlaylistManager playlistManager, PlayManager playManager, UserSession session, InvokerData invoker, int? index = null) { var plist = AutoGetPlaylist(session, invoker); - if (!index.HasValue || (index.Value >= 0 && index.Value < plist.Count)) - { - playlistManager.PlayFreelist(plist); - playlistManager.Index = index ?? 0; - } - else - throw new CommandException("Invalid starting index", CommandExceptionReason.CommandError); + if (index.HasValue && (index.Value < 0 || index.Value >= plist.Count)) + throw new CommandException(strings.error_playlist_item_index_out_of_range, CommandExceptionReason.CommandError); + + playlistManager.PlayFreelist(plist); + playlistManager.Index = index ?? 0; var item = playlistManager.Current(); if (item != null) playManager.Play(invoker, item).UnwrapThrow(); else - throw new CommandException("Nothing to play...", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_playlist_is_empty, CommandExceptionReason.CommandError); } - [Command("list queue", "Appends your playlist to the freelist.")] + [Command("list queue")] public static void CommandListQueue(PlayManager playManager, UserSession session, InvokerData invoker) { var plist = AutoGetPlaylist(session, invoker); playManager.Enqueue(plist.AsEnumerable()).UnwrapThrow(); } - [Command("list save", "Stores your current workinglist to disk.")] + [Command("list save")] [Usage("", "Changes the playlist name to before saving.")] public static void CommandListSave(PlaylistManager playlistManager, UserSession session, InvokerData invoker, string optNewName = null) { var plist = AutoGetPlaylist(session, invoker); if (!string.IsNullOrEmpty(optNewName)) { - PlaylistManager.IsNameValid(optNewName).UnwrapThrow(); + Util.IsSafeFileName(optNewName).UnwrapThrow(); plist.Name = optNewName; } playlistManager.SavePlaylist(plist).UnwrapThrow(); } - [Command("list show", "Displays all songs currently in the playlists you are working on")] + [Command("list show")] [Usage("", "Lets you specify the staring index from which songs should be listed.")] public static JsonArray CommandListShow(PlaylistManager playlistManager, UserSession session, InvokerData invoker, int? offset = null) => CommandListShow(playlistManager, session, invoker, null, offset); - [Command("list show", " Displays all songs currently in the playlists with the name ")] + [Command("list show")] [Usage(" ", "Lets you specify the starting index from which songs should be listed.")] public static JsonArray CommandListShow(PlaylistManager playlistManager, UserSession session, InvokerData invoker, string name = null, int? offset = null) { @@ -781,99 +865,89 @@ public static JsonArray CommandListShow(PlaylistManager playlistMa else plist = AutoGetPlaylist(session, invoker); - var strb = new StringBuilder(); - strb.Append("Playlist: \"").Append(plist.Name).Append("\" with ").Append(plist.Count).AppendLine(" songs."); int from = Math.Max(offset ?? 0, 0); var items = plist.AsEnumerable().Skip(from).ToArray(); - foreach (var plitem in items.Take(10)) - strb.Append(from++).Append(": ").AppendLine(plitem.DisplayString); - return new JsonArray(items, strb.ToString()); + return new JsonArray(items, it => + { + var strb = new StringBuilder(); + strb.AppendFormat(strings.cmd_list_show_header, plist.Name, plist.Count).AppendLine(); + foreach (var plitem in it.Take(10)) + strb.Append(from++).Append(": ").AppendLine(plitem.DisplayString); + return strb.ToString(); + }); } - [Command("loop", "Gets whether or not to loop the entire playlist.")] - public static JsonValue CommandLoop(PlaylistManager playlistManager) => new JsonValue(playlistManager.Loop, "Loop is " + (playlistManager.Loop ? "on" : "off")); - [Command("loop on", "Enables looping the entire playlist.")] + [Command("loop")] + public static JsonValue CommandLoop(PlaylistManager playlistManager) => new JsonValue(playlistManager.Loop, string.Format(strings.info_status_loop, playlistManager.Loop ? strings.info_on : strings.info_off)); + [Command("loop on")] public static void CommandLoopOn(PlaylistManager playlistManager) => playlistManager.Loop = true; - [Command("loop off", "Disables looping the entire playlist.")] + [Command("loop off")] public static void CommandLoopOff(PlaylistManager playlistManager) => playlistManager.Loop = false; - [Command("next", "Plays the next song in the playlist.")] + [Command("next")] public static void CommandNext(PlayManager playManager, InvokerData invoker) - { - playManager.Next(invoker).UnwrapThrow(); - } + => playManager.Next(invoker).UnwrapThrow(); - [Command("pm", "Requests a private session with the ServerBot so you can be intimate.")] + [Command("pm")] public static string CommandPm(CallerInfo caller, InvokerData invoker) { if (caller.ApiCall) - throw new CommandException("This command is not available as API", CommandExceptionReason.NotSupported); + throw new CommandException(strings.error_not_available_from_api, CommandExceptionReason.NotSupported); invoker.Visibiliy = TextMessageTargetMode.Private; - return "Hi " + (invoker.NickName ?? "Anonymous"); + return string.Format(strings.cmd_pm_hi, invoker.NickName ?? "Anonymous"); } - [Command("parse command", "Displays the AST of the requested command.")] - [Usage("", "The command to be parsed")] + [Command("parse command")] public static JsonValue CommandParse(string parameter) { if (!parameter.TrimStart().StartsWith("!", StringComparison.Ordinal)) throw new CommandException("This is not a command", CommandExceptionReason.CommandError); - try - { - var node = CommandParser.ParseCommandRequest(parameter); - var strb = new StringBuilder(); - strb.AppendLine(); - node.Write(strb, 0); - return new JsonValue(node, strb.ToString()); - } - catch (Exception ex) - { - throw new CommandException("GJ - You crashed it!!!", ex, CommandExceptionReason.CommandError); - } + + var node = CommandParser.ParseCommandRequest(parameter); + var strb = new StringBuilder(); + strb.AppendLine(); + node.Write(strb, 0); + return new JsonValue(node, strb.ToString()); } - [Command("pause", "Well, pauses the song. Undo with !play.")] + [Command("pause")] public static void CommandPause(Bot bot) => bot.PlayerConnection.Paused = true; - [Command("play", "Automatically tries to decide whether the link is a special resource (like youtube) or a direct resource (like ./hello.mp3) and starts it.")] + [Command("play")] + public static void CommandPlay(IPlayerConnection playerConnection) + => playerConnection.Paused = false; + + [Command("play")] [Usage("", "Youtube, Soundcloud, local path or file link")] - public static void CommandPlay(IPlayerConnection playerConnection, PlayManager playManager, InvokerData invoker, string parameter = null) - { - if (string.IsNullOrEmpty(parameter)) - playerConnection.Paused = false; - else - playManager.Play(invoker, parameter).UnwrapThrow(); - } + public static void CommandPlay(PlayManager playManager, InvokerData invoker, string url) + => playManager.Play(invoker, url).UnwrapThrow(); - [Command("plugin list", "Lists all found plugins.")] + [Command("plugin list")] public static JsonArray CommandPluginList(PluginManager pluginManager, Bot bot = null) - { - var overview = pluginManager.GetPluginOverview(bot); - return new JsonArray(overview, PluginManager.FormatOverview(overview)); - } + => new JsonArray(pluginManager.GetPluginOverview(bot), PluginManager.FormatOverview); - [Command("plugin unload", "Unloads a plugin.")] + [Command("plugin unload")] public static void CommandPluginUnload(PluginManager pluginManager, string identifier, Bot bot = null) { var result = pluginManager.StopPlugin(identifier, bot); if (result != PluginResponse.Ok) - throw new CommandException("Plugin error: " + result, CommandExceptionReason.CommandError); + throw new CommandException(string.Format(strings.error_plugin_error, result /*TODO*/), CommandExceptionReason.CommandError); } - [Command("plugin load", "Unloads a plugin.")] + [Command("plugin load")] public static void CommandPluginLoad(PluginManager pluginManager, string identifier, Bot bot = null) { var result = pluginManager.StartPlugin(identifier, bot); if (result != PluginResponse.Ok) - throw new CommandException("Plugin error: " + result, CommandExceptionReason.CommandError); + throw new CommandException(string.Format(strings.error_plugin_error, result /*TODO*/), CommandExceptionReason.CommandError); } - [Command("previous", "Plays the previous song in the playlist.")] + [Command("previous")] public static void CommandPrevious(PlayManager playManager, InvokerData invoker) => playManager.Previous(invoker).UnwrapThrow(); - [Command("print", "Lets you format multiple parameter to one.")] + [Command("print")] public static string CommandPrint(params string[] parameter) { // XXX << Design changes expected >> @@ -883,7 +957,7 @@ public static string CommandPrint(params string[] parameter) return strb.ToString(); } - [Command("quit", "Closes the TS3AudioBot application.")] + [Command("quit")] public static JsonEmpty CommandQuit(Core core, CallerInfo caller, UserSession session = null, string param = null) { if (caller.ApiCall) @@ -908,100 +982,101 @@ string ResponseQuit(string message) } session.SetResponse(ResponseQuit); - return new JsonEmpty("Do you really want to quit? !(yes|no)"); + return new JsonEmpty(strings.cmd_quit_confirm + YesNoOption); } - [Command("quiz", "Shows the quizmode status.")] - public static JsonValue CommandQuiz(Bot bot) => new JsonValue(bot.QuizMode, "Quizmode is " + (bot.QuizMode ? "on" : "off")); - [Command("quiz on", "Enable to hide the songnames and let your friends guess the title.")] + [Command("quiz")] + public static JsonValue CommandQuiz(Bot bot) => new JsonValue(bot.QuizMode, string.Format(strings.info_status_quizmode, bot.QuizMode ? strings.info_on : strings.info_off)); + [Command("quiz on")] public static void CommandQuizOn(Bot bot) { bot.QuizMode = true; bot.UpdateBotStatus().UnwrapThrow(); } - [Command("quiz off", "Disable to show the songnames again.")] + [Command("quiz off")] public static void CommandQuizOff(Bot bot, CallerInfo caller, InvokerData invoker) { - if (!caller.ApiCall && invoker.Visibiliy.HasValue && invoker.Visibiliy != TextMessageTargetMode.Private) - throw new CommandException("No cheating! Everybody has to see it!", CommandExceptionReason.CommandError); + if (!caller.ApiCall && invoker.Visibiliy.HasValue && invoker.Visibiliy == TextMessageTargetMode.Private) + throw new CommandException(strings.cmd_quiz_off_no_cheating, CommandExceptionReason.CommandError); bot.QuizMode = false; bot.UpdateBotStatus().UnwrapThrow(); } - [Command("random", "Gets whether or not to play playlists in random order.")] - public static JsonValue CommandRandom(PlaylistManager playlistManager) => new JsonValue(playlistManager.Random, "Random is " + (playlistManager.Random ? "on" : "off")); - [Command("random on", "Enables random playlist playback")] + [Command("random")] + public static JsonValue CommandRandom(PlaylistManager playlistManager) => new JsonValue(playlistManager.Random, string.Format(strings.info_status_random, playlistManager.Random ? strings.info_on : strings.info_off)); + [Command("random on")] public static void CommandRandomOn(PlaylistManager playlistManager) => playlistManager.Random = true; - [Command("random off", "Disables random playlist playback")] + [Command("random off")] public static void CommandRandomOff(PlaylistManager playlistManager) => playlistManager.Random = false; - [Command("random seed", "Gets the unique seed for a certain playback order")] + [Command("random seed", "cmd_random_seed_help")] public static string CommandRandomSeed(PlaylistManager playlistManager) { string seed = Util.FromSeed(playlistManager.Seed); - return string.IsNullOrEmpty(seed) ? "" : seed; + return string.IsNullOrEmpty(seed) ? strings.info_empty : seed; } - [Command("random seed", "Sets the unique seed for a certain playback order")] + [Command("random seed", "cmd_random_seed_string_help")] public static void CommandRandomSeed(PlaylistManager playlistManager, string newSeed) { if (newSeed.Any(c => !char.IsLetter(c))) - throw new CommandException("Only letters allowed", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_random_seed_only_letters_allowed, CommandExceptionReason.CommandError); playlistManager.Seed = Util.ToSeed(newSeed.ToLowerInvariant()); } - [Command("random seed", "Sets the unique seed for a certain playback order")] + [Command("random seed", "cmd_random_seed_int_help")] public static void CommandRandomSeed(PlaylistManager playlistManager, int newSeed) => playlistManager.Seed = newSeed; - [Command("repeat", "Gets whether or not to loop a single song.")] - public static JsonValue CommandRepeat(IPlayerConnection playerConnection) => new JsonValue(playerConnection.Repeated, "Repeat is " + (playerConnection.Repeated ? "on" : "off")); - [Command("repeat on", "Enables single song repeat.")] + [Command("repeat")] + public static JsonValue CommandRepeat(IPlayerConnection playerConnection) => new JsonValue(playerConnection.Repeated, string.Format(strings.info_status_repeat, playerConnection.Repeated ? strings.info_on : strings.info_off)); + [Command("repeat on")] public static void CommandRepeatOn(IPlayerConnection playerConnection) => playerConnection.Repeated = true; - [Command("repeat off", "Disables single song repeat.")] + [Command("repeat off")] public static void CommandRepeatOff(IPlayerConnection playerConnection) => playerConnection.Repeated = false; - [Command("rights can", "Returns the subset of allowed commands the caller (you) can execute.")] - public static JsonArray CommandRightsCan(RightsManager rightsManager, TeamspeakControl ts, CallerInfo caller, InvokerData invoker = null, params string[] rights) - { - var result = rightsManager.GetRightsSubset(caller, invoker, ts, rights); - return new JsonArray(result, result.Length > 0 ? string.Join(", ", result) : "No"); - } + [Command("rights can")] + public static JsonArray CommandRightsCan(ExecutionInformation info, RightsManager rightsManager, params string[] rights) + => new JsonArray(rightsManager.GetRightsSubset(info, rights), r => r.Length > 0 ? string.Join(", ", r) : strings.info_empty); - [Command("rights reload", "Reloads the rights configuration from file.")] + [Command("rights reload")] public static JsonEmpty CommandRightsReload(RightsManager rightsManager) { - if (rightsManager.ReadFile()) - return new JsonEmpty("Ok"); + if (rightsManager.Reload()) + return new JsonEmpty(strings.info_ok); // TODO: this can be done nicer by returning the errors and warnings from parsing - throw new CommandException("Error while parsing file, see log for more details", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_rights_reload_error_parsing_file, CommandExceptionReason.CommandError); } - [Command("rng", "Gets a random number.")] + [Command("rng")] [Usage("", "Gets a number between 0 and 2147483647")] [Usage("", "Gets a number between 0 and ")] [Usage(" ", "Gets a number between and ")] public static int CommandRng(int? first = null, int? second = null) { if (first.HasValue && second.HasValue) + { return Util.Random.Next(Math.Min(first.Value, second.Value), Math.Max(first.Value, second.Value)); + } else if (first.HasValue) { if (first.Value <= 0) - throw new CommandException("Value must be 0 or positive", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_rng_value_must_be_positive, CommandExceptionReason.CommandError); return Util.Random.Next(first.Value); } else + { return Util.Random.Next(); + } } - [Command("seek", "Jumps to a timemark within the current song.")] + [Command("seek")] [Usage("", "Time in seconds")] [Usage("", "Time in Minutes:Seconds")] - public static void CommandSeek(IPlayerConnection playerConnection, string parameter) + public static void CommandSeek(IPlayerConnection playerConnection, string position) { TimeSpan span; bool parsed = false; - if (parameter.Contains(":")) + if (position.Contains(":")) { - string[] splittime = parameter.Split(':'); + string[] splittime = position.Split(':'); if (splittime.Length == 2 && int.TryParse(splittime[0], out int minutes) @@ -1010,103 +1085,203 @@ public static void CommandSeek(IPlayerConnection playerConnection, string parame parsed = true; span = TimeSpan.FromSeconds(seconds) + TimeSpan.FromMinutes(minutes); } - else span = TimeSpan.MinValue; + else + { + span = TimeSpan.MinValue; + } } else { - parsed = int.TryParse(parameter, out int seconds); + parsed = int.TryParse(position, out int seconds); span = TimeSpan.FromSeconds(seconds); } if (!parsed) - throw new CommandException("The time was not in a correct format, see !help seek for more information.", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_seek_invalid_format, CommandExceptionReason.CommandError); else if (span < TimeSpan.Zero || span > playerConnection.Length) - throw new CommandException("The point of time is not within the song length.", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_seek_out_of_range, CommandExceptionReason.CommandError); else playerConnection.Position = span; } - [Command("settings", "Changes values from the settigns. Not all changes can be applied immediately.")] - [Usage("", "Get the value of a setting")] - [Usage(" ", "Set the value of a setting")] - public static JsonValue> CommandSettings(ConfigFile configManager, string key = null, string value = null) + [Command("settings")] + public static void CommandSettings() + => throw new CommandException(string.Format(strings.cmd_settings_empty_usage, "'rights.path', 'web.api.enabled', 'tools.*'"), CommandExceptionReason.MissingParameter); + + [Command("settings get")] + public static ConfigPart CommandSettingsGet(ConfBot config, string path) + => SettingsGet(config, path); + + [Command("settings set")] + public static void CommandSettingsSet(ConfBot config, string path, string value) { - var configMap = configManager.GetConfigMap(); - if (string.IsNullOrEmpty(key)) - throw new CommandException("Please specify a key like: \n " + string.Join("\n ", configMap.Take(3).Select(kvp => kvp.Key)), - CommandExceptionReason.MissingParameter); + SettingsSet(config, path, value); + if (!config.SaveWhenExists()) + { + throw new CommandException("Value was set but could not be saved to file. All changes are temporary and will be lost when the bot restarts.", + CommandExceptionReason.CommandError); + } + } - var filtered = Algorithm.SubstringFilter.Instance.Filter(configMap, key); - var filteredArr = filtered.ToArray(); + [Command("settings bot get", "cmd_settings_get_help")] + public static ConfigPart CommandSettingsBotGet(BotManager bots, ConfRoot config, string bot, string path) + { + using (var botlock = bots.GetBotLock(bot)) + { + var confBot = GetConf(botlock?.Bot, config, bot); + return CommandSettingsGet(confBot, path); + } + } - if (filteredArr.Length == 0) + [Command("settings bot set", "cmd_settings_set_help")] + public static void CommandSettingsBotSet(BotManager bots, ConfRoot config, string bot, string path, string value) + { + using (var botlock = bots.GetBotLock(bot)) { - throw new CommandException("No config key matching the pattern found", CommandExceptionReason.CommandError); + var confBot = GetConf(botlock?.Bot, config, bot); + CommandSettingsSet(confBot, path, value); } - else if (filteredArr.Length == 1) + } + + [Command("settings global get")] + public static ConfigPart CommandSettingsGlobalGet(ConfRoot config, string path) + => SettingsGet(config, path); + + [Command("settings global set")] + public static void CommandSettingsGlobalSet(ConfRoot config, string path, string value) + { + SettingsSet(config, path, value); + if (!config.Save()) + { + throw new CommandException("Value was set but could not be saved to file. All changes are temporary and will be lost when the bot restarts.", + CommandExceptionReason.CommandError); + } + } + + private static ConfBot GetConf(Bot bot, ConfRoot config, string name) + { + if (bot != null) { - if (string.IsNullOrEmpty(value)) - return new JsonValue>(filteredArr[0], filteredArr[0].Key + " = " + filteredArr[0].Value); + var mod = bot.Injector.GetModule(); + if (mod != null) + return mod; else - { - configManager.SetSetting(filteredArr[0].Key, value).UnwrapThrow(); - return null; - } + throw new CommandException(strings.error_call_unexpected_error, CommandExceptionReason.CommandError); + } + else + { + var getTemplateResult = config.GetBotTemplate(name); + if (!getTemplateResult.Ok) + throw new CommandException(strings.error_bot_does_not_exist, getTemplateResult.Error, CommandExceptionReason.CommandError); + return getTemplateResult.Value; + } + } + + private static ConfigPart SettingsGet(ConfigPart config, string path) => config.ByPathAsArray(path).SettingsGetSingle(); + + private static void SettingsSet(ConfigPart config, string path, string value) + { + var setConfig = config.ByPathAsArray(path).SettingsGetSingle(); + if (setConfig is IJsonSerializable jsonConfig) + { + var result = jsonConfig.FromJson(value); + if (!result.Ok) + throw new CommandException($"Failed to set the value ({result.Error}).", CommandExceptionReason.CommandError); // LOC: TODO + } + else + { + throw new CommandException("This value currently cannot be set.", CommandExceptionReason.CommandError); // LOC: TODO + } + } + + private static ConfigPart SettingsGetSingle(this ConfigPart[] configPartsList) + { + if (configPartsList.Length == 0) + { + throw new CommandException(strings.error_config_no_key_found, CommandExceptionReason.CommandError); + } + else if (configPartsList.Length == 1) + { + return configPartsList[0]; } else { - throw new CommandException("Found more than one matching key: \n " + string.Join("\n ", filteredArr.Take(3).Select(kvp => kvp.Key)), + throw new CommandException( + string.Format( + strings.error_config_multiple_keys_found + "\n", + string.Join("\n ", configPartsList.Take(3).Select(kvp => kvp.Key))), CommandExceptionReason.CommandError); } } - [Command("song", "Tells you the name of the current song.")] + [Command("settings help")] + public static string CommandSettingsHelp(ConfRoot config, string path) + { + var part = SettingsGet(config, path); + return string.IsNullOrEmpty(part.Documentation) ? strings.info_empty : part.Documentation; + } + + [Command("song")] public static JsonValue CommandSong(PlayManager playManager, ResourceFactoryManager factoryManager, Bot bot, CallerInfo caller, InvokerData invoker = null) { if (playManager.CurrentPlayData == null) - throw new CommandException("There is nothing on right now...", CommandExceptionReason.CommandError); + throw new CommandException(strings.info_currently_not_playing, CommandExceptionReason.CommandError); if (bot.QuizMode && !caller.ApiCall && (invoker == null || playManager.CurrentPlayData.Invoker.ClientId != invoker.ClientId)) - throw new CommandException("Sorry, you have to guess!", CommandExceptionReason.CommandError); + throw new CommandException(strings.info_quizmode_is_active, CommandExceptionReason.CommandError); return new JsonValue( playManager.CurrentPlayData.ResourceData.ResourceTitle, $"[url={factoryManager.RestoreLink(playManager.CurrentPlayData.ResourceData)}]{playManager.CurrentPlayData.ResourceData.ResourceTitle}[/url]"); } - [Command("stop", "Stops the current song.")] + [Command("song position")] + public static JsonObject CommandSongPosition(IPlayerConnection playerConnection) + { + return JsonValue.Create(new + { + position = playerConnection.Position, + length = playerConnection.Length, + }, + x => x.length.TotalHours >= 1 || x.position.TotalHours >= 1 + ? $"{x.position:hh\\:mm\\:ss}/{x.length:hh\\:mm\\:ss}" + : $"{x.position:mm\\:ss}/{x.length:mm\\:ss}" + ); + } + + [Command("stop")] public static void CommandStop(PlayManager playManager) => playManager.Stop(); - [Command("subscribe", "Lets you hear the music independent from the channel you are in.")] - public static void CommandSubscribe(ITargetManager targetManager, InvokerData invoker) + [Command("subscribe")] + public static void CommandSubscribe(IVoiceTarget targetManager, InvokerData invoker) { if (invoker.ClientId.HasValue) targetManager.WhisperClientSubscribe(invoker.ClientId.Value); } - [Command("subscribe tempchannel", "Adds your current channel to the music playback.")] - public static void CommandSubscribeTempChannel(ITargetManager targetManager, InvokerData invoker, ulong? channel = null) + [Command("subscribe tempchannel")] + public static void CommandSubscribeTempChannel(IVoiceTarget targetManager, InvokerData invoker, ulong? channel = null) { var subChan = channel ?? invoker.ChannelId ?? 0; if (subChan != 0) targetManager.WhisperChannelSubscribe(subChan, true); } - [Command("subscribe channel", "Adds your current channel to the music playback.")] - public static void CommandSubscribeChannel(ITargetManager targetManager, InvokerData invoker, ulong? channel = null) + [Command("subscribe channel")] + public static void CommandSubscribeChannel(IVoiceTarget targetManager, InvokerData invoker, ulong? channel = null) { var subChan = channel ?? invoker.ChannelId ?? 0; if (subChan != 0) targetManager.WhisperChannelSubscribe(subChan, false); } - [Command("take", "Take a substring from a string.")] + [Command("take")] [Usage(" ", "Take only parts of the text")] [Usage(" ", "Take parts, starting with the part at ")] [Usage(" ", "Specify another delimiter for the parts than spaces")] public static ICommandResult CommandTake(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes) { if (arguments.Count < 2) - throw new CommandException("Expected at least 2 parameters", CommandExceptionReason.MissingParameter); + throw new CommandException(strings.error_cmd_at_least_two_argument, CommandExceptionReason.MissingParameter); int start = 0; string delimiter = null; @@ -1114,18 +1289,18 @@ public static ICommandResult CommandTake(ExecutionInformation info, IReadOnlyLis // Get count var res = ((StringCommandResult)arguments[0].Execute(info, Array.Empty(), XCommandSystem.ReturnString)).Content; if (!int.TryParse(res, out int count) || count < 0) - throw new CommandException("Count must be an integer >= 0", CommandExceptionReason.CommandError); + throw new CommandException("Count must be an integer >= 0", CommandExceptionReason.CommandError); // LOC: TODO if (arguments.Count > 2) { // Get start res = ((StringCommandResult)arguments[1].Execute(info, Array.Empty(), XCommandSystem.ReturnString)).Content; if (!int.TryParse(res, out start) || start < 0) - throw new CommandException("Start must be an integer >= 0", CommandExceptionReason.CommandError); + throw new CommandException("Start must be an integer >= 0", CommandExceptionReason.CommandError); // LOC: TODO } + // Get delimiter if exists if (arguments.Count > 3) - // Get delimiter delimiter = ((StringCommandResult)arguments[2].Execute(info, Array.Empty(), XCommandSystem.ReturnString)).Content; string text = ((StringCommandResult)arguments[Math.Min(arguments.Count - 1, 3)] @@ -1135,7 +1310,7 @@ public static ICommandResult CommandTake(ExecutionInformation info, IReadOnlyLis ? text.Split() : text.Split(new[] { delimiter }, StringSplitOptions.None); if (splitted.Length < start + count) - throw new CommandException("Not enough arguments to take", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_take_not_enough_arguements, CommandExceptionReason.CommandError); var splittedarr = splitted.Skip(start).Take(count).ToArray(); foreach (var returnType in returnTypes) @@ -1149,56 +1324,54 @@ public static ICommandResult CommandTake(ExecutionInformation info, IReadOnlyLis throw new CommandException("Can't find a fitting return type for take", CommandExceptionReason.NoReturnMatch); } - [Command("unsubscribe", "Only lets you hear the music in active channels again.")] - public static void CommandUnsubscribe(ITargetManager targetManager, InvokerData invoker) + [Command("unsubscribe")] + public static void CommandUnsubscribe(IVoiceTarget targetManager, InvokerData invoker) { if (invoker.ClientId.HasValue) targetManager.WhisperClientUnsubscribe(invoker.ClientId.Value); } - [Command("unsubscribe channel", "Removes your current channel from the music playback.")] - public static void CommandUnsubscribeChannel(ITargetManager targetManager, InvokerData invoker, ulong? channel = null) + [Command("unsubscribe channel")] + public static void CommandUnsubscribeChannel(IVoiceTarget targetManager, InvokerData invoker, ulong? channel = null) { - var subChan = channel ?? invoker.ChannelId ?? 0; - if (subChan != 0) - targetManager.WhisperChannelUnsubscribe(subChan, false); + var subChan = channel ?? invoker.ChannelId; + if (subChan.HasValue) + targetManager.WhisperChannelUnsubscribe(subChan.Value, false); } - [Command("unsubscribe temporary", "Clears all temporary targets.")] - public static void CommandUnsubscribeTemporary(ITargetManager targetManager) => targetManager.ClearTemporary(); + [Command("unsubscribe temporary")] + public static void CommandUnsubscribeTemporary(IVoiceTarget targetManager) => targetManager.ClearTemporary(); - [Command("version", "Gets the current build version.")] - public static JsonValue CommandVersion() - { - var data = SystemData.AssemblyData; - return new JsonValue(data, data.ToLongString()); - } + [Command("version")] + public static JsonValue CommandVersion() => new JsonValue(SystemData.AssemblyData, d => d.ToLongString()); - [Command("volume", "Sets the volume level of the music.")] + [Command("volume")] [Usage("", "A new volume level between 0 and 100.")] [Usage("+/-", "Adds or subtracts a value from the current volume.")] - public static JsonValue CommandVolume(ExecutionInformation info, IPlayerConnection playerConnection, CallerInfo caller, UserSession session = null, string parameter = null) + public static JsonValue CommandVolume(ExecutionInformation info, IPlayerConnection playerConnection, CallerInfo caller, ConfBot config, UserSession session = null, string volume = null) { - if (string.IsNullOrEmpty(parameter)) - return new JsonValue(playerConnection.Volume, "Current volume: " + playerConnection.Volume); + if (string.IsNullOrEmpty(volume)) + return new JsonValue(playerConnection.Volume, string.Format(strings.cmd_volume_current, playerConnection.Volume)); - bool relPos = parameter.StartsWith("+", StringComparison.Ordinal); - bool relNeg = parameter.StartsWith("-", StringComparison.Ordinal); - string numberString = (relPos || relNeg) ? parameter.Remove(0, 1) : parameter; + bool relPos = volume.StartsWith("+", StringComparison.Ordinal); + bool relNeg = volume.StartsWith("-", StringComparison.Ordinal); + string numberString = (relPos || relNeg) ? volume.Remove(0, 1) : volume; - if (!float.TryParse(numberString, NumberStyles.Float, CultureInfo.InvariantCulture, out var volume)) - throw new CommandException("The new volume could not be parsed", CommandExceptionReason.CommandError); + if (!float.TryParse(numberString, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedVolume)) + throw new CommandException(strings.cmd_volume_parse_error, CommandExceptionReason.CommandError); float newVolume; - if (relPos) newVolume = playerConnection.Volume + volume; - else if (relNeg) newVolume = playerConnection.Volume - volume; - else newVolume = volume; + if (relPos) newVolume = playerConnection.Volume + parsedVolume; + else if (relNeg) newVolume = playerConnection.Volume - parsedVolume; + else newVolume = parsedVolume; if (newVolume < 0 || newVolume > AudioValues.MaxVolume) - throw new CommandException("The volume level must be between 0 and " + AudioValues.MaxVolume, CommandExceptionReason.CommandError); + throw new CommandException(string.Format(strings.cmd_volume_is_limited, 0, AudioValues.MaxVolume), CommandExceptionReason.CommandError); - if (newVolume <= AudioValues.MaxUserVolume || newVolume < playerConnection.Volume || caller.ApiCall) + if (newVolume <= config.Audio.MaxUserVolume || newVolume < playerConnection.Volume || caller.ApiCall) + { playerConnection.Volume = newVolume; + } else if (newVolume <= AudioValues.MaxVolume) { string ResponseVolume(string message) @@ -1208,118 +1381,101 @@ string ResponseVolume(string message) if (info.HasRights(RightHighVolume)) playerConnection.Volume = newVolume; else - return "You are not allowed to set higher volumes."; + return strings.cmd_volume_missing_high_volume_permission; } return null; } session.SetResponse(ResponseVolume); - throw new CommandException("Careful you are requesting a very high volume! Do you want to apply this? !(yes|no)", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_volume_high_volume_confirm + YesNoOption, CommandExceptionReason.CommandError); } return null; } - [Command("whisper all", "Set how to send music.")] - public static void CommandWhisperAll(ITargetManager targetManager) => CommandWhisperGroup(targetManager, GroupWhisperType.AllClients, GroupWhisperTarget.AllChannels); + [Command("whisper all")] + public static void CommandWhisperAll(IVoiceTarget targetManager) => CommandWhisperGroup(targetManager, GroupWhisperType.AllClients, GroupWhisperTarget.AllChannels); - [Command("whisper group", "Set a specific teamspeak whisper group.")] - public static void CommandWhisperGroup(ITargetManager targetManager, GroupWhisperType type, GroupWhisperTarget target, ulong? targetId = null) + [Command("whisper group")] + public static void CommandWhisperGroup(IVoiceTarget targetManager, GroupWhisperType type, GroupWhisperTarget target, ulong? targetId = null) { if (type == GroupWhisperType.ServerGroup || type == GroupWhisperType.ChannelGroup) { if (!targetId.HasValue) - throw new CommandException("This type requires an additional target", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_whisper_group_missing_target, CommandExceptionReason.CommandError); targetManager.SetGroupWhisper(type, target, targetId.Value); targetManager.SendMode = TargetSendMode.WhisperGroup; } else { if (targetId.HasValue) - throw new CommandException("This type does not take an additional target", CommandExceptionReason.CommandError); + throw new CommandException(strings.cmd_whisper_group_superfluous_target, CommandExceptionReason.CommandError); targetManager.SetGroupWhisper(type, target, 0); targetManager.SendMode = TargetSendMode.WhisperGroup; } } - [Command("whisper list", "Set how to send music.")] - public static WhisperListStruct CommandWhisperList(ITargetManager targetManager) + [Command("whisper list")] + public static JsonObject CommandWhisperList(IVoiceTarget targetManager) { - return new WhisperListStruct + return JsonValue.Create(new { SendMode = targetManager.SendMode, GroupWhisper = targetManager.SendMode == TargetSendMode.WhisperGroup ? - new WhisperListStruct.GroupWhisperStruct - { - Target = targetManager.GroupWhisperTarget, - TargetId = targetManager.GroupWhisperTargetId, - Type = targetManager.GroupWhisperType, - } - : null, + new + { + Target = targetManager.GroupWhisperTarget, + TargetId = targetManager.GroupWhisperTargetId, + Type = targetManager.GroupWhisperType, + } + : null, WhisperClients = targetManager.WhisperClients, WhisperChannel = targetManager.WhisperChannel, - }; - } - - public class WhisperListStruct - { - public TargetSendMode SendMode { get; set; } - public GroupWhisperStruct GroupWhisper { get; set; } - public IReadOnlyCollection WhisperClients { get; set; } - public IReadOnlyCollection WhisperChannel { get; set; } - - public override string ToString() + }, + x => { - var strb = new StringBuilder("Currently targeting:\n"); - switch (SendMode) + var strb = new StringBuilder(strings.cmd_whisper_list_header); + strb.AppendLine(); + switch (x.SendMode) { - case TargetSendMode.None: strb.Append("Nowhere!"); break; - case TargetSendMode.Voice: strb.Append("This channel via voice!"); break; + case TargetSendMode.None: strb.Append(strings.cmd_whisper_list_target_none); break; + case TargetSendMode.Voice: strb.Append(strings.cmd_whisper_list_target_voice); break; case TargetSendMode.Whisper: - strb.Append("Clients: [").Append(string.Join(",", WhisperClients)).Append("]\n"); - strb.Append("Channel: [").Append(string.Join(",", WhisperChannel)).Append("]"); + strb.Append(strings.cmd_whisper_list_target_whisper_clients).Append(": [").Append(string.Join(",", x.WhisperClients)).Append("]\n"); + strb.Append(strings.cmd_whisper_list_target_whisper_channel).Append(": [").Append(string.Join(",", x.WhisperChannel)).Append("]"); break; case TargetSendMode.WhisperGroup: - strb.AppendFormat("A whisper group: {0} {1} ({2})!", GroupWhisper.Type, GroupWhisper.Target, GroupWhisper.TargetId); + strb.AppendFormat(strings.cmd_whisper_list_target_whispergroup, x.GroupWhisper.Type, x.GroupWhisper.Target, x.GroupWhisper.TargetId); break; default: throw new ArgumentOutOfRangeException(); } return strb.ToString(); - } - - public class GroupWhisperStruct - { - public ulong TargetId { get; set; } - public GroupWhisperType Type { get; set; } - public GroupWhisperTarget Target { get; set; } - } + }); } - [Command("whisper off", "Enables normal voice mode.")] - public static void CommandWhisperOff(ITargetManager targetManager) => targetManager.SendMode = TargetSendMode.Voice; - - [Command("whisper subscription", "Enables default whisper subscription mode.")] - public static void CommandWhisperSubsription(ITargetManager targetManager) => targetManager.SendMode = TargetSendMode.Whisper; + [Command("whisper off")] + public static void CommandWhisperOff(IVoiceTarget targetManager) => targetManager.SendMode = TargetSendMode.Voice; - private static readonly CommandResultType[] ReturnPreferNothingAllowAny = { CommandResultType.Empty, CommandResultType.String, CommandResultType.Json, CommandResultType.Command }; + [Command("whisper subscription")] + public static void CommandWhisperSubsription(IVoiceTarget targetManager) => targetManager.SendMode = TargetSendMode.Whisper; - [Command("xecute", "Evaluates all parameter.")] + [Command("xecute")] public static void CommandXecute(ExecutionInformation info, IReadOnlyList arguments) { foreach (var arg in arguments) - arg.Execute(info, Array.Empty(), ReturnPreferNothingAllowAny); + arg.Execute(info, Array.Empty(), XCommandSystem.ReturnAnyPreferNothing); } // ReSharper enable UnusedMember.Global private static Playlist AutoGetPlaylist(UserSession session, InvokerData invoker) { if (session == null) - throw new CommandException("Missing session context", CommandExceptionReason.CommandError); + throw new CommandException(strings.error_no_session_in_context, CommandExceptionReason.MissingContext); var result = session.Get(); if (result) return result.Value; - var newPlist = new Playlist(invoker.NickName, invoker.DatabaseId); + var newPlist = new Playlist(invoker.NickName, invoker.ClientUid); session.Set(newPlist); return newPlist; } @@ -1331,30 +1487,28 @@ public static bool HasRights(this ExecutionInformation info, params string[] rig return true; if (!info.TryGet(out var rightsManager)) return false; - if (!info.TryGet(out var invoker)) invoker = null; - if (!info.TryGet(out var ts)) ts = null; - return rightsManager.HasAllRights(caller, invoker, ts, rights); + return rightsManager.HasAllRights(info, rights); } - public static R Write(this ExecutionInformation info, string message) + public static E Write(this ExecutionInformation info, string message) { - if (!info.TryGet(out var queryConnection)) - return "No teamspeak connection in context"; + if (!info.TryGet(out var ts3Client)) + return new LocalStr(strings.error_no_teamspeak_in_context); if (!info.TryGet(out var invoker)) - return "No invoker in context"; + return new LocalStr(strings.error_no_invoker_in_context); if (!invoker.Visibiliy.HasValue || !invoker.ClientId.HasValue) - return "Invoker casted Ghost Walk"; + return new LocalStr(strings.error_invoker_not_visible); switch (invoker.Visibiliy.Value) { case TextMessageTargetMode.Private: - return queryConnection.SendMessage(message, invoker.ClientId.Value); + return ts3Client.SendMessage(message, invoker.ClientId.Value); case TextMessageTargetMode.Channel: - return queryConnection.SendChannelMessage(message); + return ts3Client.SendChannelMessage(message); case TextMessageTargetMode.Server: - return queryConnection.SendServerMessage(message); + return ts3Client.SendServerMessage(message); default: throw Util.UnhandledDefault(invoker.Visibiliy.Value); } diff --git a/TS3AudioBot/NLog.config b/TS3AudioBot/NLog.config index 618fedb3..8f96ae7a 100644 --- a/TS3AudioBot/NLog.config +++ b/TS3AudioBot/NLog.config @@ -19,9 +19,9 @@ - - - - + + + + diff --git a/TS3AudioBot/PlayManager.cs b/TS3AudioBot/PlayManager.cs index ab0255a7..6bdee5ad 100644 --- a/TS3AudioBot/PlayManager.cs +++ b/TS3AudioBot/PlayManager.cs @@ -9,7 +9,9 @@ namespace TS3AudioBot { - using Helper; + using Config; + using Localization; + using Playlists; using ResourceFactories; using System; using System.Collections.Generic; @@ -19,6 +21,7 @@ public class PlayManager { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + public ConfBot Config { get; set; } public IPlayerConnection PlayerConnection { get; set; } public PlaylistManager PlaylistManager { get; set; } public ResourceFactoryManager ResourceFactoryManager { get; set; } @@ -31,29 +34,28 @@ public class PlayManager public event EventHandler BeforeResourceStopped; public event EventHandler AfterResourceStopped; - public R Enqueue(InvokerData invoker, AudioResource ar) => EnqueueInternal(invoker, new PlaylistItem(ar)); - public R Enqueue(InvokerData invoker, string message, string audioType = null) + public E Enqueue(InvokerData invoker, AudioResource ar) => EnqueueInternal(invoker, new PlaylistItem(ar)); + public E Enqueue(InvokerData invoker, string message, string audioType = null) { var result = ResourceFactoryManager.Load(message, audioType); if (!result) return result.Error; return EnqueueInternal(invoker, new PlaylistItem(result.Value.BaseData)); } - public R Enqueue(IEnumerable pli) + public E Enqueue(IEnumerable pli) { PlaylistManager.AddToFreelist(pli); - return R.OkR; + return R.Ok; } - private R EnqueueInternal(InvokerData invoker, PlaylistItem pli) + private E EnqueueInternal(InvokerData invoker, PlaylistItem pli) { - pli.Meta.ResourceOwnerDbId = invoker.DatabaseId; + pli.Meta.ResourceOwnerUid = invoker.ClientUid; PlaylistManager.AddToFreelist(pli); - - return R.OkR; + return R.Ok; } - public R Play(InvokerData invoker, PlaylistItem item) + public E Play(InvokerData invoker, PlaylistItem item) { if (item == null) throw new ArgumentNullException(nameof(item)); @@ -65,7 +67,7 @@ public R Play(InvokerData invoker, PlaylistItem item) /// The resource to load and play. /// Allows overriding certain settings for the resource. Can be null. /// Ok if successful, or an error message otherwise. - public R Play(InvokerData invoker, AudioResource ar, MetaData meta = null) + public E Play(InvokerData invoker, AudioResource ar, MetaData meta = null) { if (ar == null) throw new ArgumentNullException(nameof(ar)); @@ -81,7 +83,7 @@ public R Play(InvokerData invoker, AudioResource ar, MetaData meta = null) /// The associated resource type string to a factory. /// Allows overriding certain settings for the resource. Can be null. /// Ok if successful, or an error message otherwise. - public R Play(InvokerData invoker, string link, string audioType = null, MetaData meta = null) + public E Play(InvokerData invoker, string link, string audioType = null, MetaData meta = null) { var result = ResourceFactoryManager.Load(link, audioType); if (!result) @@ -93,10 +95,10 @@ public R Play(InvokerData invoker, string link, string audioType = null, MetaDat /// The associated resource type string to a factory. /// Allows overriding certain settings for the resource. /// Ok if successful, or an error message otherwise. - public R Play(InvokerData invoker, PlayResource play, MetaData meta) + public E Play(InvokerData invoker, PlayResource play, MetaData meta) { if (!meta.FromPlaylist) - meta.ResourceOwnerDbId = invoker.DatabaseId; + meta.ResourceOwnerUid = invoker.ClientUid; var playInfo = new PlayInfoEventArgs(invoker, play, meta); BeforeResourceStarted?.Invoke(this, playInfo); @@ -115,30 +117,32 @@ public R Play(InvokerData invoker, PlayResource play, MetaData meta) CurrentPlayData = playInfo; // TODO meta as readonly AfterResourceStarted?.Invoke(this, CurrentPlayData); - return R.OkR; + return R.Ok; } - private R StartResource(PlayResource playResource, MetaData config) + private E StartResource(PlayResource playResource, MetaData meta) { - //PlayerConnection.AudioStop(); - if (string.IsNullOrWhiteSpace(playResource.PlayUri)) - return "Internal resource error: link is empty"; + { + Log.Error("Internal resource error: link is empty (resource:{0})", playResource); + return new LocalStr(strings.error_playmgr_internal_error); + } Log.Debug("AudioResource start: {0}", playResource); var result = PlayerConnection.AudioStart(playResource.PlayUri); if (!result) { Log.Error("Error return from player: {0}", result.Error); - return $"Internal player error ({result.Error})"; + return new LocalStr(strings.error_playmgr_internal_error); } - PlayerConnection.Volume = config.Volume ?? AudioValues.DefaultVolume; + PlayerConnection.Volume = meta.Volume + ?? Math.Min(Math.Max(PlayerConnection.Volume, Config.Audio.Volume.Min), Config.Audio.Volume.Max); - return R.OkR; + return R.Ok; } - public R Next(InvokerData invoker) + public E Next(InvokerData invoker) { PlaylistItem pli = null; for (int i = 0; i < 10; i++) @@ -147,27 +151,29 @@ public R Next(InvokerData invoker) var result = Play(invoker, pli); if (result.Ok) return result; - Log.Warn("Skipping: {0} because {1}", pli.DisplayString, result.Error); + Log.Warn("Skipping: {0} because {1}", pli.DisplayString, result.Error.Str); } if (pli == null) - return "No next song could be played"; + return new LocalStr(strings.info_playmgr_no_next_song); else - return "A few songs failed to start, use !next to continue"; + return new LocalStr(string.Format(strings.error_playmgr_many_songs_failed, "!next")); } - public R Previous(InvokerData invoker) + + public E Previous(InvokerData invoker) { PlaylistItem pli = null; for (int i = 0; i < 10; i++) { if ((pli = PlaylistManager.Previous()) == null) break; - if (Play(invoker, pli)) - return R.OkR; - // optional message here that playlist entry has been skipped + var result = Play(invoker, pli); + if (result.Ok) + return result; + Log.Warn("Skipping: {0} because {1}", pli.DisplayString, result.Error.Str); } if (pli == null) - return "No previous song could be played"; + return new LocalStr(strings.info_playmgr_no_previous_song); else - return "A few songs failed to start, use !previous to continue"; + return new LocalStr(string.Format(strings.error_playmgr_many_songs_failed, "!previous")); } public void SongStoppedHook(object sender, EventArgs e) => StopInternal(true); @@ -197,8 +203,8 @@ private void StopInternal(bool songEndedByCallback) public sealed class MetaData { - /// Defaults to: invoker.DbId - Can be set if the owner of a song differs from the invoker. - public ulong? ResourceOwnerDbId { get; set; } + /// Defaults to: invoker.Uid - Can be set if the owner of a song differs from the invoker. + public string ResourceOwnerUid { get; set; } /// Defaults to: AudioFramwork.Defaultvolume - Overrides the starting volume. public float? Volume { get; set; } = null; /// Default: false - Indicates whether the song has been requested from a playlist. @@ -206,7 +212,7 @@ public sealed class MetaData public MetaData Clone() => new MetaData { - ResourceOwnerDbId = ResourceOwnerDbId, + ResourceOwnerUid = ResourceOwnerUid, FromPlaylist = FromPlaylist, Volume = Volume }; @@ -224,7 +230,6 @@ public sealed class PlayInfoEventArgs : EventArgs public PlayResource PlayResource { get; } public AudioResource ResourceData => PlayResource.BaseData; public MetaData MetaData { get; } - public ulong? Owner => MetaData.ResourceOwnerDbId ?? Invoker.DatabaseId; public PlayInfoEventArgs(InvokerData invoker, PlayResource playResource, MetaData meta) { @@ -238,17 +243,19 @@ public sealed class InvokerData { public string ClientUid { get; } public ulong? DatabaseId { get; internal set; } - public ulong? ChannelId { get; internal set; } - public ushort? ClientId { get; internal set; } - public string NickName { get; internal set; } - public string Token { get; internal set; } + public ulong? ChannelId { get; } + public ushort? ClientId { get; } + public string NickName { get; } + public string Token { get; } public TS3Client.TextMessageTargetMode? Visibiliy { get; internal set; } + // Lazy + public ulong[] ServerGroups { get; internal set; } public InvokerData(string clientUid, ulong? databaseId = null, ulong? channelId = null, ushort? clientId = null, string nickName = null, string token = null, TS3Client.TextMessageTargetMode? visibiliy = null) { - ClientUid = clientUid; + ClientUid = clientUid ?? throw new ArgumentNullException(nameof(ClientUid)); DatabaseId = databaseId; ChannelId = channelId; ClientId = clientId; @@ -276,20 +283,5 @@ public override bool Equals(object obj) public static class AudioValues { public const float MaxVolume = 100; - - internal static AudioFrameworkData audioFrameworkData; - - public static float MaxUserVolume => audioFrameworkData.MaxUserVolume; - public static float DefaultVolume => audioFrameworkData.DefaultVolume; - } - - public class AudioFrameworkData : ConfigData - { - [Info("The default volume a song should start with", "10")] - public float DefaultVolume { get; set; } - [Info("The maximum volume a normal user can request", "30")] - public float MaxUserVolume { get; set; } - [Info("How the bot should play music. Options are: whisper, voice, (!...)", "whisper")] - public string AudioMode { get; set; } } } diff --git a/TS3AudioBot/Playlists/Playlist.cs b/TS3AudioBot/Playlists/Playlist.cs new file mode 100644 index 00000000..3acc1b8c --- /dev/null +++ b/TS3AudioBot/Playlists/Playlist.cs @@ -0,0 +1,60 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Playlists +{ + using Helper; + using System.Collections.Generic; + + public class Playlist + { + // metainfo + public string Name { get; set; } + public string OwnerUid { get; set; } + // file behaviour: persistent playlist will be synced to a file + public bool FilePersistent { get; set; } + // playlist data + public int Count => resources.Count; + private readonly List resources; + + public Playlist(string name, string ownerUid = null) + { + Util.Init(out resources); + OwnerUid = ownerUid; + Name = name; + } + + public int AddItem(PlaylistItem item) + { + resources.Add(item); + return resources.Count - 1; + } + + public int InsertItem(PlaylistItem item, int index) + { + resources.Insert(index, item); + return index; + } + + public void AddRange(IEnumerable items) => resources.AddRange(items); + + public void RemoveItemAt(int i) + { + if (i < 0 || i >= resources.Count) + return; + resources.RemoveAt(i); + } + + public void Clear() => resources.Clear(); + + public IEnumerable AsEnumerable() => resources; + + public PlaylistItem GetResource(int index) => resources[index]; + } +} diff --git a/TS3AudioBot/Playlists/PlaylistItem.cs b/TS3AudioBot/Playlists/PlaylistItem.cs new file mode 100644 index 00000000..0bb39248 --- /dev/null +++ b/TS3AudioBot/Playlists/PlaylistItem.cs @@ -0,0 +1,27 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Playlists +{ + using ResourceFactories; + + public class PlaylistItem + { + public MetaData Meta { get; } + //one of these: + // playdata holds all needed information for playing + first possibility + // > can be a resource + public AudioResource Resource { get; } + + public string DisplayString => Resource.ResourceTitle ?? $"{Resource.AudioType}: {Resource.ResourceId}"; + + private PlaylistItem(MetaData meta) { Meta = meta ?? new MetaData(); } + public PlaylistItem(AudioResource resource, MetaData meta = null) : this(meta) { Resource = resource; } + } +} diff --git a/TS3AudioBot/PlaylistManager.cs b/TS3AudioBot/Playlists/PlaylistManager.cs similarity index 52% rename from TS3AudioBot/PlaylistManager.cs rename to TS3AudioBot/Playlists/PlaylistManager.cs index e70d15fa..86bf5a09 100644 --- a/TS3AudioBot/PlaylistManager.cs +++ b/TS3AudioBot/Playlists/PlaylistManager.cs @@ -7,10 +7,13 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -namespace TS3AudioBot +namespace TS3AudioBot.Playlists { using Algorithm; + using Config; using Helper; + using Localization; + using Newtonsoft.Json; using ResourceFactories; using System; using System.Collections.Generic; @@ -22,10 +25,9 @@ namespace TS3AudioBot public sealed class PlaylistManager { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - private static readonly Regex ValidPlistName = new Regex(@"^[\w-]+$", Util.DefaultRegexConfig); private static readonly Regex CleansePlaylistName = new Regex(@"[^\w-]", Util.DefaultRegexConfig); - private readonly PlaylistManagerData data; + private readonly ConfPlaylists config; private static readonly Encoding FileEncoding = Util.Utf8Encoder; private readonly Playlist freeList; private readonly Playlist trashList; @@ -65,11 +67,9 @@ public bool Random /// Loop state for the entire playlist. public bool Loop { get; set; } - // Playlistfactory related stuff - - public PlaylistManager(PlaylistManagerData pmd) + public PlaylistManager(ConfPlaylists config) { - data = pmd; + this.config = config; shuffle = new LinearFeedbackShiftRegister { Seed = Util.Random.Next() }; freeList = new Playlist(string.Empty); trashList = new Playlist(string.Empty); @@ -141,7 +141,7 @@ private void Reset() public void ClearFreelist() => freeList.Clear(); public void ClearTrash() => trashList.Clear(); - public R LoadPlaylist(string name, bool headOnly = false) + public R LoadPlaylist(string name, bool headOnly = false) { if (name.StartsWith(".", StringComparison.Ordinal)) { @@ -151,17 +151,18 @@ public R LoadPlaylist(string name, bool headOnly = false) } var fi = GetFileInfo(name); if (!fi.Exists) - return "Playlist not found"; + return new LocalStr(strings.error_playlist_not_found); using (var sr = new StreamReader(fi.Open(FileMode.Open, FileAccess.Read, FileShare.Read), FileEncoding)) { var plist = new Playlist(name); // Info: version: - // Info: owner: + // Info: owner: // Line: :: string line; + int version = 1; // read header while ((line = sr.ReadLine()) != null) @@ -177,16 +178,23 @@ public R LoadPlaylist(string name, bool headOnly = false) switch (key) { - case "version": // skip, not yet needed + case "version": + version = int.Parse(value); + if (version > 2) + return new LocalStr("The file version is too new and can't be read."); // LOC: TODO break; case "owner": - if (plist.CreatorDbId != null) - return "Invalid playlist file: duplicate userid"; - if (ulong.TryParse(value, out var userid)) - plist.CreatorDbId = userid; - else - return "Broken playlist header"; + if (plist.OwnerUid != null) + { + Log.Warn("Invalid playlist file: duplicate userid"); + return new LocalStr(strings.error_playlist_broken_file); + } + + if (version == 2) + { + plist.OwnerUid = value; + } break; } } @@ -197,34 +205,43 @@ public R LoadPlaylist(string name, bool headOnly = false) // read content while ((line = sr.ReadLine()) != null) { - var kvp = line.Split(new[] { ':' }, 3); - if (kvp.Length < 3) - { - Log.Warn("Erroneus playlist split count: {0}", line); - continue; - } - string kind = kvp[0]; - string optOwner = kvp[1]; - string content = kvp[2]; - - var meta = new MetaData(); - if (string.IsNullOrWhiteSpace(optOwner)) - meta.ResourceOwnerDbId = null; - else if (ulong.TryParse(optOwner, out var userid)) - meta.ResourceOwnerDbId = userid; - else - Log.Warn("Erroneus playlist meta data: {0}", line); - - switch (kind) + var kvp = line.Split(new[] { ':' }, 2); + if (kvp.Length < 2) continue; + + string key = kvp[0]; + string value = kvp[1]; + + switch (key) { case "rs": - var rsSplit = content.Split(new[] { ',' }, 3); - if (rsSplit.Length < 3) - goto default; - if (!string.IsNullOrWhiteSpace(rsSplit[0])) - plist.AddItem(new PlaylistItem(new AudioResource(Uri.UnescapeDataString(rsSplit[1]), Uri.UnescapeDataString(rsSplit[2]), rsSplit[0]), meta)); - else - goto default; + { + var rskvp = value.Split(new[] { ':' }, 2); + if (kvp.Length < 2) + { + Log.Warn("Erroneus playlist split count: {0}", line); + continue; + } + string optOwner = rskvp[0]; + string content = rskvp[1]; + + var rsSplit = content.Split(new[] { ',' }, 3); + if (rsSplit.Length < 3) + goto default; + if (!string.IsNullOrWhiteSpace(rsSplit[0])) + plist.AddItem(new PlaylistItem(new AudioResource(Uri.UnescapeDataString(rsSplit[1]), Uri.UnescapeDataString(rsSplit[2]), rsSplit[0]))); + else + goto default; + break; + } + + case "rsj": + var rsjdata = JsonConvert.DeserializeAnonymousType(value, new + { + type = string.Empty, + resid = string.Empty, + title = string.Empty + }); + plist.AddItem(new PlaylistItem(new AudioResource(rsjdata.resid, rsjdata.title, rsjdata.type))); break; case "id": @@ -241,95 +258,96 @@ public R LoadPlaylist(string name, bool headOnly = false) } } - public R SavePlaylist(Playlist plist) + private static R LoadChecked(R loadResult, string ownerUid) + { + if (!loadResult) + return new LocalStr($"{strings.error_playlist_broken_file} ({loadResult.Error.Str})"); + if (loadResult.Value.OwnerUid != null && loadResult.Value.OwnerUid != ownerUid) + return new LocalStr(strings.error_playlist_cannot_access_not_owned); + return loadResult; + } + + public E SavePlaylist(Playlist plist) { if (plist == null) throw new ArgumentNullException(nameof(plist)); - if (!IsNameValid(plist.Name)) - return "Invalid playlist name."; + var nameCheck = Util.IsSafeFileName(plist.Name); + if (!nameCheck) + return nameCheck.Error; - var di = new DirectoryInfo(data.PlaylistPath); + var di = new DirectoryInfo(config.Path); if (!di.Exists) - return "No playlist directory has been set up."; + return new LocalStr(strings.error_playlist_no_store_directory); var fi = GetFileInfo(plist.Name); if (fi.Exists) { - var tempList = LoadPlaylist(plist.Name, true); + var tempList = LoadChecked(LoadPlaylist(plist.Name, true), plist.OwnerUid); if (!tempList) - return "Existing playlist is corrupted, please use another name or repair the existing."; - if (tempList.Value.CreatorDbId.HasValue && tempList.Value.CreatorDbId != plist.CreatorDbId) - return "You cannot overwrite a playlist which you dont own."; + return tempList.OnlyError(); } using (var sw = new StreamWriter(fi.Open(FileMode.Create, FileAccess.Write, FileShare.Read), FileEncoding)) { - sw.WriteLine("version:1"); - - if (plist.CreatorDbId.HasValue) + sw.WriteLine("version:2"); + if (plist.OwnerUid != null) { sw.Write("owner:"); - sw.Write(plist.CreatorDbId.Value); + sw.Write(plist.OwnerUid); sw.WriteLine(); } sw.WriteLine(); - - foreach (var pli in plist.AsEnumerable()) + + using (var json = new JsonTextWriter(sw)) { - sw.Write("rs:"); - if (pli.Meta.ResourceOwnerDbId.HasValue - && (!plist.CreatorDbId.HasValue || pli.Meta.ResourceOwnerDbId.Value != plist.CreatorDbId.Value)) - sw.Write(pli.Meta.ResourceOwnerDbId.Value); - sw.Write(":"); - sw.Write(pli.Resource.AudioType); - sw.Write(","); - sw.Write(Uri.EscapeDataString(pli.Resource.ResourceId)); - sw.Write(","); - sw.Write(Uri.EscapeDataString(pli.Resource.ResourceTitle)); + json.Formatting = Formatting.None; - sw.WriteLine(); + foreach (var pli in plist.AsEnumerable()) + { + sw.Write("rsj:"); + json.WriteStartObject(); + json.WritePropertyName("type"); + json.WriteValue(pli.Resource.AudioType); + json.WritePropertyName("resid"); + json.WriteValue(pli.Resource.ResourceId); + if (pli.Resource.ResourceTitle != null) + { + json.WritePropertyName("title"); + json.WriteValue(pli.Resource.ResourceTitle); + } + json.WriteEndObject(); + json.Flush(); + sw.WriteLine(); + } } } - return R.OkR; + return R.Ok; } - private FileInfo GetFileInfo(string name) => new FileInfo(Path.Combine(data.PlaylistPath, name ?? string.Empty)); + private FileInfo GetFileInfo(string name) => new FileInfo(Path.Combine(config.Path, name ?? string.Empty)); - public R DeletePlaylist(string name, ulong requestingClientDbId, bool force = false) + public E DeletePlaylist(string name, string requestingClientUid, bool force = false) { var fi = GetFileInfo(name); if (!fi.Exists) - return "Playlist not found"; + return new LocalStr(strings.error_playlist_not_found); else if (!force) { - var tempList = LoadPlaylist(name, true); + var tempList = LoadChecked(LoadPlaylist(name, true), requestingClientUid); if (!tempList) - return "Existing playlist is corrupted, please use another name or repair the existing."; - if (tempList.Value.CreatorDbId.HasValue && tempList.Value.CreatorDbId != requestingClientDbId) - return "You cannot delete a playlist which you dont own."; + return tempList.OnlyError(); } try { fi.Delete(); - return R.OkR; + return R.Ok; } - catch (IOException) { return "File still in use"; } - catch (System.Security.SecurityException) { return "Missing rights to delete this file"; } - } - - public static R IsNameValid(string name) - { - if (string.IsNullOrEmpty(name)) - return "An empty playlist name is not valid"; - if (name.Length >= 64) - return "Length must be <64"; - if (!ValidPlistName.IsMatch(name)) - return "The new name is invalid please only use [a-zA-Z0-9_-]"; - return R.OkR; + catch (IOException) { return new LocalStr(strings.error_io_in_use); } + catch (System.Security.SecurityException) { return new LocalStr(strings.error_io_missing_permission); } } public static string CleanseName(string name) @@ -339,7 +357,7 @@ public static string CleanseName(string name) if (name.Length >= 64) name = name.Substring(0, 63); name = CleansePlaylistName.Replace(name, ""); - if (!IsNameValid(name)) + if (!Util.IsSafeFileName(name)) name = "playlist"; return name; } @@ -347,7 +365,7 @@ public static string CleanseName(string name) public IEnumerable GetAvailablePlaylists() => GetAvailablePlaylists(null); public IEnumerable GetAvailablePlaylists(string pattern) { - var di = new DirectoryInfo(data.PlaylistPath); + var di = new DirectoryInfo(config.Path); if (!di.Exists) return Array.Empty(); @@ -360,85 +378,17 @@ public IEnumerable GetAvailablePlaylists(string pattern) return fileEnu.Select(fi => fi.Name); } - private R GetSpecialPlaylist(string name) + private R GetSpecialPlaylist(string name) { if (!name.StartsWith(".", StringComparison.Ordinal)) - return "Not a reserved list type."; + throw new ArgumentException("Not a reserved list type.", nameof(name)); switch (name) { case ".queue": return freeList; case ".trash": return trashList; - default: return "Special list not found"; + default: return new LocalStr(strings.error_playlist_special_not_found); } } } - - public class PlaylistItem - { - public MetaData Meta { get; } - //one of these: - // playdata holds all needed information for playing + first possibility - // > can be a resource - public AudioResource Resource { get; } - - public string DisplayString => Resource.ResourceTitle ?? $"{Resource.AudioType}: {Resource.ResourceId}"; - - private PlaylistItem(MetaData meta) { Meta = meta ?? new MetaData(); } - public PlaylistItem(AudioResource resource, MetaData meta = null) : this(meta) { Resource = resource; } - } - - public class Playlist - { - // metainfo - public string Name { get; set; } - public ulong? CreatorDbId { get; set; } - // file behaviour: persistent playlist will be synced to a file - public bool FilePersistent { get; set; } - // playlist data - public int Count => resources.Count; - private readonly List resources; - - public Playlist(string name, ulong? creatorDbId = null) - { - Util.Init(out resources); - CreatorDbId = creatorDbId; - Name = name; - } - - public int AddItem(PlaylistItem item) - { - resources.Add(item); - return resources.Count - 1; - } - - public int InsertItem(PlaylistItem item, int index) - { - resources.Insert(index, item); - return index; - } - - public void AddRange(IEnumerable items) => resources.AddRange(items); - - public void RemoveItemAt(int i) - { - if (i < 0 || i >= resources.Count) - return; - resources.RemoveAt(i); - } - - public void Clear() => resources.Clear(); - - public IEnumerable AsEnumerable() => resources; - - public PlaylistItem GetResource(int index) => resources[index]; - } - -#pragma warning disable CS0649 - public class PlaylistManagerData : ConfigData - { - [Info("Path the playlist folder", "Playlists")] - public string PlaylistPath { get; set; } - } -#pragma warning restore CS0649 } diff --git a/TS3AudioBot/Plugins/Plugin.cs b/TS3AudioBot/Plugins/Plugin.cs index e3a8df04..3717e1ec 100644 --- a/TS3AudioBot/Plugins/Plugin.cs +++ b/TS3AudioBot/Plugins/Plugin.cs @@ -28,7 +28,6 @@ internal class Plugin : ICommandBag public CoreInjector CoreInjector { get; set; } public ResourceFactoryManager FactoryManager { get; set; } - public Rights.RightsManager RightsManager { get; set; } public CommandManager CommandManager { get; set; } private byte[] md5CacheSum; @@ -58,20 +57,24 @@ public string Name get { if (CheckStatus(null) == PluginStatus.Error) - return $"Error ({File.Name})"; + return $"{File.Name} (Error)"; + + var name = coreType?.Name ?? File.Name; switch (Type) { case PluginType.Factory: - return "Factory: " + (factoryObject?.FactoryFor ?? coreType?.Name ?? ""); + if (factoryObject?.FactoryFor != null) + return $"{factoryObject.FactoryFor}-factory"; + return $"{name} (Factory)"; case PluginType.BotPlugin: - return "BotPlugin: " + (coreType?.Name ?? ""); + return $"{name} (BotPlugin)"; case PluginType.CorePlugin: - return "CorePlugin: " + (coreType?.Name ?? ""); + return $"{name} (CorePlugin)"; case PluginType.Commands: - return "Commands: " + (coreType?.Name ?? ""); + return $"{name} (Commands)"; case PluginType.None: - return $"Unknown ({File.Name})"; + return $"{File.Name} (Unknown)"; default: throw Util.UnhandledDefault(Type); } @@ -94,8 +97,8 @@ public bool PersistentEnabled } } - public IEnumerable ExposedCommands { get; private set; } - public IEnumerable ExposedRights => ExposedCommands.Select(x => x.RequiredRight); + public IReadOnlyCollection BagCommands { get; private set; } + public IReadOnlyCollection AdditionalRights => Array.Empty(); public PluginStatus CheckStatus(Bot bot) { @@ -132,11 +135,23 @@ public PluginResponse Load() Unload(); PluginResponse result; - if (File.Extension == ".cs") + switch (File.Extension) + { + case ".cs": +#if NET46 result = PrepareSource(); - else if (File.Extension == ".dll" || File.Extension == ".exe") +#else + result = PluginResponse.NotSupported; +#endif + break; + + case ".dll": + case ".exe": result = PrepareBinary(); - else throw new InvalidProgramException(); + break; + default: + throw new InvalidProgramException(); + } status = result == PluginResponse.Ok ? PluginStatus.Ready : PluginStatus.Error; return result; @@ -185,6 +200,7 @@ private PluginResponse PrepareBinary() return InitlializeAssembly(assembly); } +#if NET46 private static CompilerParameters GenerateCompilerParameter() { var cp = new CompilerParameters(); @@ -234,6 +250,7 @@ private PluginResponse PrepareSource() } return InitlializeAssembly(result.CompiledAssembly); } +#endif private PluginResponse InitlializeAssembly(Assembly assembly) { @@ -318,17 +335,8 @@ public PluginResponse Start(Bot bot) goto case PluginStatus.Ready; case PluginStatus.Ready: - try - { - StartInternal(bot); - return PluginResponse.Ok; - } - catch (Exception ex) - { - Stop(bot); - Log.Warn("Plugin \"{0}\" failed to load: {1}", File.Name, ex); - return PluginResponse.UnknownError; - } + return StartInternal(bot) ? PluginResponse.Ok : PluginResponse.UnknownError; + case PluginStatus.Active: return PluginResponse.Ok; @@ -343,7 +351,7 @@ public PluginResponse Start(Bot bot) } } - private void StartInternal(Bot bot) + private bool StartInternal(Bot bot) { if (CheckStatus(bot) != PluginStatus.Ready) throw new InvalidOperationException("This plugin has not yet been prepared"); @@ -360,13 +368,13 @@ private void StartInternal(Bot bot) { Log.Error("This plugin needs to be activated on a bot instance."); status = PluginStatus.Error; - return; + return false; } if (pluginObjectList.ContainsKey(bot)) throw new InvalidOperationException("Plugin is already instantiated on this bot"); var pluginInstance = (IBotPlugin)Activator.CreateInstance(coreType); if (pluginObjectList.Count == 0) - StartRegisterCommands(pluginInstance, coreType); + RegisterCommands(pluginInstance, coreType); pluginObjectList.Add(bot, pluginInstance); if (!bot.Injector.TryInject(pluginInstance)) Log.Warn("Some dependencies are missing for this plugin"); @@ -375,7 +383,7 @@ private void StartInternal(Bot bot) case PluginType.CorePlugin: pluginObject = (ICorePlugin)Activator.CreateInstance(coreType); - StartRegisterCommands(pluginObject, coreType); + RegisterCommands(pluginObject, coreType); if (!CoreInjector.TryInject(pluginObject)) Log.Warn("Some dependencies are missing for this plugin"); pluginObject.Initialize(); @@ -387,37 +395,44 @@ private void StartInternal(Bot bot) break; case PluginType.Commands: - StartRegisterCommands(null, coreType); + RegisterCommands(null, coreType); break; default: throw Util.UnhandledDefault(Type); } } - catch (MissingMethodException mmex) + catch (Exception ex) { - Log.Error(mmex, "Plugins and Factories needs a parameterless constructor."); - status = PluginStatus.Error; - return; + if (ex is MissingMethodException) + Log.Error(ex, "Plugins and Factories needs a parameterless constructor."); + else + Log.Error(ex, "Plugin '{0}' failed to load: {1}.", Name, ex.Message); + Stop(bot); + if (Type != PluginType.BotPlugin) + status = PluginStatus.Error; + return false; } if (Type != PluginType.BotPlugin) status = PluginStatus.Active; + + return true; } - private void StartRegisterCommands(object obj, Type t) + private void RegisterCommands(object obj, Type t) { - var cmdBuildList = CommandManager.GetCommandMethods(obj, t); - ExposedCommands = CommandManager.GetBotCommands(cmdBuildList).ToList(); - RightsManager.RegisterRights(ExposedRights); + BagCommands = CommandManager.GetBotCommands(obj, t).ToArray(); CommandManager.RegisterCollection(this); } - private void StopUnregisterCommands() + private void UnregisterCommands() { - CommandManager.UnregisterCollection(this); - RightsManager.UnregisterRights(ExposedRights); - ExposedCommands = null; + if (BagCommands != null) + { + CommandManager.UnregisterCollection(this); + BagCommands = null; + } } /// @@ -429,9 +444,6 @@ public PluginResponse Stop(Bot bot) if (writeStatus) PersistentEnabled = false; - if (CheckStatus(bot) != PluginStatus.Active) - return PluginResponse.Ok; - switch (Type) { case PluginType.None: @@ -446,18 +458,19 @@ public PluginResponse Stop(Bot bot) } else { - if (!pluginObjectList.TryGetValue(bot, out var plugin)) - throw new InvalidOperationException("Plugin active but no instance found"); - plugin.Dispose(); - pluginObjectList.Remove(bot); + if (pluginObjectList.TryGetValue(bot, out var plugin)) + { + SaveDisposePlugin(plugin); + pluginObjectList.Remove(bot); + } } if (pluginObjectList.Count == 0) - StopUnregisterCommands(); + UnregisterCommands(); break; case PluginType.CorePlugin: - StopUnregisterCommands(); - pluginObject.Dispose(); + SaveDisposePlugin(pluginObject); + UnregisterCommands(); pluginObject = null; break; @@ -466,7 +479,7 @@ public PluginResponse Stop(Bot bot) break; case PluginType.Commands: - StopUnregisterCommands(); + UnregisterCommands(); break; default: @@ -478,6 +491,18 @@ public PluginResponse Stop(Bot bot) return PluginResponse.Ok; } + private void SaveDisposePlugin(ICorePlugin plugin) + { + try + { + plugin?.Dispose(); + } + catch (Exception ex) + { + Log.Warn(ex, "Plugin '{0}' threw an exception while disposing", Name); + } + } + public void Unload() { Stop(null); diff --git a/TS3AudioBot/Plugins/PluginManager.cs b/TS3AudioBot/Plugins/PluginManager.cs index b380b160..5addca81 100644 --- a/TS3AudioBot/Plugins/PluginManager.cs +++ b/TS3AudioBot/Plugins/PluginManager.cs @@ -9,6 +9,7 @@ namespace TS3AudioBot.Plugins { + using Config; using Helper; using System; using System.Collections.Generic; @@ -26,7 +27,6 @@ namespace TS3AudioBot.Plugins // - 0/1 Factory // - Facory name conflict // - [ Instantiate plugin (Depending on type) ] - // - Add commands to rights system // - Add commands to command manager // - Start config to system? @@ -36,15 +36,15 @@ public class PluginManager : IDisposable public Dependency.CoreInjector CoreInjector { get; set; } - private readonly PluginManagerData pluginManagerData; + private readonly ConfPlugins config; private readonly Dictionary plugins; private readonly HashSet usedIds; - public PluginManager(PluginManagerData pmd) + public PluginManager(ConfPlugins config) { Util.Init(out plugins); Util.Init(out usedIds); - pluginManagerData = pmd; + this.config = config; } private void CheckAndClearPlugins(Bot bot) @@ -56,7 +56,7 @@ private void CheckAndClearPlugins(Bot bot) /// Updates the plugin dictionary with new and changed plugins. private void CheckLocalPlugins(Bot bot) { - var dir = new DirectoryInfo(pluginManagerData.PluginPath); + var dir = new DirectoryInfo(config.Path); if (!dir.Exists) return; @@ -84,7 +84,7 @@ private void CheckLocalPlugins(Bot bot) if (IsIgnored(file)) continue; - plugin = new Plugin(file, GetFreeId(), pluginManagerData.WriteStatusFiles); + plugin = new Plugin(file, GetFreeId(), config.WriteStatusFiles); if (plugin.Load() == PluginResponse.Disabled) { @@ -225,13 +225,4 @@ public PluginStatusInfo(int id, string name, PluginStatus status, PluginType typ Type = type; } } - - public class PluginManagerData : ConfigData - { - [Info("The absolute or relative path to the plugins folder", "Plugins")] - public string PluginPath { get; set; } - - [Info("Write to .status files to store a plugin enable status persistently and restart them on launch.", "false")] - public bool WriteStatusFiles { get; set; } - } } diff --git a/TS3AudioBot/Plugins/PluginResponse.cs b/TS3AudioBot/Plugins/PluginResponse.cs index dc9e91dc..fee41898 100644 --- a/TS3AudioBot/Plugins/PluginResponse.cs +++ b/TS3AudioBot/Plugins/PluginResponse.cs @@ -22,5 +22,6 @@ public enum PluginResponse PluginNotFound, CompileError, Disabled, + NotSupported, } } diff --git a/TS3AudioBot/Properties.cs b/TS3AudioBot/Properties.cs new file mode 100644 index 00000000..1b68d558 --- /dev/null +++ b/TS3AudioBot/Properties.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("TS3ABotUnitTests")] diff --git a/TS3AudioBot/Properties/AssemblyInfo.cs b/TS3AudioBot/Properties/AssemblyInfo.cs deleted file mode 100644 index e57e962e..00000000 --- a/TS3AudioBot/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TS3AudioBot")] -[assembly: AssemblyDescription("Advanced Musicbot for Teamspeak 3")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TS3AudioBot")] -[assembly: AssemblyCopyright("Copyright © Splamy 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("cfce5d11-b274-4e76-be31-83d7935328e9")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] diff --git a/TS3AudioBot/Helper/AudioTags/AudioTagReader.cs b/TS3AudioBot/ResourceFactories/AudioTags/AudioTagReader.cs similarity index 99% rename from TS3AudioBot/Helper/AudioTags/AudioTagReader.cs rename to TS3AudioBot/ResourceFactories/AudioTags/AudioTagReader.cs index 2e4fce3c..abc1d726 100644 --- a/TS3AudioBot/Helper/AudioTags/AudioTagReader.cs +++ b/TS3AudioBot/ResourceFactories/AudioTags/AudioTagReader.cs @@ -7,7 +7,7 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -namespace TS3AudioBot.Helper.AudioTags +namespace TS3AudioBot.ResourceFactories.AudioTags { using System; using System.Collections.Generic; diff --git a/TS3AudioBot/Helper/AudioTags/BinaryReaderBigEndianExtensions.cs b/TS3AudioBot/ResourceFactories/AudioTags/BinaryReaderBigEndianExtensions.cs similarity index 98% rename from TS3AudioBot/Helper/AudioTags/BinaryReaderBigEndianExtensions.cs rename to TS3AudioBot/ResourceFactories/AudioTags/BinaryReaderBigEndianExtensions.cs index 9376e32e..67faf45a 100644 --- a/TS3AudioBot/Helper/AudioTags/BinaryReaderBigEndianExtensions.cs +++ b/TS3AudioBot/ResourceFactories/AudioTags/BinaryReaderBigEndianExtensions.cs @@ -7,7 +7,7 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -namespace TS3AudioBot.Helper.AudioTags +namespace TS3AudioBot.ResourceFactories.AudioTags { using System.IO; using System.Runtime.InteropServices; diff --git a/TS3AudioBot/ResourceFactories/AudioTags/M3uReader.cs b/TS3AudioBot/ResourceFactories/AudioTags/M3uReader.cs new file mode 100644 index 00000000..1732c522 --- /dev/null +++ b/TS3AudioBot/ResourceFactories/AudioTags/M3uReader.cs @@ -0,0 +1,125 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.ResourceFactories.AudioTags +{ + using Playlists; + using ResourceFactories; + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + + internal static class M3uReader + { + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + private const int MaxLineLength = 4096; + private static readonly byte[] ExtM3uLine = Encoding.UTF8.GetBytes("#EXTM3U"); + private static readonly byte[] ExtInfLine = Encoding.UTF8.GetBytes("#EXTINF"); + + public static R, string> TryGetData(Stream stream) + { + int read = 1; + int bufferLen = 0; + var buffer = new byte[MaxLineLength]; + var data = new List(); + string trackTitle = null; + //bool extm3u = false; + + try + { + while (true) + { + if (read > 0) + { + read = stream.Read(buffer, bufferLen, MaxLineLength - bufferLen); + bufferLen += read; + } + + // find linebreak index + int index = Array.IndexOf(buffer, (byte)'\n', 0, bufferLen); + int lb = 1; + if (index == -1) + index = Array.IndexOf(buffer, (byte)'\r', 0, bufferLen); + else if (index > 0 && buffer[index - 1] == (byte)'\r') + { + index--; + lb = 2; + } + + ReadOnlySpan line; + bool atEnd = index == -1; + if (atEnd) + { + if (bufferLen == MaxLineLength) + return "Max read buffer exceeded"; + line = buffer.AsSpan(0, bufferLen); + bufferLen = 0; + } + else + { + line = buffer.AsSpan(0, index); + } + + if (!line.IsEmpty) + { + if (line[0] == (byte)'#') + { + if (line.StartsWith(ExtInfLine)) + { + var dataSlice = line.Slice(8); + var trackInfo = dataSlice.IndexOf((byte)','); + if (trackInfo >= 0) + trackTitle = AsString(dataSlice.Slice(trackInfo + 1)); + } + else if (line.StartsWith(ExtM3uLine)) + { + //extm3u = true; ??? + } + // else: unsupported m3u tag + } + else + { + var lineStr = AsString(line); + if (Uri.TryCreate(lineStr, UriKind.RelativeOrAbsolute, out _)) + { + data.Add(new PlaylistItem(new AudioResource(lineStr, trackTitle ?? lineStr, "media"))); + trackTitle = null; + } + else + { + Log.Debug("Skipping invalid playlist entry ({0})", lineStr); + } + } + } + + if (!atEnd) + { + index += lb; + Array.Copy(buffer, index, buffer, 0, MaxLineLength - index); + bufferLen -= index; + } + + if (atEnd || bufferLen <= 0) + { + if (bufferLen < 0) + return "Unexpected buffer underfill"; + return data; + } + } + } + catch { return "Unexpected m3u parsing error"; } + } + + private static string AsString(ReadOnlySpan data) + { + return Encoding.UTF8.GetString(data.ToArray()); + } + } +} diff --git a/TS3AudioBot/ResourceFactories/BandcampFactory.cs b/TS3AudioBot/ResourceFactories/BandcampFactory.cs index a34036a5..715926c0 100644 --- a/TS3AudioBot/ResourceFactories/BandcampFactory.cs +++ b/TS3AudioBot/ResourceFactories/BandcampFactory.cs @@ -10,10 +10,11 @@ namespace TS3AudioBot.ResourceFactories { using Helper; + using Localization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; - using System.Drawing; + using System.IO; using System.Text.RegularExpressions; public class BandcampFactory : IResourceFactory, IThumbnailFactory @@ -29,41 +30,41 @@ public class BandcampFactory : IResourceFactory, IThumbnailFactory public MatchCertainty MatchResource(string uri) => BandcampUrlRegex.IsMatch(uri).ToMatchCertainty(); - public R GetResource(string url) + public R GetResource(string url) { var match = BandcampUrlRegex.Match(url); if (!match.Success) - return "Not a valid bandcamp link. Please pass the full link"; + return new LocalStr(strings.error_media_invalid_uri); var artistName = match.Groups[1].Value; var trackName = match.Groups[2].Value; var uri = new Uri($"https://{artistName}.bandcamp.com/track/{trackName}"); if (!WebWrapper.DownloadString(out string webSite, uri)) - return "Could not connect to bandcamp"; + return new LocalStr(strings.error_net_no_connection); match = TrackMainJsonRegex.Match(webSite); if (!match.Success) - return "Could not extract track data"; + return new LocalStr(strings.error_media_internal_missing + " (TrackMainJsonRegex)"); JToken jobj; try { jobj = JToken.Parse(match.Groups[1].Value); } - catch (JsonReaderException) { return "Could not parse tack data"; } + catch (JsonReaderException) { return new LocalStr(strings.error_media_internal_missing + " (TrackMainJsonRegex.JToken)"); } if (!(jobj is JArray jarr) || jarr.Count == 0) - return "No tracks"; + return new LocalStr(strings.error_media_no_stream_extracted); var firstTrack = jarr[0]; var id = firstTrack.TryCast("track_id").OkOr(null); var title = firstTrack.TryCast("title").OkOr(null); var trackObj = firstTrack["file"]?.TryCast("mp3-128").OkOr(""); if (id == null || title == null || trackObj == null) - return "No track"; + return new LocalStr(strings.error_media_no_stream_extracted); return new BandcampPlayResource(trackObj, new AudioResource(id, title, FactoryFor), GetTrackArtId(webSite)); } - public R GetResourceById(AudioResource resource) + public R GetResourceById(AudioResource resource) { var result = DownloadEmbeddedSite(resource.ResourceId); if (!result.Ok) return result.Error; @@ -79,7 +80,7 @@ public R GetResourceById(AudioResource resource) var match = TrackLinkRegex.Match(webSite); if (!match.Success) - return "Could not extract track link"; + return new LocalStr(strings.error_media_internal_missing + " (TrackLinkRegex)"); return new BandcampPlayResource(match.Groups[1].Value, resource, GetTrackArtId(webSite)); } @@ -87,24 +88,26 @@ public R GetResourceById(AudioResource resource) public string RestoreLink(string id) { var result = DownloadEmbeddedSite(id); - if (!result.Ok) return result.Error; - var webSite = result.Value; - - var match = TrackRestoreRegex.Match(webSite); - return match.Success - ? match.Groups[1].Value - : "https://bandcamp.com/EmbeddedPlayer/v=2/track={id}"; // backup when something's wrong with the website + if (result.Ok) + { + var webSite = result.Value; + var match = TrackRestoreRegex.Match(webSite); + if (match.Success) + return match.Groups[1].Value; + } + // backup when something's wrong with the website + return "https://bandcamp.com/EmbeddedPlayer/v=2/track={id}"; } - private static R DownloadEmbeddedSite(string id) + private static R DownloadEmbeddedSite(string id) { var uri = new Uri($"https://bandcamp.com/EmbeddedPlayer/v=2/track={id}"); if (!WebWrapper.DownloadString(out string webSite, uri)) - return R.Err("Could not connect to bandcamp"); - return R.OkR(webSite); + return new LocalStr(strings.error_net_no_connection); + return webSite; } - public R GetThumbnail(PlayResource playResource) + public R GetThumbnail(PlayResource playResource) { string artId; if (playResource is BandcampPlayResource bandcampPlayResource) @@ -121,7 +124,7 @@ public R GetThumbnail(PlayResource playResource) } if (string.IsNullOrEmpty(artId)) - return "No image found"; + return new LocalStr(strings.error_media_image_not_found); // 1 : 1600px/1600px // 2 : 350px/ 350px @@ -141,18 +144,7 @@ public R GetThumbnail(PlayResource playResource) // 16 : 700px/ 700px // 42 : 50px/ 50px / supporter var imgurl = new Uri($"https://f4.bcbits.com/img/a{artId}_4.jpg"); - Image img = null; - var resresult = WebWrapper.GetResponse(imgurl, (webresp) => - { - using (var stream = webresp.GetResponseStream()) - { - if (stream != null) - img = Image.FromStream(stream); - } - }); - if (resresult != ValidateCode.Ok) - return "Error while reading image"; - return img; + return WebWrapper.GetResponseUnsafe(imgurl); } private static string GetTrackArtId(string site) diff --git a/TS3AudioBot/ResourceFactories/IPlaylistFactory.cs b/TS3AudioBot/ResourceFactories/IPlaylistFactory.cs index ac28fc9d..7197d739 100644 --- a/TS3AudioBot/ResourceFactories/IPlaylistFactory.cs +++ b/TS3AudioBot/ResourceFactories/IPlaylistFactory.cs @@ -9,10 +9,14 @@ namespace TS3AudioBot.ResourceFactories { + using Localization; + using Playlists; + using System; + public interface IPlaylistFactory : IFactory { MatchCertainty MatchPlaylist(string uri); - R GetPlaylist(string url); + R GetPlaylist(string url); } } diff --git a/TS3AudioBot/ResourceFactories/IResourceFactory.cs b/TS3AudioBot/ResourceFactories/IResourceFactory.cs index 47e35a7d..5ea75fd9 100644 --- a/TS3AudioBot/ResourceFactories/IResourceFactory.cs +++ b/TS3AudioBot/ResourceFactories/IResourceFactory.cs @@ -9,6 +9,9 @@ namespace TS3AudioBot.ResourceFactories { + using Localization; + using System; + public interface IResourceFactory : IFactory { /// Check method to ask if a factory can load the given link. @@ -16,13 +19,13 @@ public interface IResourceFactory : IFactory /// True if the factory thinks it can parse it, false otherwise. MatchCertainty MatchResource(string uri); /// The factory will try to parse the uri and create a playable resource from it. - /// Any link or something similar a user can obtain to pass it here. + /// Any link or something similar a user can obtain to pass it here. /// The playable resource if successful, or an error message otherwise - R GetResource(string url); + R GetResource(string uri); /// The factory will try to parse the unique identifier of its scope of responsibility and create a playable resource from it. /// A resource containing the unique id for a song this factory is responsible for. /// The playable resource if successful, or an error message otherwise - R GetResourceById(AudioResource resource); + R GetResourceById(AudioResource resource); /// Gets a link to the original site/location. This may differ from the link the resource was orininally created. /// The unique id for a song this factory is responsible for. /// The (close to) original link if successful, null otherwise. diff --git a/TS3AudioBot/ResourceFactories/IThumbnailFactory.cs b/TS3AudioBot/ResourceFactories/IThumbnailFactory.cs index c14c92b7..91a335b7 100644 --- a/TS3AudioBot/ResourceFactories/IThumbnailFactory.cs +++ b/TS3AudioBot/ResourceFactories/IThumbnailFactory.cs @@ -9,10 +9,12 @@ namespace TS3AudioBot.ResourceFactories { - using System.Drawing; + using Localization; + using System; + using System.IO; public interface IThumbnailFactory : IFactory { - R GetThumbnail(PlayResource playResource); + R GetThumbnail(PlayResource playResource); } } diff --git a/TS3AudioBot/ResourceFactories/MatchCertainty.cs b/TS3AudioBot/ResourceFactories/MatchCertainty.cs index 05d50999..efa0e21b 100644 --- a/TS3AudioBot/ResourceFactories/MatchCertainty.cs +++ b/TS3AudioBot/ResourceFactories/MatchCertainty.cs @@ -11,7 +11,7 @@ namespace TS3AudioBot.ResourceFactories { public enum MatchCertainty { - /// "Never" sais this factory cannot use this link. + /// "Never" denotes that this factory cannot use this link. Never = 0, /// "OnlyIfLast" Only gets selected if no higher match was found. OnlyIfLast, diff --git a/TS3AudioBot/ResourceFactories/MediaFactory.cs b/TS3AudioBot/ResourceFactories/MediaFactory.cs index 1f01b9d4..9698ce71 100644 --- a/TS3AudioBot/ResourceFactories/MediaFactory.cs +++ b/TS3AudioBot/ResourceFactories/MediaFactory.cs @@ -9,21 +9,24 @@ namespace TS3AudioBot.ResourceFactories { + using AudioTags; + using Config; using Helper; - using Helper.AudioTags; + using Localization; + using Playlists; using System; using System.Collections.Generic; - using System.Drawing; using System.IO; using System.Linq; public sealed class MediaFactory : IResourceFactory, IPlaylistFactory, IThumbnailFactory { - private readonly MediaFactoryData mediaFactoryData; + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + private readonly ConfPath config; - public MediaFactory(MediaFactoryData mfd) + public MediaFactory(ConfPath config) { - mediaFactoryData = mfd; + this.config = config; } public string FactoryFor => "media"; @@ -38,12 +41,12 @@ public MatchCertainty MatchPlaylist(string uri) => File.Exists(uri) ? MatchCertainty.Maybe : MatchCertainty.OnlyIfLast; - public R GetResource(string uri) + public R GetResource(string uri) { return GetResourceById(new AudioResource(uri, null, FactoryFor)); } - public R GetResourceById(AudioResource resource) + public R GetResourceById(AudioResource resource) { var result = ValidateUri(resource.ResourceId); if (!result) @@ -62,7 +65,7 @@ public R GetResourceById(AudioResource resource) public string RestoreLink(string id) => id; - private R ValidateUri(string uri) + private R ValidateUri(string uri) { if (Uri.TryCreate(uri, UriKind.Absolute, out Uri uriResult)) { @@ -71,9 +74,9 @@ private R ValidateUri(string uri) || uriResult.Scheme == Uri.UriSchemeFtp) return ValidateWeb(uriResult); if (uriResult.Scheme == Uri.UriSchemeFile) - return ValidateFile(uriResult.OriginalString); + return ValidateFile(uri); - return R.Err(RResultCode.MediaUnknownUri.ToString()); + return new LocalStr(strings.error_media_invalid_uri); } else { @@ -88,7 +91,7 @@ private static HeaderData GetStreamHeaderData(Stream stream) return headerData; } - private static R ValidateWeb(Uri link) + private static R ValidateWeb(Uri link) { var result = WebWrapper.GetResponseUnsafe(link); if (!result.Ok) @@ -101,55 +104,53 @@ private static R ValidateWeb(Uri link) } } - private R ValidateFile(string path) + private R ValidateFile(string path) { var foundPath = FindFile(path); + Log.Trace("FindFile check result: '{0}'", foundPath); if (foundPath == null) - return R.Err(RResultCode.MediaFileNotFound.ToString()); + return new LocalStr(strings.error_media_file_not_found); try { using (var stream = File.Open(foundPath.LocalPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var headerData = GetStreamHeaderData(stream); - return R.OkR(new ResData(foundPath.LocalPath, headerData.Title) { Image = headerData.Picture }); + return new ResData(foundPath.LocalPath, headerData.Title) { Image = headerData.Picture }; } } - // TODO: correct errors - catch (PathTooLongException) { return R.Err(RResultCode.AccessDenied.ToString()); } - catch (DirectoryNotFoundException) { return R.Err(RResultCode.MediaFileNotFound.ToString()); } - catch (FileNotFoundException) { return R.Err(RResultCode.MediaFileNotFound.ToString()); } - catch (IOException) { return R.Err(RResultCode.AccessDenied.ToString()); } - catch (UnauthorizedAccessException) { return R.Err(RResultCode.AccessDenied.ToString()); } - catch (NotSupportedException) { return R.Err(RResultCode.AccessDenied.ToString()); } + catch (UnauthorizedAccessException) { return new LocalStr(strings.error_io_missing_permission); } + catch (Exception ex) + { + Log.Warn("Failed to load song \"{0}\", because {1}", path, ex.Message); + return new LocalStr(strings.error_io_unknown_error); + } } private Uri FindFile(string path) { - if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri)) - return null; - - if (uri.IsAbsoluteUri) - return File.Exists(path) || Directory.Exists(path) ? uri : null; + Log.Trace("Finding media path: '{0}'", path); try { var fullPath = Path.GetFullPath(path); if (File.Exists(fullPath) || Directory.Exists(fullPath)) return new Uri(fullPath, UriKind.Absolute); - fullPath = Path.GetFullPath(Path.Combine(mediaFactoryData.DefaultPath, path)); + fullPath = Path.GetFullPath(Path.Combine(config.Path.Value, path)); if (File.Exists(fullPath) || Directory.Exists(fullPath)) return new Uri(fullPath, UriKind.Absolute); } catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException || ex is System.Security.SecurityException) - { } + { + Log.Trace(ex, "Couldn't load resource"); + } return null; } public void Dispose() { } - public R GetPlaylist(string url) + public R GetPlaylist(string url) { var foundUri = FindFile(url); @@ -169,12 +170,14 @@ select result.Value into val return plist; } - // TODO: correct errors - catch (PathTooLongException) { return R.Err(RResultCode.AccessDenied.ToString()); } - catch (ArgumentException) { return R.Err(RResultCode.MediaFileNotFound.ToString()); } + catch (Exception ex) + { + Log.Warn("Failed to load playlist \"{0}\", because {1}", url, ex.Message); + return new LocalStr(strings.error_io_unknown_error); + } } - var m3uResult = R>.Err(string.Empty); + var m3uResult = R, string>.Err(string.Empty); if (foundUri != null && File.Exists(foundUri.OriginalString)) { using (var stream = File.OpenRead(foundUri.OriginalString)) @@ -197,10 +200,10 @@ select result.Value into val return m3uList; } - return R.Err(RResultCode.MediaFileNotFound.ToString()); + return new LocalStr(strings.error_media_file_not_found); } - private static R GetStreamFromUriUnsafe(Uri uri) + private static R GetStreamFromUriUnsafe(Uri uri) { if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps @@ -209,10 +212,10 @@ private static R GetStreamFromUriUnsafe(Uri uri) if (uri.Scheme == Uri.UriSchemeFile) return File.OpenRead(uri.LocalPath); - return RResultCode.MediaUnknownUri.ToString(); + return new LocalStr(strings.error_media_invalid_uri); } - public R GetThumbnail(PlayResource playResource) + public R GetThumbnail(PlayResource playResource) { byte[] rawImgData; @@ -234,19 +237,9 @@ public R GetThumbnail(PlayResource playResource) } if (rawImgData == null) - return "No image found"; + return new LocalStr(strings.error_media_image_not_found); - using (var memStream = new MemoryStream(rawImgData)) - { - try - { - return new Bitmap(memStream); - } - catch (ArgumentException) - { - return "Inavlid image data"; - } - } + return new MemoryStream(rawImgData); } } @@ -272,10 +265,4 @@ public MediaPlayResource(string uri, AudioResource baseData, byte[] image) : bas Image = image; } } - - public class MediaFactoryData : ConfigData - { - [Info("The default path to look for local resources.", "")] - public string DefaultPath { get; set; } - } } diff --git a/TS3AudioBot/ResourceFactories/ResourceFactoryManager.cs b/TS3AudioBot/ResourceFactories/ResourceFactoryManager.cs index 527c01f2..96014e17 100644 --- a/TS3AudioBot/ResourceFactories/ResourceFactoryManager.cs +++ b/TS3AudioBot/ResourceFactories/ResourceFactoryManager.cs @@ -10,13 +10,18 @@ namespace TS3AudioBot.ResourceFactories { using CommandSystem; + using Config; using Helper; + using Localization; + using Playlists; using Sessions; using System; using System.Collections.Generic; - using System.Drawing; + using System.IO; using System.Linq; using System.Reflection; + using System.Text; + using System.Diagnostics; public sealed class ResourceFactoryManager : IDisposable { @@ -24,28 +29,25 @@ public sealed class ResourceFactoryManager : IDisposable private const string CmdResPrepath = "from "; private const string CmdListPrepath = "list from "; - public ConfigFile Config { get; set; } public CommandManager CommandManager { get; set; } - public Rights.RightsManager RightsManager { get; set; } private readonly Dictionary allFacories; private readonly List listFactories; private readonly List resFactories; + private readonly ConfFactories config; - public ResourceFactoryManager() + public ResourceFactoryManager(ConfFactories config) { Util.Init(out allFacories); Util.Init(out resFactories); Util.Init(out listFactories); + this.config = config; } public void Initialize() { - var yfd = Config.GetDataStruct("YoutubeFactory", true); - var mfd = Config.GetDataStruct("MediaFactory", true); - - AddFactory(new MediaFactory(mfd)); - AddFactory(new YoutubeFactory(yfd)); + AddFactory(new MediaFactory(config.Media)); + AddFactory(new YoutubeFactory()); AddFactory(new SoundcloudFactory()); AddFactory(new TwitchFactory()); AddFactory(new BandcampFactory()); @@ -97,19 +99,21 @@ private static IEnumerable FilterUsable(IEnumerable<(T, MatchCertainty)> e /// An with at least /// and set. /// The playable resource if successful, or an error message otherwise. - public R Load(AudioResource resource) + public R Load(AudioResource resource) { if (resource == null) throw new ArgumentNullException(nameof(resource)); var factory = GetFactoryByType(resource.AudioType); if (factory == null) - return $"Could not load (No registered factory for \"{resource.AudioType}\" found)"; + return CouldNotLoad(string.Format(strings.error_resfac_no_registered_factory, resource.AudioType)); + var sw = Stopwatch.StartNew(); var result = factory.GetResourceById(resource); - if (!result) - return $"Could not load ({result.Error})"; - return result; + if (!result.Ok) + return CouldNotLoad(result.Error.Str); + Log.Debug("Took {0}ms to resolve resource.", sw.ElapsedMilliseconds); + return result.Value; } /// Generates a new which can be played. @@ -120,7 +124,7 @@ public R Load(AudioResource resource) /// The associated resource type string to a factory. /// Leave null to let it detect automatically. /// The playable resource if successful, or an error message otherwise. - public R Load(string message, string audioType = null) + public R Load(string message, string audioType = null) { if (string.IsNullOrWhiteSpace(message)) throw new ArgumentNullException(nameof(message)); @@ -131,31 +135,33 @@ public R Load(string message, string audioType = null) { var factory = GetFactoryByType(audioType); if (factory == null) - return $"Could not load (No registered factory for \"{audioType}\" found)"; + return CouldNotLoad(string.Format(strings.error_resfac_no_registered_factory, audioType)); var result = factory.GetResource(netlinkurl); - if (!result) - return $"Could not load ({result.Error})"; + if (!result.Ok) + return CouldNotLoad(result.Error.Str); return result; } + var sw = Stopwatch.StartNew(); var factories = FilterUsable(GetResFactoryByLink(netlinkurl)); - List<(string, string)> errors = null; + List<(string, LocalStr)> errors = null; foreach (var factory in factories) { var result = factory.GetResource(netlinkurl); - Log.Trace("ResFactory {0} tried, result: {1}", factory.FactoryFor, result.Ok ? "Ok" : result.Error); + Log.Trace("ResFactory {0} tried, result: {1}", factory.FactoryFor, result.Ok ? "Ok" : result.Error.Str); if (result) return result; - (errors = errors ?? new List<(string, string)>()).Add((factory.FactoryFor, result.Error)); + (errors = errors ?? new List<(string, LocalStr)>()).Add((factory.FactoryFor, result.Error)); } + Log.Debug("Took {0}ms to resolve resource.", sw.ElapsedMilliseconds); return ToErrorString(errors); } - public R LoadPlaylistFrom(string message) => LoadPlaylistFrom(message, null); + public R LoadPlaylistFrom(string message) => LoadPlaylistFrom(message, null); - private R LoadPlaylistFrom(string message, IPlaylistFactory listFactory) + private R LoadPlaylistFrom(string message, IPlaylistFactory listFactory) { if (string.IsNullOrWhiteSpace(message)) throw new ArgumentNullException(nameof(message)); @@ -166,14 +172,14 @@ private R LoadPlaylistFrom(string message, IPlaylistFactory listFactor return listFactory.GetPlaylist(netlinkurl); var factories = FilterUsable(GetListFactoryByLink(netlinkurl)); - List<(string, string)> errors = null; + List<(string, LocalStr)> errors = null; foreach (var factory in factories) { var result = factory.GetPlaylist(netlinkurl); - Log.Trace("ListFactory {0} tried, result: {1}", factory.FactoryFor, result.Ok ? "Ok" : result.Error); + Log.Trace("ListFactory {0} tried, result: {1}", factory.FactoryFor, result.Ok ? "Ok" : result.Error.Str); if (result) return result; - (errors = errors ?? new List<(string, string)>()).Add((factory.FactoryFor, result.Error)); + (errors = errors ?? new List<(string, LocalStr)>()).Add((factory.FactoryFor, result.Error)); } return ToErrorString(errors); @@ -185,13 +191,16 @@ public string RestoreLink(AudioResource res) return factory.RestoreLink(res.ResourceId); } - public R GetThumbnail(PlayResource playResource) + public R GetThumbnail(PlayResource playResource) { var factory = GetFactoryByType(playResource.BaseData.AudioType); if (factory == null) - return "No thumbnail factory found"; + return new LocalStr(string.Format(strings.error_resfac_no_registered_factory, playResource.BaseData.AudioType)); - return factory.GetThumbnail(playResource); + var sw = Stopwatch.StartNew(); + var result = factory.GetThumbnail(playResource); + Log.Debug("Took {0}ms to load thumbnail.", sw.ElapsedMilliseconds); + return result; } public void AddFactory(IFactory factory) @@ -216,7 +225,6 @@ public void AddFactory(IFactory factory) var factoryInfo = new FactoryData(factory, commands.ToArray()); allFacories.Add(factory.FactoryFor, factoryInfo); CommandManager.RegisterCollection(factoryInfo); - RightsManager.RegisterRights(factoryInfo.ExposedRights); } public void RemoveFactory(IFactory factory) @@ -232,16 +240,24 @@ public void RemoveFactory(IFactory factory) listFactories.Remove(listFactory); CommandManager.UnregisterCollection(factoryInfo); - RightsManager.UnregisterRights(factoryInfo.ExposedRights); } - private static string ToErrorString(IReadOnlyList<(string fact, string err)> errors) + private static LocalStr CouldNotLoad(string reason = null) { - if (errors.Count == 0) - return "Could not load (The bot is stupid)"; + if (reason == null) + return new LocalStr(strings.error_resfac_could_not_load); + var strb = new StringBuilder(strings.error_resfac_could_not_load); + strb.Append(" (").Append(reason).Append(")"); + return new LocalStr(strb.ToString()); + } + + private static LocalStr ToErrorString(List<(string fact, LocalStr err)> errors) + { + if (errors == null || errors.Count == 0) + throw new ArgumentException("No errors provided", nameof(errors)); if (errors.Count == 1) - return $"Could not load ({errors[0].fact}: {errors[0].err})"; - return "Could not load (Considered multiple factories but all failed. Try selecting one manually with !from )"; + return CouldNotLoad($"{errors[0].fact}: {errors[0].err}"); + return CouldNotLoad(strings.error_resfac_multiple_factories_failed); } public void Dispose() @@ -251,20 +267,20 @@ public void Dispose() allFacories.Clear(); } - private sealed class FactoryData : ICommandBag { private readonly FactoryCommand[] registeredCommands; + public IFactory Factory { get; } + public IReadOnlyCollection BagCommands { get; } + public IReadOnlyCollection AdditionalRights => Array.Empty(); + public FactoryData(IFactory factory, FactoryCommand[] commands) { Factory = factory; registeredCommands = commands; + BagCommands = registeredCommands.Select(x => x.Command).ToArray(); } - - public IFactory Factory { get; } - public IEnumerable ExposedCommands => registeredCommands.Select(x => x.Command); - public IEnumerable ExposedRights => ExposedCommands.Select(x => x.RequiredRight); } private abstract class FactoryCommand @@ -287,9 +303,9 @@ public PlayCommand(string audioType, string cmdPath) Command = new BotCommand(builder); } - public string PropagiatePlay(PlayManager playManager, InvokerData invoker, string parameter) + public void PropagiatePlay(PlayManager playManager, InvokerData invoker, string url) { - return playManager.Play(invoker, parameter, audioType); + playManager.Play(invoker, url, audioType).UnwrapThrow(); } } @@ -308,16 +324,12 @@ public PlayListCommand(IPlaylistFactory factory, string cmdPath) Command = new BotCommand(builder); } - public string PropagiateLoad(ResourceFactoryManager factoryManager, UserSession session, InvokerData invoker, string parameter) + public void PropagiateLoad(ResourceFactoryManager factoryManager, UserSession session, InvokerData invoker, string url) { - var result = factoryManager.LoadPlaylistFrom(parameter, factory); - - if (!result) - return result; + var playlist = factoryManager.LoadPlaylistFrom(url, factory).UnwrapThrow(); - result.Value.CreatorDbId = invoker.DatabaseId; - session.Set(result.Value); - return "Ok"; + playlist.OwnerUid = invoker.ClientUid; + session.Set(playlist); } } } diff --git a/TS3AudioBot/ResourceFactories/SoundcloudFactory.cs b/TS3AudioBot/ResourceFactories/SoundcloudFactory.cs index 07c74b1a..519c8dcd 100644 --- a/TS3AudioBot/ResourceFactories/SoundcloudFactory.cs +++ b/TS3AudioBot/ResourceFactories/SoundcloudFactory.cs @@ -10,11 +10,13 @@ namespace TS3AudioBot.ResourceFactories { using Helper; + using Localization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; + using Playlists; using System; - using System.Drawing; using System.Globalization; + using System.IO; using System.Text.RegularExpressions; public sealed class SoundcloudFactory : IResourceFactory, IPlaylistFactory, IThumbnailFactory @@ -29,34 +31,34 @@ public sealed class SoundcloudFactory : IResourceFactory, IPlaylistFactory, IThu public MatchCertainty MatchPlaylist(string uri) => MatchResource(uri); - public R GetResource(string link) + public R GetResource(string uri) { - var uri = new Uri($"https://api.soundcloud.com/resolve.json?url={Uri.EscapeUriString(link)}&client_id={SoundcloudClientId}"); - if (!WebWrapper.DownloadString(out string jsonResponse, uri)) + var uriObj = new Uri($"https://api.soundcloud.com/resolve.json?url={Uri.EscapeUriString(uri)}&client_id={SoundcloudClientId}"); + if (!WebWrapper.DownloadString(out string jsonResponse, uriObj)) { - if (!SoundcloudLink.IsMatch(link)) - return "Not a valid soundcloud link. Please pass the full link"; - return YoutubeDlWrapped(link); + if (!SoundcloudLink.IsMatch(uri)) + return new LocalStr(strings.error_media_invalid_uri); + return YoutubeDlWrapped(uri); } var parsedDict = ParseJson(jsonResponse); var resource = ParseJObjectToResource(parsedDict); if (resource == null) - return "Empty or missing response parts (parsedDict)"; + return new LocalStr(strings.error_media_internal_missing + " (parsedDict)"); return GetResourceById(resource, false); } - public R GetResourceById(AudioResource resource) => GetResourceById(resource, true); + public R GetResourceById(AudioResource resource) => GetResourceById(resource, true); - private R GetResourceById(AudioResource resource, bool allowNullName) + private R GetResourceById(AudioResource resource, bool allowNullName) { if (SoundcloudLink.IsMatch(resource.ResourceId)) return GetResource(resource.ResourceId); if (resource.ResourceTitle == null) { - if (!allowNullName) return "Could not restore null title."; + if (!allowNullName) return new LocalStr(strings.error_media_internal_missing + " (title)"); string link = RestoreLink(resource.ResourceId); - if (link == null) return "Could not restore link from id"; + if (link == null) return new LocalStr(strings.error_media_internal_missing + " (link)"); return GetResource(link); } @@ -92,7 +94,7 @@ private AudioResource ParseJObjectToResource(JToken jobj) return new AudioResource(id.Value.ToString(CultureInfo.InvariantCulture), title.Value, FactoryFor); } - private R YoutubeDlWrapped(string link) + private R YoutubeDlWrapped(string link) { Log.Debug("Falling back to youtube-dl!"); @@ -102,29 +104,29 @@ private R YoutubeDlWrapped(string link) var (title, urls) = result.Value; if (urls.Count == 0 || string.IsNullOrEmpty(title) || string.IsNullOrEmpty(urls[0])) - return "No youtube-dl response"; + return new LocalStr(strings.error_ytdl_empty_response); Log.Debug("youtube-dl succeeded!"); return new PlayResource(urls[0], new AudioResource(link, title, FactoryFor)); } - public R GetPlaylist(string url) + public R GetPlaylist(string url) { var uri = new Uri($"https://api.soundcloud.com/resolve.json?url={Uri.EscapeUriString(url)}&client_id={SoundcloudClientId}"); - if (!WebWrapper.DownloadString(out string jsonResponse, uri)) - return RResultCode.ScInvalidLink.ToString(); + if (!WebWrapper.DownloadString(out string jsonResponse, uri)) // todo: a bit janky (no response <-> error response) + return new LocalStr(strings.error_net_no_connection); var parsedDict = ParseJson(jsonResponse); if (parsedDict == null) - return "Empty or missing response parts (parsedDict)"; + return new LocalStr(strings.error_media_internal_missing + " (parsedDict)"); string name = PlaylistManager.CleanseName(parsedDict.TryCast("title").OkOr(null)); var plist = new Playlist(name); var tracksJobj = parsedDict["tracks"]; if (tracksJobj == null) - return "Empty or missing response parts (tracks)"; + return new LocalStr(strings.error_media_internal_missing + "(tracks)"); foreach (var track in tracksJobj) { @@ -138,19 +140,19 @@ public R GetPlaylist(string url) return plist; } - public R GetThumbnail(PlayResource playResource) + public R GetThumbnail(PlayResource playResource) { var uri = new Uri($"https://api.soundcloud.com/tracks/{playResource.BaseData.ResourceId}?client_id={SoundcloudClientId}"); if (!WebWrapper.DownloadString(out string jsonResponse, uri)) - return "Error or no response by soundcloud"; + return new LocalStr(strings.error_net_no_connection); var parsedDict = ParseJson(jsonResponse); if (parsedDict == null) - return "Empty or missing response parts (parsedDict)"; + return new LocalStr(strings.error_media_internal_missing + " (parsedDict)"); var imgUrl = parsedDict.TryCast("artwork_url").OkOr(null); if (imgUrl == null) - return "Empty or missing response parts (artwork_url)"; + return new LocalStr(strings.error_media_internal_missing + " (artwork_url)"); // t500x500: 500px×500px // crop : 400px×400px @@ -159,18 +161,7 @@ public R GetThumbnail(PlayResource playResource) imgUrl = imgUrl.Replace("-large", "-t300x300"); var imgurl = new Uri(imgUrl); - Image img = null; - var resresult = WebWrapper.GetResponse(imgurl, (webresp) => - { - using (var stream = webresp.GetResponseStream()) - { - if (stream != null) - img = Image.FromStream(stream); - } - }); - if (resresult != ValidateCode.Ok) - return "Error while reading image"; - return img; + return WebWrapper.GetResponseUnsafe(imgurl); } public void Dispose() { } diff --git a/TS3AudioBot/ResourceFactories/TwitchFactory.cs b/TS3AudioBot/ResourceFactories/TwitchFactory.cs index 6ccadd91..b4f6cc34 100644 --- a/TS3AudioBot/ResourceFactories/TwitchFactory.cs +++ b/TS3AudioBot/ResourceFactories/TwitchFactory.cs @@ -10,6 +10,7 @@ namespace TS3AudioBot.ResourceFactories { using Helper; + using Localization; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; @@ -26,21 +27,21 @@ public sealed class TwitchFactory : IResourceFactory public MatchCertainty MatchResource(string uri) => TwitchMatch.IsMatch(uri).ToMatchCertainty(); - public R GetResource(string url) + public R GetResource(string uri) { - var match = TwitchMatch.Match(url); + var match = TwitchMatch.Match(uri); if (!match.Success) - return RResultCode.TwitchInvalidUrl.ToString(); + return new LocalStr(strings.error_media_invalid_uri); return GetResourceById(new AudioResource(match.Groups[3].Value, null, FactoryFor)); } - public R GetResourceById(AudioResource resource) + public R GetResourceById(AudioResource resource) { var channel = resource.ResourceId; // request api token - if (!WebWrapper.DownloadString(out string jsonResponse, new Uri($"http://api.twitch.tv/api/channels/{channel}/access_token"), ("Client-ID", TwitchClientId))) - return RResultCode.NoConnection.ToString(); + if (!WebWrapper.DownloadString(out string jsonResponse, new Uri($"https://api.twitch.tv/api/channels/{channel}/access_token"), ("Client-ID", TwitchClientId))) + return new LocalStr(strings.error_net_no_connection); var jObj = JObject.Parse(jsonResponse); @@ -48,13 +49,13 @@ public R GetResourceById(AudioResource resource) var tokenResult = jObj.TryCast("token"); var sigResult = jObj.TryCast("sig"); if (!tokenResult.Ok || !sigResult.Ok) - return "Invalid api response"; + return new LocalStr(strings.error_media_internal_invalid + " (tokenResult|sigResult)"); var token = Uri.EscapeUriString(tokenResult.Value); var sig = sigResult.Value; // guaranteed to be random, chosen by fair dice roll. const int random = 4; if (!WebWrapper.DownloadString(out string m3u8, new Uri($"http://usher.twitch.tv/api/channel/hls/{channel}.m3u8?player=twitchweb&&token={token}&sig={sig}&allow_audio_only=true&allow_source=true&type=any&p={random}"))) - return RResultCode.NoConnection.ToString(); + return new LocalStr(strings.error_net_no_connection); // parse m3u8 file var dataList = new List(); @@ -62,7 +63,7 @@ public R GetResourceById(AudioResource resource) { var header = reader.ReadLine(); if (string.IsNullOrEmpty(header) || header != "#EXTM3U") - return RResultCode.TwitchMalformedM3u8File.ToString(); + return new LocalStr(strings.error_media_internal_missing + " (m3uHeader)"); while (true) { @@ -80,10 +81,12 @@ public R GetResourceById(AudioResource resource) case "EXT-X-MEDIA": string streamInfo = reader.ReadLine(); Match infoMatch; - if (string.IsNullOrEmpty(streamInfo) || - !(infoMatch = M3U8ExtMatch.Match(streamInfo)).Success || - infoMatch.Groups[1].Value != "EXT-X-STREAM-INF") - return RResultCode.TwitchMalformedM3u8File.ToString(); + if (string.IsNullOrEmpty(streamInfo) + || !(infoMatch = M3U8ExtMatch.Match(streamInfo)).Success + || infoMatch.Groups[1].Value != "EXT-X-STREAM-INF") + { + return new LocalStr(strings.error_media_internal_missing + " (m3uStream)"); + } var streamData = new StreamData(); // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,CODECS="mp4a.40.2",VIDEO="audio_only" @@ -98,8 +101,8 @@ public R GetResourceById(AudioResource resource) case "CODECS": streamData.Codec = TextUtil.StripQuotes(value); break; case "VIDEO": streamData.QualityType = Enum.TryParse(TextUtil.StripQuotes(value), out StreamQuality quality) - ? quality - : StreamQuality.unknown; break; + ? quality + : StreamQuality.unknown; break; } } @@ -113,18 +116,18 @@ public R GetResourceById(AudioResource resource) // Validation Process if (dataList.Count <= 0) - return RResultCode.TwitchNoStreamsExtracted.ToString(); + return new LocalStr(strings.error_media_no_stream_extracted); int codec = SelectStream(dataList); if (codec < 0) - return "The stream has no audio_only version."; + return new LocalStr(strings.error_media_no_stream_extracted); return new PlayResource(dataList[codec].Url, resource.ResourceTitle != null ? resource : resource.WithName($"Twitch channel: {channel}")); } private static int SelectStream(List list) => list.FindIndex(s => s.QualityType == StreamQuality.audio_only); - public string RestoreLink(string id) => "http://www.twitch.tv/" + id; + public string RestoreLink(string id) => "https://www.twitch.tv/" + id; public void Dispose() { } } diff --git a/TS3AudioBot/ResourceFactories/YoutubeDlHelper.cs b/TS3AudioBot/ResourceFactories/YoutubeDlHelper.cs index ec3ba4fe..d081b090 100644 --- a/TS3AudioBot/ResourceFactories/YoutubeDlHelper.cs +++ b/TS3AudioBot/ResourceFactories/YoutubeDlHelper.cs @@ -9,6 +9,8 @@ namespace TS3AudioBot.ResourceFactories { + using Localization; + using Config; using System; using System.Collections.Generic; using System.ComponentModel; @@ -18,14 +20,14 @@ namespace TS3AudioBot.ResourceFactories internal static class YoutubeDlHelper { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - public static YoutubeFactoryData DataObj { private get; set; } - private static string YoutubeDlPath => DataObj?.YoutubedlPath; + public static ConfPath DataObj { private get; set; } + private static string YoutubeDlPath => DataObj?.Path.Value; - public static R<(string title, IList links)> FindAndRunYoutubeDl(string id) + public static R<(string title, IList links), LocalStr> FindAndRunYoutubeDl(string id) { var ytdlPath = FindYoutubeDl(id); if (ytdlPath == null) - return "Youtube-Dl could not be found. The song/video cannot be played due to restrictions"; + return new LocalStr(strings.error_ytdl_not_found); return RunYoutubeDl(ytdlPath.Value.ytdlpath, ytdlPath.Value.param); } @@ -41,7 +43,7 @@ public static (string ytdlpath, string param)? FindYoutubeDl(string id) if (YoutubeDlPath == null) return null; - + string fullCustomPath; try { fullCustomPath = Path.GetFullPath(YoutubeDlPath); } catch (ArgumentException) @@ -67,7 +69,7 @@ public static (string ytdlpath, string param)? FindYoutubeDl(string id) return null; } - public static R<(string title, IList links)> RunYoutubeDl(string path, string args) + public static R<(string title, IList links), LocalStr> RunYoutubeDl(string path, string args) { try { @@ -89,14 +91,14 @@ public static (string ytdlpath, string param)? FindYoutubeDl(string id) if (!string.IsNullOrEmpty(result)) { Log.Error("youtube-dl failed to load the resource:\n{0}", result); - return "youtube-dl failed to load the resource"; + return new LocalStr(strings.error_ytdl_song_failed_to_load); } } return ParseResponse(tmproc.StandardOutput); } } - catch (Win32Exception) { return "Failed to run youtube-dl"; } + catch (Win32Exception) { return new LocalStr(strings.error_ytdl_failed_to_run); } } public static (string title, IList links) ParseResponse(StreamReader stream) diff --git a/TS3AudioBot/ResourceFactories/YoutubeFactory.cs b/TS3AudioBot/ResourceFactories/YoutubeFactory.cs index c5990455..c276b970 100644 --- a/TS3AudioBot/ResourceFactories/YoutubeFactory.cs +++ b/TS3AudioBot/ResourceFactories/YoutubeFactory.cs @@ -10,16 +10,16 @@ namespace TS3AudioBot.ResourceFactories { using Helper; + using Localization; + using Newtonsoft.Json; + using Playlists; using System; using System.Collections.Generic; - using System.Collections.Specialized; - using System.Drawing; using System.Globalization; + using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; - using System.Web; - using Newtonsoft.Json; public sealed class YoutubeFactory : IResourceFactory, IPlaylistFactory, IThumbnailFactory { @@ -27,93 +27,82 @@ public sealed class YoutubeFactory : IResourceFactory, IPlaylistFactory, IThumbn private static readonly Regex IdMatch = new Regex(@"((&|\?)v=|youtu\.be\/)([\w\-_]+)", Util.DefaultRegexConfig); private static readonly Regex LinkMatch = new Regex(@"^(https?\:\/\/)?(www\.|m\.)?(youtube\.|youtu\.be)", Util.DefaultRegexConfig); private static readonly Regex ListMatch = new Regex(@"(&|\?)list=([\w\-_]+)", Util.DefaultRegexConfig); - - private readonly YoutubeFactoryData data; - - public YoutubeFactory(YoutubeFactoryData yfd) - { - data = yfd; - } + private const string YoutubeProjectId = "AIzaSyBOqG5LUbGSkBfRUoYfUUea37-5xlEyxNs"; public string FactoryFor => "youtube"; - MatchCertainty IResourceFactory.MatchResource(string link) => - LinkMatch.IsMatch(link) + MatchCertainty IResourceFactory.MatchResource(string uri) => + LinkMatch.IsMatch(uri) ? MatchCertainty.Always - : IdMatch.IsMatch(link) + : IdMatch.IsMatch(uri) ? MatchCertainty.Probably : MatchCertainty.Never; - MatchCertainty IPlaylistFactory.MatchPlaylist(string link) => ListMatch.IsMatch(link) ? MatchCertainty.Probably : MatchCertainty.Never; + MatchCertainty IPlaylistFactory.MatchPlaylist(string uri) => ListMatch.IsMatch(uri) ? MatchCertainty.Probably : MatchCertainty.Never; - public R GetResource(string ytLink) + public R GetResource(string uri) { - Match matchYtId = IdMatch.Match(ytLink); + Match matchYtId = IdMatch.Match(uri); if (!matchYtId.Success) - return "The youtube id could not get parsed."; + return new LocalStr(strings.error_media_failed_to_parse_id); return GetResourceById(new AudioResource(matchYtId.Groups[3].Value, null, FactoryFor)); } - public R GetResourceById(AudioResource resource) + public R GetResourceById(AudioResource resource) { var result = ResolveResourceInternal(resource); if (result.Ok) return result; - + return YoutubeDlWrapped(resource); } - private R ResolveResourceInternal(AudioResource resource) + private R ResolveResourceInternal(AudioResource resource) { - if (!WebWrapper.DownloadString(out string resulthtml, new Uri($"http://www.youtube.com/get_video_info?video_id={resource.ResourceId}&el=info"))) - return "No connection to the youtube api could be established"; + if (!WebWrapper.DownloadString(out string resulthtml, new Uri($"https://www.youtube.com/get_video_info?video_id={resource.ResourceId}"))) + return new LocalStr(strings.error_net_no_connection); var videoTypes = new List(); - NameValueCollection dataParse = HttpUtility.ParseQueryString(resulthtml); + var dataParse = ParseQueryString(resulthtml); - string videoDataUnsplit = dataParse["url_encoded_fmt_stream_map"]; - if (videoDataUnsplit != null) + if (dataParse.TryGetValue("url_encoded_fmt_stream_map", out var videoDataUnsplit)) { - string[] videoData = videoDataUnsplit.Split(','); + string[] videoData = videoDataUnsplit[0].Split(','); foreach (string vdat in videoData) { - NameValueCollection videoparse = HttpUtility.ParseQueryString(vdat); + var videoparse = ParseQueryString(vdat); - string vLink = videoparse["url"]; - if (vLink == null) + if (!videoparse.TryGetValue("url", out var vLink)) continue; - string vType = videoparse["type"]; - if (vType == null) + if (!videoparse.TryGetValue("type", out var vType)) continue; - string vQuality = videoparse["quality"]; - if (vQuality == null) + if (!videoparse.TryGetValue("quality", out var vQuality)) continue; var vt = new VideoData() { - Link = vLink, - Codec = GetCodec(vType), - Qualitydesciption = vQuality + Link = vLink[0], + Codec = GetCodec(vType[0]), + Qualitydesciption = vQuality[0] }; videoTypes.Add(vt); } } - videoDataUnsplit = dataParse["adaptive_fmts"]; - if (videoDataUnsplit != null) + if (dataParse.TryGetValue("adaptive_fmts", out videoDataUnsplit)) { - string[] videoData = videoDataUnsplit.Split(','); + string[] videoData = videoDataUnsplit[0].Split(','); foreach (string vdat in videoData) { - NameValueCollection videoparse = HttpUtility.ParseQueryString(vdat); + var videoparse = ParseQueryString(vdat); - string vType = videoparse["type"]; - if (vType == null) + if (!videoparse.TryGetValue("type", out var vTypeArr)) continue; + var vType = vTypeArr[0]; bool audioOnly = false; if (vType.StartsWith("video/", StringComparison.Ordinal)) @@ -121,15 +110,14 @@ private R ResolveResourceInternal(AudioResource resource) else if (vType.StartsWith("audio/", StringComparison.Ordinal)) audioOnly = true; - string vLink = videoparse["url"]; - if (vLink == null) + if (!videoparse.TryGetValue("url", out var vLink)) continue; var vt = new VideoData() { Codec = GetCodec(vType), Qualitydesciption = vType, - Link = vLink + Link = vLink[0] }; if (audioOnly) vt.AudioOnly = true; @@ -142,17 +130,21 @@ private R ResolveResourceInternal(AudioResource resource) // Validation Process if (videoTypes.Count <= 0) - return "No video streams extracted."; + return new LocalStr(strings.error_media_no_stream_extracted); int codec = SelectStream(videoTypes); if (codec < 0) - return "No playable codec found"; + return new LocalStr(strings.error_media_no_stream_extracted); var result = ValidateMedia(videoTypes[codec]); if (!result.Ok) return result.Error; - return new PlayResource(videoTypes[codec].Link, resource.ResourceTitle != null ? resource : resource.WithName(dataParse["title"] ?? $"")); + var title = dataParse.TryGetValue("title", out var titleArr) + ? titleArr[0] + : $""; + + return new PlayResource(videoTypes[codec].Link, resource.ResourceTitle != null ? resource : resource.WithName(title)); } public string RestoreLink(string id) => "https://youtu.be/" + id; @@ -175,19 +167,7 @@ private static int SelectStream(List list) return autoselectIndex; } - private static R ValidateMedia(VideoData media) - { - var vcode = WebWrapper.GetResponse(new Uri(media.Link), TimeSpan.FromSeconds(3)); - - switch (vcode) - { - case ValidateCode.Ok: return R.OkR; - case ValidateCode.Restricted: return "The video cannot be played due to youtube restrictions."; - case ValidateCode.Timeout: return "No connection could be established to youtube. Please try again later."; - case ValidateCode.UnknownError: return "Unknown error occoured"; - default: throw new InvalidOperationException(); - } - } + private static E ValidateMedia(VideoData media) => WebWrapper.GetResponse(new Uri(media.Link), TimeSpan.FromSeconds(3)); private static VideoCodec GetCodec(string type) { @@ -223,11 +203,11 @@ private static VideoCodec GetCodec(string type) } } - public R GetPlaylist(string url) + public R GetPlaylist(string url) { Match matchYtId = ListMatch.Match(url); if (!matchYtId.Success) - return "Could not extract a playlist id"; + return new LocalStr(strings.error_media_failed_to_parse_id); string id = matchYtId.Groups[2].Value; var plist = new Playlist(id); @@ -242,13 +222,13 @@ public R GetPlaylist(string url) + "&playlistId=" + id + "&fields=" + Uri.EscapeDataString("items(contentDetails/videoId,snippet/title),nextPageToken") + (nextToken != null ? ("&pageToken=" + nextToken) : string.Empty) - + "&key=" + data.ApiKey); + + "&key=" + YoutubeProjectId); if (!WebWrapper.DownloadString(out string response, queryString)) - return "Web response error"; + return new LocalStr(strings.error_net_unknown); var parsed = JsonConvert.DeserializeObject(response); var videoItems = parsed.items; - YoutubePlaylistItem[] itemBuffer = new YoutubePlaylistItem[videoItems.Length]; + var itemBuffer = new YoutubePlaylistItem[videoItems.Length]; for (int i = 0; i < videoItems.Length; i++) { itemBuffer[i] = new YoutubePlaylistItem(new AudioResource( @@ -274,8 +254,8 @@ public R GetPlaylist(string url) return plist; } - - private static R YoutubeDlWrapped(AudioResource resource) + + private static R YoutubeDlWrapped(AudioResource resource) { Log.Debug("Falling back to youtube-dl!"); @@ -296,42 +276,49 @@ private static R YoutubeDlWrapped(AudioResource resource) { Uri[] uriList = urlOptions.Select(s => new Uri(s)).ToArray(); Uri bestMatch = uriList - .FirstOrDefault(u => HttpUtility.ParseQueryString(u.Query) - .GetValues("mime")? - .Any(x => x.StartsWith("audio", StringComparison.OrdinalIgnoreCase)) ?? false); + .FirstOrDefault(u => ParseQueryString(u.Query).TryGetValue("mime", out var mimes) + && mimes.Any(x => x.StartsWith("audio", StringComparison.OrdinalIgnoreCase))); url = (bestMatch ?? uriList[0]).OriginalString; } if (string.IsNullOrEmpty(title) || string.IsNullOrEmpty(url)) - return "No youtube-dl response"; + return new LocalStr(strings.error_ytdl_empty_response); Log.Debug("youtube-dl succeeded!"); return new PlayResource(url, resource.WithName(title)); } - public R GetThumbnail(PlayResource playResource) + public static Dictionary> ParseQueryString(string requestQueryString) + { + var rc = new Dictionary>(); + string[] ar1 = requestQueryString.Split('&', '?'); + foreach (string row in ar1) + { + if (string.IsNullOrEmpty(row)) continue; + int index = row.IndexOf('='); + var param = Uri.UnescapeDataString(row.Substring(0, index).Replace('+', ' ')); + if (!rc.TryGetValue(param, out var list)) + { + list = new List(); + rc[param] = list; + } + list.Add(Uri.UnescapeDataString(row.Substring(index + 1).Replace('+', ' '))); + } + return rc; + } + + public R GetThumbnail(PlayResource playResource) { if (!WebWrapper.DownloadString(out string response, - new Uri($"https://www.googleapis.com/youtube/v3/videos?part=snippet&id={playResource.BaseData.ResourceId}&key={data.ApiKey}"))) - return "No connection"; + new Uri($"https://www.googleapis.com/youtube/v3/videos?part=snippet&id={playResource.BaseData.ResourceId}&key={YoutubeProjectId}"))) + return new LocalStr(strings.error_net_no_connection); var parsed = JsonConvert.DeserializeObject(response); // default: 120px/ 90px // medium : 320px/180px // high : 480px/360px var imgurl = new Uri(parsed.items[0].snippet.thumbnails.medium.url); - Image img = null; - var resresult = WebWrapper.GetResponse(imgurl, (webresp) => - { - using (var stream = webresp.GetResponseStream()) - { - if (stream != null) - img = Image.FromStream(stream); - } - }); - if (resresult != ValidateCode.Ok) - return "Error while reading image"; - return img; + return WebWrapper.GetResponseUnsafe(imgurl); } public void Dispose() { } @@ -380,16 +367,6 @@ public class JsonThumbnail #pragma warning restore CS0649, CS0169 } -#pragma warning disable CS0649 - public class YoutubeFactoryData : ConfigData - { - [Info("A youtube apiv3 'Browser' type key", "AIzaSyBOqG5LUbGSkBfRUoYfUUea37-5xlEyxNs")] - public string ApiKey { get; set; } - [Info("Path to the youtube-dl binary or local git repository", "")] - public string YoutubedlPath { get; set; } - } -#pragma warning restore CS0649 - public sealed class VideoData { public string Link { get; set; } diff --git a/TS3AudioBot/Rights/DefaultRights.toml b/TS3AudioBot/Rights/DefaultRights.toml index 0063b5ae..6de7611b 100644 --- a/TS3AudioBot/Rights/DefaultRights.toml +++ b/TS3AudioBot/Rights/DefaultRights.toml @@ -15,8 +15,6 @@ "cmd.song", "cmd.loop", "cmd.random", - "cmd.random.seed", - "cmd.volume", # Conditionals and basic scripting "cmd.if", diff --git a/TS3AudioBot/Rights/ExecuteContext.cs b/TS3AudioBot/Rights/ExecuteContext.cs new file mode 100644 index 00000000..10c27141 --- /dev/null +++ b/TS3AudioBot/Rights/ExecuteContext.cs @@ -0,0 +1,31 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Rights +{ + using System; + using System.Collections.Generic; + using TS3Client; + + internal class ExecuteContext + { + public string Host { get; set; } + public ulong[] ServerGroups { get; set; } = Array.Empty(); + public ulong? ChannelGroupId { get; set; } + public string ClientUid { get; set; } + public bool IsApi { get; set; } + public string ApiToken { get; set; } + public string Bot { get; set; } + public TextMessageTargetMode? Visibiliy { get; set; } + + public List MatchingRules { get; } = new List(); + + public HashSet DeclAdd { get; } = new HashSet(); + } +} diff --git a/TS3AudioBot/Rights/ParseContext.cs b/TS3AudioBot/Rights/ParseContext.cs new file mode 100644 index 00000000..68b05beb --- /dev/null +++ b/TS3AudioBot/Rights/ParseContext.cs @@ -0,0 +1,66 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Rights +{ + using System.Collections.Generic; + using System.Linq; + using System.Text; + + internal class ParseContext + { + public List Declarations { get; } + public RightsGroup[] Groups { get; private set; } + public RightsRule[] Rules { get; private set; } + public List Errors { get; } + public List Warnings { get; } + public ISet RegisteredRights { get; } + + public RightsRule RootRule { get; } + public bool NeedsAvailableGroups { get; set; } = false; + public bool NeedsAvailableChanGroups { get; set; } = false; + + public ParseContext(ISet registeredRights) + { + Declarations = new List(); + RootRule = new RightsRule(); + Errors = new List(); + Warnings = new List(); + RegisteredRights = registeredRights; + } + + public void SplitDeclarations() + { + Groups = Declarations.OfType().ToArray(); + Rules = Declarations.OfType().ToArray(); + } + + public (bool hasErrors, string info) AsResult() + { + var strb = new StringBuilder(); + foreach (var warn in Warnings) + strb.Append("WRN: ").AppendLine(warn); + if (Errors.Count == 0) + { + strb.Append(string.Join("\n", Rules.Select(x => x.ToString()))); + if (strb.Length > 900) + strb.Length = 900; + return (true, strb.ToString()); + } + else + { + foreach (var err in Errors) + strb.Append("ERR: ").AppendLine(err); + if (strb.Length > 900) + strb.Length = 900; + return (false, strb.ToString()); + } + } + } +} diff --git a/TS3AudioBot/Rights/RightsDecl.cs b/TS3AudioBot/Rights/RightsDecl.cs index 0ad86ea2..0def6daa 100644 --- a/TS3AudioBot/Rights/RightsDecl.cs +++ b/TS3AudioBot/Rights/RightsDecl.cs @@ -9,6 +9,7 @@ namespace TS3AudioBot.Rights { + using Helper; using Nett; using System; using System.Collections.Generic; @@ -38,15 +39,15 @@ public virtual bool ParseKey(string key, TomlObject tomlObj, ParseContext ctx) switch (key) { case "+": - DeclAdd = TomlTools.GetValues(tomlObj); + DeclAdd = tomlObj.TryGetValueArray(); if (DeclAdd == null) ctx.Errors.Add("<+> Field has invalid data."); return true; case "-": - DeclDeny = TomlTools.GetValues(tomlObj); + DeclDeny = tomlObj.TryGetValueArray(); if (DeclDeny == null) ctx.Errors.Add("<-> Field has invalid data."); return true; case "include": - includeNames = TomlTools.GetValues(tomlObj); + includeNames = tomlObj.TryGetValueArray(); if (includeNames == null) ctx.Errors.Add(" Field has invalid data."); return true; default: @@ -85,11 +86,13 @@ public bool ResolveIncludes(ParseContext ctx) { Includes = includeNames.Select(x => ResolveGroup(x, ctx)).ToArray(); for (int i = 0; i < includeNames.Length; i++) + { if (Includes[i] == null) { ctx.Errors.Add($"Could not find group \"{includeNames[i]}\" to include."); hasErrors = true; } + } includeNames = null; } return !hasErrors; diff --git a/TS3AudioBot/Rights/RightsManager.cs b/TS3AudioBot/Rights/RightsManager.cs index 9e37d24d..27cdf0f9 100644 --- a/TS3AudioBot/Rights/RightsManager.cs +++ b/TS3AudioBot/Rights/RightsManager.cs @@ -10,14 +10,13 @@ namespace TS3AudioBot.Rights { using CommandSystem; + using Config; using Helper; using Nett; using System; using System.Collections.Generic; using System.IO; using System.Linq; - using System.Text; - using TS3Client; /// Permission system of the bot. public class RightsManager @@ -25,91 +24,65 @@ public class RightsManager private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); private const int RuleLevelSize = 2; - public CommandManager CommandManager { get; set; } - private bool needsRecalculation; - private readonly Cache cachedRights; - private readonly RightsManagerData rightsManagerData; + private readonly ConfRights config; private RightsRule rootRule; - private RightsRule[] rules; private readonly HashSet registeredRights; + private readonly object rootRuleLock = new object(); // Required Matcher Data: // This variables save whether the current rights setup has at least one rule that // need a certain additional information. // This will save us from making unnecessary query calls. - // TODO: private bool needsAvailableGroups = true; private bool needsAvailableChanGroups = true; - public RightsManager(RightsManagerData rmd) + public RightsManager(ConfRights config) { - Util.Init(out cachedRights); Util.Init(out registeredRights); - rightsManagerData = rmd; - } - - public void Initialize() - { - RegisterRights(CommandManager.AllRights); - RegisterRights(MainCommands.RightHighVolume, MainCommands.RightDeleteAllPlaylists); - if (!ReadFile()) - Log.Error("Could not read Permission file."); - } - - public void RegisterRights(params string[] rights) => RegisterRights((IEnumerable)rights); - public void RegisterRights(IEnumerable rights) - { - // TODO validate right names - registeredRights.UnionWith(rights); + this.config = config; needsRecalculation = true; } - public void UnregisterRights(params string[] rights) => UnregisterRights((IEnumerable)rights); - public void UnregisterRights(IEnumerable rights) + public void SetRightsList(IEnumerable rights) { // TODO validate right names - // optionally expand - registeredRights.ExceptWith(rights); + registeredRights.Clear(); + registeredRights.UnionWith(rights); needsRecalculation = true; } // TODO: b_client_permissionoverview_view - public bool HasAllRights(CallerInfo caller, InvokerData invoker, TeamspeakControl ts, params string[] requestedRights) + public bool HasAllRights(ExecutionInformation info, params string[] requestedRights) { - var ctx = GetRightsContext(caller, invoker, ts); - var normalizedRequest = ExpandRights(requestedRights); + var ctx = GetRightsContext(info); + var normalizedRequest = ExpandRights(requestedRights, registeredRights); return ctx.DeclAdd.IsSupersetOf(normalizedRequest); } - public string[] GetRightsSubset(CallerInfo caller, InvokerData invoker, TeamspeakControl ts, params string[] requestedRights) + public string[] GetRightsSubset(ExecutionInformation info, params string[] requestedRights) { - var ctx = GetRightsContext(caller, invoker, ts); - var normalizedRequest = ExpandRights(requestedRights); + var ctx = GetRightsContext(info); + var normalizedRequest = ExpandRights(requestedRights, registeredRights); return ctx.DeclAdd.Intersect(normalizedRequest).ToArray(); } - private ExecuteContext GetRightsContext(CallerInfo caller, InvokerData invoker, TeamspeakControl ts) + private ExecuteContext GetRightsContext(ExecutionInformation info) { - if (needsRecalculation) - { - cachedRights.Invalidate(); - needsRecalculation = false; - ReadFile(); - } + var localRootRule = TryGetRootSafe(); - ExecuteContext execCtx; - if (invoker != null) + if (info.TryGet(out var execCtx)) + return execCtx; + + if (info.TryGet(out var invoker)) { - if (cachedRights.TryGetValue(invoker.ClientUid, out execCtx)) + execCtx = new ExecuteContext { - // TODO check if all fields are same - // if yes => returen - // if no => delete from cache - return execCtx; - } - - execCtx = new ExecuteContext(); + ServerGroups = invoker.ServerGroups, + ClientUid = invoker.ClientUid, + Visibiliy = invoker.Visibiliy, + ApiToken = invoker.Token, + }; // Get Required Matcher Data: // In this region we will iteratively go through different possibilities to obtain @@ -117,41 +90,56 @@ private ExecuteContext GetRightsContext(CallerInfo caller, InvokerData invoker, // For this step we will prefer query calls which can give us more than one information // at once and lazily fall back to other calls as long as needed. - ulong[] availableGroups = null; - if (ts != null) + if (info.TryGet(out var ts)) { - if (invoker.ClientId.HasValue && - (needsAvailableGroups || needsAvailableChanGroups)) + ulong[] serverGroups = invoker.ServerGroups; + + if (invoker.ClientId.HasValue + && ((needsAvailableGroups && serverGroups == null) + || needsAvailableChanGroups)) { var result = ts.GetClientInfoById(invoker.ClientId.Value); if (result.Ok) { - availableGroups = result.Value.ServerGroups; + serverGroups = result.Value.ServerGroups; execCtx.ChannelGroupId = result.Value.ChannelGroup; } } - if (needsAvailableGroups && invoker.DatabaseId.HasValue && availableGroups == null) + if (needsAvailableGroups && serverGroups == null) { - var result = ts.GetClientServerGroups(invoker.DatabaseId.Value); - if (result.Ok) - availableGroups = result.Value; + if (!invoker.DatabaseId.HasValue) + { + var resultDbId = ts.TsFullClient.ClientGetDbIdFromUid(invoker.ClientUid); + if (resultDbId.Ok) + { + invoker.DatabaseId = resultDbId.Value.ClientDbId; + } + } + + if (invoker.DatabaseId.HasValue) + { + var result = ts.GetClientServerGroups(invoker.DatabaseId.Value); + if (result.Ok) + serverGroups = result.Value; + } } - } - if (availableGroups != null) - execCtx.AvailableGroups = availableGroups; - execCtx.ClientUid = invoker.ClientUid; - execCtx.Visibiliy = invoker.Visibiliy; - execCtx.ApiToken = invoker.Token; + execCtx.ServerGroups = serverGroups ?? execCtx.ServerGroups; + } } else { execCtx = new ExecuteContext(); } - execCtx.IsApi = caller.ApiCall; - ProcessNode(rootRule, execCtx); + if (info.TryGet(out var caller)) + execCtx.IsApi = caller.ApiCall; + if (info.TryGet(out var bot)) + execCtx.Bot = bot.Name; + + if (localRootRule != null) + ProcessNode(localRootRule, execCtx); if (execCtx.MatchingRules.Count == 0) return execCtx; @@ -159,21 +147,37 @@ private ExecuteContext GetRightsContext(CallerInfo caller, InvokerData invoker, foreach (var rule in execCtx.MatchingRules) execCtx.DeclAdd.UnionWith(rule.DeclAdd); - if (invoker != null) - cachedRights.Store(invoker.ClientUid, execCtx); + info.AddDynamicObject(execCtx); return execCtx; } + private RightsRule TryGetRootSafe() + { + var localRootRule = rootRule; + if (localRootRule != null && !needsRecalculation) + return localRootRule; + + lock (rootRuleLock) + { + if (rootRule != null && !needsRecalculation) + return rootRule; + + rootRule = ReadFile(); + return rootRule; + } + } + private static bool ProcessNode(RightsRule rule, ExecuteContext ctx) { // check if node matches if (!rule.HasMatcher() || (ctx.Host != null && rule.MatchHost.Contains(ctx.Host)) || (ctx.ClientUid != null && rule.MatchClientUid.Contains(ctx.ClientUid)) - || (ctx.AvailableGroups.Length > 0 && rule.MatchClientGroupId.Overlaps(ctx.AvailableGroups)) + || ((ctx.ServerGroups?.Length ?? 0) > 0 && rule.MatchClientGroupId.Overlaps(ctx.ServerGroups)) || (ctx.ChannelGroupId.HasValue && rule.MatchChannelGroupId.Contains(ctx.ChannelGroupId.Value)) || (ctx.ApiToken != null && rule.MatchToken.Contains(ctx.ApiToken)) + || (ctx.Bot != null && rule.MatchBot.Contains(ctx.Bot)) || (ctx.IsApi == rule.MatchIsApi) || (ctx.Visibiliy.HasValue && rule.MatchVisibility.Contains(ctx.Visibiliy.Value))) { @@ -188,74 +192,54 @@ private static bool ProcessNode(RightsRule rule, ExecuteContext ctx) return false; } + public bool Reload() + { + needsRecalculation = true; + return TryGetRootSafe() != null; + } + // Loading and Parsing - public bool ReadFile() + private RightsRule ReadFile() { try { - if (!File.Exists(rightsManagerData.RightsFile)) + if (!File.Exists(config.Path)) { Log.Info("No rights file found. Creating default."); - using (var fs = File.OpenWrite(rightsManagerData.RightsFile)) + using (var fs = File.OpenWrite(config.Path)) using (var data = Util.GetEmbeddedFile("TS3AudioBot.Rights.DefaultRights.toml")) data.CopyTo(fs); } - var table = Toml.ReadFile(rightsManagerData.RightsFile); - var ctx = new ParseContext(); + var table = Toml.ReadFile(config.Path); + var ctx = new ParseContext(registeredRights); RecalculateRights(table, ctx); foreach (var err in ctx.Errors) Log.Error(err); foreach (var warn in ctx.Warnings) Log.Warn(warn); - return ctx.Errors.Count == 0; - } - catch (Exception ex) - { - Log.Error(ex, "The rights file could not be parsed"); - return false; - } - } - public R ReadText(string text) - { - try - { - var table = Toml.ReadString(text); - var ctx = new ParseContext(); - RecalculateRights(table, ctx); - var strb = new StringBuilder(); - foreach (var warn in ctx.Warnings) - strb.Append("WRN: ").AppendLine(warn); if (ctx.Errors.Count == 0) { - strb.Append(string.Join("\n", rules.Select(x => x.ToString()))); - if (strb.Length > 900) - strb.Length = 900; - return R.OkR(strb.ToString()); - } - else - { - foreach (var err in ctx.Errors) - strb.Append("ERR: ").AppendLine(err); - if (strb.Length > 900) - strb.Length = 900; - return R.Err(strb.ToString()); + needsAvailableChanGroups = ctx.NeedsAvailableChanGroups; + needsAvailableGroups = ctx.NeedsAvailableGroups; + needsRecalculation = false; + return ctx.RootRule; } } catch (Exception ex) { - return R.Err("The rights file could not be parsed: " + ex.Message); + Log.Error(ex, "The rights file could not be parsed"); } + return null; } - private void RecalculateRights(TomlTable table, ParseContext parseCtx) + private static void RecalculateRights(TomlTable table, ParseContext parseCtx) { - rules = Array.Empty(); + var localRules = Array.Empty(); - rootRule = new RightsRule(); - if (!rootRule.ParseChilden(table, parseCtx)) + if (!parseCtx.RootRule.ParseChilden(table, parseCtx)) return; parseCtx.SplitDeclarations(); @@ -269,7 +253,7 @@ private void RecalculateRights(TomlTable table, ParseContext parseCtx) if (!CheckCyclicGroupDependencies(parseCtx)) return; - BuildLevel(rootRule); + BuildLevel(parseCtx.RootRule); LintDeclarations(parseCtx); @@ -278,12 +262,12 @@ private void RecalculateRights(TomlTable table, ParseContext parseCtx) FlattenGroups(parseCtx); - FlattenRules(rootRule); + FlattenRules(parseCtx.RootRule); - rules = parseCtx.Rules; + CheckRequiredCalls(parseCtx); } - private HashSet ExpandRights(IEnumerable rights) + private static HashSet ExpandRights(IEnumerable rights, ICollection registeredRights) { var rightsExpanded = new HashSet(); foreach (var right in rights) @@ -320,23 +304,23 @@ private HashSet ExpandRights(IEnumerable rights) /// Expands wildcard declarations to all explicit declarations. /// /// The parsing context for the current file processing. - private bool NormalizeRule(ParseContext ctx) + private static bool NormalizeRule(ParseContext ctx) { bool hasErrors = false; foreach (var rule in ctx.Rules) { - var denyNormalized = ExpandRights(rule.DeclDeny); + var denyNormalized = ExpandRights(rule.DeclDeny, ctx.RegisteredRights); rule.DeclDeny = denyNormalized.ToArray(); - var addNormalized = ExpandRights(rule.DeclAdd); + var addNormalized = ExpandRights(rule.DeclAdd, ctx.RegisteredRights); addNormalized.ExceptWith(rule.DeclDeny); rule.DeclAdd = addNormalized.ToArray(); - var undeclared = rule.DeclAdd.Except(registeredRights) - .Concat(rule.DeclDeny.Except(registeredRights)); + var undeclared = rule.DeclAdd.Except(ctx.RegisteredRights) + .Concat(rule.DeclDeny.Except(ctx.RegisteredRights)); foreach (var right in undeclared) { - ctx.Errors.Add($"Right \"{right}\" is not registered."); + ctx.Warnings.Add($"Right \"{right}\" is not registered."); hasErrors = true; } } @@ -434,8 +418,12 @@ private static void BuildLevel(RightsDecl root, int level = 0) { root.Level = level; if (root is RightsRule rootRule) + { foreach (var child in rootRule.Children) + { BuildLevel(child, level + RuleLevelSize); + } + } } /// @@ -529,48 +517,23 @@ private static void FlattenRules(RightsRule root) foreach (var child in root.ChildrenRules) FlattenRules(child); } - } - - internal class ExecuteContext - { - public string Host { get; set; } - public ulong[] AvailableGroups { get; set; } = Array.Empty(); - public ulong? ChannelGroupId { get; set; } - public string ClientUid { get; set; } - public bool IsApi { get; set; } - public string ApiToken { get; set; } - public TextMessageTargetMode? Visibiliy { get; set; } - - public List MatchingRules { get; } = new List(); - - public HashSet DeclAdd { get; } = new HashSet(); - } - - internal class ParseContext - { - public List Declarations { get; } - public RightsGroup[] Groups { get; private set; } - public RightsRule[] Rules { get; private set; } - public List Errors { get; } - public List Warnings { get; } - public ParseContext() - { - Declarations = new List(); - Errors = new List(); - Warnings = new List(); - } - - public void SplitDeclarations() + /// + /// Checks which ts3client calls need to made to get all information + /// for the required matcher. + /// + /// The parsing context for the current file processing. + private static void CheckRequiredCalls(ParseContext ctx) { - Groups = Declarations.OfType().ToArray(); - Rules = Declarations.OfType().ToArray(); + foreach (var group in ctx.Rules) + { + if (!group.HasMatcher()) + continue; + if (group.MatchClientGroupId.Count > 0) + ctx.NeedsAvailableGroups = true; + if (group.MatchChannelGroupId.Count > 0) + ctx.NeedsAvailableChanGroups = true; + } } } - - public class RightsManagerData : ConfigData - { - [Info("Path to the config file", "rights.toml")] - public string RightsFile { get; set; } - } } diff --git a/TS3AudioBot/Rights/RightsRule.cs b/TS3AudioBot/Rights/RightsRule.cs index d4a853f9..2c951df9 100644 --- a/TS3AudioBot/Rights/RightsRule.cs +++ b/TS3AudioBot/Rights/RightsRule.cs @@ -9,6 +9,7 @@ namespace TS3AudioBot.Rights { + using Helper; using Nett; using System; using System.Collections.Generic; @@ -20,8 +21,8 @@ namespace TS3AudioBot.Rights // 2) Add To Has Matches condition when empty // 3) Add To FillNull when not declared // 4) Add new case to ParseKey switch - // 5) Add match condition to RightManager.ProcessNode - // 6) Add Property in the ExecuteContext class + // 5) Add Property in the ExecuteContext class + // 6) Add match condition to RightManager.ProcessNode // 7) Set value in RightManager.GetRightsContext internal class RightsRule : RightsDecl @@ -36,6 +37,7 @@ internal class RightsRule : RightsDecl public HashSet MatchChannelGroupId { get; set; } public HashSet MatchPermission { get; set; } public HashSet MatchToken { get; set; } + public HashSet MatchBot { get; set; } public bool? MatchIsApi { get; set; } public TextMessageTargetMode[] MatchVisibility { get; set; } @@ -46,12 +48,13 @@ public RightsRule() public bool HasMatcher() { - return MatchClientGroupId.Count > 0 + return MatchHost.Count > 0 || MatchClientUid.Count > 0 - || MatchHost.Count > 0 - || MatchPermission.Count > 0 + || MatchClientGroupId.Count > 0 || MatchChannelGroupId.Count > 0 + || MatchPermission.Count > 0 || MatchToken.Count > 0 + || MatchBot.Count > 0 || MatchIsApi.HasValue || MatchVisibility.Length > 0; } @@ -65,6 +68,7 @@ public override void FillNull() if (MatchChannelGroupId == null) MatchChannelGroupId = new HashSet(); if (MatchPermission == null) MatchPermission = new HashSet(); if (MatchToken == null) MatchToken = new HashSet(); + if (MatchBot == null) MatchBot = new HashSet(); if (MatchVisibility == null) MatchVisibility = Array.Empty(); } @@ -76,41 +80,46 @@ public override bool ParseKey(string key, TomlObject tomlObj, ParseContext ctx) switch (key) { case "host": - var host = TomlTools.GetValues(tomlObj); + var host = tomlObj.TryGetValueArray(); if (host == null) ctx.Errors.Add(" Field has invalid data."); else MatchHost = new HashSet(host); return true; case "groupid": - var groupid = TomlTools.GetValues(tomlObj); + var groupid = tomlObj.TryGetValueArray(); if (groupid == null) ctx.Errors.Add(" Field has invalid data."); else MatchClientGroupId = new HashSet(groupid); return true; case "channelgroupid": - var cgroupid = TomlTools.GetValues(tomlObj); + var cgroupid = tomlObj.TryGetValueArray(); if (cgroupid == null) ctx.Errors.Add(" Field has invalid data."); else MatchChannelGroupId = new HashSet(cgroupid); return true; case "useruid": - var useruid = TomlTools.GetValues(tomlObj); + var useruid = tomlObj.TryGetValueArray(); if (useruid == null) ctx.Errors.Add(" Field has invalid data."); else MatchClientUid = new HashSet(useruid); return true; case "perm": - var perm = TomlTools.GetValues(tomlObj); + var perm = tomlObj.TryGetValueArray(); if (perm == null) ctx.Errors.Add(" Field has invalid data."); else MatchPermission = new HashSet(perm); return true; case "apitoken": - var apitoken = TomlTools.GetValues(tomlObj); + var apitoken = tomlObj.TryGetValueArray(); if (apitoken == null) ctx.Errors.Add(" Field has invalid data."); else MatchToken = new HashSet(apitoken); return true; + case "bot": + var bot = tomlObj.TryGetValueArray(); + if (bot == null) ctx.Errors.Add(" Field has invalid data."); + else MatchBot = new HashSet(bot); + return true; case "isapi": - if (!TomlTools.TryGetValue(tomlObj, out var isapi)) ctx.Errors.Add(" Field has invalid data."); + if (!tomlObj.TryGetValue(out var isapi)) ctx.Errors.Add(" Field has invalid data."); else MatchIsApi = isapi; return true; case "visibility": - var visibility = TomlTools.GetValues(tomlObj); + var visibility = tomlObj.TryGetValueArray(); if (visibility == null) ctx.Errors.Add(" Field has invalid data."); else MatchVisibility = visibility; return true; diff --git a/TS3AudioBot/Rights/TomlTools.cs b/TS3AudioBot/Rights/TomlTools.cs deleted file mode 100644 index 65adbf9f..00000000 --- a/TS3AudioBot/Rights/TomlTools.cs +++ /dev/null @@ -1,123 +0,0 @@ -// TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2017 TS3AudioBot contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3AudioBot.Rights -{ - using Nett; - using System; - using System.Linq; - using System.Text; - - internal static class TomlTools - { - public static T[] GetValues(TomlObject tomlObj) - { - if (TryGetValue(tomlObj, out T retSingleVal)) - return new[] { retSingleVal }; - else if (tomlObj.TomlType == TomlObjectType.Array) - { - var tomlArray = (TomlArray)tomlObj; - var retArr = new T[tomlArray.Length]; - for (int i = 0; i < tomlArray.Length; i++) - if (!TryGetValue(tomlArray.Items[i], out retArr[i])) - return null; - return retArr; - } - return null; - } - - public static bool TryGetValue(TomlObject tomlObj, out T value) - { - switch (tomlObj.TomlType) - { - case TomlObjectType.Int: - if (typeof(T) == typeof(long)) - { - value = ((TomlValue)tomlObj).Value; - return true; - } - else if (typeof(T) == typeof(ulong) - || typeof(T) == typeof(uint) || typeof(T) == typeof(int) - || typeof(T) == typeof(ushort) || typeof(T) == typeof(short) - || typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)) - { - try - { - value = (T)Convert.ChangeType(((TomlInt)tomlObj).Value, typeof(T)); - return true; - } - catch (OverflowException) { } - } - break; - - case TomlObjectType.Bool: - case TomlObjectType.Float: - case TomlObjectType.DateTime: - case TomlObjectType.TimeSpan: - if (tomlObj is TomlValue tomlValue && typeof(T) == tomlValue.Value.GetType()) - { - value = tomlValue.Value; - return true; - } - break; - - case TomlObjectType.String: - if (typeof(T).IsEnum) - { - try - { - value = (T)Enum.Parse(typeof(T), ((TomlString)tomlObj).Value, true); - return true; - } - catch (ArgumentException) { } - catch (OverflowException) { } - } - else if (typeof(T) == typeof(string)) - { - - value = ((TomlValue)tomlObj).Value; - return true; - } - break; - } - value = default(T); - return false; - } - - private static string ToString(TomlObject obj) - { - var strb = new StringBuilder(); - //strb.Append(" : "); - switch (obj.TomlType) - { - case TomlObjectType.Bool: strb.Append(((TomlBool) obj).Value); break; - case TomlObjectType.Int: strb.Append(((TomlInt) obj).Value); break; - case TomlObjectType.Float: strb.Append(((TomlFloat) obj).Value); break; - case TomlObjectType.String: strb.Append(((TomlString) obj).Value); break; - case TomlObjectType.DateTime: strb.Append(((TomlDateTime) obj).Value); break; - case TomlObjectType.TimeSpan: strb.Append(((TomlDuration) obj).Value); break; - case TomlObjectType.Array: - strb.Append("[ ") - .Append(string.Join(", ", ((TomlArray)obj).Items.Select(ToString))) - .Append(" ]"); - break; - case TomlObjectType.Table: - foreach (var kvp in (TomlTable)obj) - strb.Append(kvp.Key).Append(" : { ").Append(ToString(kvp.Value)).AppendLine(" }"); - break; - case TomlObjectType.ArrayOfTables: - strb.Append("[ ") - .Append(string.Join(", ", ((TomlTableArray)obj).Items.Select(ToString))) - .Append(" ]"); - break; - } - return strb.ToString(); - } - } -} diff --git a/TS3AudioBot/Sessions/SessionManager.cs b/TS3AudioBot/Sessions/SessionManager.cs index f70a6fe6..2754a667 100644 --- a/TS3AudioBot/Sessions/SessionManager.cs +++ b/TS3AudioBot/Sessions/SessionManager.cs @@ -10,6 +10,7 @@ namespace TS3AudioBot.Sessions { using Helper; + using System; using System.Collections.Generic; using TS3Client.Messages; @@ -26,16 +27,16 @@ public SessionManager() Util.Init(out openSessions); } - public UserSession CreateSession(ClientData client) + public UserSession GetOrCreateSession(ushort clientId) { lock (openSessions) { - if (openSessions.TryGetValue(client.ClientId, out var session)) + if (openSessions.TryGetValue(clientId, out var session)) return session; - Log.Debug("User {0} created session with the bot", client.Name); + Log.Debug("ClientId {0} created session with the bot", clientId); session = new UserSession(); - openSessions.Add(client.ClientId, session); + openSessions.Add(clientId, session); return session; } } @@ -47,7 +48,7 @@ public R GetSession(ushort id) if (openSessions.TryGetValue(id, out var session)) return session; else - return "Session not found"; + return R.Err; } } diff --git a/TS3AudioBot/Sessions/TokenManager.cs b/TS3AudioBot/Sessions/TokenManager.cs index 6b968c5d..fb53c45d 100644 --- a/TS3AudioBot/Sessions/TokenManager.cs +++ b/TS3AudioBot/Sessions/TokenManager.cs @@ -11,12 +11,13 @@ namespace TS3AudioBot.Sessions { using Helper; using LiteDB; + using Localization; using System; using System.Collections.Generic; public class TokenManager { - private const string TokenFormat = "{0}:" + Web.WebManager.WebRealm + ":{1}"; + private const string TokenFormat = "{0}:" + Web.WebServer.WebRealm + ":{1}"; private const string ApiTokenTable = "apiToken"; private LiteCollection dbTokenList; @@ -39,7 +40,7 @@ public void Initialize() Database.GetMetaData(ApiTokenTable); } - public R GenerateToken(string uid, TimeSpan? timeout = null) + public string GenerateToken(string uid, TimeSpan? timeout = null) { if (string.IsNullOrEmpty(uid)) throw new ArgumentNullException(nameof(uid)); @@ -65,7 +66,7 @@ public R GenerateToken(string uid, TimeSpan? timeout = null) ValidUntil = token.Timeout }); - return R.OkR(string.Format(TokenFormat, uid, token.Value)); + return string.Format(TokenFormat, uid, token.Value); } private static DateTime AddTimeSpanSafe(DateTime dateTime, TimeSpan addSpan) @@ -84,7 +85,7 @@ private static DateTime AddTimeSpanSafe(DateTime dateTime, TimeSpan addSpan) } } - internal R GetToken(string uid) + internal R GetToken(string uid) { if (liveTokenList.TryGetValue(uid, out var token) && token.ApiTokenActive) @@ -92,12 +93,12 @@ internal R GetToken(string uid) var dbToken = dbTokenList.FindById(uid); if (dbToken == null) - return "No active Token"; + return new LocalStr(strings.error_no_active_token); if (dbToken.ValidUntil < Util.GetNow()) { dbTokenList.Delete(uid); - return "No active Token"; + return new LocalStr(strings.error_no_active_token); } token = new ApiToken { Value = dbToken.Token }; diff --git a/TS3AudioBot/Sessions/UserSession.cs b/TS3AudioBot/Sessions/UserSession.cs index cc56aa4a..de451656 100644 --- a/TS3AudioBot/Sessions/UserSession.cs +++ b/TS3AudioBot/Sessions/UserSession.cs @@ -47,13 +47,13 @@ public R Get() VerifyLock(); if (assocMap == null) - return "Value not set"; + return R.Err; if (!assocMap.TryGetValue(typeof(TAssoc), out object value)) - return "Value not set"; + return R.Err; if (value?.GetType() != typeof(TData)) - return "Invalid request type"; + return R.Err; return (TData)value; } @@ -71,9 +71,9 @@ public void Set(TData data) assocMap.Add(typeof(TAssoc), data); } - public SessionToken GetLock() + public SessionLock GetLock() { - var sessionToken = new SessionToken(this); + var sessionToken = new SessionLock(this); sessionToken.Take(); return sessionToken; } @@ -84,10 +84,10 @@ private void VerifyLock() throw new InvalidOperationException("No access lock is currently active"); } - public sealed class SessionToken : IDisposable + public sealed class SessionLock : IDisposable { private readonly UserSession session; - public SessionToken(UserSession session) { this.session = session; } + public SessionLock(UserSession session) { this.session = session; } public void Take() { Monitor.Enter(session); session.lockToken = true; } public void Free() { Monitor.Exit(session); session.lockToken = false; } diff --git a/TS3AudioBot/Setup.cs b/TS3AudioBot/Setup.cs new file mode 100644 index 00000000..80c85380 --- /dev/null +++ b/TS3AudioBot/Setup.cs @@ -0,0 +1,172 @@ +namespace TS3AudioBot +{ + using Helper.Environment; + using NLog; + using System; + + internal static class Setup + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public static bool VerifyAll() + { + return VerifyLogSetup() + && VerifyMono() + && VerifyLibopus(); + } + + public static bool VerifyLogSetup() + { + if (LogManager.Configuration == null || LogManager.Configuration.AllTargets.Count == 0) + { + Console.WriteLine("No or empty NLog config found.\n" + + "You can copy the default config from TS3AudioBot/NLog.config.\n" + + "Please refer to https://github.com/NLog/NLog/wiki/Configuration-file " + + "to learn more how to set up your own logging configuration."); + + if (LogManager.Configuration == null) + { + Console.WriteLine("Create a default config to prevent this step."); + Console.WriteLine("Do you want to continue? [Y/N]"); + while (true) + { + var key = Console.ReadKey().Key; + if (key == ConsoleKey.N) + return false; + if (key == ConsoleKey.Y) + break; + } + } + } + return true; + } + + public static bool VerifyMono() + { + if (SystemData.RuntimeData.Runtime == Runtime.Mono) + { + if (SystemData.RuntimeData.SemVer == null) + { + Log.Warn("Could not find your running mono version!"); + Log.Warn("This version might not work properly."); + Log.Warn("If you encounter any problems, try installing the latest mono version by following https://www.mono-project.com/download/"); + } + else if (SystemData.RuntimeData.SemVer < new Version(5, 0, 0)) + { + Log.Error("You are running a mono version below 5.0.0!"); + Log.Error("This version is not supported and will not work properly."); + Log.Error("Install the latest mono version by following https://www.mono-project.com/download/"); + return false; + } + } + return true; + } + + public static bool VerifyLibopus() + { + bool loaded = TS3Client.Audio.Opus.NativeMethods.PreloadLibrary(); + if (!loaded) + Log.Error("Couldn't find libopus. Make sure it is installed or placed in the correct folder."); + return loaded; + } + + public static ParameterData ReadParameter(string[] args) + { + var data = new ParameterData { Exit = ExitType.No, }; + + ParameterData Cancel() { data.Exit = ExitType.Immediately; return data; } + + for (int i = 0; i < args.Length; i++) + { + // -i --interactive, minimal ui/console tool to execute basic stuff like + // create bot, excute commands + + // --setup setup the entire environment (-y to skip for user input?) + // > mono (apt-get/upgrade to latest version, + package upgade) + // > libopus (self-compile/apt-get) + // > ffmpeg (apt-get) + // > youtube-dl (repo/apt-get) + // > check NLog.config exists + // > Ask for Uid/Group id to insert into rigths.toml template + // > Crete new bot (see --new-bot) + + // --new-bot name={} address={} server_password={} ? + + switch (args[i]) + { + case "?": + case "-h": + case "--help": + Console.WriteLine(" --config -c Specifies the path to the config file."); + Console.WriteLine(" --version -V Gets the bot version."); + Console.WriteLine(" --skip-checks Skips checking the system for all required tools."); + Console.WriteLine(" --hide-banner Does not print the version information header."); + Console.WriteLine(" --non-interactive Disables console prompts from setup tools."); + Console.WriteLine(" --help -h Prints this help..."); + return Cancel(); + + case "-c": + case "--config": + if (i + 1 >= args.Length) + { + Console.WriteLine("No config file specified after \"{0}\"", args[i]); + return Cancel(); + } + data.ConfigFile = args[++i]; + break; + + case "--skip-checks": + data.SkipVerifications = true; + break; + + case "--hide-banner": + data.HideBanner = true; + break; + + case "--non-interactive": + data.NonInteractive = true; + break; + + case "-V": + case "--version": + Console.WriteLine(SystemData.AssemblyData.ToLongString()); + return Cancel(); + + default: + Console.WriteLine("Unrecognized parameter: {0}", args[i]); + return Cancel(); + } + } + return data; + } + + public static void LogHeader() + { + Log.Info("[============ TS3AudioBot started =============]"); + Log.Info("[=== Date/Time: {0} {1}", DateTime.Now.ToLongDateString(), DateTime.Now.ToLongTimeString()); + Log.Info("[=== Version: {0}", SystemData.AssemblyData); + Log.Info("[=== Platform: {0}", SystemData.PlattformData); + Log.Info("[=== Runtime: {0}", SystemData.RuntimeData.FullName); + Log.Info("[=== Opus: {0}", TS3Client.Audio.Opus.NativeMethods.Info); + // ffmpeg + // youtube-dl + Log.Info("[==============================================]"); + } + } + + internal class ParameterData + { + public ExitType Exit { get; set; } + public string ConfigFile { get; set; } + public bool SkipVerifications { get; set; } + public bool HideBanner { get; set; } + public bool NonInteractive { get; set; } + } + + internal enum ExitType + { + No, + Immediately, + AfterSetup, + } +} diff --git a/TS3AudioBot/TS3AudioBot.csproj b/TS3AudioBot/TS3AudioBot.csproj index 39514b2a..e023333d 100644 --- a/TS3AudioBot/TS3AudioBot.csproj +++ b/TS3AudioBot/TS3AudioBot.csproj @@ -1,273 +1,79 @@ - - - + + - Release - AnyCPU - {0ECC38F3-DE6E-4D7F-81EB-58B15F584635} Exe - Properties + net46;netcoreapp2.0;netstandard2.0 + 7.2 TS3AudioBot TS3AudioBot - v4.6 - 512 - - true - true - Disk - false - Foreground - 7 - Days - false - false - true - false - true - 7 - - - - + + AnyCPU + false + Media\favicon.ico - - + + false + + en + TS3AudioBot.Core + ../TS3AudioBot.ruleset - - true - bin\Debug\ - DEBUG;TRACE - full - AnyCPU - prompt - false - - - - 7 + + + true + true + false + false + false - - bin\Release\ - TRACE - true - pdbonly - AnyCPU - prompt - false - - - - - ..\packages\LiteDB.4.1.2\lib\net40\LiteDB.dll - - - ..\packages\Nett.0.9.0\lib\Net40\Nett.dll - - - ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\NLog.4.5.2\lib\net45\NLog.dll - - - ..\packages\PropertyChanged.Fody.2.2.1.0\lib\net452\PropertyChanged.dll - - - - - ..\packages\System.Memory.4.5.0-preview1-26216-02\lib\netstandard1.1\System.Memory.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0-preview1-26216-02\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + PreserveNewest Designer - - - - Designer - - - - - - {0eb99e9d-87e5-4534-a100-55d231c2b6a6} - TS3Client - - - - - False - Microsoft .NET Framework 4.5 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - + + + + + + - + + - + + True + True + strings.resx + + - - + + ResXFileCodeGenerator + strings.Designer.cs + - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - + + - - \ No newline at end of file + + diff --git a/TS3AudioBot/TeamspeakControl.cs b/TS3AudioBot/TeamspeakControl.cs deleted file mode 100644 index b38ac59b..00000000 --- a/TS3AudioBot/TeamspeakControl.cs +++ /dev/null @@ -1,410 +0,0 @@ -// TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2017 TS3AudioBot contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3AudioBot -{ - using RExtensions; - using Helper; - using System; - using System.Collections.Generic; - using System.Linq; - using TS3Client; - using TS3Client.Commands; - using TS3Client.Full; - using TS3Client.Helper; - using TS3Client.Messages; - using TS3Client.Query; - - public abstract class TeamspeakControl : IDisposable - { - private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - - public event EventHandler OnMessageReceived; - private void ExtendedTextMessage(object sender, IEnumerable eventArgs) - { - if (OnMessageReceived == null) return; - foreach (var evData in eventArgs) - { - var me = GetSelf(); - if (me.Ok && evData.InvokerId == me.Value.ClientId) - continue; - OnMessageReceived?.Invoke(sender, evData); - } - } - - public event EventHandler OnClientConnect; - private void ExtendedClientEnterView(object sender, IEnumerable eventArgs) - { - clientbufferOutdated = true; - if (OnClientConnect == null) return; - foreach (var evData in eventArgs) - { - clientbufferOutdated = true; - OnClientConnect?.Invoke(sender, evData); - } - } - - public event EventHandler OnClientDisconnect; - private void ExtendedClientLeftView(object sender, IEnumerable eventArgs) - { - clientbufferOutdated = true; - if (OnClientDisconnect == null) return; - foreach (var evData in eventArgs) - { - clientbufferOutdated = true; - OnClientDisconnect?.Invoke(sender, evData); - } - } - - public abstract event EventHandler OnBotConnected; - public abstract event EventHandler OnBotDisconnect; - - private List clientbuffer; - private bool clientbufferOutdated = true; - private readonly Cache clientDbNames; - - protected Ts3BaseFunctions tsBaseClient; - - protected TeamspeakControl(ClientType connectionType) - { - Util.Init(out clientDbNames); - Util.Init(out clientbuffer); - - if (connectionType == ClientType.Full) - tsBaseClient = new Ts3FullClient(EventDispatchType.DoubleThread); - else if (connectionType == ClientType.Query) - tsBaseClient = new Ts3QueryClient(EventDispatchType.DoubleThread); - - tsBaseClient.OnClientLeftView += ExtendedClientLeftView; - tsBaseClient.OnClientEnterView += ExtendedClientEnterView; - tsBaseClient.OnTextMessageReceived += ExtendedTextMessage; - } - - public virtual T GetLowLibrary() where T : class - { - if (typeof(T) == typeof(Ts3BaseFunctions) && tsBaseClient != null) - return tsBaseClient as T; - return null; - } - - public abstract void Connect(); - - public R SendMessage(string message, ushort clientId) - { - if (Ts3String.TokenLength(message) > Ts3Const.MaxSizeTextMessage) - return "The message to send is longer than the maximum of " + Ts3Const.MaxSizeTextMessage + " characters"; - return tsBaseClient.SendPrivateMessage(message, clientId).ToR(Extensions.ErrorFormat); - } - - public R SendChannelMessage(string message) - { - if (Ts3String.TokenLength(message) > Ts3Const.MaxSizeTextMessage) - return "The message to send is longer than the maximum of " + Ts3Const.MaxSizeTextMessage + " characters"; - return tsBaseClient.SendChannelMessage(message).ToR(Extensions.ErrorFormat); - } - - public R SendServerMessage(string message) - { - if (Ts3String.TokenLength(message) > Ts3Const.MaxSizeTextMessage) - return "The message to send is longer than the maximum of " + Ts3Const.MaxSizeTextMessage + " characters"; - return tsBaseClient.SendServerMessage(message, 1).ToR(Extensions.ErrorFormat); - } - - public R KickClientFromServer(ushort clientId) => tsBaseClient.KickClientFromServer(new[] { clientId }).ToR(Extensions.ErrorFormat); - public R KickClientFromChannel(ushort clientId) => tsBaseClient.KickClientFromChannel(new[] { clientId }).ToR(Extensions.ErrorFormat); - - public R ChangeDescription(string description) - { - var me = GetSelf(); - if (!me.Ok) - return "Internal error (me==null)"; - - return tsBaseClient.ChangeDescription(description, me.Value.ClientId).ToR(Extensions.ErrorFormat); - } - - public R ChangeBadges(string badgesString) => tsBaseClient.ChangeBadges(badgesString).ToR(Extensions.ErrorFormat); - - public R ChangeName(string name) - { - var result = tsBaseClient.ChangeName(name); - if (result.Ok) - return R.OkR; - - if (result.Error.Id == Ts3ErrorCode.parameter_invalid_size) - return "The new name is too long or invalid"; - else - return result.Error.ErrorFormat(); - } - - public R GetClientById(ushort id) - { - var result = ClientBufferRequest(client => client.ClientId == id); - if (result.Ok) return result; - Log.Debug("Slow double request due to missing or wrong permission configuration!"); - var result2 = tsBaseClient.Send("clientinfo", new CommandParameter("clid", id)).WrapSingle(); - if (!result2.Ok) - return "No client found"; - ClientData cd = result2.Value; - cd.ClientId = id; - clientbuffer.Add(cd); - return cd; - } - - public R GetClientByName(string name) - { - var refreshResult = RefreshClientBuffer(false); - if (!refreshResult) - return refreshResult.Error; - var clients = Algorithm.Filter.DefaultAlgorithm.Filter( - clientbuffer.Select(cb => new KeyValuePair(cb.Name, cb)), name).ToArray(); - if (clients.Length <= 0) - return "No client found"; - return clients[0].Value; - } - - private R ClientBufferRequest(Func pred) - { - var refreshResult = RefreshClientBuffer(false); - if (!refreshResult) - return refreshResult.Error; - var clientData = clientbuffer.FirstOrDefault(pred); - if (clientData == null) - return "No client found"; - return clientData; - } - - public abstract R GetSelf(); - - public R RefreshClientBuffer(bool force) - { - if (clientbufferOutdated || force) - { - var result = tsBaseClient.ClientList(ClientListOptions.uid); - if (!result) - return $"Clientlist failed ({result.Error.ErrorFormat()})"; - clientbuffer = result.Value.ToList(); - clientbufferOutdated = false; - } - return R.OkR; - } - - public R GetClientServerGroups(ulong dbId) - { - var result = tsBaseClient.ServerGroupsByClientDbId(dbId); - if (!result.Ok) - return "No client found."; - return result.Value.Select(csg => csg.ServerGroupId).ToArray(); - } - - public R GetDbClientByDbId(ulong clientDbId) - { - if (clientDbNames.TryGetValue(clientDbId, out var clientData)) - return clientData; - - var result = tsBaseClient.ClientDbInfo(clientDbId); - if (!result.Ok) - return "No client found."; - clientData = result.Value; - clientDbNames.Store(clientDbId, clientData); - return clientData; - } - - public R GetClientInfoById(ushort id) => tsBaseClient.ClientInfo(id).ToR("No client found."); - - internal R SetupRights(string key, MainBotData mainBotData) - { - var me = GetSelf(); - if (!me.Ok) - return me.Error; - - // Check all own server groups - var result = GetClientServerGroups(me.Value.DatabaseId); - var groups = result.Ok ? result.Value : Array.Empty(); - - // Add self to master group (via token) - if (!string.IsNullOrEmpty(key)) - tsBaseClient.PrivilegeKeyUse(key); - - // Remember new group (or check if in new group at all) - if (result.Ok) - result = GetClientServerGroups(me.Value.DatabaseId); - var groupsNew = result.Ok ? result.Value : Array.Empty(); - var groupDiff = groupsNew.Except(groups).ToArray(); - - if (mainBotData.BotGroupId == 0) - { - // Create new Bot group - var botGroup = tsBaseClient.ServerGroupAdd("ServerBot"); - if (botGroup.Ok) - { - mainBotData.BotGroupId = botGroup.Value.ServerGroupId; - - // Add self to new group - tsBaseClient.ServerGroupAddClient(botGroup.Value.ServerGroupId, me.Value.DatabaseId); - } - } - - const int max = 75; - const int ava = 500000; // max size in bytes for the avatar - - // Add various rights to the bot group - var permresult = tsBaseClient.ServerGroupAddPerm(mainBotData.BotGroupId, - new[] { - PermissionId.i_client_whisper_power, // + Required for whisper channel playing - PermissionId.i_client_private_textmessage_power, // + Communication - PermissionId.b_client_server_textmessage_send, // + Communication - PermissionId.b_client_channel_textmessage_send, // + Communication, could be used but not yet - - PermissionId.b_client_modify_dbproperties, // ? Dont know but seems also required for the next one - PermissionId.b_client_modify_description, // + Used to change the description of our bot - PermissionId.b_client_info_view, // (+) only used as fallback usually - PermissionId.b_virtualserver_client_list, // ? Dont know but seems also required for the next one - - PermissionId.i_channel_subscribe_power, // + Required to find user to communicate - PermissionId.b_virtualserver_client_dbinfo, // + Required to get basic user information for history, api, etc... - PermissionId.i_client_talk_power, // + Required for normal channel playing - PermissionId.b_client_modify_own_description, // ? not sure if this makes b_client_modify_description superfluous - - PermissionId.b_group_is_permanent, // + Group should stay even if bot disconnects - PermissionId.i_client_kick_from_channel_power, // + Optional for kicking - PermissionId.i_client_kick_from_server_power, // + Optional for kicking - PermissionId.i_client_max_clones_uid, // + In case that bot times out and tries to join again - - PermissionId.b_client_ignore_antiflood, // + The bot should be resistent to forced spam attacks - PermissionId.b_channel_join_ignore_password, // + The noble bot will not abuse this power - PermissionId.b_channel_join_permanent, // + Allow joining to all channel even on strict servers - PermissionId.b_channel_join_semi_permanent, // + Allow joining to all channel even on strict servers - - PermissionId.b_channel_join_temporary, // + Allow joining to all channel even on strict servers - PermissionId.b_channel_join_ignore_maxclients, // + Allow joining full channels - PermissionId.i_channel_join_power, // + Allow joining to all channel even on strict servers - PermissionId.b_client_permissionoverview_view, // + Scanning through given perms for rights system - - PermissionId.i_client_max_avatar_filesize, // + Uploading thumbnails as avatar - PermissionId.b_client_use_channel_commander, // + Enable channel commander - }, - new[] { - max, max, 1, 1, - 1, 1, 1, 1, - max, 1, max, 1, - 1, max, max, 4, - 1, 1, 1, 1, - 1, 1, max, 1, - ava, 1, - }, - new[] { - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, - }, - new[] { - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, false, false, - false, false, - }); - - // Leave master group again - if (groupDiff.Length > 0) - { - foreach (var grp in groupDiff) - tsBaseClient.ServerGroupDelClient(grp, me.Value.DatabaseId); - } - - if (!result) - { - Log.Warn(permresult.Error.ErrorFormat()); - return "Auto setup failed! (See logs for more details)"; - } - - return R.OkR; - } - - public R UploadAvatar(System.IO.Stream stream) => tsBaseClient.UploadAvatar(stream).ToR(Extensions.ErrorFormat); - - public R MoveTo(ulong channelId, string password = null) - { - var me = GetSelf(); - if (!me.Ok) - return me.Error; - return tsBaseClient.ClientMove(me.Value.ClientId, channelId, password).ToR("Cannot move there."); - } - - public R SetChannelCommander(bool isCommander) - { - if (!(tsBaseClient is Ts3FullClient tsFullClient)) - return "Commander mode not available"; - return tsFullClient.ChangeIsChannelCommander(isCommander).ToR("Cannot set commander mode"); - } - public R IsChannelCommander() - { - var me = GetSelf(); - if (!me.Ok) - return me.Error; - var getInfoResult = GetClientInfoById(me.Value.ClientId); - if (!getInfoResult.Ok) - return getInfoResult.Error; - return getInfoResult.Value.IsChannelCommander; - } - - public virtual void Dispose() - { - if (tsBaseClient != null) - { - tsBaseClient.Dispose(); - tsBaseClient = null; - } - } - } - - namespace RExtensions - { - internal static class RExtentions - { - public static R ToR(this E result, string message) - { - if (!result.Ok) - return R.Err(message); - else - return R.OkR; - } - - public static R ToR(this E result, Func fromError) - { - if (!result.Ok) - return R.Err(fromError(result.Error)); - else - return R.OkR; - } - - public static R ToR(this R result, string message) - { - if (!result.Ok) - return R.Err(message); - else - return R.OkR(result.Value); - } - - public static R ToR(this R result, Func fromError) - { - if (!result.Ok) - return R.Err(fromError(result.Error)); - else - return R.OkR(result.Value); - } - } - } -} diff --git a/TS3AudioBot/Ts3Client.cs b/TS3AudioBot/Ts3Client.cs new file mode 100644 index 00000000..926b6547 --- /dev/null +++ b/TS3AudioBot/Ts3Client.cs @@ -0,0 +1,715 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot +{ + using Audio; + using Config; + using Helper; + using Helper.Environment; + using Localization; + using RExtensions; + using System; + using System.Collections.Generic; + using System.Linq; + using TS3Client; + using TS3Client.Audio; + using TS3Client.Commands; + using TS3Client.Full; + using TS3Client.Helper; + using TS3Client.Messages; + + public sealed class Ts3Client : IPlayerConnection, IDisposable + { + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + private const Codec SendCodec = Codec.OpusMusic; + + public event EventHandler OnBotConnected; + public event EventHandler OnBotDisconnect; + public event EventHandler OnMessageReceived; + public event EventHandler OnClientConnect; + public event EventHandler OnClientDisconnect; + + private static readonly string[] QuitMessages = { + "I'm outta here", "You're boring", "Have a nice day", "Bye", "Good night", + "Nothing to do here", "Taking a break", "Lorem ipsum dolor sit amet…", + "Nothing can hold me back", "It's getting quiet", "Drop the bazzzzzz", + "Never gonna give you up", "Never gonna let you down", "Keep rockin' it", + "?", "c(ꙩ_Ꙩ)ꜿ", "I'll be back", "Your advertisement could be here", + "connection lost", "disconnected", "Requested by API.", + "Robert'); DROP TABLE students;--", "It works!! No, wait...", + "Notice me, senpai", ":wq" + }; + + private bool closed = false; + private TickWorker reconnectTick = null; + public static readonly TimeSpan TooManyClonesReconnectDelay = TimeSpan.FromSeconds(30); + private int reconnectCounter; + private static readonly TimeSpan[] LostConnectionReconnectDelay = new[] { + TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5) }; + private static int MaxReconnects { get; } = LostConnectionReconnectDelay.Length; + + private readonly ConfBot config; + internal Ts3FullClient TsFullClient { get; } + private IdentityData identity; + private List clientbuffer; + private bool clientbufferOutdated = true; + private readonly Cache clientDbNames; + + private readonly StallCheckPipe stallCheckPipe; + private readonly VolumePipe volumePipe; + private readonly FfmpegProducer ffmpegProducer; + private readonly PreciseTimedPipe timePipe; + private readonly PassiveMergePipe mergePipe; + private readonly EncoderPipe encoderPipe; + internal CustomTargetPipe TargetPipe { get; } + + public Ts3Client(ConfBot config) + { + Util.Init(out clientDbNames); + Util.Init(out clientbuffer); + + TsFullClient = new Ts3FullClient(EventDispatchType.DoubleThread); + TsFullClient.OnClientLeftView += ExtendedClientLeftView; + TsFullClient.OnClientEnterView += ExtendedClientEnterView; + TsFullClient.OnTextMessage += ExtendedTextMessage; + TsFullClient.OnErrorEvent += TsFullClient_OnErrorEvent; + TsFullClient.OnConnected += TsFullClient_OnConnected; + TsFullClient.OnDisconnected += TsFullClient_OnDisconnected; + + int ScaleBitrate(int value) => Math.Min(Math.Max(1, value), 255) * 1000; + + this.config = config; + this.config.Audio.Bitrate.Changed += (s, e) => encoderPipe.Bitrate = ScaleBitrate(e.NewValue); + + ffmpegProducer = new FfmpegProducer(config.GetParent().Tools.Ffmpeg); + stallCheckPipe = new StallCheckPipe(); + volumePipe = new VolumePipe(); + Volume = config.Audio.Volume.Default; + encoderPipe = new EncoderPipe(SendCodec) { Bitrate = ScaleBitrate(config.Audio.Bitrate) }; + timePipe = new PreciseTimedPipe { ReadBufferSize = encoderPipe.PacketSize }; + timePipe.Initialize(encoderPipe); + TargetPipe = new CustomTargetPipe(TsFullClient); + mergePipe = new PassiveMergePipe(); + + mergePipe.Add(ffmpegProducer); + mergePipe.Into(timePipe).Chain().Chain(stallCheckPipe).Chain(volumePipe).Chain(encoderPipe).Chain(TargetPipe); + + identity = null; + } + + public E Connect() + { + // get or compute identity + var identityConf = config.Connect.Identity; + if (string.IsNullOrEmpty(identityConf.Key)) + { + identity = Ts3Crypt.GenerateNewIdentity(); + identityConf.Key.Value = identity.PrivateKeyString; + identityConf.Offset.Value = identity.ValidKeyOffset; + } + else + { + var identityResult = Ts3Crypt.LoadIdentityDynamic(identityConf.Key.Value, identityConf.Offset.Value); + if (!identityResult.Ok) + { + Log.Error("The identity from the config file is corrupted. Remove it to generate a new one next start; or try to repair it."); + return "Corrupted identity"; + } + identity = identityResult.Value; + identityConf.Key.Value = identity.PrivateKeyString; + identityConf.Offset.Value = identity.ValidKeyOffset; + } + + // check required security level + if (identityConf.Level.Value >= 0 && identityConf.Level.Value <= 160) + UpdateIndentityToSecurityLevel(identityConf.Level.Value); + else if (identityConf.Level.Value != -1) + Log.Warn("Invalid config value for 'Level', enter a number between '0' and '160' or '-1' to adapt automatically."); + config.SaveWhenExists(); + + TsFullClient.QuitMessage = QuitMessages[Util.Random.Next(0, QuitMessages.Length)]; + return ConnectClient(); + } + + private E ConnectClient() + { + StopReconnectTickWorker(); + if (closed) + return "Bot disposed"; + + VersionSign versionSign; + if (!string.IsNullOrEmpty(config.Connect.ClientVersion.Build.Value)) + { + var versionConf = config.Connect.ClientVersion; + versionSign = new VersionSign(versionConf.Build, versionConf.Platform.Value, versionConf.Sign); + + if (!versionSign.CheckValid()) + { + Log.Warn("Invalid version sign, falling back to unknown :P"); + versionSign = VersionSign.VER_WIN_3_X_X; + } + } + else if (SystemData.IsLinux) + { + versionSign = VersionSign.VER_LIN_3_1_10; + } + else + { + versionSign = VersionSign.VER_WIN_3_1_10; + } + + try + { + var connectionConfig = new ConnectionDataFull + { + Username = config.Connect.Name, + ServerPassword = config.Connect.ServerPassword.Get(), + Address = config.Connect.Address, + Identity = identity, + VersionSign = versionSign, + DefaultChannel = config.Connect.Channel, + DefaultChannelPassword = config.Connect.ChannelPassword.Get(), + }; + config.SaveWhenExists(); + + TsFullClient.Connect(connectionConfig); + return R.Ok; + } + catch (Ts3Exception qcex) + { + Log.Info(qcex, "There is either a problem with your connection configuration, or the bot has not all permissions it needs."); + return "Connect error"; + } + } + + private void UpdateIndentityToSecurityLevel(int targetLevel) + { + if (Ts3Crypt.GetSecurityLevel(identity) < targetLevel) + { + Log.Info("Calculating up to required security level: {0}", targetLevel); + Ts3Crypt.ImproveSecurity(identity, targetLevel); + config.Connect.Identity.Offset.Value = identity.ValidKeyOffset; + } + } + + private void StopReconnectTickWorker() + { + var reconnectTickLocal = reconnectTick; + reconnectTick = null; + if (reconnectTickLocal != null) + TickPool.UnregisterTicker(reconnectTickLocal); + } + + [Obsolete(AttributeStrings.UnderDevelopment)] + public void MixInStreamOnce(StreamAudioProducer producer) + { + mergePipe.Add(producer); + producer.HitEnd += (s, e) => mergePipe.Remove(producer); + timePipe.Paused = false; + } + + #region Ts3Client functions wrapper + + public E SendMessage(string message, ushort clientId) + { + if (Ts3String.TokenLength(message) > Ts3Const.MaxSizeTextMessage) + return new LocalStr(strings.error_ts_msg_too_long); + return TsFullClient.SendPrivateMessage(message, clientId).FormatLocal(); + } + + public E SendChannelMessage(string message) + { + if (Ts3String.TokenLength(message) > Ts3Const.MaxSizeTextMessage) + return new LocalStr(strings.error_ts_msg_too_long); + return TsFullClient.SendChannelMessage(message).FormatLocal(); + } + + public E SendServerMessage(string message) + { + if (Ts3String.TokenLength(message) > Ts3Const.MaxSizeTextMessage) + return new LocalStr(strings.error_ts_msg_too_long); + return TsFullClient.SendServerMessage(message, 1).FormatLocal(); + } + + public E KickClientFromServer(ushort clientId) => TsFullClient.KickClientFromServer(new[] { clientId }).FormatLocal(); + public E KickClientFromChannel(ushort clientId) => TsFullClient.KickClientFromChannel(new[] { clientId }).FormatLocal(); + + public E ChangeDescription(string description) + => TsFullClient.ChangeDescription(description, TsFullClient.ClientId).FormatLocal(); + + public E ChangeBadges(string badgesString) + { + if (!badgesString.StartsWith("overwolf=") && !badgesString.StartsWith("badges=")) + badgesString = "overwolf=0:badges=" + badgesString; + return TsFullClient.ChangeBadges(badgesString).FormatLocal(); + } + + public E ChangeName(string name) + { + var result = TsFullClient.ChangeName(name); + if (result.Ok) + return R.Ok; + + if (result.Error.Id == Ts3ErrorCode.parameter_invalid_size) + return new LocalStr(strings.error_ts_invalid_name); + else + return result.Error.FormatLocal(); + } + + public R GetCachedClientById(ushort id) => ClientBufferRequest(client => client.ClientId == id); + + public R GetFallbackedClientById(ushort id) + { + var result = ClientBufferRequest(client => client.ClientId == id); + if (result.Ok) + return result; + Log.Warn("Slow double request due to missing or wrong permission configuration!"); + var result2 = TsFullClient.Send("clientinfo", new CommandParameter("clid", id)).WrapSingle(); + if (!result2.Ok) + return new LocalStr(strings.error_ts_no_client_found); + ClientData cd = result2.Value; + cd.ClientId = id; + clientbuffer.Add(cd); + return cd; + } + + public R GetClientByName(string name) + { + var refreshResult = RefreshClientBuffer(false); + if (!refreshResult) + return refreshResult.Error; + var clients = Algorithm.Filter.DefaultAlgorithm.Filter( + clientbuffer.Select(cb => new KeyValuePair(cb.Name, cb)), name).ToArray(); + if (clients.Length <= 0) + return new LocalStr(strings.error_ts_no_client_found); + return clients[0].Value; + } + + private R ClientBufferRequest(Predicate pred) + { + var refreshResult = RefreshClientBuffer(false); + if (!refreshResult) + return refreshResult.Error; + var clientData = clientbuffer.Find(pred); + if (clientData == null) + return new LocalStr(strings.error_ts_no_client_found); + return clientData; + } + + public E RefreshClientBuffer(bool force) + { + if (clientbufferOutdated || force) + { + var result = TsFullClient.ClientList(ClientListOptions.uid); + if (!result) + { + Log.Debug("Clientlist failed ({0})", result.Error.ErrorFormat()); + return result.Error.FormatLocal(); + } + clientbuffer = result.Value.ToList(); + clientbufferOutdated = false; + } + return R.Ok; + } + + public R GetClientServerGroups(ulong dbId) + { + var result = TsFullClient.ServerGroupsByClientDbId(dbId); + if (!result.Ok) + return new LocalStr(strings.error_ts_no_client_found); + return result.Value.Select(csg => csg.ServerGroupId).ToArray(); + } + + public R GetDbClientByDbId(ulong clientDbId) + { + if (clientDbNames.TryGetValue(clientDbId, out var clientData)) + return clientData; + + var result = TsFullClient.ClientDbInfo(clientDbId); + if (!result.Ok) + return new LocalStr(strings.error_ts_no_client_found); + clientData = result.Value; + clientDbNames.Store(clientDbId, clientData); + return clientData; + } + + public R GetClientInfoById(ushort id) => TsFullClient.ClientInfo(id).FormatLocal(() => strings.error_ts_no_client_found); + + internal bool SetupRights(string key) + { + // TODO get own dbid !!! + var dbResult = TsFullClient.ClientGetDbIdFromUid(identity.ClientUid); + if (!dbResult.Ok) + { + Log.Error("Getting own dbid failed ({0})", dbResult.Error.ErrorFormat()); + return false; + } + var myDbId = dbResult.Value.ClientDbId; + + // Check all own server groups + var getGroupResult = GetClientServerGroups(myDbId); + var groups = getGroupResult.Ok ? getGroupResult.Value : Array.Empty(); + + // Add self to master group (via token) + if (!string.IsNullOrEmpty(key)) + { + var privKeyUseResult = TsFullClient.PrivilegeKeyUse(key); + if (!privKeyUseResult.Ok) + { + Log.Error("Using privilege key failed ({0})", privKeyUseResult.Error.ErrorFormat()); + return false; + } + } + + // Remember new group (or check if in new group at all) + var groupDiff = Array.Empty(); + if (getGroupResult.Ok) + { + getGroupResult = GetClientServerGroups(myDbId); + var groupsNew = getGroupResult.Ok ? getGroupResult.Value : Array.Empty(); + groupDiff = groupsNew.Except(groups).ToArray(); + } + + if (config.BotGroupId == 0) + { + // Create new Bot group + var botGroup = TsFullClient.ServerGroupAdd("ServerBot"); + if (botGroup.Ok) + { + config.BotGroupId.Value = botGroup.Value.ServerGroupId; + + // Add self to new group + var grpresult = TsFullClient.ServerGroupAddClient(botGroup.Value.ServerGroupId, myDbId); + if (!grpresult.Ok) + Log.Error("Adding group failed ({0})", grpresult.Error.ErrorFormat()); + } + } + + const int max = 75; + const int ava = 500000; // max size in bytes for the avatar + + // Add various rights to the bot group + var permresult = TsFullClient.ServerGroupAddPerm(config.BotGroupId.Value, + new[] { + PermissionId.i_client_whisper_power, // + Required for whisper channel playing + PermissionId.i_client_private_textmessage_power, // + Communication + PermissionId.b_client_server_textmessage_send, // + Communication + PermissionId.b_client_channel_textmessage_send, // + Communication, could be used but not yet + + PermissionId.b_client_modify_dbproperties, // ? Dont know but seems also required for the next one + PermissionId.b_client_modify_description, // + Used to change the description of our bot + PermissionId.b_client_info_view, // (+) only used as fallback usually + PermissionId.b_virtualserver_client_list, // ? Dont know but seems also required for the next one + + PermissionId.i_channel_subscribe_power, // + Required to find user to communicate + PermissionId.b_virtualserver_client_dbinfo, // + Required to get basic user information for history, api, etc... + PermissionId.i_client_talk_power, // + Required for normal channel playing + PermissionId.b_client_modify_own_description, // ? not sure if this makes b_client_modify_description superfluous + + PermissionId.b_group_is_permanent, // + Group should stay even if bot disconnects + PermissionId.i_client_kick_from_channel_power, // + Optional for kicking + PermissionId.i_client_kick_from_server_power, // + Optional for kicking + PermissionId.i_client_max_clones_uid, // + In case that bot times out and tries to join again + + PermissionId.b_client_ignore_antiflood, // + The bot should be resistent to forced spam attacks + PermissionId.b_channel_join_ignore_password, // + The noble bot will not abuse this power + PermissionId.b_channel_join_permanent, // + Allow joining to all channel even on strict servers + PermissionId.b_channel_join_semi_permanent, // + Allow joining to all channel even on strict servers + + PermissionId.b_channel_join_temporary, // + Allow joining to all channel even on strict servers + PermissionId.b_channel_join_ignore_maxclients, // + Allow joining full channels + PermissionId.i_channel_join_power, // + Allow joining to all channel even on strict servers + PermissionId.b_client_permissionoverview_view, // + Scanning through given perms for rights system + + PermissionId.i_client_max_avatar_filesize, // + Uploading thumbnails as avatar + PermissionId.b_client_use_channel_commander, // + Enable channel commander + }, + new[] { + max, max, 1, 1, + 1, 1, 1, 1, + max, 1, max, 1, + 1, max, max, 4, + 1, 1, 1, 1, + 1, 1, max, 1, + ava, 1, + }, + new[] { + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, + }, + new[] { + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, false, false, + false, false, + }); + + if (!permresult) + Log.Error("Adding permissions failed ({0})", permresult.Error.ErrorFormat()); + + // Leave master group again + if (groupDiff.Length > 0) + { + foreach (var grp in groupDiff) + { + var grpresult = TsFullClient.ServerGroupDelClient(grp, myDbId); + if (!grpresult.Ok) + Log.Error("Removing group failed ({0})", grpresult.Error.ErrorFormat()); + } + } + + return true; + } + + public E UploadAvatar(System.IO.Stream stream) => TsFullClient.UploadAvatar(stream).FormatLocal(); + + public E MoveTo(ulong channelId, string password = null) + => TsFullClient.ClientMove(TsFullClient.ClientId, channelId, password).FormatLocal(() => strings.error_ts_cannot_move); + + public E SetChannelCommander(bool isCommander) + => TsFullClient.ChangeIsChannelCommander(isCommander).FormatLocal(() => strings.error_ts_cannot_set_commander); + + public R IsChannelCommander() + { + var getInfoResult = GetClientInfoById(TsFullClient.ClientId); + if (!getInfoResult.Ok) + return getInfoResult.Error; + return getInfoResult.Value.IsChannelCommander; + } + + public R GetSelf() => TsFullClient.ClientInfo(TsFullClient.ClientId).FormatLocal(); + + public void InvalidateClientBuffer() => clientbufferOutdated = true; + + #endregion + + #region Event helper + + private void TsFullClient_OnErrorEvent(object sender, CommandError error) + { + switch (error.Id) + { + case Ts3ErrorCode.whisper_no_targets: + stallCheckPipe.SetStall(); + break; + + default: + Log.Debug("Got ts3 error event: {0}", error.ErrorFormat()); + break; + } + } + + private void TsFullClient_OnDisconnected(object sender, DisconnectEventArgs e) + { + if (e.Error != null) + { + var error = e.Error; + switch (error.Id) + { + case Ts3ErrorCode.client_could_not_validate_identity: + if (config.Connect.Identity.Level.Value == -1) + { + int targetSecLevel = int.Parse(error.ExtraMessage); + UpdateIndentityToSecurityLevel(targetSecLevel); + ConnectClient(); + return; // skip triggering event, we want to reconnect + } + else + { + Log.Warn("The server reported that the security level you set is not high enough." + + "Increase the value to '{0}' or set it to '-1' to generate it on demand when connecting.", error.ExtraMessage); + } + break; + + case Ts3ErrorCode.client_too_many_clones_connected: + if (reconnectCounter++ < MaxReconnects) + { + Log.Warn("Seems like another client with the same identity is already connected. Waiting {0:0} seconds to reconnect.", + TooManyClonesReconnectDelay.TotalSeconds); + reconnectTick = TickPool.RegisterTickOnce(() => ConnectClient(), TooManyClonesReconnectDelay); + return; // skip triggering event, we want to reconnect + } + break; + + default: + Log.Warn("Could not connect: {0}", error.ErrorFormat()); + break; + } + } + else + { + Log.Debug("Bot disconnected. Reason: {0}", e.ExitReason); + + if (reconnectCounter < LostConnectionReconnectDelay.Length && !closed) + { + var delay = LostConnectionReconnectDelay[reconnectCounter++]; + Log.Info("Trying to reconnect. Delaying reconnect for {0:0} seconds", delay.TotalSeconds); + reconnectTick = TickPool.RegisterTickOnce(() => ConnectClient(), delay); + return; + } + } + + if (reconnectCounter >= LostConnectionReconnectDelay.Length) + { + Log.Warn("Could not (re)connect after {0} tries. Giving up.", reconnectCounter); + } + OnBotDisconnect?.Invoke(this, e); + } + + private void TsFullClient_OnConnected(object sender, EventArgs e) + { + StopReconnectTickWorker(); + reconnectCounter = 0; + OnBotConnected?.Invoke(this, EventArgs.Empty); + } + + private void ExtendedTextMessage(object sender, IEnumerable eventArgs) + { + if (OnMessageReceived == null) return; + foreach (var evData in eventArgs) + { + // Prevent loopback of own textmessages + if (evData.InvokerId == TsFullClient.ClientId) + continue; + OnMessageReceived?.Invoke(sender, evData); + } + } + + private void ExtendedClientEnterView(object sender, IEnumerable eventArgs) + { + clientbufferOutdated = true; + if (OnClientConnect == null) return; + foreach (var evData in eventArgs) + { + clientbufferOutdated = true; + OnClientConnect?.Invoke(sender, evData); + } + } + + private void ExtendedClientLeftView(object sender, IEnumerable eventArgs) + { + clientbufferOutdated = true; + if (OnClientDisconnect == null) return; + foreach (var evData in eventArgs) + { + clientbufferOutdated = true; + OnClientDisconnect?.Invoke(sender, evData); + } + } + + #endregion + + #region IPlayerConnection + + public event EventHandler OnSongEnd + { + add => ffmpegProducer.OnSongEnd += value; + remove => ffmpegProducer.OnSongEnd -= value; + } + + public E AudioStart(string url) + { + var result = ffmpegProducer.AudioStart(url); + if (result) + timePipe.Paused = false; + return result; + } + + public E AudioStop() + { + // TODO clean up all mixins + timePipe.Paused = true; + return ffmpegProducer.AudioStop(); + } + + public TimeSpan Length => ffmpegProducer.Length; + + public TimeSpan Position + { + get => ffmpegProducer.Position; + set => ffmpegProducer.Position = value; + } + + public float Volume + { + get => volumePipe.Volume * AudioValues.MaxVolume; + set + { + if (value < 0) + volumePipe.Volume = 0; + else if (value > AudioValues.MaxVolume) + volumePipe.Volume = AudioValues.MaxVolume; + else + volumePipe.Volume = value / AudioValues.MaxVolume; + } + } + + public bool Paused + { + get => timePipe.Paused; + set => timePipe.Paused = value; + } + + public bool Playing => !timePipe.Paused; + + public bool Repeated { get => false; set { } } + + #endregion + + public void Dispose() + { + closed = true; + StopReconnectTickWorker(); + timePipe?.Dispose(); + ffmpegProducer?.Dispose(); + encoderPipe?.Dispose(); + TsFullClient.Dispose(); + } + } + + namespace RExtensions + { + internal static class RExtentions + { + public static R FormatLocal(this R cmdErr, Func prefix = null) + { + if (cmdErr.Ok) + return cmdErr.Value; + return cmdErr.Error.FormatLocal(prefix); + } + + public static E FormatLocal(this E cmdErr, Func prefix = null) + { + if (cmdErr.Ok) + return R.Ok; + return cmdErr.Error.FormatLocal(prefix); + } + + public static LocalStr FormatLocal(this CommandError err, Func prefix = null) + { + var str = LocalizationManager.GetString("error_ts_code_" + (uint)err.Id) + ?? $"{strings.error_ts_unknown_error} ({err.Message})"; + + if (prefix != null) + str = $"{prefix()} ({str})"; + return new LocalStr(str); + } + } + } +} diff --git a/TS3AudioBot/Ts3Full.cs b/TS3AudioBot/Ts3Full.cs deleted file mode 100644 index 30242663..00000000 --- a/TS3AudioBot/Ts3Full.cs +++ /dev/null @@ -1,392 +0,0 @@ -// TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2017 TS3AudioBot contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3AudioBot -{ - using Audio; - using Helper; - using Helper.Environment; - using System; - using System.Linq; - using System.Reflection; - using TS3Client; - using TS3Client.Audio; - using TS3Client.Full; - using TS3Client.Helper; - using TS3Client.Messages; - - public sealed class Ts3Full : TeamspeakControl, IPlayerConnection - { - private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - private readonly Ts3FullClient tsFullClient; - private ClientData self; - - private const Codec SendCodec = Codec.OpusMusic; - private static readonly string[] QuitMessages = { - "I'm outta here", "You're boring", "Have a nice day", "Bye", "Good night", - "Nothing to do here", "Taking a break", "Lorem ipsum dolor sit amet…", - "Nothing can hold me back", "It's getting quiet", "Drop the bazzzzzz", - "Never gonna give you up", "Never gonna let you down", "Keep rockin' it", - "?", "c(ꙩ_Ꙩ)ꜿ", "I'll be back", "Your advertisement could be here", - "connection lost", "disconnected", "Requested by API.", - "Robert'); DROP TABLE students;--", "It works!! No, wait...", - "Notice me, senpai", ":wq" - }; - - private bool connecting = false; - public bool HasConnection => tsFullClient.Connected || tsFullClient.Connecting || connecting; - - public override event EventHandler OnBotConnected; - public override event EventHandler OnBotDisconnect; - - private readonly Ts3FullClientData ts3FullClientData; - - private IdentityData identity; - - private readonly StallCheckPipe stallCheckPipe; - private readonly VolumePipe volumePipe; - private readonly FfmpegProducer ffmpegProducer; - private readonly PreciseTimedPipe timePipe; - private readonly PassiveMergePipe mergePipe; - private readonly EncoderPipe encoderPipe; - internal CustomTargetPipe TargetPipe { get; private set; } - - public Ts3Full(Ts3FullClientData tfcd) : base(ClientType.Full) - { - tsFullClient = (Ts3FullClient)tsBaseClient; - - ts3FullClientData = tfcd; - tfcd.PropertyChanged += Tfcd_PropertyChanged; - - ffmpegProducer = new FfmpegProducer(tfcd); - stallCheckPipe = new StallCheckPipe(); - volumePipe = new VolumePipe(); - encoderPipe = new EncoderPipe(SendCodec) { Bitrate = ts3FullClientData.AudioBitrate * 1000 }; - timePipe = new PreciseTimedPipe { ReadBufferSize = encoderPipe.PacketSize }; - timePipe.Initialize(encoderPipe); - TargetPipe = new CustomTargetPipe(tsFullClient); - mergePipe = new PassiveMergePipe(); - - mergePipe.Add(ffmpegProducer); - timePipe.InStream = mergePipe; - timePipe.Chain().Chain(stallCheckPipe).Chain(volumePipe).Chain(encoderPipe).Chain(TargetPipe); - - identity = null; - } - - public override T GetLowLibrary() - { - if (typeof(T) == typeof(Ts3FullClient) && tsFullClient != null) - return tsFullClient as T; - return base.GetLowLibrary(); - } - - private void Tfcd_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(Ts3FullClientData.AudioBitrate) && sender is Ts3FullClientData tfcd) - { - var value = tfcd.AudioBitrate; - if (value <= 0 || value >= 256) - return; - encoderPipe.Bitrate = value * 1000; - } - } - - public override void Connect() - { - // get or compute identity - if (string.IsNullOrEmpty(ts3FullClientData.Identity)) - { - identity = Ts3Crypt.GenerateNewIdentity(); - ts3FullClientData.Identity = identity.PrivateKeyString; - ts3FullClientData.IdentityOffset = identity.ValidKeyOffset; - } - else - { - var identityResult = Ts3Crypt.LoadIdentityDynamic(ts3FullClientData.Identity, ts3FullClientData.IdentityOffset); - if (!identityResult.Ok) - { - Log.Error("The identity from the config file is corrupted. Remove it to generate a new one next start; or try to repair it."); - return; - } - identity = identityResult.Value; - if (ts3FullClientData.Identity != identity.PrivateKeyString) - ts3FullClientData.Identity = identity.PrivateKeyString; - if (ts3FullClientData.IdentityOffset != identity.ValidKeyOffset) - ts3FullClientData.IdentityOffset = identity.ValidKeyOffset; - } - - // check required security level - if (ts3FullClientData.IdentityLevel == "auto") { } - else if (int.TryParse(ts3FullClientData.IdentityLevel, out int targetLevel)) - UpdateIndentityToSecurityLevel(targetLevel); - else - Log.Warn("Invalid value for QueryConnection::IdentityLevel, enter a number or \"auto\"."); - - // get or compute password - if (!string.IsNullOrEmpty(ts3FullClientData.ServerPassword) - && ts3FullClientData.ServerPasswordAutoHash - && !ts3FullClientData.ServerPasswordIsHashed) - { - ts3FullClientData.ServerPassword = Ts3Crypt.HashPassword(ts3FullClientData.ServerPassword); - ts3FullClientData.ServerPasswordIsHashed = true; - } - - tsFullClient.QuitMessage = QuitMessages[Util.Random.Next(0, QuitMessages.Length)]; - tsFullClient.OnErrorEvent += TsFullClient_OnErrorEvent; - tsFullClient.OnConnected += TsFullClient_OnConnected; - tsFullClient.OnDisconnected += TsFullClient_OnDisconnected; - ConnectClient(); - } - - private void ConnectClient() - { - VersionSign verionSign = null; - if (!string.IsNullOrEmpty(ts3FullClientData.ClientVersion)) - { - var splitData = ts3FullClientData.ClientVersion.Split('|').Select(x => x.Trim()).ToArray(); - if (splitData.Length == 3) - { - verionSign = new VersionSign(splitData[0], splitData[1], splitData[2]); - } - else if (splitData.Length == 1) - { - var signType = typeof(VersionSign).GetField("VER_" + ts3FullClientData.ClientVersion, - BindingFlags.IgnoreCase | BindingFlags.Static | BindingFlags.Public); - if (signType != null) - verionSign = (VersionSign)signType.GetValue(null); - } - - if (verionSign == null) - { - Log.Warn("Invalid version sign, falling back to unknown :P"); - verionSign = VersionSign.VER_WIN_3_X_X; - } - } - else if (SystemData.IsLinux) - verionSign = VersionSign.VER_LIN_3_1_8; - else - verionSign = VersionSign.VER_WIN_3_1_8; - - connecting = true; - tsFullClient.Connect(new ConnectionDataFull - { - Username = ts3FullClientData.DefaultNickname, - ServerPassword = ts3FullClientData.ServerPasswordIsHashed - ? Password.FromHash(ts3FullClientData.ServerPassword) - : Password.FromPlain(ts3FullClientData.ServerPassword), - Address = ts3FullClientData.Address, - Identity = identity, - VersionSign = verionSign, - DefaultChannel = ts3FullClientData.DefaultChannel, - }); - } - - private void TsFullClient_OnErrorEvent(object sender, CommandError error) - { - switch (error.Id) - { - case Ts3ErrorCode.whisper_no_targets: - stallCheckPipe.SetStall(); - break; - - default: - Log.Debug("Got ts3 error event: {0}", error.ErrorFormat()); - break; - } - } - - private void TsFullClient_OnDisconnected(object sender, DisconnectEventArgs e) - { - if (e.Error != null) - { - var error = e.Error; - switch (error.Id) - { - case Ts3ErrorCode.client_could_not_validate_identity: - if (ts3FullClientData.IdentityLevel == "auto") - { - int targetSecLevel = int.Parse(error.ExtraMessage); - UpdateIndentityToSecurityLevel(targetSecLevel); - ConnectClient(); - return; // skip triggering event, we want to reconnect - } - else - { - Log.Warn("The server reported that the security level you set is not high enough." + - "Increase the value to \"{0}\" or set it to \"auto\" to generate it on demand when connecting.", error.ExtraMessage); - } - break; - - default: - Log.Warn("Could not connect: {0}", error.ErrorFormat()); - break; - } - } - else - { - Log.Debug("Bot disconnected. Reason: {0}", e.ExitReason); - } - - connecting = false; - OnBotDisconnect?.Invoke(this, EventArgs.Empty); - } - - private void TsFullClient_OnConnected(object sender, EventArgs e) - { - connecting = false; - OnBotConnected?.Invoke(this, EventArgs.Empty); - } - - private void UpdateIndentityToSecurityLevel(int targetLevel) - { - if (Ts3Crypt.GetSecurityLevel(identity) < targetLevel) - { - Log.Info("Calculating up to required security level: {0}", targetLevel); - Ts3Crypt.ImproveSecurity(identity, targetLevel); - ts3FullClientData.IdentityOffset = identity.ValidKeyOffset; - } - } - - public override R GetSelf() - { - if (self != null) - return self; - - var result = tsBaseClient.WhoAmI(); - if (!result.Ok) - return $"Could not get self ({result.Error.ErrorFormat()})"; - var data = result.Value; - var cd = new ClientData - { - Uid = identity.ClientUid, - ChannelId = data.ChannelId, - ClientId = tsFullClient.ClientId, - Name = data.Name, - ClientType = tsBaseClient.ClientType - }; - - var response = tsBaseClient - .Send("clientgetdbidfromuid", new TS3Client.Commands.CommandParameter("cluid", identity.ClientUid)) - .WrapSingle(); - if (response.Ok && ulong.TryParse(response.Value["cldbid"], out var dbId)) - cd.DatabaseId = dbId; - - self = cd; - return cd; - } - - [Obsolete(AttributeStrings.UnderDevelopment)] - public void MixInStreamOnce(StreamAudioProducer producer) - { - mergePipe.Add(producer); - producer.HitEnd += (s, e) => mergePipe.Remove(producer); - timePipe.Paused = false; - } - - #region IPlayerConnection - - public event EventHandler OnSongEnd - { - add => ffmpegProducer.OnSongEnd += value; - remove => ffmpegProducer.OnSongEnd -= value; - } - - public R AudioStart(string url) - { - var result = ffmpegProducer.AudioStart(url); - if (result) - timePipe.Paused = false; - return result; - } - - public R AudioStop() - { - // TODO clean up all mixins - timePipe.Paused = true; - return ffmpegProducer.AudioStop(); - } - - public TimeSpan Length => ffmpegProducer.Length; - - public TimeSpan Position - { - get => ffmpegProducer.Position; - set => ffmpegProducer.Position = value; - } - - public float Volume - { - get => volumePipe.Volume * AudioValues.MaxVolume; - set - { - if (value < 0 || value > AudioValues.MaxVolume) - throw new ArgumentOutOfRangeException(nameof(value)); - volumePipe.Volume = value / AudioValues.MaxVolume; - } - } - - public bool Paused - { - get => timePipe.Paused; - set => timePipe.Paused = value; - } - - public bool Playing => !timePipe.Paused; - - public bool Repeated { get => false; set { } } - - #endregion - - public override void Dispose() - { - timePipe?.Dispose(); - ffmpegProducer?.Dispose(); - encoderPipe?.Dispose(); - base.Dispose(); - } - } - - public class Ts3FullClientData : ConfigData - { - [Info("The address (and port, default: 9987) of the TeamSpeak3 server")] - public string Address { get; set; } - [Info("| DO NOT MAKE THIS KEY PUBLIC | The client identity", "")] - public string Identity { get; set; } - [Info("The client identity security offset", "0")] - public ulong IdentityOffset { get; set; } - [Info("The client identity security level which should be calculated before connecting, or \"auto\" to generate on demand.", "auto")] - public string IdentityLevel { get; set; } - [Info("The server password. Leave empty for none.")] - public string ServerPassword { get; set; } - [Info("Set this to true, if the server password is hashed.", "false")] - public bool ServerPasswordIsHashed { get; set; } - [Info("Enable this to automatically hash and store unhashed passwords.\n" + - "# (Be careful since this will overwrite the 'ServerPassword' field with the hashed value once computed)", "false")] - public bool ServerPasswordAutoHash { get; set; } - [Info("The path to ffmpeg", "ffmpeg")] - public string FfmpegPath { get; set; } - [Info("Specifies the bitrate (in kbps) for sending audio.\n" + - "# Values between 8 and 98 are supported, more or less can work but without guarantees.\n" + - "# Reference values: 32 - ok (~5KiB/s), 48 - good (~7KiB/s), 64 - very good (~9KiB/s), 92 - superb (~13KiB/s)", "48")] - public int AudioBitrate { get; set; } - [Info("Version for the client in the form of ||\n" + - "# Leave empty for default.", "")] - public string ClientVersion { get; set; } - [Info("Default Nickname when connecting", "AudioBot")] - public string DefaultNickname { get; set; } - [Info("Default Channel when connectiong\n" + - "# Use a channel path or '/', examples: 'Home/Lobby', '/5', 'Home/Afk \\/ Not Here'", "")] - public string DefaultChannel { get; set; } - [Info("The password for the default channel. Leave empty for none. Not required with permission b_channel_join_ignore_password", "")] - public string DefaultChannelPassword { get; set; } - [Info("The client badges. You can set a comma seperate string with max three GUID's. Here is a list: http://yat.qa/ressourcen/abzeichen-badges/", "overwolf=0:badges=")] - public string ClientBadges { get; set; } - } -} diff --git a/TS3AudioBot/Web/Api/JsonArray.cs b/TS3AudioBot/Web/Api/JsonArray.cs index ca2b2d9a..8d7f3ec3 100644 --- a/TS3AudioBot/Web/Api/JsonArray.cs +++ b/TS3AudioBot/Web/Api/JsonArray.cs @@ -9,8 +9,13 @@ namespace TS3AudioBot.Web.Api { + using System; + public class JsonArray : JsonValue { public JsonArray(T[] value, string msg) : base(value, msg) { } + public JsonArray(T[] value, Func asString = null) + : base(value, asString) + { } } } diff --git a/TS3AudioBot/Web/Api/JsonValue.cs b/TS3AudioBot/Web/Api/JsonValue.cs index 3d443c91..611a15b7 100644 --- a/TS3AudioBot/Web/Api/JsonValue.cs +++ b/TS3AudioBot/Web/Api/JsonValue.cs @@ -10,35 +10,36 @@ namespace TS3AudioBot.Web.Api { using CommandSystem; + using Helper; using Newtonsoft.Json; using System; - public class JsonValue : JsonValueBase + public class JsonValue : JsonValue { - public static Func AsString { get; set; } - public static Func AsJson { get; set; } + protected Func AsString { get; } - public new T Value => (T)base.Value; + new public T Value => (T)base.Value; public JsonValue(T value) : base(value) { } public JsonValue(T value, string msg) : base(value, msg) { } + public JsonValue(T value, Func asString = null) : base(value) + { + AsString = asString; + } public override object GetSerializeObject() { - if (AsJson != null) - return AsJson(Value); - else - return Value; + return Value; } public override string ToString() { if (AsStringResult == null) { - if (Value == null) - AsStringResult = string.Empty; - else if (AsString != null) + if (AsString != null) AsStringResult = AsString.Invoke(Value); + else if (Value == null) + AsStringResult = string.Empty; else AsStringResult = Value.ToString(); } @@ -46,18 +47,12 @@ public override string ToString() } } - public class JsonValueBase : JsonObject + public abstract class JsonValue : JsonObject { - static JsonValueBase() - { - JsonValue.AsString = cmd => cmd.GetHelp(); - JsonValue.AsJson = cmd => cmd.AsJsonObj; - } - protected object Value { get; } - public JsonValueBase(object value) : base(null) { Value = value; } - public JsonValueBase(object value, string msg) : base(msg ?? string.Empty) { Value = value; } + public JsonValue(object value) : base(null) { Value = value; } + public JsonValue(object value, string msg) : base(msg ?? string.Empty) { Value = value; } public override object GetSerializeObject() => Value; @@ -66,6 +61,8 @@ public override string Serialize() var seriObj = GetSerializeObject(); if (seriObj != null && XCommandSystem.BasicTypes.Contains(seriObj.GetType())) return JsonConvert.SerializeObject(this); + if (seriObj is IJsonSerializable jsonSerializable) + return jsonSerializable.ToJson(); return base.Serialize(); } @@ -75,5 +72,11 @@ public override string ToString() AsStringResult = Value?.ToString() ?? string.Empty; return AsStringResult; } + + // static creator methods for anonymous stuff + + public static JsonValue Create(T anon) => new JsonValue(anon); + public static JsonValue Create(T anon, string msg) => new JsonValue(anon, msg); + public static JsonValue Create(T anon, Func asString = null) => new JsonValue(anon, asString); } } diff --git a/TS3AudioBot/Web/Api/OpenApiGenerator.cs b/TS3AudioBot/Web/Api/OpenApiGenerator.cs new file mode 100644 index 00000000..30881c9e --- /dev/null +++ b/TS3AudioBot/Web/Api/OpenApiGenerator.cs @@ -0,0 +1,257 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2017 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3AudioBot.Web.Api +{ + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using System; + using System.Collections.Generic; + using System.Text; + using TS3AudioBot.CommandSystem; + using TS3AudioBot.CommandSystem.CommandResults; + using TS3AudioBot.CommandSystem.Commands; + using TS3AudioBot.Helper; + + public static class OpenApiGenerator + { + private static JsonSerializer seri = JsonSerializer.CreateDefault(); + + static OpenApiGenerator() + { + seri.NullValueHandling = NullValueHandling.Ignore; + } + + public static JObject Generate(CommandManager commandManager, BotInfo[] bots) + { + var paths = new JObject(); + + var addedCommandPaths = new HashSet(); + + foreach (var command in commandManager.AllCommands) + { + var token = GenerateCommand(commandManager, command, addedCommandPaths); + if (token != null) + paths.Add(token); + } + + const string defaultAuthSchemeName = "default_basic"; + + return + new JObject( + new JProperty("openapi", "3.0.0"), + JPropObj("info", + new JProperty("version", "1.0.0"), + new JProperty("title", "Ts3AudioBot API"), + new JProperty("description", "The Ts3AudioBot api interface.") + ), + new JProperty("paths", + paths + ), + new JProperty("servers", + new JArray( + new JObject( + new JProperty("url", "/api"), + new JProperty("description", "Your Ts3AudioBot server.") + ) + ).Chain(x => + { + foreach (var bot in bots) + { + x.Add(new JObject( + new JProperty("url", $"/api/bot/use/{bot.Id}/(/"), + new JProperty("description", $"Bot {bot.Name}") + )); + } + }) + ), + JPropObj("components", + JPropObj("securitySchemes", + JPropObj(defaultAuthSchemeName, + new JProperty("type", "http"), + new JProperty("scheme", "basic") + ) + ) + ), + new JProperty("security", + new JArray( + new JObject( + new JProperty(defaultAuthSchemeName, new JArray()) + ) + ) + ) + ); + } + + private static JToken GenerateCommand(CommandManager commandManager, BotCommand command, HashSet addedCommandPaths) + { + var parameters = new JArray(); + + var pathBuilder = new StringBuilder(); + pathBuilder.Append("/"); + pathBuilder.Append(command.InvokeName.Replace(' ', '/')); + foreach (var param in command.CommandParameter) + { + switch (param.kind) + { + case ParamKind.Unknown: + break; + case ParamKind.SpecialArguments: + case ParamKind.SpecialReturns: + break; + case ParamKind.Dependency: + break; + case ParamKind.NormalCommand: + case ParamKind.NormalParam: + case ParamKind.NormalArray: + if (param.kind == ParamKind.NormalArray) + pathBuilder.Append("/{").Append(param.param.Name).Append("}..."); + else + pathBuilder.Append("/{").Append(param.param.Name).Append("}"); + + var addparam = new JObject( + new JProperty("name", param.param.Name), + new JProperty("in", "path"), + new JProperty("description", "useful help"), + new JProperty("required", true) // param.optional + ); + + var paramschema = NormalToSchema(param.type); + if (paramschema != null) + addparam.Add("schema", JObject.FromObject(paramschema, seri)); + parameters.Add(addparam); + break; + default: + throw Util.UnhandledDefault(param.kind); + } + } + + var path = pathBuilder.ToString(); + + if (addedCommandPaths.Contains(path)) + return null; + addedCommandPaths.Add(path); + + // check tag + + var tags = new JArray(); + int spaceIndex = command.InvokeName.IndexOf(' '); + string baseName = spaceIndex >= 0 ? command.InvokeName.Substring(0, spaceIndex) : command.InvokeName; + var commandroot = commandManager.CommandSystem.RootCommand.GetCommand(baseName); + switch (commandroot) + { + case null: + break; + case CommandGroup group: + tags.Add(baseName); + break; + } + + // build final command + + var reponseschema = NormalToSchema(command.CommandReturn); + + return + JPropObj(path, + JPropObj("get", + new JProperty("tags", tags), + new JProperty("description", command.Description), + new JProperty("parameters", parameters), + new JProperty("responses", + new JObject().Chain(r => + { + if (reponseschema != null) + { + r.Add( + JPropObj("200", + new JProperty("description", "Successful"), + new JProperty("content", + new JObject( + JPropObj("application/json", + new JProperty("schema", JObject.FromObject(reponseschema, seri)) + ) + ) + ) + ) + ); + } + else + { + r.Add( + JPropObj("204", + new JProperty("description", "Successful") + ) + ); + } + }) + ) + ) + ); + } + + private static T Chain(this T token, Action func) where T : JToken + { + func.Invoke(token); + return token; + } + + private static JProperty JPropObj(string name, params object[] token) + { + return new JProperty(name, new JObject(token)); + } + + private static OApiSchema NormalToSchema(Type type) + { + type = FunctionCommand.UnwrapReturnType(type); + + if (type.IsArray) + { + return new OApiSchema + { + type = "array", + items = NormalToSchema(type.GetElementType()) + }; + } + + if (type == typeof(bool)) return OApiSchema.FromBasic("boolean"); + else if (type == typeof(sbyte)) return OApiSchema.FromBasic("integer", "int8"); + else if (type == typeof(byte)) return OApiSchema.FromBasic("integer", "uint8"); + else if (type == typeof(short)) return OApiSchema.FromBasic("integer", "int16"); + else if (type == typeof(ushort)) return OApiSchema.FromBasic("integer", "uint16"); + else if (type == typeof(int)) return OApiSchema.FromBasic("integer", "int32"); + else if (type == typeof(uint)) return OApiSchema.FromBasic("integer", "uint32"); + else if (type == typeof(long)) return OApiSchema.FromBasic("integer", "int64"); + else if (type == typeof(ulong)) return OApiSchema.FromBasic("integer", "uint64"); + else if (type == typeof(float)) return OApiSchema.FromBasic("number", "float"); + else if (type == typeof(double)) return OApiSchema.FromBasic("number", "double"); + else if (type == typeof(TimeSpan)) return OApiSchema.FromBasic("string", "duration"); + else if (type == typeof(DateTime)) return OApiSchema.FromBasic("string", "date-time"); + else if (type == typeof(string)) return OApiSchema.FromBasic("string", null); + else if (type == typeof(JsonEmpty) || type == typeof(void)) return null; + else if (type == typeof(JsonObject) || type == typeof(object) || type == typeof(ICommandResult)) return OApiSchema.FromBasic("object"); + else if (type == typeof(ICommand)) return OApiSchema.FromBasic("λ"); + else + { + return OApiSchema.FromBasic(type.Name); + } + } + + class OApiSchema + { + public string type { get; set; } + public string format { get; set; } + public OApiSchema additionalProperties { get; set; } + public OApiSchema items { get; set; } + + public static OApiSchema FromBasic(string type, string format = null) => new OApiSchema { type = type, format = format }; + + public OApiSchema ObjWrap() => new OApiSchema { type = "object", additionalProperties = this }; + } + } +} diff --git a/TS3AudioBot/Web/Api/WebApi.cs b/TS3AudioBot/Web/Api/WebApi.cs index 86635828..050215b5 100644 --- a/TS3AudioBot/Web/Api/WebApi.cs +++ b/TS3AudioBot/Web/Api/WebApi.cs @@ -96,9 +96,10 @@ private void ProcessApiV1Call(Uri uri, HttpListenerResponse response, InvokerDat else if (res.ResultType == CommandResultType.Json) { response.StatusCode = (int)HttpStatusCode.OK; - var sRes = (JsonCommandResult)res; + var returnJson = (JsonCommandResult)res; + var returnString = returnJson.JsonObject.Serialize(); using (var responseStream = new StreamWriter(response.OutputStream)) - responseStream.Write(sRes.JsonObject.Serialize()); + responseStream.Write(returnString); } } catch (CommandException ex) @@ -137,9 +138,12 @@ private static void ReturnError(CommandExceptionReason reason, string message, H response.StatusCode = (int)HttpStatusCode.Forbidden; break; - case CommandExceptionReason.CommandError: case CommandExceptionReason.AmbiguousCall: case CommandExceptionReason.MissingParameter: + response.StatusCode = (int)HttpStatusCode.BadRequest; + break; + + case CommandExceptionReason.CommandError: case CommandExceptionReason.NoReturnMatch: case CommandExceptionReason.MissingContext: response.StatusCode = 422; // Unprocessable Entity @@ -176,7 +180,7 @@ private static void UnescapeAstTree(AstNode node) } } - private R<(bool anonymous, InvokerData invoker)> Authenticate(HttpListenerContext context) + private R<(bool anonymous, InvokerData invoker), string> Authenticate(HttpListenerContext context) { var identity = GetIdentity(context); if (identity == null) @@ -187,10 +191,8 @@ private static void UnescapeAstTree(AstNode node) return ErrorNoUserOrToken; var token = result.Value; - var invoker = new InvokerData(identity.Name) - { - Token = token.Value, - }; + var invoker = new InvokerData(identity.Name, + token: token.Value); switch (identity.AuthenticationType) { @@ -208,11 +210,11 @@ private static void UnescapeAstTree(AstNode node) if (!identityDigest.IsAuthenticated) { var newNonce = token.CreateNonce(); - context.Response.AddHeader("WWW-Authenticate", $"Digest realm=\"{WebManager.WebRealm}\", nonce=\"{newNonce.Value}\""); + context.Response.AddHeader("WWW-Authenticate", $"Digest realm=\"{WebServer.WebRealm}\", nonce=\"{newNonce.Value}\""); return InfoNonceAdded; } - if (identityDigest.Realm != WebManager.WebRealm) + if (identityDigest.Realm != WebServer.WebRealm) return ErrorUnknownRealm; if (identityDigest.Uri != context.Request.RawUrl) @@ -231,7 +233,7 @@ private static void UnescapeAstTree(AstNode node) ApiNonce nextNonce = token.UseNonce(identityDigest.Nonce); if (nextNonce == null) return ErrorAuthFailure; - context.Response.AddHeader("WWW-Authenticate", $"Digest realm=\"{WebManager.WebRealm}\", nonce=\"{nextNonce.Value}\""); + context.Response.AddHeader("WWW-Authenticate", $"Digest realm=\"{WebServer.WebRealm}\", nonce=\"{nextNonce.Value}\""); return (false, invoker); diff --git a/TS3AudioBot/Web/Interface/FileProvider.cs b/TS3AudioBot/Web/Interface/FileProvider.cs index 7dda70fe..e0a5e23a 100644 --- a/TS3AudioBot/Web/Interface/FileProvider.cs +++ b/TS3AudioBot/Web/Interface/FileProvider.cs @@ -15,7 +15,6 @@ namespace TS3AudioBot.Web.Interface public class FileProvider : ISiteProvider { private byte[] rawData; - private bool loadedOnce = false; public FileInfo LocalFile { get; } public string MimeType { get; } @@ -35,7 +34,7 @@ public byte[] GetData() { LocalFile.Refresh(); - if (!loadedOnce || LocalFile.LastWriteTime >= lastWrite) + if (rawData == null || LocalFile.LastWriteTime >= lastWrite) { rawData = File.ReadAllBytes(LocalFile.FullName); lastWrite = LocalFile.LastWriteTime; diff --git a/TS3AudioBot/Web/Interface/WebDisplay.cs b/TS3AudioBot/Web/Interface/WebDisplay.cs index 92a55885..9808648b 100644 --- a/TS3AudioBot/Web/Interface/WebDisplay.cs +++ b/TS3AudioBot/Web/Interface/WebDisplay.cs @@ -35,12 +35,12 @@ public sealed class WebDisplay : WebComponent, IDisposable { ".less", "text/css" }, }; - public WebDisplay(WebData webData) + public WebDisplay(Config.ConfWebInterface webData) { DirectoryInfo baseDir = null; - if (string.IsNullOrEmpty(webData.WebinterfaceHostPath)) + if (string.IsNullOrEmpty(webData.Path)) { - for (int i = 0; i < 4; i++) + for (int i = 0; i < 5; i++) { var up = Path.Combine(Enumerable.Repeat("..", i).ToArray()); var checkDir = Path.Combine(up, "WebInterface"); @@ -51,8 +51,8 @@ public WebDisplay(WebData webData) } } } - else if (Directory.Exists(webData.WebinterfaceHostPath)) - baseDir = new DirectoryInfo(webData.WebinterfaceHostPath); + else if (Directory.Exists(webData.Path)) + baseDir = new DirectoryInfo(webData.Path); if (baseDir == null) throw new InvalidOperationException("Can't find a WebInterface path to host. Try specifying the path to host in the config"); @@ -60,6 +60,7 @@ public WebDisplay(WebData webData) var dir = new FolderProvider(baseDir); map.Map("/", dir); map.Map("/site/", dir); + map.Map("/openapi/", new FolderProvider(new DirectoryInfo(Path.Combine(baseDir.FullName, "openapi")))); Site404 = map.TryGetSite(new Uri("http://localhost/404.html")); map.Map("/", map.TryGetSite(new Uri("http://localhost/index.html"))); diff --git a/TS3AudioBot/Web/UriExt.cs b/TS3AudioBot/Web/UriExt.cs deleted file mode 100644 index 8c0accb6..00000000 --- a/TS3AudioBot/Web/UriExt.cs +++ /dev/null @@ -1,24 +0,0 @@ -// TS3AudioBot - An advanced Musicbot for Teamspeak 3 -// Copyright (C) 2017 TS3AudioBot contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3AudioBot.Web -{ - using System; - using System.Collections.Specialized; - using System.Web; - - [Serializable] - public class UriExt : Uri - { - private NameValueCollection queryParam; - public NameValueCollection QueryParam => queryParam ?? (queryParam = HttpUtility.ParseQueryString(Query)); - public UriExt(Uri copy) : base(copy.OriginalString) { } - public UriExt(string uri) : base(uri) { } - } -} diff --git a/TS3AudioBot/Web/WebComponent.cs b/TS3AudioBot/Web/WebComponent.cs index ab4d975d..d973e13e 100644 --- a/TS3AudioBot/Web/WebComponent.cs +++ b/TS3AudioBot/Web/WebComponent.cs @@ -14,7 +14,7 @@ namespace TS3AudioBot.Web public abstract class WebComponent { - protected static readonly Uri Dummy = new Uri("http://dummy/"); + internal static readonly Uri Dummy = new Uri("http://dummy/"); public abstract void DispatchCall(HttpListenerContext context); } diff --git a/TS3AudioBot/Web/WebManager.cs b/TS3AudioBot/Web/WebServer.cs similarity index 72% rename from TS3AudioBot/Web/WebManager.cs rename to TS3AudioBot/Web/WebServer.cs index 6a6a20ff..7ccb3c88 100644 --- a/TS3AudioBot/Web/WebManager.cs +++ b/TS3AudioBot/Web/WebServer.cs @@ -9,6 +9,7 @@ namespace TS3AudioBot.Web { + using Config; using Dependency; using Helper; using Helper.Environment; @@ -17,7 +18,7 @@ namespace TS3AudioBot.Web using System.Net; using System.Threading; - public sealed class WebManager : IDisposable + public sealed class WebServer : IDisposable { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); public const string WebRealm = "ts3ab"; @@ -27,17 +28,17 @@ public sealed class WebManager : IDisposable private HttpListener webListener; private Thread serverThread; - private readonly WebData webData; private bool startWebServer; + private readonly ConfWeb config; public CoreInjector Injector { get; set; } public Api.WebApi Api { get; private set; } public Interface.WebDisplay Display { get; private set; } - public WebManager(WebData webData) + public WebServer(ConfWeb config) { - this.webData = webData; + this.config = config; } public void Initialize() @@ -47,21 +48,21 @@ public void Initialize() InitializeSubcomponents(); - StartServerAsync(); + StartServerThread(); } private void InitializeSubcomponents() { startWebServer = false; - if (webData.EnableApi) + if (config.Api.Enabled) { Api = new Api.WebApi(); Injector.RegisterModule(Api); startWebServer = true; } - if (webData.EnableWebinterface) + if (config.Interface.Enabled) { - Display = new Interface.WebDisplay(webData); + Display = new Interface.WebDisplay(config.Interface); Injector.RegisterModule(Display); startWebServer = true; } @@ -79,17 +80,17 @@ private void InitializeSubcomponents() private void ReloadHostPaths() { - localhost = new Uri($"http://localhost:{webData.Port}/"); + localhost = new Uri($"http://localhost:{config.Port.Value}/"); if (Util.IsAdmin || SystemData.IsLinux) // todo: hostlist { - var addrs = webData.HostAddress.SplitNoEmpty(' '); - hostPaths = new Uri[addrs.Length + 1]; + var addrs = config.Hosts.Value; + hostPaths = new Uri[addrs.Count + 1]; hostPaths[0] = localhost; - for (int i = 0; i < addrs.Length; i++) + for (int i = 0; i < addrs.Count; i++) { - var uriBuilder = new UriBuilder(addrs[i]) { Port = webData.Port }; + var uriBuilder = new UriBuilder(addrs[i]) { Port = config.Port }; hostPaths[i + 1] = uriBuilder.Uri; } } @@ -121,7 +122,7 @@ private static AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRe return AuthenticationSchemes.Anonymous; } - public void StartServerAsync() + public void StartServerThread() { if (!startWebServer) return; @@ -144,6 +145,8 @@ public void EnterWebLoop() return; } // TODO + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + while (webListener?.IsListening ?? false) { try @@ -158,8 +161,9 @@ public void EnterWebLoop() } catch (NullReferenceException) { return; } - Log.Info("{0} Requested: {1}", remoteAddress, context.Request.Url.PathAndQuery); - if (context.Request.Url.AbsolutePath.StartsWith("/api/", true, CultureInfo.InvariantCulture)) + var rawRequest = new Uri(WebComponent.Dummy, context.Request.RawUrl); + Log.Info("{0} Requested: {1}", remoteAddress, rawRequest.PathAndQuery); + if (rawRequest.AbsolutePath.StartsWith("/api/", true, CultureInfo.InvariantCulture)) Api?.DispatchCall(context); else Display?.DispatchCall(context); @@ -178,22 +182,4 @@ public void Dispose() webListener = null; } } - - public class WebData : ConfigData - { - [Info("A space seperated list of all urls the web api should be possible to be accessed with", "")] - public string HostAddress { get; set; } - - [Info("The port for the api server", "8180")] - public ushort Port { get; set; } - - [Info("If you want to start the web api server.", "false")] - public bool EnableApi { get; set; } - - [Info("If you want to start the webinterface server", "false")] - public bool EnableWebinterface { get; set; } - - [Info("The folder to host. Leave empty to let the bot look for default locations.", "")] - public string WebinterfaceHostPath { get; set; } - } } diff --git a/TS3AudioBot/packages.config b/TS3AudioBot/packages.config deleted file mode 100644 index ae75f007..00000000 --- a/TS3AudioBot/packages.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/TS3Client/Audio/AudioInterfaces.cs b/TS3Client/Audio/AudioInterfaces.cs index c9d2a2ec..2bca213e 100644 --- a/TS3Client/Audio/AudioInterfaces.cs +++ b/TS3Client/Audio/AudioInterfaces.cs @@ -48,4 +48,18 @@ public interface ISampleInfo int Channels { get; } int BitsPerSample { get; } } + + public sealed class SampleInfo : ISampleInfo + { + public int SampleRate { get; } + public int Channels { get; } + public int BitsPerSample { get; } + + public SampleInfo(int sampleRate, int channels, int bitsPerSample) + { + SampleRate = sampleRate; + Channels = channels; + BitsPerSample = bitsPerSample; + } + } } diff --git a/TS3Client/Audio/AudioPipeExtensions.cs b/TS3Client/Audio/AudioPipeExtensions.cs index 973f99db..d0e52ce4 100644 --- a/TS3Client/Audio/AudioPipeExtensions.cs +++ b/TS3Client/Audio/AudioPipeExtensions.cs @@ -39,5 +39,18 @@ public static T Chain(this IAudioActiveProducer producer, T addConsumer) wher init?.Invoke(addConsumer); return producer.Chain(addConsumer); } + + public static T Into(this IAudioPassiveProducer producer, T reader) where T : IAudioActiveConsumer, new() + { + reader.InStream = producer; + return reader; + } + + public static T Into(this IAudioPassiveProducer producer, Action init = null) where T : IAudioActiveConsumer, new() + { + var reader = new T(); + init?.Invoke(reader); + return producer.Into(reader); + } } } diff --git a/TS3Client/Audio/AudioTools.cs b/TS3Client/Audio/AudioTools.cs new file mode 100644 index 00000000..7ea39dec --- /dev/null +++ b/TS3Client/Audio/AudioTools.cs @@ -0,0 +1,34 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3Client.Audio +{ + using System.Runtime.InteropServices; + + public static class AudioTools + { + public static bool TryMonoToStereo(byte[] pcm, ref int length) + { + if (length / 2 >= pcm.Length) + return false; + + var shortArr = MemoryMarshal.Cast(pcm); + + for (int i = (length / 2) - 1; i >= 0; i--) + { + shortArr[i * 2 + 0] = shortArr[i]; + shortArr[i * 2 + 1] = shortArr[i]; + } + + length *= 2; + + return true; + } + } +} diff --git a/TS3Client/Audio/ActiveCheckPipe.cs b/TS3Client/Audio/CheckActivePipe.cs similarity index 86% rename from TS3Client/Audio/ActiveCheckPipe.cs rename to TS3Client/Audio/CheckActivePipe.cs index 1dc74a12..a9a84789 100644 --- a/TS3Client/Audio/ActiveCheckPipe.cs +++ b/TS3Client/Audio/CheckActivePipe.cs @@ -11,14 +11,14 @@ namespace TS3Client.Audio { using System; - public class ActiveCheckPipe : IAudioPipe + public class CheckActivePipe : IAudioPipe { public bool Active => OutStream?.Active ?? false; public IAudioPassiveConsumer OutStream { get; set; } public void Write(Span data, Meta meta) { - if (OutStream == null || data.Length == 0 || !Active) + if (OutStream == null || data.IsEmpty || !Active) return; OutStream?.Write(data, meta); diff --git a/TS3Client/Audio/ClientMixdown.cs b/TS3Client/Audio/ClientMixdown.cs new file mode 100644 index 00000000..777bb826 --- /dev/null +++ b/TS3Client/Audio/ClientMixdown.cs @@ -0,0 +1,94 @@ +namespace TS3Client.Audio +{ + using Helper; + using System; + using System.Collections.Generic; + + public class ClientMixdown : PassiveMergePipe, IAudioPassiveConsumer + { + public bool Active => true; + + private const int BufferSize = 4096 * 8; + + private readonly Dictionary mixdownBuffer; + + public ClientMixdown() + { + Util.Init(out mixdownBuffer); + } + + public void Write(Span data, Meta meta) + { + if (data.IsEmpty) + return; + + if (!mixdownBuffer.TryGetValue(meta.In.Sender, out var mix)) + { + mix = new ClientMix(BufferSize); + mixdownBuffer.Add(meta.In.Sender, mix); + Add(mix); + } + + mix.Write(data, meta); + /* + List> remove = null; + foreach (var item in mixdownBuffer) + { + if (item.Value.Length == 0) + { + remove = remove ?? new List>(); + remove.Add(item); + } + } + + if (remove != null) + { + foreach (var item in remove) + { + mixdownBuffer.Remove(item.Key); + Remove(item.Value); + } + }*/ + } + + public class ClientMix : IAudioPassiveProducer + { + public byte[] Buffer { get; } + public int Length { get; set; } = 0; + public Meta LastMeta { get; set; } + + private readonly object rwLock = new object(); + + public ClientMix(int bufferSize) + { + Buffer = new byte[bufferSize]; + } + + public void Write(Span data, Meta meta) + { + lock (rwLock) + { + int take = Math.Min(data.Length, Buffer.Length - Length); + data.Slice(0, take).CopyTo(Buffer.AsSpan(Length)); + Length += take; + LastMeta = meta; + } + } + + public int Read(byte[] buffer, int offset, int length, out Meta meta) + { + lock (rwLock) + { + int take = Math.Min(Length, length); + + Array.Copy(Buffer, 0, buffer, offset, take); + Array.Copy(Buffer, take, Buffer, 0, Buffer.Length - take); + Length -= take; + + meta = default; + return take; + } + } + } + } +} diff --git a/TS3Client/Audio/DecoderPipe.cs b/TS3Client/Audio/DecoderPipe.cs new file mode 100644 index 00000000..a7bef4f1 --- /dev/null +++ b/TS3Client/Audio/DecoderPipe.cs @@ -0,0 +1,85 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3Client.Audio +{ + using Opus; + using System; + + public class DecoderPipe : IAudioPipe, IDisposable, ISampleInfo + { + public bool Active => OutStream?.Active ?? false; + public IAudioPassiveConsumer OutStream { get; set; } + + public int SampleRate { get; } = 48_000; + public int Channels { get; } = 2; + public int BitsPerSample { get; } = 16; + + // opus + private OpusDecoder opusVoiceDecoder; + private OpusDecoder opusMusicDecoder; + + private readonly byte[] decodedBuffer; + + public IAudioPassiveConsumer OpusVoicePipeOut { get; set; } + public IAudioActiveProducer OpusVoicePipeIn { get; set; } + + public DecoderPipe() + { + decodedBuffer = new byte[4096 * 2]; + } + + public void Write(Span data, Meta meta) + { + if (OutStream == null || !meta.Codec.HasValue) + return; + + switch (meta.Codec.Value) + { + case Codec.SpeexNarrowband: + throw new NotSupportedException(); + case Codec.SpeexWideband: + throw new NotSupportedException(); + case Codec.SpeexUltraWideband: + throw new NotSupportedException(); + case Codec.CeltMono: + throw new NotSupportedException(); + + case Codec.OpusVoice: + { + opusVoiceDecoder = opusVoiceDecoder ?? OpusDecoder.Create(48000, 1); + var decodedData = opusVoiceDecoder.Decode(data, decodedBuffer.AsSpan(0, decodedBuffer.Length / 2)); + int dataLength = decodedData.Length; + if (!AudioTools.TryMonoToStereo(decodedBuffer, ref dataLength)) + return; + OutStream?.Write(decodedBuffer.AsSpan(0, dataLength), meta); + } + break; + + case Codec.OpusMusic: + { + opusMusicDecoder = opusMusicDecoder ?? OpusDecoder.Create(48000, 2); + var decodedData = opusMusicDecoder.Decode(data, decodedBuffer); + OutStream?.Write(decodedData, meta); + } + break; + + default: + // Cannot decode + return; + } + } + + public void Dispose() + { + opusVoiceDecoder?.Dispose(); + opusMusicDecoder?.Dispose(); + } + } +} diff --git a/TS3Client/Audio/EncoderPipe.cs b/TS3Client/Audio/EncoderPipe.cs index 9c63570f..887f7edf 100644 --- a/TS3Client/Audio/EncoderPipe.cs +++ b/TS3Client/Audio/EncoderPipe.cs @@ -32,7 +32,7 @@ public class EncoderPipe : IAudioPipe, IDisposable, ISampleInfo // todo add upper limit to buffer size and drop everying over private byte[] notEncodedBuffer = Array.Empty(); private int notEncodedLength; - private readonly byte[] encodedBuffer; + private readonly byte[] encodedBuffer = new byte[4096]; public EncoderPipe(Codec codec) { @@ -71,7 +71,6 @@ public EncoderPipe(Codec codec) BitsPerSample = 16; PacketSize = opusEncoder.FrameByteCount(SegmentFrames); - encodedBuffer = new byte[opusEncoder.MaxDataBytes]; } public void Write(Span data, Meta meta) @@ -86,10 +85,10 @@ public void Write(Span data, Meta meta) Array.Copy(notEncodedBuffer, 0, tmpSoundBuffer, 0, notEncodedLength); notEncodedBuffer = tmpSoundBuffer; } - + var soundBuffer = notEncodedBuffer.AsSpan(); data.CopyTo(soundBuffer.Slice(notEncodedLength)); - + int segmentCount = newSoundBufferLength / PacketSize; int segmentsEnd = segmentCount * PacketSize; notEncodedLength = newSoundBufferLength - segmentsEnd; @@ -97,8 +96,8 @@ public void Write(Span data, Meta meta) for (int i = 0; i < segmentCount; i++) { var encodedData = opusEncoder.Encode(soundBuffer.Slice(i * PacketSize, PacketSize), PacketSize, encodedBuffer); - if (meta != null) - meta.Codec = Codec; // TODO copy ? + meta = meta ?? new Meta(); + meta.Codec = Codec; // TODO copy ? OutStream?.Write(encodedData, meta); } diff --git a/TS3Client/Audio/Opus/NativeMethods.cs b/TS3Client/Audio/Opus/NativeMethods.cs index e97610b0..de3d8664 100644 --- a/TS3Client/Audio/Opus/NativeMethods.cs +++ b/TS3Client/Audio/Opus/NativeMethods.cs @@ -30,9 +30,22 @@ namespace TS3Client.Audio.Opus /// public static class NativeMethods { + private static bool isPreloaded = false; + private static bool wasPreloadSuccessful = false; + static NativeMethods() { - NativeWinDllLoader.DirectLoadLibrary("libopus"); + PreloadLibrary(); + } + + public static bool PreloadLibrary() + { + if (!isPreloaded) + { + wasPreloadSuccessful = NativeLibraryLoader.DirectLoadLibrary("libopus", () => opus_get_version_string()); + isPreloaded = true; + } + return wasPreloadSuccessful; } public static string Info @@ -41,7 +54,7 @@ public static string Info { var verStrPtr = opus_get_version_string(); var verString = Marshal.PtrToStringAnsi(verStrPtr); - return $"{verString} ({NativeWinDllLoader.ArchFolder})"; + return $"{verString} ({NativeLibraryLoader.ArchFolder})"; } } @@ -63,7 +76,7 @@ public static string Info internal static extern void opus_decoder_destroy(IntPtr decoder); [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)] - internal static extern int opus_decode(IntPtr st, byte[] data, int len, byte[] pcm, int frameSize, int decodeFec); + internal static extern int opus_decode(IntPtr st, ref byte data, int len, ref byte pcm, int frameSize, int decodeFec); [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)] internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value); diff --git a/TS3Client/Audio/Opus/OpusDecoder.cs b/TS3Client/Audio/Opus/OpusDecoder.cs index 45c5734a..564acab6 100644 --- a/TS3Client/Audio/Opus/OpusDecoder.cs +++ b/TS3Client/Audio/Opus/OpusDecoder.cs @@ -1,5 +1,5 @@ // Copyright 2012 John Carruthers -// +// // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including @@ -7,10 +7,10 @@ // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: -// +// // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -60,7 +60,6 @@ private OpusDecoder(IntPtr decoder, int outputSamplingRate, int outputChannels) this.decoder = decoder; OutputSamplingRate = outputSamplingRate; OutputChannels = outputChannels; - MaxDataBytes = 4000; } /// @@ -68,27 +67,31 @@ private OpusDecoder(IntPtr decoder, int outputSamplingRate, int outputChannels) /// /// Opus encoded data to decode, null for dropped packet. /// Length of data to decode. - /// Set to the length of the decoded sample data. + /// PCM audio samples buffer. /// PCM audio samples. - public byte[] Decode(byte[] inputOpusData, int dataLength, out int decodedLength) + public Span Decode(Span inputOpusData, Span outputDecodedBuffer) { if (disposed) throw new ObjectDisposedException("OpusDecoder"); - byte[] decoded = new byte[MaxDataBytes]; - int frameCount = FrameCount(MaxDataBytes); - int length; - - if (inputOpusData != null) - length = NativeMethods.opus_decode(decoder, inputOpusData, dataLength, decoded, frameCount, 0); - else - length = NativeMethods.opus_decode(decoder, null, 0, decoded, frameCount, (ForwardErrorCorrection) ? 1 : 0); + if (inputOpusData.Length == 0) + return Span.Empty; + + int frameSize = FrameCount(outputDecodedBuffer.Length); + + // TODO fix hacky ref implementation once there is a good alternative with spans + int length = NativeMethods.opus_decode(decoder, ref inputOpusData[0], inputOpusData.Length, ref outputDecodedBuffer[0], frameSize, 0); - decodedLength = length * 2; if (length < 0) throw new Exception("Decoding failed - " + (Errors)length); - return decoded; + // TODO implement forward error corrected packet + //else + // length = NativeMethods.opus_decode(decoder, null, 0, decoded, frameCount, (ForwardErrorCorrection) ? 1 : 0); + + var decodedLength = length * 2 * OutputChannels; + + return outputDecodedBuffer.Slice(0, decodedLength); } /// @@ -114,11 +117,6 @@ public int FrameCount(int bufferSize) /// public int OutputChannels { get; private set; } - /// - /// Gets or sets the size of memory allocated for decoding data. - /// - public int MaxDataBytes { get; set; } - /// /// Gets or sets whether forward error correction is enabled or not. /// diff --git a/TS3Client/Audio/Opus/OpusEncoder.cs b/TS3Client/Audio/Opus/OpusEncoder.cs index d9e2782d..26b30084 100644 --- a/TS3Client/Audio/Opus/OpusEncoder.cs +++ b/TS3Client/Audio/Opus/OpusEncoder.cs @@ -62,7 +62,6 @@ private OpusEncoder(IntPtr encoder, int inputSamplingRate, int inputChannels, Ap InputSamplingRate = inputSamplingRate; InputChannels = inputChannels; Application = application; - MaxDataBytes = 4000; } /// @@ -76,30 +75,28 @@ public Span Encode(Span inputPcmSamples, int sampleLength, byte[] ou { if (disposed) throw new ObjectDisposedException("OpusEncoder"); - if (outputEncodedBuffer.Length < MaxDataBytes) - throw new ArgumentException("Array must be at least MaxDataBytes long", nameof(outputEncodedBuffer)); - int frames = FrameCount(inputPcmSamples); + int frames = FrameCount(inputPcmSamples.Length); // TODO fix hacky ref implementation once there is a good alternative with spans int encodedLength = NativeMethods.opus_encode(encoder, ref inputPcmSamples[0], frames, outputEncodedBuffer, sampleLength); if (encodedLength < 0) throw new Exception("Encoding failed - " + (Errors)encodedLength); - return new Span(outputEncodedBuffer, 0, encodedLength); + return outputEncodedBuffer.AsSpan(0, encodedLength); } /// /// Determines the number of frames in the PCM samples. /// - /// + /// /// - public int FrameCount(ReadOnlySpan pcmSamples) + public int FrameCount(int bufferSize) { // seems like bitrate should be required const int bitrate = 16; int bytesPerSample = (bitrate / 8) * InputChannels; - return pcmSamples.Length / bytesPerSample; + return bufferSize / bytesPerSample; } /// @@ -129,12 +126,6 @@ public int FrameByteCount(int frameCount) /// public Application Application { get; private set; } - /// - /// Gets or sets the size of memory allocated for reading encoded data. - /// 4000 is recommended. - /// - public int MaxDataBytes { get; set; } - /// /// Gets or sets the bitrate setting of the encoding. /// diff --git a/TS3Client/Audio/PassiveMergePipe.cs b/TS3Client/Audio/PassiveMergePipe.cs index 06c093e9..89f024cb 100644 --- a/TS3Client/Audio/PassiveMergePipe.cs +++ b/TS3Client/Audio/PassiveMergePipe.cs @@ -11,6 +11,7 @@ namespace TS3Client.Audio { using System; using System.Collections.Generic; + using System.Runtime.InteropServices; public class PassiveMergePipe : IAudioPassiveProducer { @@ -18,6 +19,7 @@ public class PassiveMergePipe : IAudioPassiveProducer private readonly List producerList = new List(); private readonly object listLock = new object(); private bool changed; + private readonly int[] accBuffer = new int[4096]; public void Add(IAudioPassiveProducer addProducer) { @@ -66,23 +68,25 @@ public int Read(byte[] buffer, int offset, int length, out Meta meta) if (safeProducerList.Count == 1) return safeProducerList[0].Read(buffer, offset, length, out meta); - var pcmBuffer = buffer.AsSpan().NonPortableCast(); - var acc = new int[pcmBuffer.Length]; + int maxReadLength = Math.Min(accBuffer.Length, length); + Array.Clear(accBuffer, 0, maxReadLength); + + var pcmBuffer = MemoryMarshal.Cast(buffer); int read = 0; foreach (var producer in safeProducerList) { - int ppread = producer.Read(buffer, 0, length, out meta); + int ppread = producer.Read(buffer, offset, maxReadLength, out meta); if (ppread == 0) continue; read = Math.Max(read, ppread); for (int i = 0; i < ppread / 2; i++) - acc[i] += pcmBuffer[i]; + accBuffer[i] += pcmBuffer[i]; } for (int i = 0; i < read / 2; i++) - pcmBuffer[i] = (short)Math.Max(Math.Min(acc[i], short.MaxValue), short.MinValue); + pcmBuffer[i] = (short)Math.Max(Math.Min(accBuffer[i], short.MaxValue), short.MinValue); return read; } diff --git a/TS3Client/Audio/PassiveSplitterPipe.cs b/TS3Client/Audio/PassiveSplitterPipe.cs index 13aac1bf..0ecac17a 100644 --- a/TS3Client/Audio/PassiveSplitterPipe.cs +++ b/TS3Client/Audio/PassiveSplitterPipe.cs @@ -13,7 +13,7 @@ namespace TS3Client.Audio using System.Collections.Generic; using System.Linq; - public class PassiveSplitterPipe : IAudioPassiveConsumer + public class PassiveSplitterPipe : IAudioPipe { public bool Active => consumerList.Count > 0 && consumerList.Any(x => x.Active); private readonly List safeConsumerList = new List(); diff --git a/TS3Client/Audio/PreciseTimedPipe.cs b/TS3Client/Audio/PreciseTimedPipe.cs index a465b03d..19536b36 100644 --- a/TS3Client/Audio/PreciseTimedPipe.cs +++ b/TS3Client/Audio/PreciseTimedPipe.cs @@ -21,7 +21,7 @@ public class PreciseTimedPipe : IAudioActiveConsumer, IAudioActiveProducer, IDis public TimeSpan AudioBufferLength { get; set; } = TimeSpan.FromMilliseconds(20); public TimeSpan SendCheckInterval { get; set; } = TimeSpan.FromMilliseconds(5); - public int ReadBufferSize { get; set; } = 2048; + public int ReadBufferSize { get; set; } = 960 * 4; private byte[] readBuffer = Array.Empty(); private readonly object lockObject = new object(); private Thread tickThread; diff --git a/TS3Client/Audio/StaticMetaPipe.cs b/TS3Client/Audio/StaticMetaPipe.cs index d520b90c..ce2a073c 100644 --- a/TS3Client/Audio/StaticMetaPipe.cs +++ b/TS3Client/Audio/StaticMetaPipe.cs @@ -62,6 +62,7 @@ public void Write(Span data, Meta meta) if (OutStream == null || SendMode == TargetSendMode.None) return; + meta = meta ?? new Meta(); meta.Out = meta.Out ?? new MetaOut(); meta.Out.SendMode = SendMode; switch (SendMode) diff --git a/TS3Client/Audio/StreamAudioProducer.cs b/TS3Client/Audio/StreamAudioProducer.cs index a0abe450..371861c7 100644 --- a/TS3Client/Audio/StreamAudioProducer.cs +++ b/TS3Client/Audio/StreamAudioProducer.cs @@ -19,7 +19,7 @@ public class StreamAudioProducer : IAudioPassiveProducer public int Read(byte[] buffer, int offset, int length, out Meta meta) { - meta = default(Meta); + meta = default; int read = stream.Read(buffer, offset, length); if (read < length) HitEnd?.Invoke(this, EventArgs.Empty); diff --git a/TS3Client/Audio/VolumePipe.cs b/TS3Client/Audio/VolumePipe.cs index 47e7bb76..34d85c28 100644 --- a/TS3Client/Audio/VolumePipe.cs +++ b/TS3Client/Audio/VolumePipe.cs @@ -10,6 +10,7 @@ namespace TS3Client.Audio { using System; + using System.Runtime.InteropServices; public class VolumePipe : IAudioPipe { @@ -27,13 +28,13 @@ public static void AdjustVolume(Span audioSamples, float volume) else if (IsAbout(volume, 0.5f)) { // fast calculation for *0.5 volume - var shortArr = audioSamples.NonPortableCast(); + var shortArr = MemoryMarshal.Cast(audioSamples); for (int i = 0; i < shortArr.Length; i++) shortArr[i] = (short)(shortArr[i] >> 1); } else { - var shortArr = audioSamples.NonPortableCast(); + var shortArr = MemoryMarshal.Cast(audioSamples); for (int i = 0; i < shortArr.Length; i++) shortArr[i] = (short)Math.Max(Math.Min(shortArr[i] * volume, short.MaxValue), short.MinValue); } diff --git a/TS3Client/Commands/Ts3Command.cs b/TS3Client/Commands/Ts3Command.cs index 740db346..7d41e214 100644 --- a/TS3Client/Commands/Ts3Command.cs +++ b/TS3Client/Commands/Ts3Command.cs @@ -17,15 +17,17 @@ namespace TS3Client.Commands using System.Text.RegularExpressions; /// Builds TeamSpeak (query) commands from parameters. - public sealed class Ts3Command + public class Ts3Command { private static readonly Regex CommandMatch = new Regex(@"[a-z0-9_]+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ECMAScript); public static List NoParameter => new List(); + protected string raw = null; + protected bool cached = false; internal bool ExpectResponse { get; set; } public string Command { get; } private readonly List parameter; - + /// Creates a new command. /// The command name. [DebuggerStepThrough] @@ -42,12 +44,15 @@ public Ts3Command(string command, List parameter) this.parameter = parameter; } - public Ts3Command AppendParameter(ICommandPart addParameter) + [DebuggerStepThrough] + public virtual Ts3Command AppendParameter(ICommandPart addParameter) { + cached = false; parameter.Add(addParameter); return this; } + [DebuggerStepThrough] internal Ts3Command ExpectsResponse(bool expects) { ExpectResponse = expects; @@ -56,7 +61,15 @@ internal Ts3Command ExpectsResponse(bool expects) /// Builds this command to the query-like command. /// The formatted query-like command. - public override string ToString() => BuildToString(Command, parameter); + public override string ToString() + { + if (!cached) + { + raw = BuildToString(Command, parameter); + cached = true; + } + return raw; + } /// Builds the command from its parameters and returns the query-like command. /// The command name. @@ -127,4 +140,23 @@ public static string BuildToString(string command, IEnumerable par return strb.ToString(); } } + + public class Ts3RawCommand : Ts3Command + { + public Ts3RawCommand(string raw) : base(null) + { + this.raw = raw; + this.cached = true; + } + + public override Ts3Command AppendParameter(ICommandPart addParameter) + { + throw new InvalidOperationException("Raw commands cannot be extented"); + } + + public override string ToString() + { + return raw; + } + } } diff --git a/TS3Client/Commands/Ts3String.cs b/TS3Client/Commands/Ts3String.cs index b48d7de1..563deade 100644 --- a/TS3Client/Commands/Ts3String.cs +++ b/TS3Client/Commands/Ts3String.cs @@ -15,7 +15,7 @@ namespace TS3Client.Commands public static class Ts3String { - public static string Escape(string stringToEscape) => Escape(stringToEscape.AsReadOnlySpan()); + public static string Escape(string stringToEscape) => Escape(stringToEscape.AsSpan()); public static string Escape(ReadOnlySpan stringToEscape) { @@ -39,7 +39,7 @@ public static string Escape(ReadOnlySpan stringToEscape) return strb.ToString(); } - public static string Unescape(string stringToUnescape) => Unescape(stringToUnescape.AsReadOnlySpan()); + public static string Unescape(string stringToUnescape) => Unescape(stringToUnescape.AsSpan()); public static string Unescape(ReadOnlySpan stringToUnescape) { @@ -69,6 +69,36 @@ public static string Unescape(ReadOnlySpan stringToUnescape) return strb.ToString(); } + public static string Unescape(ReadOnlySpan stringToUnescape) + { + // The unescaped string is always equal or shorter than the original. + var strb = new byte[stringToUnescape.Length]; + int writepos = 0; + for (int i = 0; i < stringToUnescape.Length; i++) + { + byte c = stringToUnescape[i]; + if (c == (byte)'\\') + { + if (++i >= stringToUnescape.Length) throw new FormatException(); + switch (stringToUnescape[i]) + { + case (byte)'v': strb[writepos++] = (byte)'\v'; break; // Vertical Tab + case (byte)'t': strb[writepos++] = (byte)'\t'; break; // Horizontal Tab + case (byte)'r': strb[writepos++] = (byte)'\r'; break; // Carriage Return + case (byte)'n': strb[writepos++] = (byte)'\n'; break; // Newline + case (byte)'f': strb[writepos++] = (byte)'\f'; break; // Formfeed + case (byte)'p': strb[writepos++] = (byte)'|'; break; // Pipe + case (byte)'s': strb[writepos++] = (byte)' '; break; // Whitespace + case (byte)'/': strb[writepos++] = (byte)'/'; break; // Slash + case (byte)'\\': strb[writepos++] = (byte)'\\'; break; // Backslash + default: throw new FormatException(); + } + } + else strb[writepos++] = c; + } + return Encoding.UTF8.GetString(strb, 0, writepos); + } + public static int TokenLength(string str) => str.Length + str.Count(IsDoubleChar); public static bool IsDoubleChar(char c) diff --git a/TS3Client/ConnectionData.cs b/TS3Client/ConnectionData.cs index a6d64d83..7a3e848c 100644 --- a/TS3Client/ConnectionData.cs +++ b/TS3Client/ConnectionData.cs @@ -36,7 +36,7 @@ public class ConnectionDataFull : ConnectionData /// The display username. public string Username { get; set; } /// The server password. Leave null if none. - public Password ServerPassword { get; set; } = new Password(); + public Password ServerPassword { get; set; } = Password.Empty; /// /// The default channel this client should try to join when connecting. /// The channel can be specified with either the channel name path, example: "Lobby/Home". @@ -44,40 +44,18 @@ public class ConnectionDataFull : ConnectionData /// public string DefaultChannel { get; set; } = string.Empty; /// Password for the default channel. Leave null if none. - public Password DefaultChannelPassword { get; set; } = new Password(); + public Password DefaultChannelPassword { get; set; } = Password.Empty; } - public class Password + public readonly struct Password { - private string hashedPassword; - private string plainPassword; - /// - /// This can be set to true, when the password is already hashed. - /// The hash works like this: base64(sha1(password)) - /// - private bool isHashed; - public string HashedPassword - { - get - { - if (isHashed && hashedPassword == null) - return string.Empty; - if (!isHashed) - HashedPassword = Ts3Crypt.HashPassword(plainPassword); - return hashedPassword; - } - set - { - hashedPassword = value; - plainPassword = null; - isHashed = true; - } - } - public string PlainPassword { set { plainPassword = value; isHashed = false; } } + public static readonly Password Empty = FromHash(string.Empty); + + public string HashedPassword { get; } - public Password() { } - public static Password FromHash(string hash) => new Password() { HashedPassword = hash }; - public static Password FromPlain(string pass) => new Password() { PlainPassword = pass }; + private Password(string hashed) { HashedPassword = hashed; } + public static Password FromHash(string hash) => new Password(hash); + public static Password FromPlain(string pass) => new Password(Ts3Crypt.HashPassword(pass)); public static implicit operator Password(string pass) => FromPlain(pass); } diff --git a/TS3Client/Declarations b/TS3Client/Declarations index 8f67b954..217a329d 160000 --- a/TS3Client/Declarations +++ b/TS3Client/Declarations @@ -1 +1 @@ -Subproject commit 8f67b9546982f9ba2eae5884aa59a9fb97910e25 +Subproject commit 217a329d76326028235d198e7c901ffd681ce3c0 diff --git a/TS3Client/FileTransferManager.cs b/TS3Client/FileTransferManager.cs index 179139c7..353b6145 100644 --- a/TS3Client/FileTransferManager.cs +++ b/TS3Client/FileTransferManager.cs @@ -16,8 +16,10 @@ namespace TS3Client using System.IO; using System.Linq; using System.Net.Sockets; + using System.Security.Cryptography; using System.Text; using System.Threading; + using System.Threading.Tasks; using ClientUidT = System.String; using ClientDbIdT = System.UInt64; @@ -62,14 +64,18 @@ public R UploadFile(FileInfo file, ChannelIdT c /// False will throw an exception if the file already exists. /// The password for the channel. /// True will the stream after the upload is finished. + /// Will generate a md5 sum of the uploaded file. /// A token to track the file transfer. - public R UploadFile(Stream stream, ChannelIdT channel, string path, bool overwrite = false, string channelPassword = "", bool closeStream = false) + public R UploadFile(Stream stream, ChannelIdT channel, string path, bool overwrite = false, string channelPassword = "", bool closeStream = true, bool createMd5 = false) { ushort cftid = GetFreeTransferId(); var request = parent.FileTransferInitUpload(channel, path, channelPassword, cftid, stream.Length, overwrite, false); if (!request.Ok) + { + if (closeStream) stream.Close(); return request.Error; - var token = new FileTransferToken(stream, request.Value, channel, path, channelPassword, stream.Length) { CloseStreamWhenDone = closeStream }; + } + var token = new FileTransferToken(stream, request.Value, channel, path, channelPassword, stream.Length, createMd5) { CloseStreamWhenDone = closeStream }; StartWorker(token); return token; } @@ -90,12 +96,15 @@ public R DownloadFile(FileInfo file, ChannelIdT /// The password for the channel. /// True will the stream after the download is finished. /// A token to track the file transfer. - public R DownloadFile(Stream stream, ChannelIdT channel, string path, string channelPassword = "", bool closeStream = false) + public R DownloadFile(Stream stream, ChannelIdT channel, string path, string channelPassword = "", bool closeStream = true) { ushort cftid = GetFreeTransferId(); var request = parent.FileTransferInitDownload(channel, path, channelPassword, cftid, 0); if (!request.Ok) + { + if (closeStream) stream.Close(); return request.Error; + } var token = new FileTransferToken(stream, request.Value, channel, path, channelPassword, 0) { CloseStreamWhenDone = closeStream }; StartWorker(token); return token; @@ -166,7 +175,7 @@ public void Abort(FileTransferToken token, bool delete = false) { lock (token) { - if (token.Status != TransferStatus.Trasfering && token.Status != TransferStatus.Waiting) + if (token.Status != TransferStatus.Transfering && token.Status != TransferStatus.Waiting) return; parent.FileTransferStop(token.ServerTransferId, delete); token.Status = TransferStatus.Cancelled; @@ -184,7 +193,7 @@ public R GetStats(FileTransferToken token) { lock (token) { - if (token.Status != TransferStatus.Trasfering) + if (token.Status != TransferStatus.Transfering) return Util.CustomError("No transfer found"); } var result = parent.FileTransferList(); @@ -214,7 +223,7 @@ private void TransferLoop() { if (token.Status != TransferStatus.Waiting) continue; - token.Status = TransferStatus.Trasfering; + token.Status = TransferStatus.Transfering; } Log.Trace("Creating new file transfer connection to {0}", parent.remoteAddress); @@ -226,6 +235,7 @@ private void TransferLoop() token.Status = TransferStatus.Failed; continue; } + using (var md5Dig = token.CreateMd5 ? MD5.Create() : null) using (var stream = client.GetStream()) { byte[] keyBytes = Encoding.ASCII.GetBytes(token.TransferKey); @@ -236,7 +246,18 @@ private void TransferLoop() if (token.Direction == TransferDirection.Upload) { - token.LocalStream.CopyTo(stream); + // https://referencesource.microsoft.com/#mscorlib/system/io/stream.cs,2a0f078c2e0c0aa8,references + const int bufferSize = 81920; + var buffer = new byte[bufferSize]; + int read; + md5Dig?.Initialize(); + while ((read = token.LocalStream.Read(buffer, 0, buffer.Length)) != 0) + { + stream.Write(buffer, 0, read); + md5Dig?.TransformBlock(buffer, 0, read, buffer, 0); + } + md5Dig?.TransformFinalBlock(Array.Empty(), 0, 0); + token.Md5Sum = md5Dig?.Hash; } else // Download { @@ -248,7 +269,7 @@ private void TransferLoop() } lock (token) { - if (token.Status == TransferStatus.Trasfering && token.LocalStream.Position == token.Size) + if (token.Status == TransferStatus.Transfering && token.LocalStream.Position == token.Size) { token.Status = TransferStatus.Done; if (token.CloseStreamWhenDone) @@ -287,24 +308,26 @@ public sealed class FileTransferToken public long SeekPosition { get; internal set; } public string TransferKey { get; internal set; } public bool CloseStreamWhenDone { get; set; } + public bool CreateMd5 { get; } + public byte[] Md5Sum { get; internal set; } public TransferStatus Status { get; internal set; } public FileTransferToken(Stream localStream, FileUpload upload, ChannelIdT channelId, - string path, string channelPassword, long size) + string path, string channelPassword, long size, bool createMd5) : this(localStream, upload.ClientFileTransferId, upload.ServerFileTransferId, TransferDirection.Upload, - channelId, path, channelPassword, upload.Port, upload.SeekPosistion, upload.FileTransferKey, size) + channelId, path, channelPassword, upload.Port, upload.SeekPosistion, upload.FileTransferKey, size, createMd5) { } public FileTransferToken(Stream localStream, FileDownload download, ChannelIdT channelId, string path, string channelPassword, long seekPos) : this(localStream, download.ClientFileTransferId, download.ServerFileTransferId, TransferDirection.Download, - channelId, path, channelPassword, download.Port, seekPos, download.FileTransferKey, download.Size) + channelId, path, channelPassword, download.Port, seekPos, download.FileTransferKey, download.Size, false) { } public FileTransferToken(Stream localStream, ushort cftid, ushort sftid, TransferDirection dir, ChannelIdT channelId, string path, string channelPassword, ushort port, long seekPos, - string transferKey, long size) + string transferKey, long size, bool createMd5) { CloseStreamWhenDone = false; Status = TransferStatus.Waiting; @@ -319,13 +342,24 @@ public FileTransferToken(Stream localStream, ushort cftid, ushort sftid, SeekPosition = seekPos; TransferKey = transferKey; Size = size; + CreateMd5 = createMd5; } public void Wait() { - while (Status == TransferStatus.Waiting || Status == TransferStatus.Trasfering) + while (Status == TransferStatus.Waiting || Status == TransferStatus.Transfering) Thread.Sleep(10); } + + public async Task WaitAsync(CancellationToken token) + { + while (Status == TransferStatus.Waiting || Status == TransferStatus.Transfering) + { + if (token.IsCancellationRequested) + return; + await Task.Delay(10).ConfigureAwait(false); + } + } } public enum TransferDirection @@ -337,7 +371,7 @@ public enum TransferDirection public enum TransferStatus { Waiting, - Trasfering, + Transfering, Done, Cancelled, Failed, diff --git a/TS3Client/Full/BasePacket.cs b/TS3Client/Full/BasePacket.cs deleted file mode 100644 index e4a32050..00000000 --- a/TS3Client/Full/BasePacket.cs +++ /dev/null @@ -1,89 +0,0 @@ -// TS3Client - A free TeamSpeak3 client implementation -// Copyright (C) 2017 TS3Client contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3Client.Full -{ - using Helper; - using System; - - internal abstract class BasePacket - { - public PacketType PacketType - { - get => (PacketType)(PacketTypeFlagged & 0x0F); - set => PacketTypeFlagged = (byte)((PacketTypeFlagged & 0xF0) | ((byte)value & 0x0F)); - } - public PacketFlags PacketFlags - { - get => (PacketFlags)(PacketTypeFlagged & 0xF0); - set => PacketTypeFlagged = (byte)((PacketTypeFlagged & 0x0F) | ((byte)value & 0xF0)); - } - public byte PacketTypeFlagged { get; set; } - public ushort PacketId { get; set; } - public uint GenerationId { get; set; } - public int Size => Data.Length; - public abstract bool FromServer { get; } - public abstract int HeaderLength { get; } - - public byte[] Raw { get; set; } - public byte[] Header { get; protected set; } - public byte[] Data { get; set; } - - public bool FragmentedFlag - { - get => (PacketFlags & PacketFlags.Fragmented) != 0; - set - { - if (value) PacketTypeFlagged |= (byte)PacketFlags.Fragmented; - else PacketTypeFlagged &= (byte)~PacketFlags.Fragmented; - } - } - public bool NewProtocolFlag - { - get => (PacketFlags & PacketFlags.Newprotocol) != 0; - set - { - if (value) PacketTypeFlagged |= (byte)PacketFlags.Newprotocol; - else PacketTypeFlagged &= (byte)~PacketFlags.Newprotocol; - } - } - public bool CompressedFlag - { - get => (PacketFlags & PacketFlags.Compressed) != 0; - set - { - if (value) PacketTypeFlagged |= (byte)PacketFlags.Compressed; - else PacketTypeFlagged &= (byte)~PacketFlags.Compressed; - } - } - public bool UnencryptedFlag - { - get => (PacketFlags & PacketFlags.Unencrypted) != 0; - set - { - if (value) PacketTypeFlagged |= (byte)PacketFlags.Unencrypted; - else PacketTypeFlagged &= (byte)~PacketFlags.Unencrypted; - } - } - - public override string ToString() - { - return $"Type: {PacketType}\tFlags: [ " + - $"{(FragmentedFlag ? "F" : "_")} {(NewProtocolFlag ? "N" : "_")} " + - $"{(CompressedFlag ? "C" : "_")} {(UnencryptedFlag ? "U" : "_")} ]\t" + - $"Id: {PacketId}\n" + - $" MAC: { (Raw == null ? string.Empty : DebugUtil.DebugToHex(Raw.AsSpan().Slice(0, 8))) }\t" + - $" Header: { DebugUtil.DebugToHex(Header) }\n" + - $" Data: { DebugUtil.DebugToHex(Data) }"; - } - - public void BuildHeader() => BuildHeader(Header.AsSpan()); - public abstract void BuildHeader(Span into); - } -} diff --git a/TS3Client/Full/Book/Book.cs b/TS3Client/Full/Book/Book.cs new file mode 100644 index 00000000..65264a4c --- /dev/null +++ b/TS3Client/Full/Book/Book.cs @@ -0,0 +1,151 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3Client.Full.Book +{ + using Messages; + +#pragma warning disable CS8019 + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + using i8 = System.SByte; + using u8 = System.Byte; + using i16 = System.Int16; + using u16 = System.UInt16; + using i32 = System.Int32; + using u32 = System.UInt32; + using i64 = System.Int64; + using u64 = System.UInt64; + using f32 = System.Single; + using d64 = System.Double; + using str = System.String; + + using Duration = System.TimeSpan; + using DurationSeconds = System.TimeSpan; + using DurationMilliseconds = System.TimeSpan; + using SocketAddr = System.Net.IPAddress; + + using Uid = System.String; + using ClientDbId = System.UInt64; + using ClientId = System.UInt16; + using ChannelId = System.UInt64; + using ServerGroupId = System.UInt64; + using ChannelGroupId = System.UInt64; + using IconHash = System.Int32; + using ConnectionId = System.UInt32; +#pragma warning restore CS8019 + + public partial class Connection + { + // TODO + // Many operations can be checked if they were successful (like remove or get). + // In cases which this fails we should print an error. + + private void SetServer(Server server) + { + Server = server; + } + + private Channel GetChannel(ChannelId id) + { + if (Server.Channels.TryGetValue(id, out var channel)) + return channel; + return null; + } + + private void SetChannel(Channel channel, ChannelId id) + { + Server.Channels[id] = channel; + } + + private void RemoveChannel(ChannelId id) + { + Server.Channels.Remove(id); + } + + private Client GetClient(ClientId id) + { + if (Server.Clients.TryGetValue(id, out var client)) + return client; + return null; + } + + private void SetClient(Client client, ClientId id) + { + Server.Clients[id] = client; + } + + private void RemoveClient(ClientId id) + { + Server.Clients.Remove(id); + } + + private void SetConnectionClientData(ConnectionClientData connectionClientData, ClientId id) + { + if (!Server.Clients.TryGetValue(id, out var client)) + return; + client.ConnectionData = connectionClientData; + } + + private void SetServerGroup(ServerGroup serverGroup, ServerGroupId id) + { + Server.Groups[id] = serverGroup; + } + + private Server GetServer() + { + return Server; + } + + // Manual move functions + + private (u16, MaxFamilyClients) MaxClientsCcFun(ChannelCreated msg) => MaxClientsFun(msg.MaxClients, msg.IsMaxClientsUnlimited, msg.MaxFamilyClients, msg.IsMaxFamilyClientsUnlimited, msg.InheritsMaxFamilyClients); + private (u16, MaxFamilyClients) MaxClientsCeFun(ChannelEdited msg) => MaxClientsFun(msg.MaxClients, msg.IsMaxClientsUnlimited, msg.MaxFamilyClients, msg.IsMaxFamilyClientsUnlimited, msg.InheritsMaxFamilyClients); + private (u16, MaxFamilyClients) MaxClientsClFun(ChannelList msg) => MaxClientsFun(msg.MaxClients, msg.IsMaxClientsUnlimited, msg.MaxFamilyClients, msg.IsMaxFamilyClientsUnlimited, msg.InheritsMaxFamilyClients); + private (u16, MaxFamilyClients) MaxClientsFun(i32 MaxClients, bool IsMaxClientsUnlimited, i32 MaxFamilyClients, bool IsMaxFamilyClientsUnlimited, bool InheritsMaxFamilyClients) + { + u16 maxClient; + if (IsMaxClientsUnlimited) + maxClient = u16.MaxValue; // TODO to optional + else + maxClient = (u16)Math.Max(Math.Min(ushort.MaxValue, MaxClients), 0); + var fam = new MaxFamilyClients(); + if (IsMaxFamilyClientsUnlimited) fam.LimitKind = MaxFamilyClientsKind.Unlimited; + else if (InheritsMaxFamilyClients) fam.LimitKind = MaxFamilyClientsKind.Inherited; + else + { + fam.LimitKind = MaxFamilyClientsKind.Limited; + fam.MaxFamiliyClients = (u16)Math.Max(Math.Min(ushort.MaxValue, MaxFamilyClients), 0); + } + return (maxClient, fam); + } + + private ChannelType ChannelTypeCcFun(ChannelCreated msg) => default; // TODO + private ChannelType ChannelTypeCeFun(ChannelEdited msg) => default; // TODO + private ChannelType ChannelTypeClFun(ChannelList msg) => default; // TODO + + private str AwayFun(ClientEnterView msg) => default; // TODO + private TalkPowerRequest TalkPowerFun(ClientEnterView msg) => default; // TODO + private str[] BadgesFun(ClientEnterView msg) => Array.Empty(); // TODO + + private SocketAddr AddressFun(ConnectionInfo msg) => SocketAddr.Any; // TODO + + private void SetClientDataFun(InitServer initServer) + { + OwnClient = initServer.ClientId; + } + + private static bool ReturnFalse(T msg) => false; + private static object ReturnNone(T msg) => null; + } +} diff --git a/TS3Client/Full/Book/SpecialTypes.cs b/TS3Client/Full/Book/SpecialTypes.cs index a80216d8..42329882 100644 --- a/TS3Client/Full/Book/SpecialTypes.cs +++ b/TS3Client/Full/Book/SpecialTypes.cs @@ -6,17 +6,20 @@ // // You should have received a copy of the Open Software License along with this // program. If not, see . + namespace TS3Client.Full.Book { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - public struct MaxFamilyClients { + public ushort MaxFamiliyClients { get; internal set; } + public MaxFamilyClientsKind LimitKind { get; internal set; } + } + public enum MaxFamilyClientsKind + { + Unlimited, + Inherited, + Limited, } public enum ChannelType diff --git a/TS3Client/Full/C2SPacket.cs b/TS3Client/Full/C2SPacket.cs deleted file mode 100644 index c7c26c9d..00000000 --- a/TS3Client/Full/C2SPacket.cs +++ /dev/null @@ -1,49 +0,0 @@ -// TS3Client - A free TeamSpeak3 client implementation -// Copyright (C) 2017 TS3Client contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3Client.Full -{ - using System; - using System.Buffers.Binary; - - internal sealed class C2SPacket : BasePacket - { - public const int HeaderLen = 5; - - public ushort ClientId { get; set; } - public override bool FromServer { get; } = false; - public override int HeaderLength { get; } = HeaderLen; - - public DateTime FirstSendTime { get; set; } - public DateTime LastSendTime { get; set; } - - public C2SPacket(byte[] data, PacketType type) - { - Data = data; - PacketType = type; - Header = new byte[HeaderLen]; - } - - public override void BuildHeader(Span into) - { - BinaryPrimitives.WriteUInt16BigEndian(into.Slice(0, 2), PacketId); - BinaryPrimitives.WriteUInt16BigEndian(into.Slice(2, 2), ClientId); - into[4] = PacketTypeFlagged; -#if DEBUG - into.CopyTo(Header.AsSpan()); -#endif - } - - public override string ToString() - { - BuildHeader(); - return base.ToString(); - } - } -} diff --git a/TS3Client/Full/GenerationWindow.cs b/TS3Client/Full/GenerationWindow.cs new file mode 100644 index 00000000..c432b4dd --- /dev/null +++ b/TS3Client/Full/GenerationWindow.cs @@ -0,0 +1,104 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3Client.Full +{ + using System; + + public sealed class GenerationWindow + { + public int MappedBaseOffset { get; set; } + public uint Generation { get; set; } + public int Mod { get; } + public int ReceiveWindow { get; } + + public GenerationWindow(int mod, int? windowSize = null) + { + Mod = mod; + ReceiveWindow = windowSize ?? (Mod / 2); + } + + public bool SetAndDrag(int mappedValue) + { + var inWindow = IsInWindow(mappedValue); + if (inWindow) + AdvanceToExcluded(mappedValue); + return inWindow; + } + + public void Advance(int amount) + { + if (amount > Mod) + throw new Exception("Cannot advance more than one generation"); + if (amount < 0) + throw new Exception("Cannot advance backwards"); + if (amount == 0) + return; + int newBaseOffset = MappedBaseOffset + amount; + if (newBaseOffset >= Mod) + { + Generation += (uint)(newBaseOffset / Mod); + newBaseOffset %= Mod; + } + MappedBaseOffset = newBaseOffset; + } + + public void AdvanceToExcluded(int mappedValue) + { + var moveDist = (mappedValue - MappedBaseOffset) + 1; + if (moveDist <= 0) + return; + Advance(moveDist); + } + + public bool IsInWindow(int mappedValue) + { + int maxOffset = MappedBaseOffset + ReceiveWindow; + if (maxOffset < Mod) + { + return mappedValue >= MappedBaseOffset && mappedValue < maxOffset; + } + else + { + return mappedValue >= MappedBaseOffset || mappedValue < maxOffset - Mod; + } + } + + public bool IsNextGen(int mappedValue) => + MappedBaseOffset > (Mod - ReceiveWindow) + && mappedValue < (MappedBaseOffset + ReceiveWindow) - Mod; + + public uint GetGeneration(int mappedValue) => (uint)(Generation + (IsNextGen(mappedValue) ? 1 : 0)); + + public int MappedToIndex(int mappedValue) + { + if (mappedValue >= Mod) + throw new ArgumentOutOfRangeException(nameof(mappedValue)); + + if (IsNextGen(mappedValue)) + { + // | XX X> | <= The part from BaseOffset to MappedMod is small enough to consider packets with wrapped numbers again + // /\ NewValue /\ BaseOffset + return (mappedValue + Mod) - MappedBaseOffset; + } + else + { + // | X> XX | + // /\ BaseOffset /\ NewValue // normal case + return mappedValue - MappedBaseOffset; + } + } + + public void Reset() + { + MappedBaseOffset = 0; + Generation = 0; + } + } +} diff --git a/TS3Client/Full/License.cs b/TS3Client/Full/License.cs index 5ee05cff..e36e9916 100644 --- a/TS3Client/Full/License.cs +++ b/TS3Client/Full/License.cs @@ -26,7 +26,7 @@ public class Licenses public List Blocks { get; set; } - public static R Parse(ReadOnlySpan data) + public static R Parse(ReadOnlySpan data) { if (data.Length < 1) return "License too short"; @@ -72,7 +72,7 @@ public abstract class LicenseBlock public DateTime NotValidAfter { get; set; } public byte[] Hash { get; set; } - public static R<(LicenseBlock block, int read)> Parse(ReadOnlySpan data) + public static R<(LicenseBlock block, int read), string> Parse(ReadOnlySpan data) { if (data.Length < MinBlockLen) { @@ -123,12 +123,12 @@ public abstract class LicenseBlock var allLen = MinBlockLen + read; var hash = Ts3Crypt.Hash512It(data.Slice(1, allLen - 1).ToArray()); - block.Hash = hash.AsSpan().Slice(0, 32).ToArray(); + block.Hash = hash.AsSpan(0, 32).ToArray(); return (block, allLen); } - private static R<(string str, int read)> ReadNullString(ReadOnlySpan data) + private static R<(string str, int read), string> ReadNullString(ReadOnlySpan data) { var termIndex = data.IndexOf((byte)0); // C# what? if (termIndex >= 0) diff --git a/TS3Client/Full/NetworkStats.cs b/TS3Client/Full/NetworkStats.cs index 029de432..d27eb93d 100644 --- a/TS3Client/Full/NetworkStats.cs +++ b/TS3Client/Full/NetworkStats.cs @@ -30,7 +30,7 @@ public sealed class NetworkStats private static readonly TimeSpan TimeMinute = TimeSpan.FromMinutes(1); private readonly object queueLock = new object(); - internal void LogOutPacket(C2SPacket packet) + internal void LogOutPacket(ref Packet packet) { var kind = TypeToKind(packet.PacketType); outPackets[(int)kind]++; @@ -42,7 +42,7 @@ internal void LogOutPacket(C2SPacket packet) } } - internal void LogInPacket(S2CPacket packet) + internal void LogInPacket(ref Packet packet) { var kind = TypeToKind(packet.PacketType); inPackets[(int)kind]++; @@ -93,7 +93,7 @@ private static PacketKind TypeToKind(PacketType type) private static long[] GetWithin(Queue queue, TimeSpan time) { var now = Util.Now; - long[] bandwidth = new long[3]; + var bandwidth = new long[3]; foreach (var pack in queue.Reverse()) if (now - pack.SendPoint <= time) bandwidth[(int)pack.Kind] += pack.Size; @@ -207,13 +207,13 @@ private enum PacketKind Control, } - private struct PacketData + private readonly struct PacketData { - public long Size { get; } + public int Size { get; } public DateTime SendPoint { get; } public PacketKind Kind { get; } - public PacketData(long size, DateTime sendPoint, PacketKind kind) { Size = size; SendPoint = sendPoint; Kind = kind; } + public PacketData(int size, DateTime sendPoint, PacketKind kind) { Size = size; SendPoint = sendPoint; Kind = kind; } } } } diff --git a/TS3Client/Full/Packet.cs b/TS3Client/Full/Packet.cs new file mode 100644 index 00000000..fe7dcc73 --- /dev/null +++ b/TS3Client/Full/Packet.cs @@ -0,0 +1,192 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +namespace TS3Client.Full +{ + using Helper; + using System; + using System.Buffers.Binary; + using System.Runtime.CompilerServices; + + internal struct Packet + { + public static bool FromServer { get; } = typeof(TDir) == typeof(S2C); + public static int HeaderLength { get; } = typeof(TDir) == typeof(S2C) ? S2C.HeaderLen : C2S.HeaderLen; + + public PacketType PacketType + { + get => (PacketType)(PacketTypeFlagged & 0x0F); + set => PacketTypeFlagged = (byte)((PacketTypeFlagged & 0xF0) | ((byte)value & 0x0F)); + } + public PacketFlags PacketFlags + { + get => (PacketFlags)(PacketTypeFlagged & 0xF0); + set => PacketTypeFlagged = (byte)((PacketTypeFlagged & 0x0F) | ((byte)value & 0xF0)); + } + public byte PacketTypeFlagged { get; set; } + public ushort PacketId { get; set; } + public uint GenerationId { get; set; } + public int Size => Data.Length; + + public TDir HeaderExt { get; set; } + + public byte[] Raw { get; private set; } + public byte[] Header { get; private set; } + public byte[] Data { get; set; } + + public bool FragmentedFlag + { + get => (PacketFlags & PacketFlags.Fragmented) != 0; + set + { + if (value) PacketTypeFlagged |= (byte)PacketFlags.Fragmented; + else PacketTypeFlagged &= (byte)~PacketFlags.Fragmented; + } + } + public bool NewProtocolFlag + { + get => (PacketFlags & PacketFlags.Newprotocol) != 0; + set + { + if (value) PacketTypeFlagged |= (byte)PacketFlags.Newprotocol; + else PacketTypeFlagged &= (byte)~PacketFlags.Newprotocol; + } + } + public bool CompressedFlag + { + get => (PacketFlags & PacketFlags.Compressed) != 0; + set + { + if (value) PacketTypeFlagged |= (byte)PacketFlags.Compressed; + else PacketTypeFlagged &= (byte)~PacketFlags.Compressed; + } + } + public bool UnencryptedFlag + { + get => (PacketFlags & PacketFlags.Unencrypted) != 0; + set + { + if (value) PacketTypeFlagged |= (byte)PacketFlags.Unencrypted; + else PacketTypeFlagged &= (byte)~PacketFlags.Unencrypted; + } + } + + public Packet(ReadOnlySpan data, PacketType packetType, ushort packetId, uint generationId) : this() + { + Raw = new byte[data.Length + HeaderLength + Ts3Crypt.MacLen]; + Header = new byte[HeaderLength]; + Data = data.ToArray(); + PacketType = packetType; + PacketId = packetId; + GenerationId = generationId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Packet? FromRaw(byte[] raw) + { + if (raw.Length < HeaderLength + Ts3Crypt.MacLen) + return null; + var packet = new Packet + { + Raw = raw, + Header = new byte[HeaderLength], + }; + packet.FromHeader(); + return packet; + } + + public override string ToString() + { + return $"Type: {PacketType}\tFlags: [ " + + $"{(FragmentedFlag ? "F" : "_")} {(NewProtocolFlag ? "N" : "_")} " + + $"{(CompressedFlag ? "C" : "_")} {(UnencryptedFlag ? "U" : "_")} ]\t" + + $"Id: {PacketId}\n" + + $" MAC: { (Raw == null ? string.Empty : DebugUtil.DebugToHex(Raw.AsSpan(0, 8))) }\t" + + $" Header: { DebugUtil.DebugToHex(Header) }\n" + + $" Data: { DebugUtil.DebugToHex(Data) }"; + } + + public void BuildHeader() => BuildHeader(Header); + public void BuildHeader(Span into) + { + // typeof(..) and casts get jitted away, don't worry :) + if (typeof(TDir) == typeof(S2C)) + { + BinaryPrimitives.WriteUInt16BigEndian(into.Slice(0, 2), PacketId); + into[2] = PacketTypeFlagged; + } + else if (typeof(TDir) == typeof(C2S)) + { + var self = (C2S)(object)HeaderExt; + BinaryPrimitives.WriteUInt16BigEndian(into.Slice(0, 2), PacketId); + BinaryPrimitives.WriteUInt16BigEndian(into.Slice(2, 2), self.ClientId); + into[4] = PacketTypeFlagged; + } + else + { + throw new NotSupportedException(); + } +#if DEBUG + into.CopyTo(Header.AsSpan()); +#endif + } + + public void FromHeader() + { + // typeof(..) and casts get jitted away, don't worry :) + var rawSpan = Raw.AsSpan(); + if (typeof(TDir) == typeof(S2C)) + { + PacketId = BinaryPrimitives.ReadUInt16BigEndian(rawSpan.Slice(Ts3Crypt.MacLen)); + PacketTypeFlagged = Raw[Ts3Crypt.MacLen + 2]; + } + else if (typeof(TDir) == typeof(C2S)) + { + var ext = new C2S(); + PacketId = BinaryPrimitives.ReadUInt16BigEndian(rawSpan.Slice(Ts3Crypt.MacLen)); + ext.ClientId = BinaryPrimitives.ReadUInt16BigEndian(rawSpan.Slice(Ts3Crypt.MacLen + 2)); + PacketTypeFlagged = Raw[Ts3Crypt.MacLen + 4]; + HeaderExt = (TDir)(object)ext; + } + else + { + throw new NotSupportedException(); + } + } + } + + internal class ResendPacket + { + public /*readonly*/ Packet Packet; + public DateTime FirstSendTime { get; set; } + public DateTime LastSendTime { get; set; } + + public ResendPacket(Packet packet) + { + Packet = packet; + var now = Util.Now; + FirstSendTime = now; + LastSendTime = now; + } + + public override string ToString() => $"RS(first:{FirstSendTime},last:{LastSendTime}) => {Packet}"; + } + + internal struct C2S + { + public const int HeaderLen = 5; + + public ushort ClientId { get; set; } + } + + internal struct S2C + { + public const int HeaderLen = 3; + } +} diff --git a/TS3Client/Full/PacketHandler.cs b/TS3Client/Full/PacketHandler.cs index 62c8aa8a..745cf55e 100644 --- a/TS3Client/Full/PacketHandler.cs +++ b/TS3Client/Full/PacketHandler.cs @@ -20,20 +20,16 @@ namespace TS3Client.Full using System.Net.Sockets; using System.Threading; - internal sealed class PacketHandler + internal sealed class PacketHandler : PacketHandler { /// Greatest allowed packet size, including the complete header. - private const int MaxPacketSize = 500; - private const int HeaderSize = 13; + private const int MaxOutPacketSize = 500; + private static readonly int OutHeaderSize = Ts3Crypt.MacLen + Packet.HeaderLength; + private static readonly int MaxOutContentSize = MaxOutPacketSize - OutHeaderSize; private const int MaxDecompressedSize = 1024 * 1024; // ServerDefault: 40000 (check original code again) - private const int ReceivePacketWindowSize = 100; + private const int ReceivePacketWindowSize = 128; - private static readonly Logger LoggerRtt = LogManager.GetLogger("TS3Client.PacketHandler.Rtt"); - private static readonly Logger LoggerRaw = LogManager.GetLogger("TS3Client.PacketHandler.Raw"); - private static readonly Logger LoggerRawVoice = LogManager.GetLogger("TS3Client.PacketHandler.Raw.Voice"); - private static readonly Logger LoggerTimeout = LogManager.GetLogger("TS3Client.PacketHandler.Timeout"); // Timout calculations - private static readonly TimeSpan PacketTimeout = TimeSpan.FromSeconds(30); /// The SmoothedRoundTripTime holds the smoothed average time /// it takes for a packet to get ack'd. private TimeSpan smoothedRtt; @@ -42,30 +38,26 @@ internal sealed class PacketHandler /// Holds the current RetransmissionTimeOut, which determines the timespan until /// a packet is considered to be lost. private TimeSpan currentRto; - /// Smoothing factor for the SmoothedRtt. - private const float AlphaSmooth = 0.125f; - /// Smoothing factor for the SmoothedRttDev. - private const float BetaSmooth = 0.25f; - /// The maximum wait time to retransmit a packet. - private static readonly TimeSpan MaxRetryInterval = TimeSpan.FromMilliseconds(1000); - /// The timeout check loop interval. - private static readonly TimeSpan ClockResolution = TimeSpan.FromMilliseconds(100); - private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(1); private readonly Stopwatch pingTimer = new Stopwatch(); + private readonly Stopwatch lastMessageTimer = new Stopwatch(); private ushort lastSentPingId; private ushort lastReceivedPingId; + // Out Packets private readonly ushort[] packetCounter; private readonly uint[] generationCounter; - private C2SPacket initPacketCheck; - private readonly Dictionary packetAckManager; - private readonly RingQueue receiveQueue; - private readonly RingQueue receiveQueueLow; + private ResendPacket initPacketCheck; + private readonly Dictionary> packetAckManager; + // In Packets + private readonly GenerationWindow receiveWindowVoice; + private readonly GenerationWindow receiveWindowVoiceWhisper; + private readonly RingQueue> receiveQueueCommand; + private readonly RingQueue> receiveQueueCommandLow; + // ==== private readonly object sendLoopLock = new object(); private readonly AutoResetEvent sendLoopPulse = new AutoResetEvent(false); private readonly Ts3Crypt ts3Crypt; private UdpClient udpClient; - private Thread resendThread; private int resendThreadId; public NetworkStats NetworkStats { get; } @@ -75,22 +67,54 @@ internal sealed class PacketHandler public Reason? ExitReason { get; set; } private bool Closed => ExitReason != null; + public event PacketEvent PacketEvent; + public PacketHandler(Ts3Crypt ts3Crypt) { - packetAckManager = new Dictionary(); - receiveQueue = new RingQueue(ReceivePacketWindowSize, ushort.MaxValue + 1); - receiveQueueLow = new RingQueue(ReceivePacketWindowSize, ushort.MaxValue + 1); + Util.Init(out packetAckManager); + receiveQueueCommand = new RingQueue>(ReceivePacketWindowSize, ushort.MaxValue + 1); + receiveQueueCommandLow = new RingQueue>(ReceivePacketWindowSize, ushort.MaxValue + 1); + receiveWindowVoice = new GenerationWindow(ushort.MaxValue + 1); + receiveWindowVoiceWhisper = new GenerationWindow(ushort.MaxValue + 1); + NetworkStats = new NetworkStats(); - packetCounter = new ushort[9]; - generationCounter = new uint[9]; + packetCounter = new ushort[Ts3Crypt.PacketTypeKinds]; + generationCounter = new uint[Ts3Crypt.PacketTypeKinds]; this.ts3Crypt = ts3Crypt; resendThreadId = -1; } public void Connect(IPEndPoint address) { - resendThread = new Thread(ResendLoop) { Name = "PacketHandler" }; + Initialize(address, true); + // The old client used to send 'clientinitiv' as the first message. + // All newer server still ack it but do not require it anymore. + // Therefore there is no use in seding it. + // We still have to increase the packet counter as if we had sent + // it because the packed-ids the server expects are fixed. + IncPacketCounter(PacketType.Command); + // Send the actual new init packet. + AddOutgoingPacket(ts3Crypt.ProcessInit1(null).Value, PacketType.Init1); + } + + public void Listen(IPEndPoint address) + { + lock (sendLoopLock) + { + Initialize(address, false); + // dummy + initPacketCheck = new ResendPacket(new Packet(Array.Empty(), 0, 0, 0)) + { + FirstSendTime = DateTime.MaxValue, + LastSendTime = DateTime.MaxValue + }; + } + } + + private void Initialize(IPEndPoint address, bool connect) + { + var resendThread = new Thread(ResendLoop) { Name = "PacketHandler" }; resendThreadId = resendThread.ManagedThreadId; lock (sendLoopLock) @@ -102,34 +126,37 @@ public void Connect(IPEndPoint address) currentRto = MaxRetryInterval; lastSentPingId = 0; lastReceivedPingId = 0; + lastMessageTimer.Restart(); initPacketCheck = null; packetAckManager.Clear(); - receiveQueue.Clear(); - receiveQueueLow.Clear(); + receiveQueueCommand.Clear(); + receiveQueueCommandLow.Clear(); + receiveWindowVoice.Reset(); + receiveWindowVoiceWhisper.Reset(); Array.Clear(packetCounter, 0, packetCounter.Length); Array.Clear(generationCounter, 0, generationCounter.Length); NetworkStats.Reset(); - ConnectUdpClient(address); + udpClient?.Dispose(); + try + { + if (connect) + { + remoteAddress = address; + udpClient = new UdpClient(address.AddressFamily); + udpClient.Connect(address); + } + else + { + remoteAddress = null; + udpClient = new UdpClient(address); + } + } + catch (SocketException ex) { throw new Ts3Exception("Could not connect", ex); } } resendThread.Start(); - - AddOutgoingPacket(ts3Crypt.ProcessInit1(null).Value, PacketType.Init1); - } - - private void ConnectUdpClient(IPEndPoint address) - { - ((IDisposable)udpClient)?.Dispose(); - - try - { - remoteAddress = address; - udpClient = new UdpClient(remoteAddress.AddressFamily); - udpClient.Connect(remoteAddress); - } - catch (SocketException ex) { throw new Ts3Exception("Could not connect", ex); } } public void Stop(Reason closeReason = Reason.LeftServer) @@ -137,25 +164,25 @@ public void Stop(Reason closeReason = Reason.LeftServer) resendThreadId = -1; lock (sendLoopLock) { - ((IDisposable)udpClient)?.Dispose(); + udpClient?.Dispose(); if (!ExitReason.HasValue) ExitReason = closeReason; sendLoopPulse.Set(); } } - public void AddOutgoingPacket(ReadOnlySpan packet, PacketType packetType, PacketFlags addFlags = PacketFlags.None) + public E AddOutgoingPacket(ReadOnlySpan packet, PacketType packetType, PacketFlags addFlags = PacketFlags.None) { lock (sendLoopLock) { if (Closed) - return; + return "Connection closed"; if (NeedsSplitting(packet.Length) && packetType != PacketType.VoiceWhisper) { // VoiceWhisper packets are for some reason excluded if (packetType == PacketType.Voice) - return; // Exception maybe ??? This happens when a voice packet is bigger than the allowed size + return "Voice packet too big"; // This happens when a voice packet is bigger than the allowed size var tmpCompress = QuickerLz.Compress(packet, 1); if (tmpCompress.Length < packet.Length) @@ -166,107 +193,24 @@ public void AddOutgoingPacket(ReadOnlySpan packet, PacketType packetType, if (NeedsSplitting(packet.Length)) { - AddOutgoingSplitData(packet, packetType, addFlags); - return; + return AddOutgoingSplitData(packet, packetType, addFlags); } } - SendOutgoingData(packet, packetType, addFlags); + return SendOutgoingData(packet, packetType, addFlags); } } - private void SendOutgoingData(ReadOnlySpan data, PacketType packetType, PacketFlags flags = PacketFlags.None) - { - var packet = new C2SPacket(data.ToArray(), packetType); - - lock (sendLoopLock) - { - var ids = GetPacketCounter(packet.PacketType); - if (ts3Crypt.CryptoInitComplete) - IncPacketCounter(packet.PacketType); - - packet.PacketId = ids.Id; - packet.GenerationId = ids.Generation; - packet.ClientId = ClientId; - packet.PacketFlags |= flags; - - switch (packet.PacketType) - { - case PacketType.Voice: - case PacketType.VoiceWhisper: - packet.PacketFlags |= PacketFlags.Unencrypted; - BinaryPrimitives.WriteUInt16BigEndian(packet.Data.AsSpan(), packet.PacketId); - LoggerRawVoice.Trace("[O] {0}", packet); - break; - - case PacketType.Command: - case PacketType.CommandLow: - packet.PacketFlags |= PacketFlags.Newprotocol; - packetAckManager.Add(packet.PacketId, packet); - LoggerRaw.Debug("[O] {0}", packet); - break; - - case PacketType.Ping: - lastSentPingId = packet.PacketId; - packet.PacketFlags |= PacketFlags.Unencrypted; - LoggerRaw.Trace("[O] Ping {0}", packet.PacketId); - break; - - case PacketType.Pong: - packet.PacketFlags |= PacketFlags.Unencrypted; - LoggerRaw.Trace("[O] Pong {0}", BinaryPrimitives.ReadUInt16BigEndian(packet.Data)); - break; - - case PacketType.Ack: - case PacketType.AckLow: - LoggerRaw.Debug("[O] Acking {1}: {0}", BinaryPrimitives.ReadUInt16BigEndian(packet.Data), packet.PacketType); - break; - - case PacketType.Init1: - packet.PacketFlags |= PacketFlags.Unencrypted; - initPacketCheck = packet; - LoggerRaw.Debug("[O] InitID: {0}", packet.Data[4]); - LoggerRaw.Trace("[O] {0}", packet); - break; - - default: throw Util.UnhandledDefault(packet.PacketType); - } - - ts3Crypt.Encrypt(packet); - - packet.FirstSendTime = Util.Now; - SendRaw(packet); - } - } - - private IdTuple GetPacketCounter(PacketType packetType) - => (packetType != PacketType.Init1) - ? new IdTuple(packetCounter[(int)packetType], generationCounter[(int)packetType]) - : new IdTuple(101, 0); - - public void IncPacketCounter(PacketType packetType) - { - unchecked { packetCounter[(int)packetType]++; } - if (packetCounter[(int)packetType] == 0) - generationCounter[(int)packetType]++; - } - - public void CryptoInitDone() - { - if (!ts3Crypt.CryptoInitComplete) - throw new InvalidOperationException($"{nameof(CryptoInitDone)} was called although it isn't initialized"); - IncPacketCounter(PacketType.Command); - } - - private void AddOutgoingSplitData(ReadOnlySpan rawData, PacketType packetType, PacketFlags addFlags = PacketFlags.None) + private E AddOutgoingSplitData(ReadOnlySpan rawData, PacketType packetType, PacketFlags addFlags = PacketFlags.None) { int pos = 0; bool first = true; bool last; - const int maxContent = MaxPacketSize - HeaderSize; + // TODO check if "packBuffer.FreeSlots >= packetSplit.Count" + do { - int blockSize = Math.Min(maxContent, rawData.Length - pos); + int blockSize = Math.Min(MaxOutContentSize, rawData.Length - pos); if (blockSize <= 0) break; var flags = PacketFlags.None; @@ -279,87 +223,176 @@ private void AddOutgoingSplitData(ReadOnlySpan rawData, PacketType packetT first = false; } - SendOutgoingData(rawData.Slice(pos, blockSize), packetType, flags); + var sendResult = SendOutgoingData(rawData.Slice(pos, blockSize), packetType, flags); + if (!sendResult.Ok) + return sendResult; + pos += blockSize; } while (!last); + + return R.Ok; } - private static bool NeedsSplitting(int dataSize) => dataSize + HeaderSize > MaxPacketSize; + // is always locked on 'sendLoopLock' from a higher call + private E SendOutgoingData(ReadOnlySpan data, PacketType packetType, PacketFlags flags = PacketFlags.None) + { + var ids = GetPacketCounter(packetType); + IncPacketCounter(packetType); - public S2CPacket FetchPacket() + var packet = new Packet(data, packetType, ids.Id, ids.Generation) { PacketType = packetType }; + if (typeof(TOut) == typeof(C2S)) // TODO: XXX + { + var meta = (C2S)(object)packet.HeaderExt; + meta.ClientId = ClientId; + packet.HeaderExt = (TOut)(object)meta; + } + packet.PacketFlags |= flags; + + switch (packet.PacketType) + { + case PacketType.Voice: + case PacketType.VoiceWhisper: + packet.PacketFlags |= PacketFlags.Unencrypted; + BinaryPrimitives.WriteUInt16BigEndian(packet.Data, packet.PacketId); + LoggerRawVoice.Trace("[O] {0}", packet); + break; + + case PacketType.Command: + case PacketType.CommandLow: + packet.PacketFlags |= PacketFlags.Newprotocol; + var resendPacket = new ResendPacket(packet); + packetAckManager.Add(packet.PacketId, resendPacket); + LoggerRaw.Debug("[O] {0}", packet); + break; + + case PacketType.Ping: + lastSentPingId = packet.PacketId; + packet.PacketFlags |= PacketFlags.Unencrypted; + LoggerRaw.Trace("[O] Ping {0}", packet.PacketId); + break; + + case PacketType.Pong: + packet.PacketFlags |= PacketFlags.Unencrypted; + LoggerRaw.Trace("[O] Pong {0}", BinaryPrimitives.ReadUInt16BigEndian(packet.Data)); + break; + + case PacketType.Ack: + case PacketType.AckLow: + LoggerRaw.Debug("[O] Acking {1}: {0}", BinaryPrimitives.ReadUInt16BigEndian(packet.Data), packet.PacketType); + break; + + case PacketType.Init1: + packet.PacketFlags |= PacketFlags.Unencrypted; + initPacketCheck = new ResendPacket(packet); + LoggerRaw.Debug("[O] InitID: {0}", packet.Data[4]); + LoggerRaw.Trace("[O] {0}", packet); + break; + + default: throw Util.UnhandledDefault(packet.PacketType); + } + + ts3Crypt.Encrypt(ref packet); + + return SendRaw(ref packet); + } + + private (ushort Id, uint Generation) GetPacketCounter(PacketType packetType) + => (packetType != PacketType.Init1) + ? (packetCounter[(int)packetType], generationCounter[(int)packetType]) + : (101, 0); + + private void IncPacketCounter(PacketType packetType) + { + unchecked { packetCounter[(int)packetType]++; } + if (packetCounter[(int)packetType] == 0) + generationCounter[(int)packetType]++; + } + + private static bool NeedsSplitting(int dataSize) => dataSize + OutHeaderSize > MaxOutPacketSize; + + public void FetchPackets() { while (true) { if (Closed) - return null; - - if (TryFetchPacket(receiveQueue, out var packet)) - return packet; - if (TryFetchPacket(receiveQueueLow, out packet)) - return packet; + return; var dummy = new IPEndPoint(IPAddress.Any, 0); byte[] buffer; try { buffer = udpClient.Receive(ref dummy); } - catch (IOException) { return null; } - catch (SocketException) { return null; } - catch (ObjectDisposedException) { return null; } + catch (IOException) { return; } + catch (SocketException) { return; } + catch (ObjectDisposedException) { return; } if (dummy.Address.Equals(remoteAddress.Address) && dummy.Port != remoteAddress.Port) continue; - packet = Ts3Crypt.GetS2CPacket(buffer); + var optpacket = Packet.FromRaw(buffer); // Invalid packet, ignore - if (packet == null) + if (optpacket == null) { - LoggerRaw.Debug("Dropping invalid packet: {0}", DebugUtil.DebugToHex(buffer)); + LoggerRaw.Warn("Dropping invalid packet: {0}", DebugUtil.DebugToHex(buffer)); continue; } + var packet = optpacket.Value; + + // DebubToHex is costly and allocates, precheck before logging + if (LoggerRaw.IsTraceEnabled) + LoggerRaw.Trace("[I] Raw {0}", DebugUtil.DebugToHex(packet.Raw)); - GenerateGenerationId(packet); - if (!ts3Crypt.Decrypt(packet)) + FindIncommingGenerationId(ref packet); + if (!ts3Crypt.Decrypt(ref packet)) + { + LoggerRaw.Warn("Dropping not decryptable packet: {0}", DebugUtil.DebugToHex(packet.Raw)); continue; + } - NetworkStats.LogInPacket(packet); + lastMessageTimer.Restart(); + NetworkStats.LogInPacket(ref packet); + bool passPacketToEvent = true; switch (packet.PacketType) { case PacketType.Voice: + LoggerRawVoice.Trace("[I] {0}", packet); + passPacketToEvent = ReceiveVoice(ref packet, receiveWindowVoice); + break; case PacketType.VoiceWhisper: LoggerRawVoice.Trace("[I] {0}", packet); + passPacketToEvent = ReceiveVoice(ref packet, receiveWindowVoiceWhisper); break; case PacketType.Command: LoggerRaw.Debug("[I] {0}", packet); - packet = ReceiveCommand(packet, receiveQueue, PacketType.Ack); + passPacketToEvent = ReceiveCommand(ref packet, receiveQueueCommand, PacketType.Ack); break; case PacketType.CommandLow: LoggerRaw.Debug("[I] {0}", packet); - packet = ReceiveCommand(packet, receiveQueueLow, PacketType.AckLow); + passPacketToEvent = ReceiveCommand(ref packet, receiveQueueCommandLow, PacketType.AckLow); break; case PacketType.Ping: LoggerRaw.Trace("[I] Ping {0}", packet.PacketId); - ReceivePing(packet); + ReceivePing(ref packet); break; case PacketType.Pong: LoggerRaw.Trace("[I] Pong {0}", BinaryPrimitives.ReadUInt16BigEndian(packet.Data)); - ReceivePong(packet); + passPacketToEvent = ReceivePong(ref packet); break; case PacketType.Ack: LoggerRaw.Debug("[I] Acking: {0}", BinaryPrimitives.ReadUInt16BigEndian(packet.Data)); - packet = ReceiveAck(packet); + passPacketToEvent = ReceiveAck(ref packet); break; case PacketType.AckLow: break; case PacketType.Init1: if (!LoggerRaw.IsTraceEnabled) LoggerRaw.Debug("[I] InitID: {0}", packet.Data[0]); if (!LoggerRaw.IsDebugEnabled) LoggerRaw.Trace("[I] {0}", packet); - ReceiveInitAck(packet); + passPacketToEvent = ReceiveInitAck(ref packet); break; default: throw Util.UnhandledDefault(packet.PacketType); } - if (packet != null) - return packet; + if (passPacketToEvent) + PacketEvent?.Invoke(ref packet); } } @@ -367,42 +400,48 @@ public S2CPacket FetchPacket() // These methods are for low level packet processing which the // rather high level TS3FullClient should not worry about. - private void GenerateGenerationId(S2CPacket packet) + private void FindIncommingGenerationId(ref Packet packet) { - // TODO rework this for all packet types - RingQueue packetQueue; + GenerationWindow window; switch (packet.PacketType) { - case PacketType.Command: packetQueue = receiveQueue; break; - case PacketType.CommandLow: packetQueue = receiveQueueLow; break; + case PacketType.Voice: window = receiveWindowVoice; break; + case PacketType.VoiceWhisper: window = receiveWindowVoiceWhisper; break; + case PacketType.Command: window = receiveQueueCommand.Window; break; + case PacketType.CommandLow: window = receiveQueueCommandLow.Window; break; default: return; } - packet.GenerationId = packetQueue.GetGeneration(packet.PacketId); + packet.GenerationId = window.GetGeneration(packet.PacketId); } - private S2CPacket ReceiveCommand(S2CPacket packet, RingQueue packetQueue, PacketType ackType) + private bool ReceiveVoice(ref Packet packet, GenerationWindow window) + => window.SetAndDrag(packet.PacketId); + + private bool ReceiveCommand(ref Packet packet, RingQueue> packetQueue, PacketType ackType) { var setStatus = packetQueue.IsSet(packet.PacketId); // Check if we cannot accept this packet since it doesn't fit into the receive window if (setStatus == ItemSetStatus.OutOfWindowNotSet) - return null; + return false; - packet.GenerationId = packetQueue.GetGeneration(packet.PacketId); SendAck(packet.PacketId, ackType); // Check if we already have this packet and only need to ack it. if (setStatus == ItemSetStatus.InWindowSet || setStatus == ItemSetStatus.OutOfWindowSet) - return null; + return false; packetQueue.Set(packet.PacketId, packet); - return TryFetchPacket(packetQueue, out var retPacket) ? retPacket : null; + while (TryFetchPacket(packetQueue, out packet)) + PacketEvent?.Invoke(ref packet); + + return false; } - private static bool TryFetchPacket(RingQueue packetQueue, out S2CPacket packet) + private static bool TryFetchPacket(RingQueue> packetQueue, out Packet packet) { - if (packetQueue.Count <= 0) { packet = null; return false; } + if (packetQueue.Count <= 0) { packet = default; return false; } int take = 0; int takeLen = 0; @@ -430,7 +469,7 @@ private static bool TryFetchPacket(RingQueue packetQueue, out S2CPack } } - if (!hasStart || !hasEnd) { packet = null; return false; } + if (!hasStart || !hasEnd) { packet = default; return false; } // GET if (!packetQueue.TryDequeue(out packet)) @@ -443,14 +482,14 @@ private static bool TryFetchPacket(RingQueue packetQueue, out S2CPack // for loop at 0th element int curCopyPos = packet.Size; - Array.Copy(packet.Data, 0, preFinalArray, 0, packet.Size); + packet.Data.CopyTo(preFinalArray.AsSpan(0, packet.Size)); for (int i = 1; i < take; i++) { - if (!packetQueue.TryDequeue(out S2CPacket nextPacket)) + if (!packetQueue.TryDequeue(out var nextPacket)) throw new InvalidOperationException("Packet in queue got missing (?)"); - Array.Copy(nextPacket.Data, 0, preFinalArray, curCopyPos, nextPacket.Size); + nextPacket.Data.CopyTo(preFinalArray.AsSpan(curCopyPos, nextPacket.Size)); curCopyPos += nextPacket.Size; } packet.Data = preFinalArray; @@ -482,11 +521,10 @@ private void SendAck(ushort ackId, PacketType ackType) throw new InvalidOperationException("Packet type is not an Ack-type"); } - private S2CPacket ReceiveAck(S2CPacket packet) + private bool ReceiveAck(ref Packet packet) { - if (packet.Data.Length < 2) - return null; - ushort packetId = BinaryPrimitives.ReadUInt16BigEndian(packet.Data); + if (!BinaryPrimitives.TryReadUInt16BigEndian(packet.Data, out var packetId)) + return false; lock (sendLoopLock) { @@ -496,7 +534,7 @@ private S2CPacket ReceiveAck(S2CPacket packet) packetAckManager.Remove(packetId); } } - return packet; + return true; } private void SendPing() @@ -505,7 +543,7 @@ private void SendPing() AddOutgoingPacket(Array.Empty(), PacketType.Ping); } - private void ReceivePing(S2CPacket packet) + private void ReceivePing(ref Packet packet) { var idDiff = packet.PacketId - lastReceivedPingId; if (idDiff > 1 && idDiff < ReceivePacketWindowSize) @@ -517,9 +555,10 @@ private void ReceivePing(S2CPacket packet) AddOutgoingPacket(pongData, PacketType.Pong); } - private void ReceivePong(S2CPacket packet) + private bool ReceivePong(ref Packet packet) { - ushort answerId = BinaryPrimitives.ReadUInt16BigEndian(packet.Data); + if (!BinaryPrimitives.TryReadUInt16BigEndian(packet.Data, out var answerId)) + return false; if (lastSentPingId == answerId) { @@ -527,29 +566,32 @@ private void ReceivePong(S2CPacket packet) UpdateRto(rtt); NetworkStats.AddPing(rtt); } + return true; } - public void ReceivedFinalInitAck() => ReceiveInitAck(null, true); + public void ReceivedFinalInitAck() + { + initPacketCheck = null; + } - private void ReceiveInitAck(S2CPacket packet, bool done = false) + private bool ReceiveInitAck(ref Packet packet) { lock (sendLoopLock) { - if (initPacketCheck == null || packet == null) - { - if (done) - initPacketCheck = null; - return; - } + if (initPacketCheck == null) + return true; // optional: add random number check from init data - var forwardData = ts3Crypt.ProcessInit1(packet.Data); + var forwardData = ts3Crypt.ProcessInit1(packet.Data); if (!forwardData.Ok) { LoggerRaw.Debug("Error init: {0}", forwardData.Error); - return; + return false; } initPacketCheck = null; + if (forwardData.Value.Length == 0) // TODO XXX + return true; AddOutgoingPacket(forwardData.Value, PacketType.Init1); + return true; } } @@ -586,36 +628,51 @@ private void ResendLoop() if (Closed) break; - if ((packetAckManager.Count > 0 && ResendPackets(packetAckManager.Values, now)) || - (initPacketCheck != null && ResendPacket(initPacketCheck, now))) + if ((packetAckManager.Count > 0 && ResendPackets(packetAckManager.Values)) + || (initPacketCheck != null && ResendPacket(initPacketCheck))) { Stop(Reason.Timeout); return; } } - var nextTest = pingCheck - now + PingInterval; + var nextTest = now - pingCheck - PingInterval; // we need to check if CryptoInitComplete because while false packet ids won't be incremented - if (nextTest < TimeSpan.Zero && ts3Crypt.CryptoInitComplete) + if (nextTest > TimeSpan.Zero && ts3Crypt.CryptoInitComplete) { - pingCheck += PingInterval; + // Check that the last ping is more than PingInterval but not more than + // 2*PingInterval away. This might happen for e.g. when the process was + // suspended. If it was too long ago, reset the ping tick to now. + if (nextTest > PingInterval) + pingCheck = now; + else + pingCheck += PingInterval; SendPing(); } - // TODO implement ping-timeout here + + var elapsed = lastMessageTimer.Elapsed; + if (elapsed > PacketTimeout) + { + LoggerTimeout.Debug("TIMEOUT: Got no ping packet response for {0}", elapsed); + Stop(Reason.Timeout); + return; + } + sendLoopPulse.WaitOne(ClockResolution); } } - private bool ResendPackets(IEnumerable packetList, DateTime now) + private bool ResendPackets(IEnumerable> packetList) { foreach (var outgoingPacket in packetList) - if (ResendPacket(outgoingPacket, now)) + if (ResendPacket(outgoingPacket)) return true; return false; } - private bool ResendPacket(C2SPacket packet, DateTime now) + private bool ResendPacket(ResendPacket packet) { + var now = Util.Now; // Check if the packet timed out completely if (packet.FirstSendTime < now - PacketTimeout) { @@ -630,26 +687,55 @@ private bool ResendPacket(C2SPacket packet, DateTime now) currentRto = currentRto + currentRto; if (currentRto > MaxRetryInterval) currentRto = MaxRetryInterval; - SendRaw(packet); + packet.LastSendTime = Util.Now; + SendRaw(ref packet.Packet); } return false; } - private void SendRaw(C2SPacket packet) + private E SendRaw(ref Packet packet) { - packet.LastSendTime = Util.Now; - NetworkStats.LogOutPacket(packet); - LoggerRaw.Trace("[O] Raw: {0}", DebugUtil.DebugToHex(packet.Raw)); - udpClient.Send(packet.Raw, packet.Raw.Length); + NetworkStats.LogOutPacket(ref packet); + + // DebubToHex is costly and allocates, precheck before logging + if (LoggerRaw.IsTraceEnabled) + LoggerRaw.Trace("[O] Raw: {0}", DebugUtil.DebugToHex(packet.Raw)); + + try + { + udpClient.Send(packet.Raw, packet.Raw.Length); // , remoteAddress // TODO + return R.Ok; + } + catch (SocketException ex) + { + LoggerRaw.Warn(ex, "Failed to deliver packet (Err:{0})", ex.SocketErrorCode); + return "Socket send error"; + } } } - internal struct IdTuple + internal class PacketHandler { - public ushort Id { get; } - public uint Generation { get; } + protected static readonly Logger LoggerRtt = LogManager.GetLogger("TS3Client.PacketHandler.Rtt"); + protected static readonly Logger LoggerRaw = LogManager.GetLogger("TS3Client.PacketHandler.Raw"); + protected static readonly Logger LoggerRawVoice = LogManager.GetLogger("TS3Client.PacketHandler.Raw.Voice"); + protected static readonly Logger LoggerTimeout = LogManager.GetLogger("TS3Client.PacketHandler.Timeout"); - public IdTuple(ushort id, uint generation) { Id = id; Generation = generation; } + /// Elapsed time since first send timestamp until the connection is considered lost. + protected static readonly TimeSpan PacketTimeout = TimeSpan.FromSeconds(20); + /// Smoothing factor for the SmoothedRtt. + protected const float AlphaSmooth = 0.125f; + /// Smoothing factor for the SmoothedRttDev. + protected const float BetaSmooth = 0.25f; + /// The maximum wait time to retransmit a packet. + protected static readonly TimeSpan MaxRetryInterval = TimeSpan.FromMilliseconds(1000); + /// The timeout check loop interval. + protected static readonly TimeSpan ClockResolution = TimeSpan.FromMilliseconds(100); + protected static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(1); + + protected PacketHandler() { } } + + internal delegate void PacketEvent(ref Packet packet); } diff --git a/TS3Client/Full/QuickerLz.cs b/TS3Client/Full/QuickerLz.cs index f30167d3..acaef8ae 100644 --- a/TS3Client/Full/QuickerLz.cs +++ b/TS3Client/Full/QuickerLz.cs @@ -10,6 +10,7 @@ namespace TS3Client.Full { using System; + using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; @@ -20,10 +21,10 @@ public static class QuickerLz private const uint SetControl = 0x8000_0000; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCompressedSize(byte[] data) => (data[0] & 0x02) != 0 ? ReadI32(data, 1) : data[1]; + public static int GetCompressedSize(ReadOnlySpan data) => (data[0] & 0x02) != 0 ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(1)) : data[1]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetDecompressedSize(byte[] data) => (data[0] & 0x02) != 0 ? ReadI32(data, 5) : data[2]; + public static int GetDecompressedSize(ReadOnlySpan data) => (data[0] & 0x02) != 0 ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(5)) : data[2]; [ThreadStatic] private static int[] hashtable; @@ -35,12 +36,13 @@ public static class QuickerLz public static Span Compress(ReadOnlySpan data, int level) { if (level != 1) // && level != 3 - throw new ArgumentException("This QuickLZ implementation supports only level 1 and 3 compress"); + throw new ArgumentException("This QuickLZ implementation supports only level 1 compress"); // (and 3) if (data.Length >= int.MaxValue) throw new ArgumentException($"This QuickLZ can only compress up to {int.MaxValue}"); int headerlen = data.Length < 216 ? 3 : 9; var dest = new byte[data.Length + 400]; + var destSpan = dest.AsSpan(); int destPos = headerlen + 4; uint control = SetControl; @@ -64,14 +66,13 @@ public static Span Compress(ReadOnlySpan data, int level) { if (sourcePos > data.Length / 2 && destPos > sourcePos - (sourcePos / 32)) { - var destSpan = dest.AsSpan(); data.CopyTo(destSpan.Slice(headerlen)); - //Array.Copy(data, 0, dest, headerlen, data.Length); destPos = headerlen + data.Length; - WriteHeader(dest, destPos, data.Length, level, headerlen, false); - return destSpan.Slice(0, destPos); + destSpan = destSpan.Slice(0, destPos); + WriteHeader(destSpan, data.Length, level, headerlen, false); + return destSpan; } - WriteU32(dest, controlPos, (control >> 1) | SetControl); // C + BinaryPrimitives.WriteUInt32LittleEndian(destSpan.Slice(controlPos), (control >> 1) | SetControl); // C controlPos = destPos; destPos += 4; control = SetControl; @@ -90,7 +91,7 @@ public static Span Compress(ReadOnlySpan data, int level) || sourcePos == offset + 1 && unmatched >= 3 && sourcePos > 3 - && Is6Same(data, sourcePos - 3))) + && Is6Same(data.Slice(sourcePos - 3)))) { control = (control >> 1) | SetControl; int matchlen = 3; @@ -99,7 +100,7 @@ public static Span Compress(ReadOnlySpan data, int level) matchlen++; if (matchlen < 18) { - WriteU16(dest, destPos, hash << 4 | (matchlen - 2)); + BinaryPrimitives.WriteUInt16LittleEndian(destSpan.Slice(destPos), (ushort)(hash << 4 | (matchlen - 2))); destPos += 2; } else @@ -125,7 +126,7 @@ public static Span Compress(ReadOnlySpan data, int level) { if ((control & 1) != 0) { - WriteU32(dest, controlPos, (control >> 1) | SetControl); // C + BinaryPrimitives.WriteUInt32LittleEndian(destSpan.Slice(controlPos), (control >> 1) | SetControl); // C controlPos = destPos; destPos += 4; control = SetControl; @@ -136,19 +137,20 @@ public static Span Compress(ReadOnlySpan data, int level) while ((control & 1) == 0) control >>= 1; - WriteU32(dest, controlPos, (control >> 1) | SetControl); // C + BinaryPrimitives.WriteUInt32LittleEndian(destSpan.Slice(controlPos), (control >> 1) | SetControl); // C - WriteHeader(dest, destPos, data.Length, level, headerlen, true); - return new Span(dest, 0, destPos); + destSpan = destSpan.Slice(0, destPos); + WriteHeader(destSpan, data.Length, level, headerlen, true); + return destSpan; } - public static byte[] Decompress(byte[] data, int maxSize) + public static byte[] Decompress(ReadOnlySpan data, int maxSize) { // Read header byte flags = data[0]; int level = (flags >> 2) & 0b11; - if (level != 3 && level != 1) - throw new NotSupportedException("This QuickLZ implementation supports only level 1 and 3 decompress"); + if (level != 1) // && level != 3 + throw new NotSupportedException("This QuickLZ implementation supports only level 1 decompress"); // (and 3) int headerlen = (flags & 0x02) != 0 ? 9 : 3; int compressedSize = GetCompressedSize(data); @@ -164,7 +166,7 @@ public static byte[] Decompress(byte[] data, int maxSize) // Uncompressed if (compressedSize - headerlen != decompressedSize) throw new InvalidDataException("Compressed and uncompressed size of uncompressed data do not match"); - Array.Copy(data, headerlen, dest, 0, decompressedSize); + data.Slice(headerlen).CopyTo(dest.AsSpan(0, decompressedSize)); return dest; } @@ -174,7 +176,7 @@ public static byte[] Decompress(byte[] data, int maxSize) int sourcePos = headerlen; int destPos = 0; int nextHashed = 0; - + if (hashtable == null) hashtable = new int[TableSize]; Array.Clear(hashtable, 0, TableSize); @@ -182,7 +184,7 @@ public static byte[] Decompress(byte[] data, int maxSize) { if (control == 1) { - control = ReadU32(data, sourcePos); + control = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(sourcePos)); sourcePos += 4; } @@ -231,7 +233,7 @@ public static byte[] Decompress(byte[] data, int maxSize) return dest; } - private static void WriteHeader(byte[] dest, int destLen, int srcLen, int level, int headerlen, bool compressed) + private static void WriteHeader(Span dest, int srcLen, int level, int headerlen, bool compressed) { byte flags; if (compressed) @@ -243,15 +245,15 @@ private static void WriteHeader(byte[] dest, int destLen, int srcLen, int level, { // short header dest[0] = flags; - dest[1] = (byte)destLen; + dest[1] = (byte)dest.Length; dest[2] = (byte)srcLen; } else if (headerlen == 9) { // long header dest[0] = (byte)(flags | 0x02); - WriteI32(dest, 1, destLen); - WriteI32(dest, 5, srcLen); + BinaryPrimitives.WriteInt32LittleEndian(dest.Slice(1), dest.Length); + BinaryPrimitives.WriteInt32LittleEndian(dest.Slice(5), srcLen); } else { @@ -259,12 +261,6 @@ private static void WriteHeader(byte[] dest, int destLen, int srcLen, int level, } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteU16(byte[] outArr, int outOff, int value) - { - outArr[outOff + 0] = unchecked((byte)(value >> 0)); - outArr[outOff + 1] = unchecked((byte)(value >> 8)); - } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Write24(byte[] outArr, int outOff, int value) @@ -274,47 +270,21 @@ private static void Write24(byte[] outArr, int outOff, int value) outArr[outOff + 2] = unchecked((byte)(value >> 16)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteI32(byte[] outArr, int outOff, int value) - { - outArr[outOff + 0] = unchecked((byte)(value >> 00)); - outArr[outOff + 1] = unchecked((byte)(value >> 08)); - outArr[outOff + 2] = unchecked((byte)(value >> 16)); - outArr[outOff + 3] = unchecked((byte)(value >> 24)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteU32(byte[] outArr, int outOff, uint value) - { - outArr[outOff + 0] = unchecked((byte)(value >> 00)); - outArr[outOff + 1] = unchecked((byte)(value >> 08)); - outArr[outOff + 2] = unchecked((byte)(value >> 16)); - outArr[outOff + 3] = unchecked((byte)(value >> 24)); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Read24(ReadOnlySpan intArr, int inOff) => unchecked(intArr[inOff] | (intArr[inOff + 1] << 8) | (intArr[inOff + 2] << 16)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int ReadI32(byte[] intArr, int inOff) - => unchecked(intArr[inOff] | (intArr[inOff + 1] << 8) | (intArr[inOff + 2] << 16) | (intArr[inOff + 3] << 24)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint ReadU32(byte[] intArr, int inOff) - => unchecked((uint)(intArr[inOff] | (intArr[inOff + 1] << 8) | (intArr[inOff + 2] << 16) | (intArr[inOff + 3] << 24))); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Hash(int value) => ((value >> 12) ^ value) & 0xfff; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool Is6Same(ReadOnlySpan arr, int i) + private static bool Is6Same(ReadOnlySpan arr) { - return arr[i + 0] == arr[i + 1] - && arr[i + 1] == arr[i + 2] - && arr[i + 2] == arr[i + 3] - && arr[i + 3] == arr[i + 4] - && arr[i + 4] == arr[i + 5]; + return arr[0] == arr[1] + && arr[1] == arr[2] + && arr[2] == arr[3] + && arr[3] == arr[4] + && arr[4] == arr[5]; } /// Copy [start; start + length) bytes from `data` to the end of `data` diff --git a/TS3Client/Full/RingQueue.cs b/TS3Client/Full/RingQueue.cs index 0e7eaa77..e06d4654 100644 --- a/TS3Client/Full/RingQueue.cs +++ b/TS3Client/Full/RingQueue.cs @@ -13,38 +13,27 @@ namespace TS3Client.Full /// Provides a ring queue with packet offset and direct item access functionality. /// Item type - public class RingQueue + public sealed class RingQueue { private const int InitialBufferSize = 16; private int currentStart; - private int currentLength; private T[] ringBuffer; private bool[] ringBufferSet; - private int mappedBaseOffset; - private readonly int mappedMod; - private uint generation; - + public int Count { get; private set; } // = currentLength public int MaxBufferSize { get; } - public int Count => currentLength; + public GenerationWindow Window { get; } public RingQueue(int maxBufferSize, int mod) { - if (maxBufferSize == -1) - { - MaxBufferSize = (mod / 2) - 1; - } - else - { - if (maxBufferSize >= mod) - throw new ArgumentOutOfRangeException(nameof(mod), "Modulo must be bigger than buffer size"); - MaxBufferSize = maxBufferSize; - } - var setBufferSize = Math.Min(InitialBufferSize, maxBufferSize); + if (maxBufferSize >= mod) + throw new ArgumentOutOfRangeException(nameof(mod), "Modulo must be bigger than buffer size"); + MaxBufferSize = maxBufferSize; + var setBufferSize = Math.Min(InitialBufferSize, MaxBufferSize); ringBuffer = new T[setBufferSize]; ringBufferSet = new bool[setBufferSize]; - mappedMod = mod; + Window = new GenerationWindow(mod, MaxBufferSize); Clear(); } @@ -55,16 +44,16 @@ private void BufferSet(int index, T value) BufferExtend(index); int local = IndexToLocal(index); int newLength = local - currentStart + 1 + (local >= currentStart ? 0 : ringBuffer.Length); - currentLength = Math.Max(currentLength, newLength); + Count = Math.Max(Count, newLength); ringBuffer[local] = value; ringBufferSet[local] = true; } - private T BufferGet(int index) + private ref T BufferGet(int index) { BufferExtend(index); int local = IndexToLocal(index); - return ringBuffer[local]; + return ref ringBuffer[local]; } private bool StateGet(int index) @@ -80,10 +69,10 @@ private void BufferPop() // clear data to allow them to be collected by gc // when in debug it might be nice to see what was there #if !DEBUG - ringBuffer[currentStart] = default(T); + ringBuffer[currentStart] = default; #endif currentStart = (currentStart + 1) % ringBuffer.Length; - currentLength--; + Count--; } private void BufferExtend(int index) @@ -112,35 +101,16 @@ private void BufferExtend(int index) public void Set(int mappedValue, T value) { - int index = MappedToIndex(mappedValue); + int index = Window.MappedToIndex(mappedValue); if (IsSetIndex(index) != ItemSetStatus.InWindowNotSet) throw new ArgumentOutOfRangeException(nameof(mappedValue), "Object cannot be set."); BufferSet(index, value); } - private int MappedToIndex(int mappedValue) - { - if (mappedValue >= mappedMod) - throw new ArgumentOutOfRangeException(nameof(mappedValue)); - - if (IsNextGen(mappedValue)) - { - // | XX X> | <= The part from BaseOffset to MappedMod is small enough to consider packets with wrapped numbers again - // /\ NewValue /\ BaseOffset - return (mappedValue + mappedMod) - mappedBaseOffset; - } - else - { - // | X> XX | - // /\ BaseOffset /\ NewValue // normal case - return mappedValue - mappedBaseOffset; - } - } - public ItemSetStatus IsSet(int mappedValue) { - int index = MappedToIndex(mappedValue); + int index = Window.MappedToIndex(mappedValue); return IsSetIndex(index); } @@ -148,24 +118,18 @@ private ItemSetStatus IsSetIndex(int index) { if (index < 0) return ItemSetStatus.OutOfWindowSet; - if (index > currentLength && index < MaxBufferSize) + if (index > Count && index < MaxBufferSize) return ItemSetStatus.InWindowNotSet; if (index >= MaxBufferSize) return ItemSetStatus.OutOfWindowNotSet; return StateGet(index) ? ItemSetStatus.InWindowSet : ItemSetStatus.InWindowNotSet; } - public bool IsNextGen(int mappedValue) => mappedBaseOffset > mappedMod - MaxBufferSize && mappedValue < MaxBufferSize; - - public uint GetGeneration(int mappedValue) => (uint)(generation + (IsNextGen(mappedValue) ? 1 : 0)); - public bool TryDequeue(out T value) { if (!TryPeekStart(0, out value)) return false; BufferPop(); - mappedBaseOffset = (mappedBaseOffset + 1) % mappedMod; - if (mappedBaseOffset == 0) - generation++; + Window.Advance(1); return true; } @@ -174,9 +138,9 @@ public bool TryPeekStart(int index, out T value) if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); - if (index >= Count || currentLength <= 0 || !StateGet(index)) + if (index >= Count || Count <= 0 || !StateGet(index)) { - value = default(T); + value = default; return false; } else @@ -189,10 +153,9 @@ public bool TryPeekStart(int index, out T value) public void Clear() { currentStart = 0; - currentLength = 0; + Count = 0; Array.Clear(ringBufferSet, 0, ringBufferSet.Length); - mappedBaseOffset = 0; - generation = 0; + Window.Reset(); } } diff --git a/TS3Client/Full/S2CPacket.cs b/TS3Client/Full/S2CPacket.cs deleted file mode 100644 index 0ce93782..00000000 --- a/TS3Client/Full/S2CPacket.cs +++ /dev/null @@ -1,37 +0,0 @@ -// TS3Client - A free TeamSpeak3 client implementation -// Copyright (C) 2017 TS3Client contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the Open Software License v. 3.0 -// -// You should have received a copy of the Open Software License along with this -// program. If not, see . - -namespace TS3Client.Full -{ - using System; - using System.Buffers.Binary; - - internal sealed class S2CPacket : BasePacket - { - public const int HeaderLen = 3; - - public override bool FromServer { get; } = true; - public override int HeaderLength { get; } = HeaderLen; - - public S2CPacket(byte[] raw) - { - Raw = raw; - Header = new byte[HeaderLen]; - } - - public override void BuildHeader(Span into) - { - BinaryPrimitives.WriteUInt16BigEndian(into.Slice(0, 2), PacketId); - into[2] = PacketTypeFlagged; -#if DEBUG - into.CopyTo(Header.AsSpan()); -#endif - } - } -} diff --git a/TS3Client/Full/Ts3Crypt.cs b/TS3Client/Full/Ts3Crypt.cs index 51d95b85..3c6313a8 100644 --- a/TS3Client/Full/Ts3Crypt.cs +++ b/TS3Client/Full/Ts3Crypt.cs @@ -27,7 +27,6 @@ namespace TS3Client.Full using System.Buffers.Binary; using System.Diagnostics; using System.Linq; - using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -44,8 +43,8 @@ public sealed class Ts3Crypt private readonly EaxBlockCipher eaxCipher = new EaxBlockCipher(new AesEngine()); private static readonly Regex IdentityRegex = new Regex(@"^(?\d+)V(?[\w\/\+]+={0,2})$", RegexOptions.ECMAScript | RegexOptions.CultureInvariant); - private const int MacLen = 8; - private const int PacketTypeKinds = 9; + internal const int MacLen = 8; + internal const int PacketTypeKinds = 9; public IdentityData Identity { get; set; } @@ -85,7 +84,7 @@ internal void Reset() /// A number which determines the security level of an identity. /// The last brute forced number. Default 0: will take the current keyOffset. /// The identity information. - public static R LoadIdentityDynamic(string key, ulong keyOffset = 0, ulong lastCheckedKeyOffset = 0) + public static R LoadIdentityDynamic(string key, ulong keyOffset = 0, ulong lastCheckedKeyOffset = 0) { var ts3identity = DeobfuscateAndImportTs3Identity(key); if (ts3identity.Ok) @@ -99,7 +98,7 @@ public static R LoadIdentityDynamic(string key, ulong keyOffset = /// A number which determines the security level of an identity. /// The last brute forced number. Default 0: will take the current keyOffset. /// The identity information. - public static R LoadIdentity(string key, ulong keyOffset, ulong lastCheckedKeyOffset = 0) + public static R LoadIdentity(string key, ulong keyOffset, ulong lastCheckedKeyOffset = 0) { // Note: libtomcrypt stores the private AND public key when exporting a private key // This makes importing very convenient :) @@ -124,7 +123,7 @@ private static IdentityData LoadIdentity(ECPoint publicKey, BigInteger privateKe private static readonly ECKeyGenerationParameters KeyGenParams = new ECKeyGenerationParameters(X9ObjectIdentifiers.Prime256v1, new SecureRandom()); - private static R ImportPublicKey(byte[] asnByteArray) + private static R ImportPublicKey(byte[] asnByteArray) { try { @@ -138,7 +137,7 @@ private static R ImportPublicKey(byte[] asnByteArray) catch (Exception) { return "Could not import public key"; } } - private static R<(ECPoint publicKey, BigInteger privateKey)> ImportKeyDynamic(byte[] asnByteArray) + private static R<(ECPoint publicKey, BigInteger privateKey), string> ImportKeyDynamic(byte[] asnByteArray) { BigInteger privateKey = null; ECPoint publicKey = null; @@ -211,7 +210,7 @@ internal static ECPoint RestorePublicFromPrivateKey(BigInteger privateKey) private static readonly byte[] Ts3IdentityObfuscationKey = Encoding.ASCII.GetBytes("b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"); - public static R DeobfuscateAndImportTs3Identity(string identity) + public static R DeobfuscateAndImportTs3Identity(string identity) { var match = IdentityRegex.Match(identity); if (!match.Success) @@ -228,7 +227,7 @@ public static R DeobfuscateAndImportTs3Identity(string identity) if (ident.Value.Length < 20) return "Identity too short"; - int nullIdx = identityArr.AsSpan().Slice(20).IndexOf((byte)0); + int nullIdx = identityArr.AsSpan(20).IndexOf((byte)0); var hash = Hash1It(identityArr, 20, nullIdx < 0 ? identityArr.Length - 20 : nullIdx); XorBinary(identityArr, hash, 20, identityArr); @@ -237,7 +236,7 @@ public static R DeobfuscateAndImportTs3Identity(string identity) if (System.Buffers.Text.Base64.DecodeFromUtf8InPlace(identityArr, out var length) != System.Buffers.OperationStatus.Done) return "Invalid deobfuscated base64 string"; - var importRes = ImportKeyDynamic(identityArr.AsSpan().Slice(0, length).ToArray()); + var importRes = ImportKeyDynamic(identityArr.AsSpan(0, length).ToArray()); if (!importRes.Ok) return importRes.Error; @@ -253,7 +252,7 @@ public static R DeobfuscateAndImportTs3Identity(string identity) /// The alpha key from clientinit encoded in base64. /// The beta key from clientinit encoded in base64. /// The omega key from clientinit encoded in base64. - internal R CryptoInit(string alpha, string beta, string omega) + internal E CryptoInit(string alpha, string beta, string omega) { if (Identity == null) throw new InvalidOperationException($"No identity has been imported or created. Use the {nameof(LoadIdentity)} or {nameof(GenerateNewIdentity)} method before."); @@ -292,7 +291,7 @@ private byte[] GetSharedSecret(ECPoint publicKeyPoint) /// The alpha key from clientinit. /// The beta key from clientinit. /// The omega key from clientinit. - private R SetSharedSecret(ReadOnlySpan alpha, ReadOnlySpan beta, ReadOnlySpan sharedKey) + private E SetSharedSecret(ReadOnlySpan alpha, ReadOnlySpan beta, ReadOnlySpan sharedKey) { if (beta.Length != 10 && beta.Length != 54) return $"Invalid beta size ({beta.Length})"; @@ -302,7 +301,7 @@ private R SetSharedSecret(ReadOnlySpan alpha, ReadOnlySpan beta, Rea // applying hashes to get the required values for ts3 XorBinary(sharedKey, alpha, alpha.Length, ivStruct); - XorBinary(sharedKey.Slice(10), beta, beta.Length, ivStruct.AsSpan().Slice(10)); + XorBinary(sharedKey.Slice(10), beta, beta.Length, ivStruct.AsSpan(10)); // creating a dummy signature which will be used on packets which dont use a real encryption signature (like plain voice) var buffer2 = Hash1It(ivStruct, 0, ivStruct.Length); @@ -310,10 +309,10 @@ private R SetSharedSecret(ReadOnlySpan alpha, ReadOnlySpan beta, Rea alphaTmp = null; CryptoInitComplete = true; - return R.OkR; + return R.Ok; } - internal R CryptoInit2(string license, string omega, string proof, string beta, byte[] privateKey) + internal E CryptoInit2(string license, string omega, string proof, string beta, byte[] privateKey) { var licenseBytes = Base64Decode(license); if (!licenseBytes.Ok) return "license parameter is invalid"; @@ -328,7 +327,7 @@ internal R CryptoInit2(string license, string omega, string proof, string beta, // Verify that our connection isn't tampered with if (!VerifySign(serverPublicKey.Value, licenseBytes.Value, proofBytes.Value)) - return "The init proof is not valid. Your connection might be tampered with or the sever is an idiot."; + return "The init proof is not valid. Your connection might be tampered with or the server is an idiot."; var sw = Stopwatch.StartNew(); var licenseChainR = Licenses.Parse(licenseBytes.Value); @@ -363,17 +362,35 @@ private static byte[] GetSharedSecret2(ReadOnlySpan publicKey, ReadOnlySpa return bytes; } - internal R ProcessInit1(byte[] data) + internal R ProcessInit1(byte[] data) { const int versionLen = 4; const int initTypeLen = 1; + const string packetInvalid = "Invalid Init1 packet"; + const string packetTooShort = packetInvalid + " (too short)"; + const string packetInvalidStep = packetInvalid + " (invalid step)"; + const string packetInvalidLength = packetInvalid + " (invalid length)"; + int? type = null; if (data != null) { - type = data[0]; - if (data.Length < initTypeLen) - return "Invalid Init1 packet (too short)"; + if (Packet.FromServer) + { + if (data.Length < initTypeLen) + return packetTooShort; + type = data[0]; + if (type != 1 && type != 3 && type != 0x7F) + return packetInvalidStep; + } + else + { + if (data.Length < versionLen + initTypeLen) + return packetTooShort; + type = data[4]; + if (type != 0 && type != 2 && type != 4) + return packetInvalidStep; + } } byte[] sendData; @@ -386,9 +403,16 @@ internal R ProcessInit1(byte[] data) sendData = new byte[versionLen + initTypeLen + 4 + 4 + 8]; Array.Copy(Initversion, 0, sendData, 0, versionLen); // initVersion sendData[versionLen] = 0x00; // initType - BinaryPrimitives.WriteUInt32BigEndian(sendData.AsSpan().Slice(versionLen + initTypeLen), Util.UnixNow);// 4byte timestamp - for (int i = 0; i < 4; i++) - sendData[i + versionLen + initTypeLen + 4] = (byte)Util.Random.Next(0, 256); // 4byte random + BinaryPrimitives.WriteUInt32BigEndian(sendData.AsSpan(versionLen + initTypeLen), Util.UnixNow); // 4byte timestamp + BinaryPrimitives.WriteInt32BigEndian(sendData.AsSpan(versionLen + initTypeLen + 4), Util.Random.Next()); // 4byte random + return sendData; + + case 0: + if (data.Length != 21) + return packetInvalidLength; + sendData = new byte[initTypeLen + 16 + 4]; + sendData[0] = 0x01; // initType + BinaryPrimitives.WriteUInt32BigEndian(sendData.AsSpan(initTypeLen + 16), BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(versionLen + initTypeLen + 4))); return sendData; case 1: @@ -398,18 +422,30 @@ internal R ProcessInit1(byte[] data) sendData = new byte[versionLen + initTypeLen + 16 + 4]; Array.Copy(Initversion, 0, sendData, 0, versionLen); // initVersion sendData[versionLen] = 0x02; // initType - Array.Copy(data, 1, sendData, versionLen + initTypeLen, 20); + Array.Copy(data, initTypeLen, sendData, versionLen + initTypeLen, 20); return sendData; case 5: - var errorNum = BinaryPrimitives.ReadUInt32LittleEndian(data.AsReadOnlySpan().Slice(1)); + var errorNum = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(initTypeLen)); if (Enum.IsDefined(typeof(Ts3ErrorCode), errorNum)) return $"Got Init1(1) error: {(Ts3ErrorCode)errorNum}"; return $"Got Init1(1) undefined error code: {errorNum}"; default: - return "Invalid or unrecognized Init1(1) packet"; + return packetInvalidLength; } + case 2: + if (data.Length != versionLen + initTypeLen + 16 + 4) + return packetInvalidLength; + sendData = new byte[initTypeLen + 64 + 64 + 4 + 100]; + sendData[0] = 0x03; // initType + sendData[initTypeLen + 64 - 1] = 1; // dummy x to 1 + sendData[initTypeLen + 64 + 64 - 1] = 1; // dummy n to 1 + BinaryPrimitives.WriteInt32BigEndian(sendData.AsSpan(initTypeLen + 64 + 64), 1); // dummy level to 1 + return sendData; + case 3: + if (data.Length != initTypeLen + 64 + 64 + 4 + 100) + return packetInvalidLength; alphaTmp = new byte[10]; Util.Random.NextBytes(alphaTmp); var alpha = Convert.ToBase64String(alphaTmp); @@ -422,13 +458,13 @@ internal R ProcessInit1(byte[] data) var textBytes = Util.Encoder.GetBytes(initAdd); // Prepare solution - int level = BinaryPrimitives.ReadInt32BigEndian(data.AsReadOnlySpan().Slice(initTypeLen + 128)); + int level = BinaryPrimitives.ReadInt32BigEndian(data.AsSpan(initTypeLen + 128)); var y = SolveRsaChallange(data, initTypeLen, level); if (!y.Ok) - return y; + return y.Error; // Copy bytes for this result: [Version..., InitType..., data..., y..., text...] - sendData = new byte[versionLen + initTypeLen + 232 + 64 + textBytes.Length]; + sendData = new byte[versionLen + initTypeLen + 64 + 64 + 4 + 100 + 64 + textBytes.Length]; // Copy this.Version Array.Copy(Initversion, 0, sendData, 0, versionLen); // Write InitType @@ -441,6 +477,12 @@ internal R ProcessInit1(byte[] data) Array.Copy(textBytes, 0, sendData, versionLen + initTypeLen + 232 + 64, textBytes.Length); return sendData; + case 4: + if (data.Length < versionLen + initTypeLen + 64 + 64 + 4 + 100 + 64) + return packetTooShort; + // TODO check result + return Array.Empty(); + default: return $"Got invalid Init1({type}) packet id"; } @@ -451,7 +493,7 @@ internal R ProcessInit1(byte[] data) /// The offset of x and n in the data array. /// The exponent to x. /// The y value, unsigned, as a BigInteger bytearray. - private static R SolveRsaChallange(byte[] data, int offset, int level) + private static R SolveRsaChallange(byte[] data, int offset, int level) { if (level < 0 || level > 1_000_000) return "RSA challange level is not within an acceptable range"; @@ -465,7 +507,7 @@ private static R SolveRsaChallange(byte[] data, int offset, int level) internal static (byte[] publicKey, byte[] privateKey) GenerateTemporaryKey() { var privateKey = new byte[32]; - using (var rng = RandomNumberGenerator.Create()) + using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create()) rng.GetBytes(privateKey); ScalarOperations.sc_clamp(privateKey); @@ -480,20 +522,20 @@ internal static (byte[] publicKey, byte[] privateKey) GenerateTemporaryKey() #region ENCRYPTION/DECRYPTION - internal void Encrypt(BasePacket packet) + internal void Encrypt(ref Packet packet) { if (packet.PacketType == PacketType.Init1) { - FakeEncrypt(packet, Ts3InitMac); + FakeEncrypt(ref packet, Ts3InitMac); return; } if (packet.UnencryptedFlag) { - FakeEncrypt(packet, fakeSignature); + FakeEncrypt(ref packet, fakeSignature); return; } - var (key, nonce) = GetKeyNonce(packet.FromServer, packet.PacketId, packet.GenerationId, packet.PacketType); + var (key, nonce) = GetKeyNonce(Packet.FromServer, packet.PacketId, packet.GenerationId, packet.PacketType, !CryptoInitComplete); packet.BuildHeader(); ICipherParameters ivAndKey = new AeadParameters(new KeyParameter(key), 8 * MacLen, nonce, packet.Header); @@ -515,69 +557,65 @@ internal void Encrypt(BasePacket packet) // to build the final TS3/libtomcrypt we need to copy it into another order // len is Data.Length + Mac.Length - packet.Raw = new byte[packet.HeaderLength + len]; + // //packet.Raw = new byte[Packet.HeaderLength + len]; // Copy the Mac from [Data..., Mac...] to [Mac..., Header..., Data...] Array.Copy(result, len - MacLen, packet.Raw, 0, MacLen); // Copy the Header from packet.Header to [Mac..., Header..., Data...] - Array.Copy(packet.Header, 0, packet.Raw, MacLen, packet.HeaderLength); + Array.Copy(packet.Header, 0, packet.Raw, MacLen, Packet.HeaderLength); // Copy the Data from [Data..., Mac...] to [Mac..., Header..., Data...] - Array.Copy(result, 0, packet.Raw, MacLen + packet.HeaderLength, len - MacLen); + Array.Copy(result, 0, packet.Raw, MacLen + Packet.HeaderLength, len - MacLen); // Raw is now [Mac..., Header..., Data...] } - private static void FakeEncrypt(BasePacket packet, byte[] mac) + private static void FakeEncrypt(ref Packet packet, byte[] mac) { - packet.Raw = new byte[packet.Data.Length + MacLen + packet.HeaderLength]; + // //packet.Raw = new byte[packet.Data.Length + MacLen + Packet.HeaderLength]; // Copy the Mac from [Mac...] to [Mac..., Header..., Data...] Array.Copy(mac, 0, packet.Raw, 0, MacLen); // Copy the Header from packet.Header to [Mac..., Header..., Data...] - packet.BuildHeader(packet.Raw.AsSpan().Slice(MacLen, packet.HeaderLength)); + packet.BuildHeader(packet.Raw.AsSpan(MacLen, Packet.HeaderLength)); // Copy the Data from packet.Data to [Mac..., Header..., Data...] - Array.Copy(packet.Data, 0, packet.Raw, MacLen + packet.HeaderLength, packet.Data.Length); + Array.Copy(packet.Data, 0, packet.Raw, MacLen + Packet.HeaderLength, packet.Data.Length); // Raw is now [Mac..., Header..., Data...] } - internal static S2CPacket GetS2CPacket(byte[] data) - { - if (data.Length < S2CPacket.HeaderLen + MacLen) - return null; - - return new S2CPacket(data) - { - PacketTypeFlagged = data[MacLen + 2], - PacketId = BinaryPrimitives.ReadUInt16BigEndian(data.AsReadOnlySpan().Slice(MacLen)), - }; - } - - internal static C2SPacket GetC2SPacket(byte[] data) - { - if (data.Length < C2SPacket.HeaderLen + MacLen) - return null; - // TODO standartize packet direction generation see s2c/c2s - return new C2SPacket(null, 0) - { - Raw = data, - PacketTypeFlagged = data[MacLen + 4], - PacketId = BinaryPrimitives.ReadUInt16BigEndian(data.AsReadOnlySpan().Slice(MacLen)), - }; - } - - internal bool Decrypt(BasePacket packet) + internal bool Decrypt(ref Packet packet) { if (packet.PacketType == PacketType.Init1) - return FakeDecrypt(packet, Ts3InitMac); + return FakeDecrypt(ref packet, Ts3InitMac); if (packet.UnencryptedFlag) - return FakeDecrypt(packet, fakeSignature); - - return DecryptData(packet); + return FakeDecrypt(ref packet, fakeSignature); + + var decryptResult = DecryptData(ref packet, !CryptoInitComplete); + if (decryptResult) + return true; + + // This is a hacky workaround for a special ack: + // We send these two packets simultaneously: + // - [Id:1] clientek (dummy-encrypted) + // - [Id:2] clientinit (session-encrypted) + // We get an ack for each with the same encryption scheme. + // We can't know for sure which ack comes first and therefore + // whether the dummy or session key should be used. + // In case we actually picked the wrong key, try it again + // with the dummy key. + if (packet.PacketType == PacketType.Ack && packet.PacketId <= 2) + { + Log.Debug("Using shady ack workaround."); + return DecryptData(ref packet, true); + } + else + { + return false; + } } - private bool DecryptData(BasePacket packet) + private bool DecryptData(ref Packet packet, bool dummyEncryption) { - Array.Copy(packet.Raw, MacLen, packet.Header, 0, packet.HeaderLength); - var (key, nonce) = GetKeyNonce(packet.FromServer, packet.PacketId, packet.GenerationId, packet.PacketType); - int dataLen = packet.Raw.Length - (MacLen + packet.HeaderLength); + Array.Copy(packet.Raw, MacLen, packet.Header, 0, Packet.HeaderLength); + var (key, nonce) = GetKeyNonce(Packet.FromServer, packet.PacketId, packet.GenerationId, packet.PacketType, dummyEncryption); + int dataLen = packet.Raw.Length - (MacLen + Packet.HeaderLength); ICipherParameters ivAndKey = new AeadParameters(new KeyParameter(key), 8 * MacLen, nonce, packet.Header); try @@ -588,7 +626,7 @@ private bool DecryptData(BasePacket packet) eaxCipher.Init(false, ivAndKey); result = new byte[eaxCipher.GetOutputSize(dataLen + MacLen)]; - int len = eaxCipher.ProcessBytes(packet.Raw, MacLen + packet.HeaderLength, dataLen, result, 0); + int len = eaxCipher.ProcessBytes(packet.Raw, MacLen + Packet.HeaderLength, dataLen, result, 0); len += eaxCipher.ProcessBytes(packet.Raw, 0, MacLen, result, len); len += eaxCipher.DoFinal(result, len); @@ -602,13 +640,13 @@ private bool DecryptData(BasePacket packet) return true; } - private static bool FakeDecrypt(BasePacket packet, byte[] mac) + private static bool FakeDecrypt(ref Packet packet, byte[] mac) { if (!CheckEqual(packet.Raw, mac, MacLen)) return false; - int dataLen = packet.Raw.Length - (MacLen + packet.HeaderLength); + int dataLen = packet.Raw.Length - (MacLen + Packet.HeaderLength); packet.Data = new byte[dataLen]; - Array.Copy(packet.Raw, MacLen + packet.HeaderLength, packet.Data, 0, dataLen); + Array.Copy(packet.Raw, MacLen + Packet.HeaderLength, packet.Data, 0, dataLen); return true; } @@ -618,25 +656,25 @@ private static bool FakeDecrypt(BasePacket packet, byte[] mac) /// Each time the packetId reaches 65535 the next packet will go on with 0 and the generationId will be increased by 1. /// The packetType. /// A tuple of (key, nonce) - private (byte[] key, byte[] nonce) GetKeyNonce(bool fromServer, ushort packetId, uint generationId, PacketType packetType) + private (byte[] key, byte[] nonce) GetKeyNonce(bool fromServer, ushort packetId, uint generationId, PacketType packetType, bool dummyEncryption) { - if (!CryptoInitComplete) + if (dummyEncryption) return DummyKeyAndNonceTuple; // only the lower 4 bits are used for the real packetType - byte packetTypeRaw = (byte)packetType; + var packetTypeRaw = (byte)packetType; int cacheIndex = packetTypeRaw * (fromServer ? 1 : 2); if (!cachedKeyNonces[cacheIndex].HasValue || cachedKeyNonces[cacheIndex].Value.generation != generationId) { // this part of the key/nonce is fixed by the message direction and packetType - byte[] tmpToHash = new byte[ivStruct.Length == 20 ? 26 : 70]; + var tmpToHash = new byte[ivStruct.Length == 20 ? 26 : 70]; tmpToHash[0] = fromServer ? (byte)0x30 : (byte)0x31; tmpToHash[1] = packetTypeRaw; - BinaryPrimitives.WriteUInt32BigEndian(tmpToHash.AsSpan().Slice(2), generationId); + BinaryPrimitives.WriteUInt32BigEndian(tmpToHash.AsSpan(2), generationId); Array.Copy(ivStruct, 0, tmpToHash, 6, ivStruct.Length); var result = Hash256It(tmpToHash).AsSpan(); @@ -644,8 +682,8 @@ private static bool FakeDecrypt(BasePacket packet, byte[] mac) cachedKeyNonces[cacheIndex] = (result.Slice(0, 16).ToArray(), result.Slice(16, 16).ToArray(), generationId); } - byte[] key = new byte[16]; - byte[] nonce = new byte[16]; + var key = new byte[16]; + var nonce = new byte[16]; Array.Copy(cachedKeyNonces[cacheIndex].Value.key, 0, key, 0, 16); Array.Copy(cachedKeyNonces[cacheIndex].Value.nonce, 0, nonce, 0, 16); @@ -678,13 +716,13 @@ private static void XorBinary(ReadOnlySpan a, ReadOnlySpan b, int le outBuf[i] = (byte)(a[i] ^ b[i]); } - private static readonly SHA1Managed Sha1HashInternal = new SHA1Managed(); + private static readonly System.Security.Cryptography.SHA1Managed Sha1HashInternal = new System.Security.Cryptography.SHA1Managed(); private static readonly Sha256Digest Sha256Hash = new Sha256Digest(); private static readonly Sha512Digest Sha512Hash = new Sha512Digest(); internal static byte[] Hash1It(byte[] data, int offset = 0, int len = 0) => HashItInternal(Sha1HashInternal, data, offset, len); internal static byte[] Hash256It(byte[] data, int offset = 0, int len = 0) => HashIt(Sha256Hash, data, offset, len); internal static byte[] Hash512It(byte[] data, int offset = 0, int len = 0) => HashIt(Sha512Hash, data, offset, len); - private static byte[] HashItInternal(HashAlgorithm hashAlgo, byte[] data, int offset = 0, int len = 0) + private static byte[] HashItInternal(System.Security.Cryptography.HashAlgorithm hashAlgo, byte[] data, int offset = 0, int len = 0) { lock (hashAlgo) { @@ -704,6 +742,12 @@ private static byte[] HashIt(IDigest hashAlgo, byte[] data, int offset = 0, int return result; } + /// + /// Hashes a password like TeamSpeak. + /// The hash works like this: base64(sha1(password)) + /// + /// The password to hash. + /// The hashed password. public static string HashPassword(string password) { if (string.IsNullOrEmpty(password)) @@ -750,7 +794,7 @@ public static void VersionSelfCheck() } } - private static R Base64Decode(string str) + private static R Base64Decode(string str) { try { return Convert.FromBase64String(str); } catch (FormatException) { return "Malformed base64 string"; } @@ -771,8 +815,8 @@ private static R Base64Decode(string str) /// The targeted level. public static void ImproveSecurity(IdentityData identity, int toLevel) { - byte[] hashBuffer = new byte[identity.PublicKeyString.Length + MaxUlongStringLen]; - byte[] pubKeyBytes = Encoding.ASCII.GetBytes(identity.PublicKeyString); + var hashBuffer = new byte[identity.PublicKeyString.Length + MaxUlongStringLen]; + var pubKeyBytes = Encoding.ASCII.GetBytes(identity.PublicKeyString); Array.Copy(pubKeyBytes, 0, hashBuffer, 0, pubKeyBytes.Length); identity.LastCheckedKeyOffset = Math.Max(identity.ValidKeyOffset, identity.LastCheckedKeyOffset); @@ -793,8 +837,8 @@ public static void ImproveSecurity(IdentityData identity, int toLevel) public static int GetSecurityLevel(IdentityData identity) { - byte[] hashBuffer = new byte[identity.PublicKeyString.Length + MaxUlongStringLen]; - byte[] pubKeyBytes = Encoding.ASCII.GetBytes(identity.PublicKeyString); + var hashBuffer = new byte[identity.PublicKeyString.Length + MaxUlongStringLen]; + var pubKeyBytes = Encoding.ASCII.GetBytes(identity.PublicKeyString); Array.Copy(pubKeyBytes, 0, hashBuffer, 0, pubKeyBytes.Length); return GetSecurityLevel(hashBuffer, pubKeyBytes.Length, identity.ValidKeyOffset); } @@ -881,18 +925,5 @@ private static bool ValidateHash(byte[] data, int reqLevel) } #endregion - - enum CryptoVer - { - Unknown, - /// - /// Supported on server <3.1. - /// Supported on all clients. - Version1 = 1, - /// - /// Supported on server >=3.1. - /// Supported on clients >=3.1.6. - Version2 = 2, - } } } diff --git a/TS3Client/Full/Ts3FullClient.cs b/TS3Client/Full/Ts3FullClient.cs index f4248972..5487feac 100644 --- a/TS3Client/Full/Ts3FullClient.cs +++ b/TS3Client/Full/Ts3FullClient.cs @@ -22,14 +22,15 @@ namespace TS3Client.Full using ChannelIdT = System.UInt64; using ClientDbIdT = System.UInt64; using ClientIdT = System.UInt16; - using CmdR = E; + using CmdR = System.E; + using Uid = System.String; /// Creates a full TeamSpeak3 client with voice capabilities. - public sealed class Ts3FullClient : Ts3BaseFunctions, IAudioActiveProducer, IAudioPassiveConsumer + public sealed partial class Ts3FullClient : Ts3BaseFunctions, IAudioActiveProducer, IAudioPassiveConsumer { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); private readonly Ts3Crypt ts3Crypt; - private readonly PacketHandler packetHandler; + private readonly PacketHandler packetHandler; private readonly AsyncMessageProcessor msgProc; private readonly object statusLock = new object(); @@ -49,11 +50,8 @@ public sealed class Ts3FullClient : Ts3BaseFunctions, IAudioActiveProducer, IAud public override bool Connected { get { lock (statusLock) return status == Ts3ClientStatus.Connected; } } public override bool Connecting { get { lock (statusLock) return status == Ts3ClientStatus.Connecting; } } private ConnectionDataFull connectionDataFull; + public Book.Connection Book { get; set; } //= new Book.Connection(); - public override event NotifyEventHandler OnTextMessageReceived; - public override event NotifyEventHandler OnClientEnterView; - public override event NotifyEventHandler OnClientLeftView; - public event NotifyEventHandler OnClientMoved; public override event EventHandler OnConnected; public override event EventHandler OnDisconnected; public event EventHandler OnErrorEvent; @@ -65,7 +63,7 @@ public Ts3FullClient(EventDispatchType dispatcherType) { status = Ts3ClientStatus.Disconnected; ts3Crypt = new Ts3Crypt(); - packetHandler = new PacketHandler(ts3Crypt); + packetHandler = new PacketHandler(ts3Crypt); msgProc = new AsyncMessageProcessor(); dispatcher = EventDispatcherHelper.Create(dispatcherType); context = new ConnectionContext { WasExit = true }; @@ -161,174 +159,83 @@ private void DisconnectInternal(ConnectionContext ctx, CommandError error = null OnDisconnected?.Invoke(this, new DisconnectEventArgs(packetHandler.ExitReason ?? Reason.LeftServer, error)); } - private void InvokeEvent(LazyNotification lazyNotification) + private void NetworkLoop(object ctxObject) { - var notification = lazyNotification.Notifications; - switch (lazyNotification.NotifyType) - { - case NotificationType.ChannelCreated: break; - case NotificationType.ChannelDeleted: break; - case NotificationType.ChannelChanged: break; - case NotificationType.ChannelEdited: break; - case NotificationType.ChannelMoved: break; - case NotificationType.ChannelPasswordChanged: break; - case NotificationType.ClientEnterView: OnClientEnterView?.Invoke(this, notification.Cast()); break; - case NotificationType.ClientLeftView: - var clientLeftArr = notification.Cast().ToArray(); - var leftViewEvent = clientLeftArr.FirstOrDefault(clv => clv.ClientId == packetHandler.ClientId); - if (leftViewEvent != null) - { - packetHandler.ExitReason = leftViewEvent.Reason; - DisconnectInternal(context, setStatus: Ts3ClientStatus.Disconnected); - break; - } - OnClientLeftView?.Invoke(this, clientLeftArr); - break; - - case NotificationType.ClientMoved: OnClientMoved?.Invoke(this, notification.Cast()); break; - case NotificationType.ServerEdited: break; - case NotificationType.TextMessage: OnTextMessageReceived?.Invoke(this, notification.Cast()); break; - case NotificationType.TokenUsed: break; - // full client events - case NotificationType.InitIvExpand: { var result = lazyNotification.WrapSingle(); if (result.Ok) ProcessInitIvExpand(result.Value); } break; - case NotificationType.InitIvExpand2: { var result = lazyNotification.WrapSingle(); if (result.Ok) ProcessInitIvExpand2(result.Value); } break; - case NotificationType.InitServer: { var result = lazyNotification.WrapSingle(); if (result.Ok) ProcessInitServer(result.Value); } break; - case NotificationType.ChannelList: break; - case NotificationType.ChannelListFinished: ChannelSubscribeAll(); break; - case NotificationType.ClientNeededPermissions: break; - case NotificationType.ClientChannelGroupChanged: break; - case NotificationType.ClientServerGroupAdded: break; - case NotificationType.ConnectionInfo: break; - case NotificationType.ConnectionInfoRequest: ProcessConnectionInfoRequest(); break; - case NotificationType.ChannelSubscribed: break; - case NotificationType.ChannelUnsubscribed: break; - case NotificationType.ClientChatComposing: break; - case NotificationType.ServerGroupList: break; - case NotificationType.ClientServerGroup: break; - case NotificationType.FileUpload: break; - case NotificationType.FileDownload: break; - case NotificationType.FileTransfer: break; - case NotificationType.FileTransferStatus: break; - case NotificationType.FileList: break; - case NotificationType.FileListFinished: break; - case NotificationType.FileInfoTs: break; - case NotificationType.ChannelGroupList: break; - case NotificationType.PluginCommand: { var result = lazyNotification.WrapSingle(); if (result.Ok) ProcessPluginRequest(result.Value); } break; - // special - case NotificationType.CommandError: - { - var result = lazyNotification.WrapSingle(); - var error = result.Ok ? result.Value : Util.CustomError("Got empty error while connecting."); + var ctx = (ConnectionContext)ctxObject; + packetHandler.PacketEvent += (ref Packet packet) => { PacketEvent(ctx, ref packet); }; - bool skipError = false; - bool disconnect = false; - lock (statusLock) - { - if (status == Ts3ClientStatus.Connecting) - { - disconnect = true; - skipError = true; - } - } + packetHandler.FetchPackets(); - if (disconnect) - DisconnectInternal(context, error, Ts3ClientStatus.Disconnected); - if (!skipError) - OnErrorEvent?.Invoke(this, error); - } - break; - case NotificationType.Unknown: - default: throw Util.UnhandledDefault(lazyNotification.NotifyType); + lock (statusLock) + { + DisconnectInternal(ctx, setStatus: Ts3ClientStatus.Disconnected); } } - private void NetworkLoop(object ctxObject) + private void PacketEvent(ConnectionContext ctx, ref Packet packet) { - var ctx = (ConnectionContext)ctxObject; - - while (true) + lock (statusLock) { - lock (statusLock) - { - if (ctx.WasExit) - break; - } - - var packet = packetHandler.FetchPacket(); - if (packet == null) - break; + if (ctx.WasExit) + return; - lock (statusLock) + switch (packet.PacketType) { - if (ctx.WasExit) - break; + case PacketType.Command: + case PacketType.CommandLow: + LogCmd.ConditionalDebug("[I] {0}", Util.Encoder.GetString(packet.Data)); + var result = msgProc.PushMessage(packet.Data); + if (result.HasValue) + dispatcher.Invoke(result.Value); + break; - switch (packet.PacketType) + case PacketType.Voice: + case PacketType.VoiceWhisper: + OutStream?.Write(packet.Data, new Meta { - case PacketType.Command: - case PacketType.CommandLow: - string message = Util.Encoder.GetString(packet.Data, 0, packet.Data.Length); - LogCmd.Debug("[I] {0}", message); - var result = msgProc.PushMessage(message); - if (result.HasValue) - dispatcher.Invoke(result.Value); - break; - - case PacketType.Voice: - case PacketType.VoiceWhisper: - OutStream?.Write(packet.Data, new Meta - { - In = new MetaIn - { - Whisper = packet.PacketType == PacketType.VoiceWhisper - } - }); - break; - - case PacketType.Init1: - // Init error - if (packet.Data.Length == 5 && packet.Data[0] == 1) + In = new MetaIn { - var errorNum = BinaryPrimitives.ReadUInt32LittleEndian(packet.Data.AsReadOnlySpan().Slice(1)); - if (Enum.IsDefined(typeof(Ts3ErrorCode), errorNum)) - Log.Info("Got init error: {0}", (Ts3ErrorCode)errorNum); - else - Log.Warn("Got undefined init error: {0}", errorNum); - DisconnectInternal(ctx, setStatus: Ts3ClientStatus.Disconnected); + Whisper = packet.PacketType == PacketType.VoiceWhisper } - break; + }); + break; + + case PacketType.Init1: + // Init error + if (packet.Data.Length == 5 && packet.Data[0] == 1) + { + var errorNum = BinaryPrimitives.ReadUInt32LittleEndian(packet.Data.AsSpan(1)); + if (Enum.IsDefined(typeof(Ts3ErrorCode), errorNum)) + Log.Info("Got init error: {0}", (Ts3ErrorCode)errorNum); + else + Log.Warn("Got undefined init error: {0}", errorNum); + DisconnectInternal(ctx, setStatus: Ts3ClientStatus.Disconnected); } + break; } } - - lock (statusLock) - { - DisconnectInternal(ctx, setStatus: Ts3ClientStatus.Disconnected); - } } - private void ProcessInitIvExpand(InitIvExpand initIvExpand) + // Local event processing + + partial void ProcessEachInitIvExpand(InitIvExpand initIvExpand) { packetHandler.ReceivedFinalInitAck(); var result = ts3Crypt.CryptoInit(initIvExpand.Alpha, initIvExpand.Beta, initIvExpand.Omega); if (!result) { - DisconnectInternal(context, Util.CustomError($@"Failed to calculate shared secret: {result.Error}")); + DisconnectInternal(context, Util.CustomError($"Failed to calculate shared secret: {result.Error}")); return; } - packetHandler.CryptoInitDone(); - DefaultClientInit(); } - private void ProcessInitIvExpand2(InitIvExpand2 initIvExpand2) + partial void ProcessEachInitIvExpand2(InitIvExpand2 initIvExpand2) { packetHandler.ReceivedFinalInitAck(); - packetHandler.IncPacketCounter(PacketType.Command); - var (publicKey, privateKey) = Ts3Crypt.GenerateTemporaryKey(); var ekBase64 = Convert.ToBase64String(publicKey); @@ -343,25 +250,14 @@ private void ProcessInitIvExpand2(InitIvExpand2 initIvExpand2) var result = ts3Crypt.CryptoInit2(initIvExpand2.License, initIvExpand2.Omega, initIvExpand2.Proof, initIvExpand2.Beta, privateKey); if (!result) { - DisconnectInternal(context, Util.CustomError($@"Failed to calculate shared secret: {result.Error}")); + DisconnectInternal(context, Util.CustomError($"Failed to calculate shared secret: {result.Error}")); return; } - packetHandler.CryptoInitDone(); - DefaultClientInit(); } - private CmdR DefaultClientInit() => ClientInit( - connectionDataFull.Username, - true, true, - connectionDataFull.DefaultChannel, - connectionDataFull.DefaultChannelPassword.HashedPassword, - connectionDataFull.ServerPassword.HashedPassword, - string.Empty, string.Empty, string.Empty, - connectionDataFull.Identity.ClientUid, VersionSign); - - private void ProcessInitServer(InitServer initServer) + partial void ProcessEachInitServer(InitServer initServer) { packetHandler.ClientId = initServer.ClientId; @@ -370,17 +266,61 @@ private void ProcessInitServer(InitServer initServer) OnConnected?.Invoke(this, EventArgs.Empty); } - private void ProcessConnectionInfoRequest() + partial void ProcessEachPluginCommand(PluginCommand cmd) { - SendNoResponsed(packetHandler.NetworkStats.GenerateStatusAnswer()); + if (cmd.Name == "cliententerview" && cmd.Data == "version") + SendPluginCommand("cliententerview", "TAB", PluginTargetMode.Server); } - private void ProcessPluginRequest(PluginCommand cmd) + partial void ProcessEachCommandError(CommandError error) { - if (cmd.Name == "cliententerview" && cmd.Data == "version") - SendPluginCommand("cliententerview", "TAB", PluginTargetMode.Client); + bool skipError = false; + bool disconnect = false; + lock (statusLock) + { + if (status == Ts3ClientStatus.Connecting) + { + disconnect = true; + skipError = true; + } + } + + if (disconnect) + DisconnectInternal(context, error, Ts3ClientStatus.Disconnected); + if (!skipError) + OnErrorEvent?.Invoke(this, error); + } + + partial void ProcessEachClientLeftView(ClientLeftView clientLeftView) + { + if (clientLeftView.ClientId == packetHandler.ClientId) + { + packetHandler.ExitReason = clientLeftView.Reason; + DisconnectInternal(context, setStatus: Ts3ClientStatus.Disconnected); + } + } + + partial void ProcessEachChannelListFinished(ChannelListFinished _) + { + ChannelSubscribeAll(); + } + + partial void ProcessEachConnectionInfoRequest(ConnectionInfoRequest _) + { + SendNoResponsed(packetHandler.NetworkStats.GenerateStatusAnswer()); } + // *** + + private CmdR DefaultClientInit() => ClientInit( + connectionDataFull.Username, + true, true, + connectionDataFull.DefaultChannel, + connectionDataFull.DefaultChannelPassword.HashedPassword, + connectionDataFull.ServerPassword.HashedPassword, + string.Empty, string.Empty, string.Empty, + connectionDataFull.Identity.ClientUid, VersionSign); + /// /// Sends a command to the server. Commands look exactly like query commands and mostly also behave identically. /// NOTE: Do not expect all commands to work exactly like in the query documentation. @@ -391,7 +331,7 @@ private void ProcessPluginRequest(PluginCommand cmd) /// if the client hangs after a special command ( will return null instead). /// Returns R(OK) with an enumeration of the deserialized and split up in objects data. /// Or R(ERR) with the returned error if no response is expected. - public override R, CommandError> SendCommand(Ts3Command com) + public override R SendCommand(Ts3Command com) { using (var wb = new WaitBlock(false)) { @@ -441,10 +381,10 @@ private E SendCommandBase(WaitBlock wb, Ts3Command com) byte[] data = Util.Encoder.GetBytes(message); packetHandler.AddOutgoingPacket(data, PacketType.Command); } - return E.OkR; + return R.Ok; } - public async Task, CommandError>> SendCommandAsync(Ts3Command com) where T : IResponse, new() + public async Task> SendCommandAsync(Ts3Command com) where T : IResponse, new() { using (var wb = new WaitBlock(true)) { @@ -452,7 +392,7 @@ private E SendCommandBase(WaitBlock wb, Ts3Command com) if (!result.Ok) return result.Error; if (com.ExpectResponse) - return await wb.WaitForMessageAsync(); + return await wb.WaitForMessageAsync().ConfigureAwait(false); else // This might not be the nicest way to return in this case // but we don't know what the response is, so this acceptable. @@ -551,9 +491,9 @@ public void SendAudio(ReadOnlySpan data, Codec codec) // > X is a ushort in H2N order of an own audio packet counter // it seems it can be the same as the packet counter so we will let the packethandler do it. // > Y is the codec byte (see Enum) - var tmpBuffer = new byte[data.Length + 3]; + var tmpBuffer = new byte[data.Length + 3]; // stackalloc tmpBuffer[2] = (byte)codec; - data.CopyTo(tmpBuffer.AsSpan().Slice(3)); + data.CopyTo(tmpBuffer.AsSpan(3)); packetHandler.AddOutgoingPacket(tmpBuffer, PacketType.Voice); } @@ -643,19 +583,10 @@ public override R ServerGroupAdd(string na .WrapSingle(); } - public override R, CommandError> ServerGroupsByClientDbId(ClientDbIdT clDbId) - { - var result = SendNotifyCommand(new Ts3Command("servergroupsbyclientid", new List { + public override R ServerGroupsByClientDbId(ClientDbIdT clDbId) + => SendNotifyCommand(new Ts3Command("servergroupsbyclientid", new List { new CommandParameter("cldbid", clDbId) }), - NotificationType.ClientServerGroup); - if (!result.Ok) - return result.Error; - - return R, CommandError>.OkR( - result.Value.Notifications - .Cast() - .Where(x => x.ClientDbId == clDbId)); - } + NotificationType.ClientServerGroup).UnwrapNotification(); public override R FileTransferInitUpload(ChannelIdT channelId, string path, string channelPassword, ushort clientTransferId, long fileSize, bool overwrite, bool resume) @@ -704,24 +635,43 @@ public override R FileTransferInitDownload(ChannelId } } - public override R, CommandError> FileTransferList() + public override R FileTransferList() => SendNotifyCommand(new Ts3Command("ftlist"), NotificationType.FileTransfer).UnwrapNotification(); - public override R, CommandError> FileTransferGetFileList(ChannelIdT channelId, string path, string channelPassword = "") + public override R FileTransferGetFileList(ChannelIdT channelId, string path, string channelPassword = "") => SendNotifyCommand(new Ts3Command("ftgetfilelist", new List() { new CommandParameter("cid", channelId), new CommandParameter("path", path), new CommandParameter("cpw", channelPassword) }), NotificationType.FileList).UnwrapNotification(); - public override R, CommandError> FileTransferGetFileInfo(ChannelIdT channelId, string[] path, string channelPassword = "") + public override R FileTransferGetFileInfo(ChannelIdT channelId, string[] path, string channelPassword = "") => SendNotifyCommand(new Ts3Command("ftgetfileinfo", new List() { new CommandParameter("cid", channelId), new CommandParameter("cpw", channelPassword), new CommandMultiParameter("name", path) }), NotificationType.FileInfoTs).UnwrapNotification(); + public override R ClientGetDbIdFromUid(Uid clientUid) + { + var result = SendNotifyCommand(new Ts3Command("clientgetdbidfromuid", new List { + new CommandParameter("cluid", clientUid) }), + NotificationType.ClientDbIdFromUid); + if (!result.Ok) + return result.Error; + return result.Value.Notifications + .Cast() + .Where(x => x.ClientUid == clientUid) + .Take(1) + .WrapSingle(); + } + + public override R GetClientIds(Uid clientUid) + => SendNotifyCommand(new Ts3Command("clientgetids", new List() { + new CommandParameter("cluid", clientUid) }), + NotificationType.ClientIds).UnwrapNotification(); + #endregion private enum Ts3ClientStatus diff --git a/TS3Client/Full/Ts3Server.cs b/TS3Client/Full/Ts3Server.cs new file mode 100644 index 00000000..e8c410cc --- /dev/null +++ b/TS3Client/Full/Ts3Server.cs @@ -0,0 +1,159 @@ +namespace TS3Client.Full +{ + using Commands; + using Helper; + using Messages; + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Net; + using CmdR = System.E; + + public class Ts3Server : IDisposable + { + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); + private readonly Ts3Crypt ts3Crypt; + private readonly PacketHandler packetHandler; + private readonly AsyncMessageProcessor msgProc; + private readonly IEventDispatcher dispatcher; + bool initCheckDone = false; + public bool Init { get; set; } = false; + + private ConnectionContext context; + private readonly object statusLock = new object(); + + public Ts3Server() + { + ts3Crypt = new Ts3Crypt(); + ts3Crypt.Identity = Ts3Crypt.GenerateNewIdentity(0); + packetHandler = new PacketHandler(ts3Crypt); + msgProc = new AsyncMessageProcessor(); + dispatcher = EventDispatcherHelper.Create(EventDispatchType.AutoThreadPooled); + } + + private void InvokeEvent(LazyNotification lazyNotification) + { + if (!initCheckDone) + return; + + var notification = lazyNotification.Notifications; + switch (lazyNotification.NotifyType) + { + case NotificationType.ClientInit: { var result = lazyNotification.WrapSingle(); if (result.Ok) ProcessClientInit(result.Value); } break; + case NotificationType.ClientInitIv: { var result = lazyNotification.WrapSingle(); if (result.Ok) ProcessClientInitIv(result.Value); } break; + case NotificationType.Unknown: + throw Util.UnhandledDefault(lazyNotification.NotifyType); + } + } + + public void Listen(IPEndPoint addr) + { + packetHandler.Listen(addr); + context = new ConnectionContext(); + dispatcher.Init(NetworkLoop, InvokeEvent, context); + dispatcher.EnterEventLoop(); + } + + private void NetworkLoop(object ctxObject) + { + var ctx = (ConnectionContext)ctxObject; + packetHandler.PacketEvent += (ref Packet packet) => { PacketEvent(ctx, ref packet); }; + + packetHandler.FetchPackets(); + } + + private void PacketEvent(ConnectionContext ctx, ref Packet packet) + { + if (ctx.WasExit) + return; + switch (packet.PacketType) + { + case PacketType.Command: + case PacketType.CommandLow: + var result = msgProc.PushMessage(packet.Data); + if (result.HasValue) + dispatcher.Invoke(result.Value); + break; + + case PacketType.Init1: + if (packet.Data.Length >= 301 && packet.Data[4] == 4) + { + initCheckDone = true; + var resultI = msgProc.PushMessage(packet.Data.AsMemory(301, packet.Data.Length - 301)); + if (resultI.HasValue) + dispatcher.Invoke(resultI.Value); + } + break; + } + } + + [DebuggerStepThrough] + protected CmdR SendNoResponsed(Ts3Command command) + => SendCommand(command.ExpectsResponse(false)); + + public R SendCommand(Ts3Command com) where T : IResponse, new() + { + using (var wb = new WaitBlock(false)) + { + var result = SendCommandBase(wb, com); + if (!result.Ok) + return result.Error; + if (com.ExpectResponse) + return wb.WaitForMessage(); + else + // This might not be the nicest way to return in this case + // but we don't know what the response is, so this acceptable. + return Util.NoResultCommandError; + } + } + + private E SendCommandBase(WaitBlock wb, Ts3Command com) + { + if (context.WasExit || com.ExpectResponse) + return Util.TimeOutCommandError; + + var message = com.ToString(); + byte[] data = Util.Encoder.GetBytes(message); + packetHandler.AddOutgoingPacket(data, PacketType.Command); + return R.Ok; + } + + private void ProcessClientInitIv(ClientInitIv clientInitIv) + { + Log.Info("clientinitiv in"); + lock (statusLock) + { + if (ts3Crypt.CryptoInitComplete) + return; + + var beta = new byte[10]; + Util.Random.NextBytes(beta); + var betaStr = Convert.ToBase64String(beta); + + InitIvExpand(clientInitIv.Alpha, betaStr, ts3Crypt.Identity.PublicKeyString); + + ts3Crypt.CryptoInit(clientInitIv.Alpha, betaStr, clientInitIv.Omega); + } + } + + private void ProcessClientInit(ClientInit clientInit) + { + Init = true; + Console.WriteLine("init!"); + File.AppendAllText("sign.out", $"{clientInit.ClientVersion},{clientInit.ClientPlattform},{clientInit.ClientVersionSign}\n"); + } + + public CmdR InitIvExpand(string alpha, string beta, string omega) + => SendNoResponsed( + new Ts3Command("initivexpand", new List { + new CommandParameter("alpha", alpha), + new CommandParameter("beta", beta), + new CommandParameter("omega", omega) })); + + public void Dispose() + { + packetHandler.Stop(); + } + } +} diff --git a/TS3Client/Generated/Book.cs b/TS3Client/Generated/Book.cs index baa3a726..0836891c 100644 --- a/TS3Client/Generated/Book.cs +++ b/TS3Client/Generated/Book.cs @@ -18,18 +18,14 @@ - - - - - namespace TS3Client.Full.Book { using System; using System.Collections.Generic; - using i8 = System.Byte; - using u8 = System.SByte; + #pragma warning disable CS8019 // Ignore unused imports + using i8 = System.SByte; + using u8 = System.Byte; using i16 = System.Int16; using u16 = System.UInt16; using i32 = System.Int32; @@ -53,10 +49,17 @@ namespace TS3Client.Full.Book using ChannelGroupId = System.UInt64; using IconHash = System.Int32; using ConnectionId = System.UInt32; +#pragma warning restore CS8019 public sealed partial class ServerGroup { - public ServerGroupId Id { get; } + public ServerGroup() + { + + } + + + public ServerGroupId Id { get; internal set; } public str Name { get; set; } public GroupType GroupType { get; set; } public IconHash IconId { get; set; } @@ -70,6 +73,12 @@ public sealed partial class ServerGroup public sealed partial class File { + public File() + { + + } + + public str Path { get; set; } public str Name { get; set; } public i64 Size { get; set; } @@ -79,12 +88,24 @@ public sealed partial class File public sealed partial class OptionalChannelData { + public OptionalChannelData() + { + + } + + public str Description { get; set; } } public sealed partial class Channel { - public ChannelId Id { get; } + public Channel() + { + + } + + + public ChannelId Id { get; internal set; } public ChannelId Parent { get; set; } public str Name { get; set; } public str Topic { get; set; } @@ -100,75 +121,95 @@ public sealed partial class Channel public bool IsUnencrypted { get; set; } public Duration DeleteDelay { get; set; } public i32 NeededTalkPower { get; set; } - public bool ForcedSilence { get; } + public bool ForcedSilence { get; internal set; } public str PhoneticName { get; set; } public IconHash IconId { get; set; } public bool IsPrivate { get; set; } - public OptionalChannelData OptionalData { get; } + public OptionalChannelData OptionalData { get; internal set; } } public sealed partial class OptionalClientData { + public OptionalClientData() + { + + } + + public str Version { get; set; } public str Platform { get; set; } - public str LoginName { get; } - public DateTime Created { get; } - public DateTime LastConnected { get; } - public u32 TotalConnection { get; } - public u64 MonthBytesUploaded { get; } - public u64 MonthBytesDownloaded { get; } - public u64 TotalBytesUploaded { get; } - public u64 TotalBytesDownloaded { get; } + public str LoginName { get; internal set; } + public DateTime Created { get; internal set; } + public DateTime LastConnected { get; internal set; } + public u32 TotalConnection { get; internal set; } + public u64 MonthBytesUploaded { get; internal set; } + public u64 MonthBytesDownloaded { get; internal set; } + public u64 TotalBytesUploaded { get; internal set; } + public u64 TotalBytesDownloaded { get; internal set; } } public sealed partial class ConnectionClientData { - public Duration Ping { get; } - public Duration PingDeviation { get; } - public Duration ConnectedTime { get; } - public SocketAddr ClientAddress { get; } - public u64 PacketsSentSpeech { get; } - public u64 PacketsSentKeepalive { get; } - public u64 PacketsSentControl { get; } - public u64 BytesSentSpeech { get; } - public u64 BytesSentKeepalive { get; } - public u64 BytesSentControl { get; } - public u64 PacketsReceivedSpeech { get; } - public u64 PacketsReceivedKeepalive { get; } - public u64 PacketsReceivedControl { get; } - public u64 BytesReceivedSpeech { get; } - public u64 BytesReceivedKeepalive { get; } - public u64 BytesReceivedControl { get; } - public f32 ServerToClientPacketlossSpeech { get; } - public f32 ServerToClientPacketlossKeepalive { get; } - public f32 ServerToClientPacketlossControl { get; } - public f32 ServerToClientPacketlossTotal { get; } - public f32 ClientToServerPacketlossSpeech { get; } - public f32 ClientToServerPacketlossKeepalive { get; } - public f32 ClientToServerPacketlossControl { get; } - public f32 ClientToServerPacketlossTotal { get; } - public u64 BandwidthSentLastSecondSpeech { get; } - public u64 BandwidthSentLastSecondKeepalive { get; } - public u64 BandwidthSentLastSecondControl { get; } - public u64 BandwidthSentLastMinuteSpeech { get; } - public u64 BandwidthSentLastMinuteKeepalive { get; } - public u64 BandwidthSentLastMinuteControl { get; } - public u64 BandwidthReceivedLastSecondSpeech { get; } - public u64 BandwidthReceivedLastSecondKeepalive { get; } - public u64 BandwidthReceivedLastSecondControl { get; } - public u64 BandwidthReceivedLastMinuteSpeech { get; } - public u64 BandwidthReceivedLastMinuteKeepalive { get; } - public u64 BandwidthReceivedLastMinuteControl { get; } - public u64 FiletransferBandwidthSent { get; } - public u64 FiletransferBandwidthReceived { get; } - public Duration IdleTime { get; } + public ConnectionClientData() + { + + } + + + public Duration Ping { get; internal set; } + public Duration PingDeviation { get; internal set; } + public Duration ConnectedTime { get; internal set; } + public SocketAddr ClientAddress { get; internal set; } + public u64 PacketsSentSpeech { get; internal set; } + public u64 PacketsSentKeepalive { get; internal set; } + public u64 PacketsSentControl { get; internal set; } + public u64 BytesSentSpeech { get; internal set; } + public u64 BytesSentKeepalive { get; internal set; } + public u64 BytesSentControl { get; internal set; } + public u64 PacketsReceivedSpeech { get; internal set; } + public u64 PacketsReceivedKeepalive { get; internal set; } + public u64 PacketsReceivedControl { get; internal set; } + public u64 BytesReceivedSpeech { get; internal set; } + public u64 BytesReceivedKeepalive { get; internal set; } + public u64 BytesReceivedControl { get; internal set; } + public f32 ServerToClientPacketlossSpeech { get; internal set; } + public f32 ServerToClientPacketlossKeepalive { get; internal set; } + public f32 ServerToClientPacketlossControl { get; internal set; } + public f32 ServerToClientPacketlossTotal { get; internal set; } + public f32 ClientToServerPacketlossSpeech { get; internal set; } + public f32 ClientToServerPacketlossKeepalive { get; internal set; } + public f32 ClientToServerPacketlossControl { get; internal set; } + public f32 ClientToServerPacketlossTotal { get; internal set; } + public u64 BandwidthSentLastSecondSpeech { get; internal set; } + public u64 BandwidthSentLastSecondKeepalive { get; internal set; } + public u64 BandwidthSentLastSecondControl { get; internal set; } + public u64 BandwidthSentLastMinuteSpeech { get; internal set; } + public u64 BandwidthSentLastMinuteKeepalive { get; internal set; } + public u64 BandwidthSentLastMinuteControl { get; internal set; } + public u64 BandwidthReceivedLastSecondSpeech { get; internal set; } + public u64 BandwidthReceivedLastSecondKeepalive { get; internal set; } + public u64 BandwidthReceivedLastSecondControl { get; internal set; } + public u64 BandwidthReceivedLastMinuteSpeech { get; internal set; } + public u64 BandwidthReceivedLastMinuteKeepalive { get; internal set; } + public u64 BandwidthReceivedLastMinuteControl { get; internal set; } + public u64 FiletransferBandwidthSent { get; internal set; } + public u64 FiletransferBandwidthReceived { get; internal set; } + public Duration IdleTime { get; internal set; } } public sealed partial class Client { - public ClientId Id { get; } + public Client() + { + ServerGroups = new List(); + Badges = new List(); + + } + + + public ClientId Id { get; internal set; } public ChannelId Channel { get; set; } - public Uid Uid { get; } + public Uid Uid { get; internal set; } public str Name { get; set; } public bool InputMuted { get; set; } public bool OutputMuted { get; set; } @@ -178,34 +219,40 @@ public sealed partial class Client public bool TalkPowerGranted { get; set; } public str Metadata { get; set; } public bool IsRecording { get; set; } - public ClientDbId DatabaseId { get; } + public ClientDbId DatabaseId { get; internal set; } public ChannelGroupId ChannelGroup { get; set; } public List ServerGroups { get; set; } public str AwayMessage { get; set; } - public ClientType ClientType { get; } - public str AvatarHash { get; } - public i32 TalkPower { get; } - public TalkPowerRequest TalkPowerRequest { get; } + public ClientType ClientType { get; internal set; } + public str AvatarHash { get; internal set; } + public i32 TalkPower { get; internal set; } + public TalkPowerRequest TalkPowerRequest { get; internal set; } public str Description { get; set; } public bool IsPrioritySpeaker { get; set; } - public u32 UnreadMessages { get; } + public u32 UnreadMessages { get; internal set; } public str PhoneticName { get; set; } - public i32 NeededServerqueryViewPower { get; } - public IconHash IconId { get; } + public i32 NeededServerqueryViewPower { get; internal set; } + public IconHash IconId { get; internal set; } public bool IsChannelCommander { get; set; } - public str CountryCode { get; } - public ChannelId InheritedChannelGroupFromChannel { get; } + public str CountryCode { get; internal set; } + public ChannelId InheritedChannelGroupFromChannel { get; internal set; } public List Badges { get; set; } - public OptionalClientData OptionalData { get; } - public ConnectionClientData ConnectionData { get; } + public OptionalClientData OptionalData { get; internal set; } + public ConnectionClientData ConnectionData { get; internal set; } } public sealed partial class OptionalServerData { - public u32 ConnectionCount { get; } - public u64 ChannelCount { get; } - public Duration Uptime { get; } - public bool HasPassword { get; } + public OptionalServerData() + { + + } + + + public u32 ConnectionCount { get; internal set; } + public u64 ChannelCount { get; internal set; } + public Duration Uptime { get; internal set; } + public bool HasPassword { get; internal set; } public ChannelGroupId DefaultChannelAdminGroup { get; set; } public u64 MaxDownloadTotalBandwith { get; set; } public u64 MaxUploadTotalBandwith { get; set; } @@ -215,18 +262,18 @@ public sealed partial class OptionalServerData public u16 MinClientsForceSilence { get; set; } public u32 AntifloodPointsTickReduce { get; set; } public u32 AntifloodPointsNeededCommandBlock { get; set; } - public u16 ClientCount { get; } - public u32 QueryCount { get; } - public u32 QueryOnlineCount { get; } + public u16 ClientCount { get; internal set; } + public u32 QueryCount { get; internal set; } + public u32 QueryOnlineCount { get; internal set; } public u64 DownloadQuota { get; set; } public u64 UploadQuota { get; set; } - public u64 MonthBytesDownloaded { get; } - public u64 MonthBytesUploaded { get; } - public u64 TotalBytesDownloaded { get; } - public u64 TotalBytesUploaded { get; } - public u16 Port { get; } + public u64 MonthBytesDownloaded { get; internal set; } + public u64 MonthBytesUploaded { get; internal set; } + public u64 TotalBytesDownloaded { get; internal set; } + public u64 TotalBytesUploaded { get; internal set; } + public u16 Port { get; internal set; } public bool Autostart { get; set; } - public str MachineId { get; } + public str MachineId { get; internal set; } public u8 NeededIdentitySecurityLevel { get; set; } public bool LogClient { get; set; } public bool LogQuery { get; set; } @@ -234,47 +281,63 @@ public sealed partial class OptionalServerData public bool LogPermissions { get; set; } public bool LogServer { get; set; } public bool LogFileTransfer { get; set; } - public DateTime MinClientVersion { get; } + public DateTime MinClientVersion { get; internal set; } public u16 ReservedSlots { get; set; } - public f32 TotalPacketlossSpeech { get; } - public f32 TotalPacketlossKeepalive { get; } - public f32 TotalPacketlossControl { get; } - public f32 TotalPacketlossTotal { get; } - public Duration TotalPing { get; } + public f32 TotalPacketlossSpeech { get; internal set; } + public f32 TotalPacketlossKeepalive { get; internal set; } + public f32 TotalPacketlossControl { get; internal set; } + public f32 TotalPacketlossTotal { get; internal set; } + public Duration TotalPing { get; internal set; } public bool WeblistEnabled { get; set; } - public DateTime MinAndroidVersion { get; } - public DateTime MinIosVersion { get; } + public DateTime MinAndroidVersion { get; internal set; } + public DateTime MinIosVersion { get; internal set; } } public sealed partial class ConnectionServerData { - public u64 FileTransferBandwidthSent { get; } - public u64 FileTransferBandwidthReceived { get; } - public u64 FileTransferBytesSentTotal { get; } - public u64 FileTransferBytesReceivedTotal { get; } - public u64 PacketsSentTotal { get; } - public u64 BytesSentTotal { get; } - public u64 PacketsReceivedTotal { get; } - public u64 BytesReceivedTotal { get; } - public u64 BandwidthSentLastSecondTotal { get; } - public u64 BandwidthSentLastMinuteTotal { get; } - public u64 BandwidthReceivedLastSecondTotal { get; } - public u64 BandwidthReceivedLastMinuteTotal { get; } - public Duration ConnectedTime { get; } - public f32 PacketlossTotal { get; } - public Duration Ping { get; } + public ConnectionServerData() + { + + } + + + public u64 FileTransferBandwidthSent { get; internal set; } + public u64 FileTransferBandwidthReceived { get; internal set; } + public u64 FileTransferBytesSentTotal { get; internal set; } + public u64 FileTransferBytesReceivedTotal { get; internal set; } + public u64 PacketsSentTotal { get; internal set; } + public u64 BytesSentTotal { get; internal set; } + public u64 PacketsReceivedTotal { get; internal set; } + public u64 BytesReceivedTotal { get; internal set; } + public u64 BandwidthSentLastSecondTotal { get; internal set; } + public u64 BandwidthSentLastMinuteTotal { get; internal set; } + public u64 BandwidthReceivedLastSecondTotal { get; internal set; } + public u64 BandwidthReceivedLastMinuteTotal { get; internal set; } + public Duration ConnectedTime { get; internal set; } + public f32 PacketlossTotal { get; internal set; } + public Duration Ping { get; internal set; } } public sealed partial class Server { - public Uid Uid { get; } - public u64 VirtualServerId { get; } - public str Name { get; } - public str WelcomeMessage { get; } - public str Platform { get; } - public str Version { get; } - public u16 MaxClients { get; } - public DateTime Created { get; } + public Server() + { + Ip = new List(); + Clients = new Dictionary(); + Channels = new Dictionary(); + Groups = new Dictionary(); + + } + + + public Uid Uid { get; internal set; } + public u64 VirtualServerId { get; internal set; } + public str Name { get; internal set; } + public str WelcomeMessage { get; internal set; } + public str Platform { get; internal set; } + public str Version { get; internal set; } + public u16 MaxClients { get; internal set; } + public DateTime Created { get; internal set; } public CodecEncryptionMode CodecEncryptionMode { get; set; } public str Hostmessage { get; set; } public HostMessageMode HostmessageMode { get; set; } @@ -288,33 +351,45 @@ public sealed partial class Server public str HostbuttonUrl { get; set; } public str HostbuttonGfxUrl { get; set; } public str PhoneticName { get; set; } - public IconHash IconId { get; } - public List Ip { get; } - public bool AskForPrivilegekey { get; } + public IconHash IconId { get; internal set; } + public List Ip { get; internal set; } + public bool AskForPrivilegekey { get; internal set; } public HostBannerMode HostbannerMode { get; set; } public Duration TempChannelDefaultDeleteDelay { get; set; } - public u16 ProtocolVersion { get; } - public LicenseType License { get; } - public OptionalServerData OptionalData { get; } - public ConnectionServerData ConnectionData { get; } - public Dictionary Clients { get; } - public Dictionary Channels { get; } - public Dictionary Groups { get; } + public u16 ProtocolVersion { get; internal set; } + public LicenseType License { get; internal set; } + public OptionalServerData OptionalData { get; internal set; } + public ConnectionServerData ConnectionData { get; internal set; } + public Dictionary Clients { get; internal set; } + public Dictionary Channels { get; internal set; } + public Dictionary Groups { get; internal set; } } public sealed partial class Connection { - public ConnectionId Id { get; } - public ClientId OwnClient { get; } - public Server Server { get; } + public Connection() + { + + } + + + public ConnectionId Id { get; internal set; } + public ClientId OwnClient { get; internal set; } + public Server Server { get; internal set; } } public sealed partial class ChatEntry { - public ClientId SenderClient { get; } - public str Text { get; } - public DateTime Date { get; } - public TextMessageTargetMode Mode { get; } + public ChatEntry() + { + + } + + + public ClientId SenderClient { get; internal set; } + public str Text { get; internal set; } + public DateTime Date { get; internal set; } + public TextMessageTargetMode Mode { get; internal set; } } } \ No newline at end of file diff --git a/TS3Client/Generated/Book.tt b/TS3Client/Generated/Book.tt index 0aedcf32..66aea509 100644 --- a/TS3Client/Generated/Book.tt +++ b/TS3Client/Generated/Book.tt @@ -28,7 +28,24 @@ foreach (var struc in gen.@struct) { #> public sealed partial class <#= struc.name #> - {<# foreach(var prop in struc.properties) { + { + public <#= struc.name #>() + { + <# ClearIndent(); + PushIndent("\t\t\t"); + foreach(var prop in struc.properties) { + switch(prop.mod) { + case "array": + WriteLine($"{prop.name} = new List<{prop.type}>();"); + break; + case "map": + WriteLine($"{prop.name} = new Dictionary<{prop.key},{prop.type}>();"); + break; + } + } #> + } + + <# foreach(var prop in struc.properties) { if (!prop.get.Value && !prop.set.Value) continue; #> public <# @@ -43,7 +60,7 @@ foreach (var struc in gen.@struct) Write(prop.type); break; } - #> <#= prop.name #> { <#= prop.get.Value?"get;":""#> <#= prop.set.Value?"set;":""#> }<# } #> + #> <#= prop.name #> { <#= prop.get.Value?"":"internal " #>get; <#= prop.set.Value?"":"internal " #>set; }<# } #> } <# } diff --git a/TS3Client/Generated/M2B.cs b/TS3Client/Generated/M2B.cs new file mode 100644 index 00000000..1d17b1ee --- /dev/null +++ b/TS3Client/Generated/M2B.cs @@ -0,0 +1,363 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + + + + + + + + + + + + + + + + + + + + +namespace TS3Client.Full.Book +{ + using Messages; + using System.Linq; + + #pragma warning disable CS8019 // Ignore unused imports + using i8 = System.SByte; + using u8 = System.Byte; + using i16 = System.Int16; + using u16 = System.UInt16; + using i32 = System.Int32; + using u32 = System.UInt32; + using i64 = System.Int64; + using u64 = System.UInt64; + using f32 = System.Single; + using d64 = System.Double; + using str = System.String; + + using Duration = System.TimeSpan; + using DurationSeconds = System.TimeSpan; + using DurationMilliseconds = System.TimeSpan; + using SocketAddr = System.Net.IPAddress; + + using Uid = System.String; + using ClientDbId = System.UInt64; + using ClientId = System.UInt16; + using ChannelId = System.UInt64; + using ServerGroupId = System.UInt64; + using ChannelGroupId = System.UInt64; + using IconHash = System.Int32; + using ConnectionId = System.UInt32; +#pragma warning restore CS8019 + + public partial class Connection + { +#pragma warning disable IDE0017 // Ignore "Object initialization can be simplified" + + public void UpdateInitServer(InitServer msg) + { + var obj = new Server(); + obj.Platform = msg.ServerPlatform; + obj.Version = msg.ServerVersion; + obj.Created = msg.ServerCreated; + { var tmp = msg.ServerIp; if (tmp != null) obj.Ip.AddRange(tmp); } + SetClientDataFun(msg); + obj.WelcomeMessage = msg.WelcomeMessage; + obj.MaxClients = msg.MaxClients; + obj.Hostmessage = msg.Hostmessage; + obj.HostmessageMode = msg.HostmessageMode; + obj.VirtualServerId = msg.VirtualServerId; + obj.AskForPrivilegekey = msg.AskForPrivilegekey; + obj.ProtocolVersion = msg.ProtocolVersion; + obj.Name = msg.Name; + obj.CodecEncryptionMode = msg.CodecEncryptionMode; + obj.DefaultServerGroup = msg.DefaultServerGroup; + obj.DefaultChannelGroup = msg.DefaultChannelGroup; + obj.HostbannerUrl = msg.HostbannerUrl; + obj.HostbannerGfxUrl = msg.HostbannerGfxUrl; + obj.HostbannerGfxInterval = msg.HostbannerGfxInterval; + obj.PrioritySpeakerDimmModificator = msg.PrioritySpeakerDimmModificator; + obj.HostbuttonTooltip = msg.HostbuttonTooltip; + obj.HostbuttonUrl = msg.HostbuttonUrl; + obj.HostbuttonGfxUrl = msg.HostbuttonGfxUrl; + obj.PhoneticName = msg.PhoneticName; + obj.IconId = msg.IconId; + obj.HostbannerMode = msg.HostbannerMode; + obj.TempChannelDefaultDeleteDelay = msg.TempChannelDefaultDeleteDelay; + SetServer(obj); + + } + + + public void UpdateChannelCreated(ChannelCreated msg) + { + var obj = new Channel(); + obj.Parent = msg.ChannelParentId; + { + var tmp = MaxClientsCcFun(msg); + obj.MaxClients = tmp.Item1; + obj.MaxFamilyClients = tmp.Item2; + } + obj.ChannelType = ChannelTypeCcFun(msg); + obj.ForcedSilence = ReturnFalse(msg); + obj.IsPrivate = ReturnFalse(msg); + obj.OptionalData = null; + obj.Order = msg.Order; + obj.Name = msg.Name; + obj.Topic = msg.Topic; + obj.IsDefault = msg.IsDefault; + obj.HasPassword = msg.HasPassword; + obj.Codec = msg.Codec; + obj.CodecQuality = msg.CodecQuality; + obj.NeededTalkPower = msg.NeededTalkPower; + obj.IconId = msg.IconId; + obj.CodecLatencyFactor = msg.CodecLatencyFactor; + obj.IsUnencrypted = msg.IsUnencrypted; + obj.DeleteDelay = msg.DeleteDelay; + obj.PhoneticName = msg.PhoneticName; + SetChannel(obj, msg.ChannelId); + + } + + + public void UpdateChannelDeleted(ChannelDeleted msg) + { + + RemoveChannel(msg.ChannelId); + } + + + public void UpdateChannelEdited(ChannelEdited msg) + { + var obj = GetChannel(msg.ChannelId); + { + var tmp = MaxClientsCeFun(msg); + obj.MaxClients = tmp.Item1; + obj.MaxFamilyClients = tmp.Item2; + } + obj.ChannelType = ChannelTypeCeFun(msg); + obj.Order = msg.Order; + obj.Name = msg.Name; + obj.Topic = msg.Topic; + obj.IsDefault = msg.IsDefault; + obj.HasPassword = msg.HasPassword; + obj.Codec = msg.Codec; + obj.CodecQuality = msg.CodecQuality; + obj.NeededTalkPower = msg.NeededTalkPower; + obj.IconId = msg.IconId; + obj.CodecLatencyFactor = msg.CodecLatencyFactor; + obj.IsUnencrypted = msg.IsUnencrypted; + obj.DeleteDelay = msg.DeleteDelay; + obj.PhoneticName = msg.PhoneticName; + + } + + + public void UpdateChannelList(ChannelList msg) + { + var obj = new Channel(); + obj.Parent = msg.ChannelParentId; + { + var tmp = MaxClientsClFun(msg); + obj.MaxClients = tmp.Item1; + obj.MaxFamilyClients = tmp.Item2; + } + obj.ChannelType = ChannelTypeClFun(msg); + obj.OptionalData = null; + obj.Name = msg.Name; + obj.Topic = msg.Topic; + obj.Codec = msg.Codec; + obj.CodecQuality = msg.CodecQuality; + obj.Order = msg.Order; + obj.IsDefault = msg.IsDefault; + obj.HasPassword = msg.HasPassword; + obj.CodecLatencyFactor = msg.CodecLatencyFactor; + obj.IsUnencrypted = msg.IsUnencrypted; + obj.DeleteDelay = msg.DeleteDelay; + obj.NeededTalkPower = msg.NeededTalkPower; + obj.ForcedSilence = msg.ForcedSilence; + obj.PhoneticName = msg.PhoneticName; + obj.IconId = msg.IconId; + obj.IsPrivate = msg.IsPrivate; + SetChannel(obj, msg.ChannelId); + + } + + + public void UpdateChannelMoved(ChannelMoved msg) + { + var obj = GetChannel(msg.ChannelId); + obj.Parent = msg.ChannelParentId; + obj.Order = msg.Order; + + } + + + public void UpdateClientChannelGroupChanged(ClientChannelGroupChanged msg) + { + var obj = GetClient(msg.ClientId); + obj.ChannelGroup = msg.ChannelGroup; + + } + + + public void UpdateClientEnterView(ClientEnterView msg) + { + var obj = new Client(); + obj.Channel = msg.TargetChannelId; + obj.AwayMessage = AwayFun(msg); + obj.TalkPowerRequest = TalkPowerFun(msg); + obj.OptionalData = null; + obj.ConnectionData = null; + { var tmp = BadgesFun(msg); if (tmp != null) obj.Badges.AddRange(tmp); } + obj.DatabaseId = msg.DatabaseId; + obj.Name = msg.Name; + obj.ClientType = msg.ClientType; + obj.Uid = msg.Uid; + obj.AvatarHash = msg.AvatarHash; + obj.Description = msg.Description; + obj.IconId = msg.IconId; + obj.InputMuted = msg.InputMuted; + obj.OutputMuted = msg.OutputMuted; + obj.OutputOnlyMuted = msg.OutputOnlyMuted; + obj.InputHardwareEnabled = msg.InputHardwareEnabled; + obj.OutputHardwareEnabled = msg.OutputHardwareEnabled; + obj.Metadata = msg.Metadata; + obj.IsRecording = msg.IsRecording; + obj.ChannelGroup = msg.ChannelGroup; + obj.InheritedChannelGroupFromChannel = msg.InheritedChannelGroupFromChannel; + { var tmp = msg.ServerGroups; if (tmp != null) obj.ServerGroups.AddRange(tmp); } + obj.TalkPower = msg.TalkPower; + obj.TalkPowerGranted = msg.TalkPowerGranted; + obj.IsPrioritySpeaker = msg.IsPrioritySpeaker; + obj.UnreadMessages = msg.UnreadMessages; + obj.PhoneticName = msg.PhoneticName; + obj.NeededServerqueryViewPower = msg.NeededServerqueryViewPower; + obj.IsChannelCommander = msg.IsChannelCommander; + obj.CountryCode = msg.CountryCode; + SetClient(obj, msg.ClientId); + + } + + + public void UpdateClientLeftView(ClientLeftView msg) + { + + RemoveClient(msg.ClientId); + } + + + public void UpdateClientMoved(ClientMoved msg) + { + var obj = GetClient(msg.ClientId); + obj.Channel = msg.TargetChannelId; + + } + + + public void UpdateConnectionInfo(ConnectionInfo msg) + { + var obj = new ConnectionClientData(); + obj.ClientAddress = AddressFun(msg); + obj.Ping = msg.Ping; + obj.PingDeviation = msg.PingDeviation; + obj.ConnectedTime = msg.ConnectedTime; + obj.PacketsSentSpeech = msg.PacketsSentSpeech; + obj.PacketsSentKeepalive = msg.PacketsSentKeepalive; + obj.PacketsSentControl = msg.PacketsSentControl; + obj.BytesSentSpeech = msg.BytesSentSpeech; + obj.BytesSentKeepalive = msg.BytesSentKeepalive; + obj.BytesSentControl = msg.BytesSentControl; + obj.PacketsReceivedSpeech = msg.PacketsReceivedSpeech; + obj.PacketsReceivedKeepalive = msg.PacketsReceivedKeepalive; + obj.PacketsReceivedControl = msg.PacketsReceivedControl; + obj.BytesReceivedSpeech = msg.BytesReceivedSpeech; + obj.BytesReceivedKeepalive = msg.BytesReceivedKeepalive; + obj.BytesReceivedControl = msg.BytesReceivedControl; + obj.ServerToClientPacketlossSpeech = msg.ServerToClientPacketlossSpeech; + obj.ServerToClientPacketlossKeepalive = msg.ServerToClientPacketlossKeepalive; + obj.ServerToClientPacketlossControl = msg.ServerToClientPacketlossControl; + obj.ServerToClientPacketlossTotal = msg.ServerToClientPacketlossTotal; + obj.ClientToServerPacketlossSpeech = msg.ClientToServerPacketlossSpeech; + obj.ClientToServerPacketlossKeepalive = msg.ClientToServerPacketlossKeepalive; + obj.ClientToServerPacketlossControl = msg.ClientToServerPacketlossControl; + obj.ClientToServerPacketlossTotal = msg.ClientToServerPacketlossTotal; + obj.BandwidthSentLastSecondSpeech = msg.BandwidthSentLastSecondSpeech; + obj.BandwidthSentLastSecondKeepalive = msg.BandwidthSentLastSecondKeepalive; + obj.BandwidthSentLastSecondControl = msg.BandwidthSentLastSecondControl; + obj.BandwidthSentLastMinuteSpeech = msg.BandwidthSentLastMinuteSpeech; + obj.BandwidthSentLastMinuteKeepalive = msg.BandwidthSentLastMinuteKeepalive; + obj.BandwidthSentLastMinuteControl = msg.BandwidthSentLastMinuteControl; + obj.BandwidthReceivedLastSecondSpeech = msg.BandwidthReceivedLastSecondSpeech; + obj.BandwidthReceivedLastSecondKeepalive = msg.BandwidthReceivedLastSecondKeepalive; + obj.BandwidthReceivedLastSecondControl = msg.BandwidthReceivedLastSecondControl; + obj.BandwidthReceivedLastMinuteSpeech = msg.BandwidthReceivedLastMinuteSpeech; + obj.BandwidthReceivedLastMinuteKeepalive = msg.BandwidthReceivedLastMinuteKeepalive; + obj.BandwidthReceivedLastMinuteControl = msg.BandwidthReceivedLastMinuteControl; + obj.FiletransferBandwidthSent = msg.FiletransferBandwidthSent; + obj.FiletransferBandwidthReceived = msg.FiletransferBandwidthReceived; + obj.IdleTime = msg.IdleTime; + SetConnectionClientData(obj, msg.ClientId); + + } + + + public void UpdateClientServerGroupAdded(ClientServerGroupAdded msg) + { + var obj = GetClient(msg.ClientId); + obj.ServerGroups.Add(msg.ServerGroupId); + + } + + + public void UpdateServerGroupList(ServerGroupList msg) + { + var obj = new ServerGroup(); + obj.Name = msg.Name; + obj.GroupType = msg.GroupType; + obj.IconId = msg.IconId; + obj.IsPermanent = msg.IsPermanent; + obj.SortId = msg.SortId; + obj.NamingMode = msg.NamingMode; + obj.NeededModifyPower = msg.NeededModifyPower; + obj.NeededMemberAddPower = msg.NeededMemberAddPower; + obj.NeededMemberRemovePower = msg.NeededMemberRemovePower; + SetServerGroup(obj, msg.ServerGroupId); + + } + + + public void UpdateServerEdited(ServerEdited msg) + { + var obj = GetServer(); + obj.Name = msg.Name; + obj.CodecEncryptionMode = msg.CodecEncryptionMode; + obj.DefaultServerGroup = msg.DefaultServerGroup; + obj.DefaultChannelGroup = msg.DefaultChannelGroup; + obj.HostbannerUrl = msg.HostbannerUrl; + obj.HostbannerGfxUrl = msg.HostbannerGfxUrl; + obj.HostbannerGfxInterval = msg.HostbannerGfxInterval; + obj.PrioritySpeakerDimmModificator = msg.PrioritySpeakerDimmModificator; + obj.HostbuttonTooltip = msg.HostbuttonTooltip; + obj.HostbuttonUrl = msg.HostbuttonUrl; + obj.HostbuttonGfxUrl = msg.HostbuttonGfxUrl; + obj.PhoneticName = msg.PhoneticName; + obj.IconId = msg.IconId; + obj.HostbannerMode = msg.HostbannerMode; + obj.TempChannelDefaultDeleteDelay = msg.TempChannelDefaultDeleteDelay; + + } + + +#pragma warning restore IDE0017 + } +} \ No newline at end of file diff --git a/TS3Client/Generated/M2B.tt b/TS3Client/Generated/M2B.tt new file mode 100644 index 00000000..e7d64303 --- /dev/null +++ b/TS3Client/Generated/M2B.tt @@ -0,0 +1,113 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +<#@ template debug="true" hostSpecific="true" language="C#" #> +<#@ include file="M2BParser.ttinclude" once="true" #> +<#@ include file="MessageParser.ttinclude" once="true" #> +<#@ include file="BookParser.ttinclude" once="true" #> +<#@ output extension=".cs" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<# +var genbook = BookDeclarations.Parse(Host.ResolvePath("../Declarations/BookDeclarations.toml")); +var genmsg = Messages.Parse(Host.ResolvePath("../Declarations/Messages.toml")); +var genm2b = M2BDeclarations.Parse(Host.ResolvePath("../Declarations/MessagesToBook.toml"), genmsg, genbook); +#> + +namespace TS3Client.Full.Book +{ + using Messages; + using System.Linq; + + <#= ConversionSet #> + + public partial class Connection + { +#pragma warning disable IDE0017 // Ignore "Object initialization can be simplified" + <# foreach (var rule in genm2b.rule) { + var msg = genmsg.NotifiesSorted.First(x => x.name == rule.from); + var bookItem = genbook.@struct.First(x => x.name == rule.to); + #> + public void Update<#= msg.name #>(<#= msg.name #> msg) + { + <# var idStr = string.Join(", ", rule.id.Select(x => $"msg.{x}")); + + ClearIndent(); + PushIndent("\t\t\t"); + switch (rule.operation) + { + case "add": + case "update": + if (rule.operation == "add") + WriteLine($"var obj = new {rule.to}();"); + else + WriteLine($"var obj = Get{rule.to}({idStr});"); + + foreach (var prop in rule.properties) { + void WriteMove(string from, string to) { + var bookProp = bookItem.properties.FirstOrDefault(x => x.name == to); + if(bookProp == null) { + Warn($"No property found: '{to}'"); + return; + } + + if (prop.operation == null) { + if (bookProp.mod == null) { + WriteLine($"obj.{to} = {from};"); + } else if (bookProp.mod == "array") { + WriteLine($"{{ var tmp = {from}; if (tmp != null) obj.{to}.AddRange(tmp); }}"); + } else { + throw new Exception("Unknown mod type: " + bookProp.mod); + } + } else if (prop.operation == "add") { + WriteLine($"obj.{to}.Add({from});"); + } else if (prop.operation == "remove") { + WriteLine($"obj.{to}.Remove({from});"); + } else + throw new Exception("Unknown operation: " + prop.operation); + } + + if (prop.from != null) + { + WriteMove($"msg.{prop.from}", prop.to); + } else /* function */ { + if (prop.function == "ReturnNone") + WriteMove($"null", prop.tolist[0]); + else if (prop.function == "VoidFun") { /* Do Nothing */ } + else if(prop.tolist.Length == 0) + WriteLine($"{prop.function}(msg);"); + else if (prop.tolist.Length == 1) + WriteMove($"{prop.function}(msg)", prop.tolist[0]); + else + { + WriteLine("{"); + WriteLine($"var tmp = {prop.function}(msg);", prop.to); + for (int i = 0; i < prop.tolist.Length; i++) + WriteMove($"tmp.Item{(i + 1)}", prop.tolist[i]); + WriteLine("}"); + } + } + } + if (rule.operation == "add") { + WriteLine($"Set{rule.to}(obj{(string.IsNullOrEmpty(idStr) ? "" : (", " + idStr))});"); + } + break; + + case "remove":#> + Remove<#=rule.to#>(<#=idStr#>);<# + break; + } #> + } + + <# } #> +#pragma warning restore IDE0017 + } +} \ No newline at end of file diff --git a/TS3Client/Generated/M2BParser.ttinclude b/TS3Client/Generated/M2BParser.ttinclude new file mode 100644 index 00000000..76065db3 --- /dev/null +++ b/TS3Client/Generated/M2BParser.ttinclude @@ -0,0 +1,59 @@ +<#@ include file="Util.ttinclude" once="true" #> +<#+ +public class M2BDeclarations +{ + public static M2BDeclarations Parse(string file, Messages msgs, BookDeclarations book) + { + var toml = Nett.Toml.ReadFile(file); + + foreach (var rule in toml.rule) + { + rule.properties = rule.properties ?? new List(); + + // Add implicit move operations + var msg = msgs.GetOrderedMsg().First(x => x.name == rule.from); + var msgProps = msg.attributes.Select(x => msgs.GetField(x).fld); + var bookItem = book.@struct.First(x => x.name == rule.to); + var funcResults = new HashSet(rule.properties.Where(x => x.tolist != null).SelectMany(x => x.tolist)); + + foreach (var prop in msgProps) + { + if (funcResults.Contains(prop.pretty)) + continue; + + if (bookItem.properties.Any(x => x.name == prop.pretty)) + { + // chek already exists + + rule.properties.Add(new M2BPropMove { + from = prop.pretty, + to = prop.pretty, + }); + } + } + } + + return toml; + } + + public M2BRule[] rule { get; set; } + + public class M2BRule + { + public string from { get; set; } + public string[] id { get; set; } + public string to { get; set; } + public string operation { get; set; } + public List properties { get; set; } + } + + public class M2BPropMove + { + public string from { get; set; } + public string to { get; set; } + public string function { get; set; } + public string[] tolist { get; set; } + public string operation { get; set; } + } +} +#> \ No newline at end of file diff --git a/TS3Client/Generated/MessageParser.ttinclude b/TS3Client/Generated/MessageParser.ttinclude index d3335519..42b98ae1 100644 --- a/TS3Client/Generated/MessageParser.ttinclude +++ b/TS3Client/Generated/MessageParser.ttinclude @@ -3,6 +3,7 @@ <#@ import namespace="System.Collections.Generic" #> <#@ include file="Util.ttinclude" once="true" #> <#+ +public static IEnumerable OnlyS2C(IEnumerable enu) => enu.Where(x => x.s2c.Value); public class Messages { public static Messages Parse(string file) @@ -28,7 +29,7 @@ public class Messages { // transfer all default confs msg.s2c = msg.s2c ?? grp.@default.s2c; - msg.c2s = msg.c2s ?? grp.@default.s2c; + msg.c2s = msg.c2s ?? grp.@default.c2s; msg.response = msg.response ?? grp.@default.response; msg.low = msg.low ?? grp.@default.low; msg.np = msg.np ?? grp.@default.np; @@ -55,7 +56,6 @@ public class Messages public string doc { get; set; } public bool isArray => mod == "array"; - public string prettyClean => pretty.Trim('?'); public string typeFin => type + (isArray ? "[]" : ""); } diff --git a/TS3Client/Generated/Messages.cs b/TS3Client/Generated/Messages.cs index 5f3b8212..a9f8bfdf 100644 --- a/TS3Client/Generated/Messages.cs +++ b/TS3Client/Generated/Messages.cs @@ -21,20 +21,18 @@ - - - - - namespace TS3Client.Messages { using Commands; using Helper; using System; + using System.Collections.Generic; using System.Globalization; + using System.Buffers.Text; - using i8 = System.Byte; - using u8 = System.SByte; + #pragma warning disable CS8019 // Ignore unused imports + using i8 = System.SByte; + using u8 = System.Byte; using i16 = System.Int16; using u16 = System.UInt16; using i32 = System.Int32; @@ -58,6 +56,7 @@ namespace TS3Client.Messages using ChannelGroupId = System.UInt64; using IconHash = System.Int32; using ConnectionId = System.UInt32; +#pragma warning restore CS8019 public sealed class ChannelChanged : INotification { @@ -66,17 +65,30 @@ public sealed class ChannelChanged : INotification public ChannelId ChannelId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelChanged[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + } + } + + } } public sealed class ChannelCreated : INotification @@ -96,11 +108,11 @@ public sealed class ChannelCreated : INotification public bool IsPermanent { get; set; } public bool IsSemiPermanent { get; set; } public Codec Codec { get; set; } - public i8 CodecQuality { get; set; } + public u8 CodecQuality { get; set; } public i32 NeededTalkPower { get; set; } public IconHash IconId { get; set; } - public u16 MaxClients { get; set; } - public u16 MaxFamilyClients { get; set; } + public i32 MaxClients { get; set; } + public i32 MaxFamilyClients { get; set; } public i32 CodecLatencyFactor { get; set; } public bool IsUnencrypted { get; set; } public DurationSeconds DeleteDelay { get; set; } @@ -110,41 +122,78 @@ public sealed class ChannelCreated : INotification public str PhoneticName { get; set; } public ChannelId ChannelParentId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "channel_order": Order = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_order": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Order = oval; } break; case "channel_name": Name = Ts3String.Unescape(value); break; case "channel_topic": Topic = Ts3String.Unescape(value); break; case "channel_flag_default": IsDefault = value.Length > 0 && value[0] != '0'; break; case "channel_flag_password": HasPassword = value.Length > 0 && value[0] != '0'; break; case "channel_flag_permanent": IsPermanent = value.Length > 0 && value[0] != '0'; break; case "channel_flag_semi_permanent": IsSemiPermanent = value.Length > 0 && value[0] != '0'; break; - case "channel_codec": Codec = (Codec)u8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_codec_quality": CodecQuality = i8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_needed_talk_power": NeededTalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "channel_maxclients": MaxClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_maxfamilyclients": MaxFamilyClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_codec_latency_factor": CodecLatencyFactor = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_codec": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) Codec = (Codec)oval; } break; + case "channel_codec_quality": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) CodecQuality = oval; } break; + case "channel_needed_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededTalkPower = oval; } break; + case "channel_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; + case "channel_maxclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxClients = oval; } break; + case "channel_maxfamilyclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxFamilyClients = oval; } break; + case "channel_codec_latency_factor": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) CodecLatencyFactor = oval; } break; case "channel_codec_is_unencrypted": IsUnencrypted = value.Length > 0 && value[0] != '0'; break; - case "channel_delete_delay": DeleteDelay = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "channel_delete_delay": { if(Utf8Parser.TryParse(value, out double oval, out _)) DeleteDelay = TimeSpan.FromSeconds(oval); } break; case "channel_flag_maxclients_unlimited": IsMaxClientsUnlimited = value.Length > 0 && value[0] != '0'; break; case "channel_flag_maxfamilyclients_unlimited": IsMaxFamilyClientsUnlimited = value.Length > 0 && value[0] != '0'; break; case "channel_flag_maxfamilyclients_inherited": InheritsMaxFamilyClients = value.Length > 0 && value[0] != '0'; break; case "channel_name_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "cpid": ChannelParentId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cpid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelParentId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelCreated[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "channel_order": foreach(var toi in toc) { toi.Order = Order; } break; + case "channel_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "channel_topic": foreach(var toi in toc) { toi.Topic = Topic; } break; + case "channel_flag_default": foreach(var toi in toc) { toi.IsDefault = IsDefault; } break; + case "channel_flag_password": foreach(var toi in toc) { toi.HasPassword = HasPassword; } break; + case "channel_flag_permanent": foreach(var toi in toc) { toi.IsPermanent = IsPermanent; } break; + case "channel_flag_semi_permanent": foreach(var toi in toc) { toi.IsSemiPermanent = IsSemiPermanent; } break; + case "channel_codec": foreach(var toi in toc) { toi.Codec = Codec; } break; + case "channel_codec_quality": foreach(var toi in toc) { toi.CodecQuality = CodecQuality; } break; + case "channel_needed_talk_power": foreach(var toi in toc) { toi.NeededTalkPower = NeededTalkPower; } break; + case "channel_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "channel_maxclients": foreach(var toi in toc) { toi.MaxClients = MaxClients; } break; + case "channel_maxfamilyclients": foreach(var toi in toc) { toi.MaxFamilyClients = MaxFamilyClients; } break; + case "channel_codec_latency_factor": foreach(var toi in toc) { toi.CodecLatencyFactor = CodecLatencyFactor; } break; + case "channel_codec_is_unencrypted": foreach(var toi in toc) { toi.IsUnencrypted = IsUnencrypted; } break; + case "channel_delete_delay": foreach(var toi in toc) { toi.DeleteDelay = DeleteDelay; } break; + case "channel_flag_maxclients_unlimited": foreach(var toi in toc) { toi.IsMaxClientsUnlimited = IsMaxClientsUnlimited; } break; + case "channel_flag_maxfamilyclients_unlimited": foreach(var toi in toc) { toi.IsMaxFamilyClientsUnlimited = IsMaxFamilyClientsUnlimited; } break; + case "channel_flag_maxfamilyclients_inherited": foreach(var toi in toc) { toi.InheritsMaxFamilyClients = InheritsMaxFamilyClients; } break; + case "channel_name_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "cpid": foreach(var toi in toc) { toi.ChannelParentId = ChannelParentId; } break; + } + } + + } } public sealed class ChannelData : IResponse @@ -166,41 +215,72 @@ public sealed class ChannelData : IResponse public bool IsPermanent { get; set; } public bool IsSemiPermanent { get; set; } public Codec Codec { get; set; } - public i8 CodecQuality { get; set; } + public u8 CodecQuality { get; set; } public i32 NeededTalkPower { get; set; } public IconHash IconId { get; set; } - public u16 MaxClients { get; set; } - public u16 MaxFamilyClients { get; set; } + public i32 MaxClients { get; set; } + public i32 MaxFamilyClients { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "id": Id = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "pid": ParentChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "seconds_empty": DurationEmpty = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "total_clients_family": TotalFamilyClients = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "total_clients": TotalClients = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_needed_subscribe_power": NeededSubscribePower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_order": Order = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "id": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) Id = oval; } break; + case "pid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ParentChannelId = oval; } break; + case "seconds_empty": { if(Utf8Parser.TryParse(value, out double oval, out _)) DurationEmpty = TimeSpan.FromSeconds(oval); } break; + case "total_clients_family": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TotalFamilyClients = oval; } break; + case "total_clients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TotalClients = oval; } break; + case "channel_needed_subscribe_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededSubscribePower = oval; } break; + case "channel_order": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Order = oval; } break; case "channel_name": Name = Ts3String.Unescape(value); break; case "channel_topic": Topic = Ts3String.Unescape(value); break; case "channel_flag_default": IsDefault = value.Length > 0 && value[0] != '0'; break; case "channel_flag_password": HasPassword = value.Length > 0 && value[0] != '0'; break; case "channel_flag_permanent": IsPermanent = value.Length > 0 && value[0] != '0'; break; case "channel_flag_semi_permanent": IsSemiPermanent = value.Length > 0 && value[0] != '0'; break; - case "channel_codec": Codec = (Codec)u8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_codec_quality": CodecQuality = i8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_needed_talk_power": NeededTalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "channel_maxclients": MaxClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_maxfamilyclients": MaxFamilyClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_codec": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) Codec = (Codec)oval; } break; + case "channel_codec_quality": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) CodecQuality = oval; } break; + case "channel_needed_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededTalkPower = oval; } break; + case "channel_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; + case "channel_maxclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxClients = oval; } break; + case "channel_maxfamilyclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxFamilyClients = oval; } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelData[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "id": foreach(var toi in toc) { toi.Id = Id; } break; + case "pid": foreach(var toi in toc) { toi.ParentChannelId = ParentChannelId; } break; + case "seconds_empty": foreach(var toi in toc) { toi.DurationEmpty = DurationEmpty; } break; + case "total_clients_family": foreach(var toi in toc) { toi.TotalFamilyClients = TotalFamilyClients; } break; + case "total_clients": foreach(var toi in toc) { toi.TotalClients = TotalClients; } break; + case "channel_needed_subscribe_power": foreach(var toi in toc) { toi.NeededSubscribePower = NeededSubscribePower; } break; + case "channel_order": foreach(var toi in toc) { toi.Order = Order; } break; + case "channel_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "channel_topic": foreach(var toi in toc) { toi.Topic = Topic; } break; + case "channel_flag_default": foreach(var toi in toc) { toi.IsDefault = IsDefault; } break; + case "channel_flag_password": foreach(var toi in toc) { toi.HasPassword = HasPassword; } break; + case "channel_flag_permanent": foreach(var toi in toc) { toi.IsPermanent = IsPermanent; } break; + case "channel_flag_semi_permanent": foreach(var toi in toc) { toi.IsSemiPermanent = IsSemiPermanent; } break; + case "channel_codec": foreach(var toi in toc) { toi.Codec = Codec; } break; + case "channel_codec_quality": foreach(var toi in toc) { toi.CodecQuality = CodecQuality; } break; + case "channel_needed_talk_power": foreach(var toi in toc) { toi.NeededTalkPower = NeededTalkPower; } break; + case "channel_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "channel_maxclients": foreach(var toi in toc) { toi.MaxClients = MaxClients; } break; + case "channel_maxfamilyclients": foreach(var toi in toc) { toi.MaxFamilyClients = MaxFamilyClients; } break; + } + } + + } } public sealed class ChannelDeleted : INotification @@ -213,20 +293,36 @@ public sealed class ChannelDeleted : INotification public str InvokerName { get; set; } public Uid InvokerUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelDeleted[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + } + } + + } } public sealed class ChannelEdited : INotification @@ -246,11 +342,11 @@ public sealed class ChannelEdited : INotification public bool IsPermanent { get; set; } public bool IsSemiPermanent { get; set; } public Codec Codec { get; set; } - public i8 CodecQuality { get; set; } + public u8 CodecQuality { get; set; } public i32 NeededTalkPower { get; set; } public IconHash IconId { get; set; } - public u16 MaxClients { get; set; } - public u16 MaxFamilyClients { get; set; } + public i32 MaxClients { get; set; } + public i32 MaxFamilyClients { get; set; } public i32 CodecLatencyFactor { get; set; } public bool IsUnencrypted { get; set; } public DurationSeconds DeleteDelay { get; set; } @@ -260,41 +356,78 @@ public sealed class ChannelEdited : INotification public str PhoneticName { get; set; } public Reason Reason { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "channel_order": Order = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_order": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Order = oval; } break; case "channel_name": Name = Ts3String.Unescape(value); break; case "channel_topic": Topic = Ts3String.Unescape(value); break; case "channel_flag_default": IsDefault = value.Length > 0 && value[0] != '0'; break; case "channel_flag_password": HasPassword = value.Length > 0 && value[0] != '0'; break; case "channel_flag_permanent": IsPermanent = value.Length > 0 && value[0] != '0'; break; case "channel_flag_semi_permanent": IsSemiPermanent = value.Length > 0 && value[0] != '0'; break; - case "channel_codec": Codec = (Codec)u8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_codec_quality": CodecQuality = i8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_needed_talk_power": NeededTalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "channel_maxclients": MaxClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_maxfamilyclients": MaxFamilyClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_codec_latency_factor": CodecLatencyFactor = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_codec": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) Codec = (Codec)oval; } break; + case "channel_codec_quality": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) CodecQuality = oval; } break; + case "channel_needed_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededTalkPower = oval; } break; + case "channel_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; + case "channel_maxclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxClients = oval; } break; + case "channel_maxfamilyclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxFamilyClients = oval; } break; + case "channel_codec_latency_factor": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) CodecLatencyFactor = oval; } break; case "channel_codec_is_unencrypted": IsUnencrypted = value.Length > 0 && value[0] != '0'; break; - case "channel_delete_delay": DeleteDelay = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "channel_delete_delay": { if(Utf8Parser.TryParse(value, out double oval, out _)) DeleteDelay = TimeSpan.FromSeconds(oval); } break; case "channel_flag_maxclients_unlimited": IsMaxClientsUnlimited = value.Length > 0 && value[0] != '0'; break; case "channel_flag_maxfamilyclients_unlimited": IsMaxFamilyClientsUnlimited = value.Length > 0 && value[0] != '0'; break; case "channel_flag_maxfamilyclients_inherited": InheritsMaxFamilyClients = value.Length > 0 && value[0] != '0'; break; case "channel_name_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "reasonid": { if (!Enum.TryParse(value.NewString(), out Reason val)) throw new FormatException(); Reason = val; } break; + case "reasonid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Reason = (Reason)oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelEdited[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "channel_order": foreach(var toi in toc) { toi.Order = Order; } break; + case "channel_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "channel_topic": foreach(var toi in toc) { toi.Topic = Topic; } break; + case "channel_flag_default": foreach(var toi in toc) { toi.IsDefault = IsDefault; } break; + case "channel_flag_password": foreach(var toi in toc) { toi.HasPassword = HasPassword; } break; + case "channel_flag_permanent": foreach(var toi in toc) { toi.IsPermanent = IsPermanent; } break; + case "channel_flag_semi_permanent": foreach(var toi in toc) { toi.IsSemiPermanent = IsSemiPermanent; } break; + case "channel_codec": foreach(var toi in toc) { toi.Codec = Codec; } break; + case "channel_codec_quality": foreach(var toi in toc) { toi.CodecQuality = CodecQuality; } break; + case "channel_needed_talk_power": foreach(var toi in toc) { toi.NeededTalkPower = NeededTalkPower; } break; + case "channel_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "channel_maxclients": foreach(var toi in toc) { toi.MaxClients = MaxClients; } break; + case "channel_maxfamilyclients": foreach(var toi in toc) { toi.MaxFamilyClients = MaxFamilyClients; } break; + case "channel_codec_latency_factor": foreach(var toi in toc) { toi.CodecLatencyFactor = CodecLatencyFactor; } break; + case "channel_codec_is_unencrypted": foreach(var toi in toc) { toi.IsUnencrypted = IsUnencrypted; } break; + case "channel_delete_delay": foreach(var toi in toc) { toi.DeleteDelay = DeleteDelay; } break; + case "channel_flag_maxclients_unlimited": foreach(var toi in toc) { toi.IsMaxClientsUnlimited = IsMaxClientsUnlimited; } break; + case "channel_flag_maxfamilyclients_unlimited": foreach(var toi in toc) { toi.IsMaxFamilyClientsUnlimited = IsMaxFamilyClientsUnlimited; } break; + case "channel_flag_maxfamilyclients_inherited": foreach(var toi in toc) { toi.InheritsMaxFamilyClients = InheritsMaxFamilyClients; } break; + case "channel_name_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "reasonid": foreach(var toi in toc) { toi.Reason = Reason; } break; + } + } + + } } public sealed class ChannelGroupList : INotification @@ -313,26 +446,48 @@ public sealed class ChannelGroupList : INotification public i32 NeededMemberAddPower { get; set; } public i32 NeededMemberRemovePower { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cgid": ChannelGroup = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cgid": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) ChannelGroup = oval; } break; case "name": Name = Ts3String.Unescape(value); break; - case "type": { if (!Enum.TryParse(value.NewString(), out GroupType val)) throw new FormatException(); GroupType = val; } break; - case "iconid": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "type": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) GroupType = (GroupType)oval; } break; + case "iconid": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; case "savedb": IsPermanent = value.Length > 0 && value[0] != '0'; break; - case "sortid": SortId = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "namemode": { if (!Enum.TryParse(value.NewString(), out GroupNamingMode val)) throw new FormatException(); NamingMode = val; } break; - case "n_modifyp": NeededModifyPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "n_member_addp": NeededMemberAddPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "n_member_remove_p": NeededMemberRemovePower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "sortid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) SortId = oval; } break; + case "namemode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NamingMode = (GroupNamingMode)oval; } break; + case "n_modifyp": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededModifyPower = oval; } break; + case "n_member_addp": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededMemberAddPower = oval; } break; + case "n_member_remove_p": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededMemberRemovePower = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelGroupList[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cgid": foreach(var toi in toc) { toi.ChannelGroup = ChannelGroup; } break; + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "type": foreach(var toi in toc) { toi.GroupType = GroupType; } break; + case "iconid": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "savedb": foreach(var toi in toc) { toi.IsPermanent = IsPermanent; } break; + case "sortid": foreach(var toi in toc) { toi.SortId = SortId; } break; + case "namemode": foreach(var toi in toc) { toi.NamingMode = NamingMode; } break; + case "n_modifyp": foreach(var toi in toc) { toi.NeededModifyPower = NeededModifyPower; } break; + case "n_member_addp": foreach(var toi in toc) { toi.NeededMemberAddPower = NeededMemberAddPower; } break; + case "n_member_remove_p": foreach(var toi in toc) { toi.NeededMemberRemovePower = NeededMemberRemovePower; } break; + } + } + + } } public sealed class ChannelList : INotification @@ -345,9 +500,9 @@ public sealed class ChannelList : INotification public str Name { get; set; } public str Topic { get; set; } public Codec Codec { get; set; } - public i8 CodecQuality { get; set; } - public u16 MaxClients { get; set; } - public u16 MaxFamilyClients { get; set; } + public u8 CodecQuality { get; set; } + public i32 MaxClients { get; set; } + public i32 MaxFamilyClients { get; set; } public i32 Order { get; set; } public bool IsPermanent { get; set; } public bool IsSemiPermanent { get; set; } @@ -365,40 +520,76 @@ public sealed class ChannelList : INotification public IconHash IconId { get; set; } public bool IsPrivate { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cpid": ChannelParentId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "cpid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelParentId = oval; } break; case "channel_name": Name = Ts3String.Unescape(value); break; case "channel_topic": Topic = Ts3String.Unescape(value); break; - case "channel_codec": Codec = (Codec)u8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_codec_quality": CodecQuality = i8.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_maxclients": MaxClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_maxfamilyclients": MaxFamilyClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "channel_order": Order = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_codec": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) Codec = (Codec)oval; } break; + case "channel_codec_quality": { if(Utf8Parser.TryParse(value, out u8 oval, out _)) CodecQuality = oval; } break; + case "channel_maxclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxClients = oval; } break; + case "channel_maxfamilyclients": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MaxFamilyClients = oval; } break; + case "channel_order": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Order = oval; } break; case "channel_flag_permanent": IsPermanent = value.Length > 0 && value[0] != '0'; break; case "channel_flag_semi_permanent": IsSemiPermanent = value.Length > 0 && value[0] != '0'; break; case "channel_flag_default": IsDefault = value.Length > 0 && value[0] != '0'; break; case "channel_flag_password": HasPassword = value.Length > 0 && value[0] != '0'; break; - case "channel_codec_latency_factor": CodecLatencyFactor = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_codec_latency_factor": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) CodecLatencyFactor = oval; } break; case "channel_codec_is_unencrypted": IsUnencrypted = value.Length > 0 && value[0] != '0'; break; - case "channel_delete_delay": DeleteDelay = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "channel_delete_delay": { if(Utf8Parser.TryParse(value, out double oval, out _)) DeleteDelay = TimeSpan.FromSeconds(oval); } break; case "channel_flag_maxclients_unlimited": IsMaxClientsUnlimited = value.Length > 0 && value[0] != '0'; break; case "channel_flag_maxfamilyclients_unlimited": IsMaxFamilyClientsUnlimited = value.Length > 0 && value[0] != '0'; break; case "channel_flag_maxfamilyclients_inherited": InheritsMaxFamilyClients = value.Length > 0 && value[0] != '0'; break; - case "channel_needed_talk_power": NeededTalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "channel_needed_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededTalkPower = oval; } break; case "channel_forced_silence": ForcedSilence = value.Length > 0 && value[0] != '0'; break; case "channel_name_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "channel_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "channel_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; case "channel_flag_private": IsPrivate = value.Length > 0 && value[0] != '0'; break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelList[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "cpid": foreach(var toi in toc) { toi.ChannelParentId = ChannelParentId; } break; + case "channel_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "channel_topic": foreach(var toi in toc) { toi.Topic = Topic; } break; + case "channel_codec": foreach(var toi in toc) { toi.Codec = Codec; } break; + case "channel_codec_quality": foreach(var toi in toc) { toi.CodecQuality = CodecQuality; } break; + case "channel_maxclients": foreach(var toi in toc) { toi.MaxClients = MaxClients; } break; + case "channel_maxfamilyclients": foreach(var toi in toc) { toi.MaxFamilyClients = MaxFamilyClients; } break; + case "channel_order": foreach(var toi in toc) { toi.Order = Order; } break; + case "channel_flag_permanent": foreach(var toi in toc) { toi.IsPermanent = IsPermanent; } break; + case "channel_flag_semi_permanent": foreach(var toi in toc) { toi.IsSemiPermanent = IsSemiPermanent; } break; + case "channel_flag_default": foreach(var toi in toc) { toi.IsDefault = IsDefault; } break; + case "channel_flag_password": foreach(var toi in toc) { toi.HasPassword = HasPassword; } break; + case "channel_codec_latency_factor": foreach(var toi in toc) { toi.CodecLatencyFactor = CodecLatencyFactor; } break; + case "channel_codec_is_unencrypted": foreach(var toi in toc) { toi.IsUnencrypted = IsUnencrypted; } break; + case "channel_delete_delay": foreach(var toi in toc) { toi.DeleteDelay = DeleteDelay; } break; + case "channel_flag_maxclients_unlimited": foreach(var toi in toc) { toi.IsMaxClientsUnlimited = IsMaxClientsUnlimited; } break; + case "channel_flag_maxfamilyclients_unlimited": foreach(var toi in toc) { toi.IsMaxFamilyClientsUnlimited = IsMaxFamilyClientsUnlimited; } break; + case "channel_flag_maxfamilyclients_inherited": foreach(var toi in toc) { toi.InheritsMaxFamilyClients = InheritsMaxFamilyClients; } break; + case "channel_needed_talk_power": foreach(var toi in toc) { toi.NeededTalkPower = NeededTalkPower; } break; + case "channel_forced_silence": foreach(var toi in toc) { toi.ForcedSilence = ForcedSilence; } break; + case "channel_name_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "channel_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "channel_flag_private": foreach(var toi in toc) { toi.IsPrivate = IsPrivate; } break; + } + } + + } } public sealed class ChannelListFinished : INotification @@ -407,9 +598,12 @@ public sealed class ChannelListFinished : INotification - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { + } + public void Expand(IMessage[] to, IEnumerable flds) + { } } @@ -426,23 +620,42 @@ public sealed class ChannelMoved : INotification public Reason Reason { get; set; } public ChannelId ChannelParentId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "order": Order = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "order": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Order = oval; } break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "reasonid": { if (!Enum.TryParse(value.NewString(), out Reason val)) throw new FormatException(); Reason = val; } break; - case "cpid": ChannelParentId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "reasonid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Reason = (Reason)oval; } break; + case "cpid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelParentId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelMoved[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "order": foreach(var toi in toc) { toi.Order = Order; } break; + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "reasonid": foreach(var toi in toc) { toi.Reason = Reason; } break; + case "cpid": foreach(var toi in toc) { toi.ChannelParentId = ChannelParentId; } break; + } + } + + } } public sealed class ChannelPasswordChanged : INotification @@ -452,17 +665,30 @@ public sealed class ChannelPasswordChanged : INotification public ChannelId ChannelId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelPasswordChanged[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + } + } + + } } public sealed class ChannelSubscribed : INotification @@ -473,18 +699,32 @@ public sealed class ChannelSubscribed : INotification public ChannelId ChannelId { get; set; } public DurationSeconds EmptySince { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "es": EmptySince = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "es": { if(Utf8Parser.TryParse(value, out double oval, out _)) EmptySince = TimeSpan.FromSeconds(oval); } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelSubscribed[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "es": foreach(var toi in toc) { toi.EmptySince = EmptySince; } break; + } + } + + } } public sealed class ChannelUnsubscribed : INotification @@ -494,17 +734,30 @@ public sealed class ChannelUnsubscribed : INotification public ChannelId ChannelId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ChannelUnsubscribed[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + } + } + + } } public sealed class ClientChannelGroupChanged : INotification @@ -519,22 +772,40 @@ public sealed class ClientChannelGroupChanged : INotification public ChannelId ChannelId { get; set; } public ClientId ClientId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; - case "cgid": ChannelGroup = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cgi": ChannelGroupIndex = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cgid": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) ChannelGroup = oval; } break; + case "cgi": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) ChannelGroupIndex = oval; } break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientChannelGroupChanged[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "cgid": foreach(var toi in toc) { toi.ChannelGroup = ChannelGroup; } break; + case "cgi": foreach(var toi in toc) { toi.ChannelGroupIndex = ChannelGroupIndex; } break; + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + } + } + + } } public sealed class ClientChatComposing : INotification @@ -545,18 +816,32 @@ public sealed class ClientChatComposing : INotification public ClientId ClientId { get; set; } public Uid ClientUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; case "cluid": ClientUid = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientChatComposing[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + } + } + + } } public sealed class ClientData : IResponse @@ -571,22 +856,40 @@ public sealed class ClientData : IResponse public str Name { get; set; } public ClientType ClientType { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; case "client_unique_identifier": Uid = Ts3String.Unescape(value); break; - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_database_id": DatabaseId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "client_database_id": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) DatabaseId = oval; } break; case "client_nickname": Name = Ts3String.Unescape(value); break; - case "client_type": { if (!Enum.TryParse(value.NewString(), out ClientType val)) throw new FormatException(); ClientType = val; } break; + case "client_type": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) ClientType = (ClientType)oval; } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientData[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "client_unique_identifier": foreach(var toi in toc) { toi.Uid = Uid; } break; + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "client_database_id": foreach(var toi in toc) { toi.DatabaseId = DatabaseId; } break; + case "client_nickname": foreach(var toi in toc) { toi.Name = Name; } break; + case "client_type": foreach(var toi in toc) { toi.ClientType = ClientType; } break; + } + } + + } } public sealed class ClientDbData : IResponse @@ -613,34 +916,100 @@ public sealed class ClientDbData : IResponse public i64 TotalDownloadQuota { get; set; } public str Base64HashClientUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { case "client_lastip": LastIp = Ts3String.Unescape(value); break; - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; case "client_unique_identifier": Uid = Ts3String.Unescape(value); break; - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_database_id": DatabaseId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; + case "client_database_id": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) DatabaseId = oval; } break; case "client_nickname": Name = Ts3String.Unescape(value); break; - case "client_type": { if (!Enum.TryParse(value.NewString(), out ClientType val)) throw new FormatException(); ClientType = val; } break; + case "client_type": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) ClientType = (ClientType)oval; } break; case "client_flag_avatar": AvatarHash = Ts3String.Unescape(value); break; case "client_description": Description = Ts3String.Unescape(value); break; - case "client_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "client_created": CreationDate = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "client_lastconnected": LastConnected = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "client_totalconnections": TotalConnections = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_month_bytes_uploaded": MonthlyUploadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_month_bytes_downloaded": MonthlyDownloadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_total_bytes_uploaded": TotalUploadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_total_bytes_downloaded": TotalDownloadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; + case "client_created": { if(Utf8Parser.TryParse(value, out double oval, out _)) CreationDate = Util.UnixTimeStart.AddSeconds(oval); } break; + case "client_lastconnected": { if(Utf8Parser.TryParse(value, out double oval, out _)) LastConnected = Util.UnixTimeStart.AddSeconds(oval); } break; + case "client_totalconnections": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TotalConnections = oval; } break; + case "client_month_bytes_uploaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) MonthlyUploadQuota = oval; } break; + case "client_month_bytes_downloaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) MonthlyDownloadQuota = oval; } break; + case "client_total_bytes_uploaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) TotalUploadQuota = oval; } break; + case "client_total_bytes_downloaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) TotalDownloadQuota = oval; } break; case "client_base64HashClientUID": Base64HashClientUid = Ts3String.Unescape(value); break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientDbData[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "client_lastip": foreach(var toi in toc) { toi.LastIp = LastIp; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "client_unique_identifier": foreach(var toi in toc) { toi.Uid = Uid; } break; + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "client_database_id": foreach(var toi in toc) { toi.DatabaseId = DatabaseId; } break; + case "client_nickname": foreach(var toi in toc) { toi.Name = Name; } break; + case "client_type": foreach(var toi in toc) { toi.ClientType = ClientType; } break; + case "client_flag_avatar": foreach(var toi in toc) { toi.AvatarHash = AvatarHash; } break; + case "client_description": foreach(var toi in toc) { toi.Description = Description; } break; + case "client_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "client_created": foreach(var toi in toc) { toi.CreationDate = CreationDate; } break; + case "client_lastconnected": foreach(var toi in toc) { toi.LastConnected = LastConnected; } break; + case "client_totalconnections": foreach(var toi in toc) { toi.TotalConnections = TotalConnections; } break; + case "client_month_bytes_uploaded": foreach(var toi in toc) { toi.MonthlyUploadQuota = MonthlyUploadQuota; } break; + case "client_month_bytes_downloaded": foreach(var toi in toc) { toi.MonthlyDownloadQuota = MonthlyDownloadQuota; } break; + case "client_total_bytes_uploaded": foreach(var toi in toc) { toi.TotalUploadQuota = TotalUploadQuota; } break; + case "client_total_bytes_downloaded": foreach(var toi in toc) { toi.TotalDownloadQuota = TotalDownloadQuota; } break; + case "client_base64HashClientUID": foreach(var toi in toc) { toi.Base64HashClientUid = Base64HashClientUid; } break; + } + } + + } + } + + public sealed class ClientDbIdFromUid : INotification, IResponse + { + public NotificationType NotifyType { get; } = NotificationType.ClientDbIdFromUid; + public string ReturnCode { get; set; } + + public Uid ClientUid { get; set; } + public ClientDbId ClientDbId { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "cluid": ClientUid = Ts3String.Unescape(value); break; + case "cldbid": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) ClientDbId = oval; } break; + case "return_code": ReturnCode = Ts3String.Unescape(value); break; + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientDbIdFromUid[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + case "cldbid": foreach(var toi in toc) { toi.ClientDbId = ClientDbId; } break; + } + } + + } } public sealed class ClientEnterView : INotification @@ -686,26 +1055,25 @@ public sealed class ClientEnterView : INotification public str CountryCode { get; set; } public str Badges { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "reasonid": { if (!Enum.TryParse(value.NewString(), out Reason val)) throw new FormatException(); Reason = val; } break; - case "ctid": TargetChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "reasonid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Reason = (Reason)oval; } break; + case "ctid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) TargetChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_database_id": DatabaseId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "client_database_id": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) DatabaseId = oval; } break; case "client_nickname": Name = Ts3String.Unescape(value); break; - case "client_type": { if (!Enum.TryParse(value.NewString(), out ClientType val)) throw new FormatException(); ClientType = val; } break; - case "cfid": SourceChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_type": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) ClientType = (ClientType)oval; } break; + case "cfid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) SourceChannelId = oval; } break; case "client_unique_identifier": Uid = Ts3String.Unescape(value); break; case "client_flag_avatar": AvatarHash = Ts3String.Unescape(value); break; case "client_description": Description = Ts3String.Unescape(value); break; - case "client_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "client_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; case "client_input_muted": InputMuted = value.Length > 0 && value[0] != '0'; break; case "client_output_muted": OutputMuted = value.Length > 0 && value[0] != '0'; break; case "client_outputonly_muted": OutputOnlyMuted = value.Length > 0 && value[0] != '0'; break; @@ -713,19 +1081,19 @@ public void SetField(string name, ReadOnlySpan value) case "client_output_hardware": OutputHardwareEnabled = value.Length > 0 && value[0] != '0'; break; case "client_meta_data": Metadata = Ts3String.Unescape(value); break; case "client_is_recording": IsRecording = value.Length > 0 && value[0] != '0'; break; - case "client_channel_group_id": ChannelGroup = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_channel_group_inherited_channel_id": InheritedChannelGroupFromChannel = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_servergroups": { if(value.Length == 0) ServerGroups = Array.Empty(); else { var ss = new SpanSplitter(); ss.First(value, ','); int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++; ServerGroups = new ServerGroupId[cnt + 1]; for(int i = 0; i < cnt + 1; i++) { ServerGroups[i] = ServerGroupId.Parse(ss.Trim(value).NewString(), CultureInfo.InvariantCulture); if (i < cnt) value = ss.Next(value); } } } break; + case "client_channel_group_id": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) ChannelGroup = oval; } break; + case "client_channel_group_inherited_channel_id": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) InheritedChannelGroupFromChannel = oval; } break; + case "client_servergroups": { if(value.Length == 0) ServerGroups = Array.Empty(); else { var ss = new SpanSplitter(); ss.First(value, (byte)','); int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++; ServerGroups = new ServerGroupId[cnt + 1]; for(int i = 0; i < cnt + 1; i++) { { if(Utf8Parser.TryParse(ss.Trim(value), out ServerGroupId oval, out _)) ServerGroups[i] = oval; } if (i < cnt) value = ss.Next(value); } } } break; case "client_away": IsAway = value.Length > 0 && value[0] != '0'; break; case "client_away_message": AwayMessage = Ts3String.Unescape(value); break; - case "client_talk_power": TalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_talk_request": TalkPowerRequestTime = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "client_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TalkPower = oval; } break; + case "client_talk_request": { if(Utf8Parser.TryParse(value, out double oval, out _)) TalkPowerRequestTime = Util.UnixTimeStart.AddSeconds(oval); } break; case "client_talk_request_msg": TalkPowerRequestMessage = Ts3String.Unescape(value); break; case "client_is_talker": TalkPowerGranted = value.Length > 0 && value[0] != '0'; break; case "client_is_priority_speaker": IsPrioritySpeaker = value.Length > 0 && value[0] != '0'; break; - case "client_unread_messages": UnreadMessages = u32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_unread_messages": { if(Utf8Parser.TryParse(value, out u32 oval, out _)) UnreadMessages = oval; } break; case "client_nickname_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "client_needed_serverquery_view_power": NeededServerqueryViewPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_needed_serverquery_view_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededServerqueryViewPower = oval; } break; case "client_is_channel_commander": IsChannelCommander = value.Length > 0 && value[0] != '0'; break; case "client_country": CountryCode = Ts3String.Unescape(value); break; case "client_badges": Badges = Ts3String.Unescape(value); break; @@ -733,6 +1101,95 @@ public void SetField(string name, ReadOnlySpan value) } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientEnterView[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "reasonid": foreach(var toi in toc) { toi.Reason = Reason; } break; + case "ctid": foreach(var toi in toc) { toi.TargetChannelId = TargetChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "client_database_id": foreach(var toi in toc) { toi.DatabaseId = DatabaseId; } break; + case "client_nickname": foreach(var toi in toc) { toi.Name = Name; } break; + case "client_type": foreach(var toi in toc) { toi.ClientType = ClientType; } break; + case "cfid": foreach(var toi in toc) { toi.SourceChannelId = SourceChannelId; } break; + case "client_unique_identifier": foreach(var toi in toc) { toi.Uid = Uid; } break; + case "client_flag_avatar": foreach(var toi in toc) { toi.AvatarHash = AvatarHash; } break; + case "client_description": foreach(var toi in toc) { toi.Description = Description; } break; + case "client_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "client_input_muted": foreach(var toi in toc) { toi.InputMuted = InputMuted; } break; + case "client_output_muted": foreach(var toi in toc) { toi.OutputMuted = OutputMuted; } break; + case "client_outputonly_muted": foreach(var toi in toc) { toi.OutputOnlyMuted = OutputOnlyMuted; } break; + case "client_input_hardware": foreach(var toi in toc) { toi.InputHardwareEnabled = InputHardwareEnabled; } break; + case "client_output_hardware": foreach(var toi in toc) { toi.OutputHardwareEnabled = OutputHardwareEnabled; } break; + case "client_meta_data": foreach(var toi in toc) { toi.Metadata = Metadata; } break; + case "client_is_recording": foreach(var toi in toc) { toi.IsRecording = IsRecording; } break; + case "client_channel_group_id": foreach(var toi in toc) { toi.ChannelGroup = ChannelGroup; } break; + case "client_channel_group_inherited_channel_id": foreach(var toi in toc) { toi.InheritedChannelGroupFromChannel = InheritedChannelGroupFromChannel; } break; + case "client_servergroups": foreach(var toi in toc) { toi.ServerGroups = ServerGroups; } break; + case "client_away": foreach(var toi in toc) { toi.IsAway = IsAway; } break; + case "client_away_message": foreach(var toi in toc) { toi.AwayMessage = AwayMessage; } break; + case "client_talk_power": foreach(var toi in toc) { toi.TalkPower = TalkPower; } break; + case "client_talk_request": foreach(var toi in toc) { toi.TalkPowerRequestTime = TalkPowerRequestTime; } break; + case "client_talk_request_msg": foreach(var toi in toc) { toi.TalkPowerRequestMessage = TalkPowerRequestMessage; } break; + case "client_is_talker": foreach(var toi in toc) { toi.TalkPowerGranted = TalkPowerGranted; } break; + case "client_is_priority_speaker": foreach(var toi in toc) { toi.IsPrioritySpeaker = IsPrioritySpeaker; } break; + case "client_unread_messages": foreach(var toi in toc) { toi.UnreadMessages = UnreadMessages; } break; + case "client_nickname_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "client_needed_serverquery_view_power": foreach(var toi in toc) { toi.NeededServerqueryViewPower = NeededServerqueryViewPower; } break; + case "client_is_channel_commander": foreach(var toi in toc) { toi.IsChannelCommander = IsChannelCommander; } break; + case "client_country": foreach(var toi in toc) { toi.CountryCode = CountryCode; } break; + case "client_badges": foreach(var toi in toc) { toi.Badges = Badges; } break; + } + } + + } + } + + public sealed class ClientIds : INotification, IResponse + { + public NotificationType NotifyType { get; } = NotificationType.ClientIds; + public string ReturnCode { get; set; } + + public Uid ClientUid { get; set; } + public ClientId ClientId { get; set; } + public str Name { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "cluid": ClientUid = Ts3String.Unescape(value); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "name": Name = Ts3String.Unescape(value); break; + case "return_code": ReturnCode = Ts3String.Unescape(value); break; + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientIds[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + } + } + + } } public sealed class ClientInfo : IResponse @@ -800,13 +1257,12 @@ public sealed class ClientInfo : IResponse public str Description { get; set; } public IconHash IconId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "client_idle_time": ClientIdleTime = TimeSpan.FromMilliseconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "client_idle_time": { if(Utf8Parser.TryParse(value, out double oval, out _)) ClientIdleTime = TimeSpan.FromMilliseconds(oval); } break; case "client_version": ClientVersion = Ts3String.Unescape(value); break; case "client_version_sign": ClientVersionSign = Ts3String.Unescape(value); break; case "client_platform": ClientPlattform = Ts3String.Unescape(value); break; @@ -814,23 +1270,23 @@ public void SetField(string name, ReadOnlySpan value) case "client_security_hash": SecurityHash = Ts3String.Unescape(value); break; case "client_login_name": LoginName = Ts3String.Unescape(value); break; case "client_default_token": DefaultToken = Ts3String.Unescape(value); break; - case "connection_filetransfer_bandwidth_sent": FiletransferBandwidthSent = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_filetransfer_bandwidth_received": FiletransferBandwidthReceived = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_sent_total": PacketsSentTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_received_total": PacketsReceivedTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_sent_total": BytesSentTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_received_total": BytesReceivedTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_second_total": BandwidthSentLastSecondTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_second_total": BandwidthReceivedLastSecondTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_minute_total": BandwidthSentLastMinuteTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_minute_total": BandwidthReceivedLastMinuteTotal = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_connected_time": ConnectedTime = TimeSpan.FromMilliseconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "connection_filetransfer_bandwidth_sent": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) FiletransferBandwidthSent = oval; } break; + case "connection_filetransfer_bandwidth_received": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) FiletransferBandwidthReceived = oval; } break; + case "connection_packets_sent_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsSentTotal = oval; } break; + case "connection_packets_received_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsReceivedTotal = oval; } break; + case "connection_bytes_sent_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesSentTotal = oval; } break; + case "connection_bytes_received_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesReceivedTotal = oval; } break; + case "connection_bandwidth_sent_last_second_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastSecondTotal = oval; } break; + case "connection_bandwidth_received_last_second_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastSecondTotal = oval; } break; + case "connection_bandwidth_sent_last_minute_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastMinuteTotal = oval; } break; + case "connection_bandwidth_received_last_minute_total": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastMinuteTotal = oval; } break; + case "connection_connected_time": { if(Utf8Parser.TryParse(value, out double oval, out _)) ConnectedTime = TimeSpan.FromMilliseconds(oval); } break; case "connection_client_ip": Ip = Ts3String.Unescape(value); break; - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; case "client_unique_identifier": Uid = Ts3String.Unescape(value); break; - case "client_database_id": DatabaseId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_database_id": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) DatabaseId = oval; } break; case "client_nickname": Name = Ts3String.Unescape(value); break; - case "client_type": { if (!Enum.TryParse(value.NewString(), out ClientType val)) throw new FormatException(); ClientType = val; } break; + case "client_type": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) ClientType = (ClientType)oval; } break; case "client_input_muted": InputMuted = value.Length > 0 && value[0] != '0'; break; case "client_output_muted": OutputMuted = value.Length > 0 && value[0] != '0'; break; case "client_outputonly_muted": OutputOnlyMuted = value.Length > 0 && value[0] != '0'; break; @@ -838,37 +1294,220 @@ public void SetField(string name, ReadOnlySpan value) case "client_output_hardware": OutputHardwareEnabled = value.Length > 0 && value[0] != '0'; break; case "client_meta_data": Metadata = Ts3String.Unescape(value); break; case "client_is_recording": IsRecording = value.Length > 0 && value[0] != '0'; break; - case "client_channel_group_id": ChannelGroup = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_channel_group_inherited_channel_id": InheritedChannelGroupFromChannel = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_servergroups": { if(value.Length == 0) ServerGroups = Array.Empty(); else { var ss = new SpanSplitter(); ss.First(value, ','); int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++; ServerGroups = new ServerGroupId[cnt + 1]; for(int i = 0; i < cnt + 1; i++) { ServerGroups[i] = ServerGroupId.Parse(ss.Trim(value).NewString(), CultureInfo.InvariantCulture); if (i < cnt) value = ss.Next(value); } } } break; + case "client_channel_group_id": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) ChannelGroup = oval; } break; + case "client_channel_group_inherited_channel_id": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) InheritedChannelGroupFromChannel = oval; } break; + case "client_servergroups": { if(value.Length == 0) ServerGroups = Array.Empty(); else { var ss = new SpanSplitter(); ss.First(value, (byte)','); int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++; ServerGroups = new ServerGroupId[cnt + 1]; for(int i = 0; i < cnt + 1; i++) { { if(Utf8Parser.TryParse(ss.Trim(value), out ServerGroupId oval, out _)) ServerGroups[i] = oval; } if (i < cnt) value = ss.Next(value); } } } break; case "client_away": IsAway = value.Length > 0 && value[0] != '0'; break; case "client_away_message": AwayMessage = Ts3String.Unescape(value); break; - case "client_talk_power": TalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_talk_request": TalkPowerRequestTime = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "client_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TalkPower = oval; } break; + case "client_talk_request": { if(Utf8Parser.TryParse(value, out double oval, out _)) TalkPowerRequestTime = Util.UnixTimeStart.AddSeconds(oval); } break; case "client_talk_request_msg": TalkPowerRequestMessage = Ts3String.Unescape(value); break; case "client_is_talker": TalkPowerGranted = value.Length > 0 && value[0] != '0'; break; case "client_is_priority_speaker": IsPrioritySpeaker = value.Length > 0 && value[0] != '0'; break; - case "client_unread_messages": UnreadMessages = u32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_unread_messages": { if(Utf8Parser.TryParse(value, out u32 oval, out _)) UnreadMessages = oval; } break; case "client_nickname_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "client_needed_serverquery_view_power": NeededServerqueryViewPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_needed_serverquery_view_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededServerqueryViewPower = oval; } break; case "client_is_channel_commander": IsChannelCommander = value.Length > 0 && value[0] != '0'; break; case "client_country": CountryCode = Ts3String.Unescape(value); break; case "client_badges": Badges = Ts3String.Unescape(value); break; - case "client_created": CreationDate = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "client_lastconnected": LastConnected = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "client_totalconnections": TotalConnections = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_month_bytes_uploaded": MonthlyUploadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_month_bytes_downloaded": MonthlyDownloadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_total_bytes_uploaded": TotalUploadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_total_bytes_downloaded": TotalDownloadQuota = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_created": { if(Utf8Parser.TryParse(value, out double oval, out _)) CreationDate = Util.UnixTimeStart.AddSeconds(oval); } break; + case "client_lastconnected": { if(Utf8Parser.TryParse(value, out double oval, out _)) LastConnected = Util.UnixTimeStart.AddSeconds(oval); } break; + case "client_totalconnections": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TotalConnections = oval; } break; + case "client_month_bytes_uploaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) MonthlyUploadQuota = oval; } break; + case "client_month_bytes_downloaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) MonthlyDownloadQuota = oval; } break; + case "client_total_bytes_uploaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) TotalUploadQuota = oval; } break; + case "client_total_bytes_downloaded": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) TotalDownloadQuota = oval; } break; case "client_base64HashClientUID": Base64HashClientUid = Ts3String.Unescape(value); break; case "client_flag_avatar": AvatarHash = Ts3String.Unescape(value); break; case "client_description": Description = Ts3String.Unescape(value); break; - case "client_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "client_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientInfo[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "client_idle_time": foreach(var toi in toc) { toi.ClientIdleTime = ClientIdleTime; } break; + case "client_version": foreach(var toi in toc) { toi.ClientVersion = ClientVersion; } break; + case "client_version_sign": foreach(var toi in toc) { toi.ClientVersionSign = ClientVersionSign; } break; + case "client_platform": foreach(var toi in toc) { toi.ClientPlattform = ClientPlattform; } break; + case "client_default_channel": foreach(var toi in toc) { toi.DefaultChannel = DefaultChannel; } break; + case "client_security_hash": foreach(var toi in toc) { toi.SecurityHash = SecurityHash; } break; + case "client_login_name": foreach(var toi in toc) { toi.LoginName = LoginName; } break; + case "client_default_token": foreach(var toi in toc) { toi.DefaultToken = DefaultToken; } break; + case "connection_filetransfer_bandwidth_sent": foreach(var toi in toc) { toi.FiletransferBandwidthSent = FiletransferBandwidthSent; } break; + case "connection_filetransfer_bandwidth_received": foreach(var toi in toc) { toi.FiletransferBandwidthReceived = FiletransferBandwidthReceived; } break; + case "connection_packets_sent_total": foreach(var toi in toc) { toi.PacketsSentTotal = PacketsSentTotal; } break; + case "connection_packets_received_total": foreach(var toi in toc) { toi.PacketsReceivedTotal = PacketsReceivedTotal; } break; + case "connection_bytes_sent_total": foreach(var toi in toc) { toi.BytesSentTotal = BytesSentTotal; } break; + case "connection_bytes_received_total": foreach(var toi in toc) { toi.BytesReceivedTotal = BytesReceivedTotal; } break; + case "connection_bandwidth_sent_last_second_total": foreach(var toi in toc) { toi.BandwidthSentLastSecondTotal = BandwidthSentLastSecondTotal; } break; + case "connection_bandwidth_received_last_second_total": foreach(var toi in toc) { toi.BandwidthReceivedLastSecondTotal = BandwidthReceivedLastSecondTotal; } break; + case "connection_bandwidth_sent_last_minute_total": foreach(var toi in toc) { toi.BandwidthSentLastMinuteTotal = BandwidthSentLastMinuteTotal; } break; + case "connection_bandwidth_received_last_minute_total": foreach(var toi in toc) { toi.BandwidthReceivedLastMinuteTotal = BandwidthReceivedLastMinuteTotal; } break; + case "connection_connected_time": foreach(var toi in toc) { toi.ConnectedTime = ConnectedTime; } break; + case "connection_client_ip": foreach(var toi in toc) { toi.Ip = Ip; } break; + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "client_unique_identifier": foreach(var toi in toc) { toi.Uid = Uid; } break; + case "client_database_id": foreach(var toi in toc) { toi.DatabaseId = DatabaseId; } break; + case "client_nickname": foreach(var toi in toc) { toi.Name = Name; } break; + case "client_type": foreach(var toi in toc) { toi.ClientType = ClientType; } break; + case "client_input_muted": foreach(var toi in toc) { toi.InputMuted = InputMuted; } break; + case "client_output_muted": foreach(var toi in toc) { toi.OutputMuted = OutputMuted; } break; + case "client_outputonly_muted": foreach(var toi in toc) { toi.OutputOnlyMuted = OutputOnlyMuted; } break; + case "client_input_hardware": foreach(var toi in toc) { toi.InputHardwareEnabled = InputHardwareEnabled; } break; + case "client_output_hardware": foreach(var toi in toc) { toi.OutputHardwareEnabled = OutputHardwareEnabled; } break; + case "client_meta_data": foreach(var toi in toc) { toi.Metadata = Metadata; } break; + case "client_is_recording": foreach(var toi in toc) { toi.IsRecording = IsRecording; } break; + case "client_channel_group_id": foreach(var toi in toc) { toi.ChannelGroup = ChannelGroup; } break; + case "client_channel_group_inherited_channel_id": foreach(var toi in toc) { toi.InheritedChannelGroupFromChannel = InheritedChannelGroupFromChannel; } break; + case "client_servergroups": foreach(var toi in toc) { toi.ServerGroups = ServerGroups; } break; + case "client_away": foreach(var toi in toc) { toi.IsAway = IsAway; } break; + case "client_away_message": foreach(var toi in toc) { toi.AwayMessage = AwayMessage; } break; + case "client_talk_power": foreach(var toi in toc) { toi.TalkPower = TalkPower; } break; + case "client_talk_request": foreach(var toi in toc) { toi.TalkPowerRequestTime = TalkPowerRequestTime; } break; + case "client_talk_request_msg": foreach(var toi in toc) { toi.TalkPowerRequestMessage = TalkPowerRequestMessage; } break; + case "client_is_talker": foreach(var toi in toc) { toi.TalkPowerGranted = TalkPowerGranted; } break; + case "client_is_priority_speaker": foreach(var toi in toc) { toi.IsPrioritySpeaker = IsPrioritySpeaker; } break; + case "client_unread_messages": foreach(var toi in toc) { toi.UnreadMessages = UnreadMessages; } break; + case "client_nickname_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "client_needed_serverquery_view_power": foreach(var toi in toc) { toi.NeededServerqueryViewPower = NeededServerqueryViewPower; } break; + case "client_is_channel_commander": foreach(var toi in toc) { toi.IsChannelCommander = IsChannelCommander; } break; + case "client_country": foreach(var toi in toc) { toi.CountryCode = CountryCode; } break; + case "client_badges": foreach(var toi in toc) { toi.Badges = Badges; } break; + case "client_created": foreach(var toi in toc) { toi.CreationDate = CreationDate; } break; + case "client_lastconnected": foreach(var toi in toc) { toi.LastConnected = LastConnected; } break; + case "client_totalconnections": foreach(var toi in toc) { toi.TotalConnections = TotalConnections; } break; + case "client_month_bytes_uploaded": foreach(var toi in toc) { toi.MonthlyUploadQuota = MonthlyUploadQuota; } break; + case "client_month_bytes_downloaded": foreach(var toi in toc) { toi.MonthlyDownloadQuota = MonthlyDownloadQuota; } break; + case "client_total_bytes_uploaded": foreach(var toi in toc) { toi.TotalUploadQuota = TotalUploadQuota; } break; + case "client_total_bytes_downloaded": foreach(var toi in toc) { toi.TotalDownloadQuota = TotalDownloadQuota; } break; + case "client_base64HashClientUID": foreach(var toi in toc) { toi.Base64HashClientUid = Base64HashClientUid; } break; + case "client_flag_avatar": foreach(var toi in toc) { toi.AvatarHash = AvatarHash; } break; + case "client_description": foreach(var toi in toc) { toi.Description = Description; } break; + case "client_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + } + } + + } + } + + public sealed class ClientInit : INotification + { + public NotificationType NotifyType { get; } = NotificationType.ClientInit; + + + public str Name { get; set; } + public str ClientVersion { get; set; } + public str ClientPlattform { get; set; } + public bool InputHardwareEnabled { get; set; } + public bool OutputHardwareEnabled { get; set; } + public str DefaultChannel { get; set; } + public str DefaultChannelPassword { get; set; } + public str ServerPassword { get; set; } + public str Metadata { get; set; } + public str ClientVersionSign { get; set; } + public u64 ClientKeyOffset { get; set; } + public str PhoneticName { get; set; } + public str DefaultToken { get; set; } + public str HardwareId { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "client_nickname": Name = Ts3String.Unescape(value); break; + case "client_version": ClientVersion = Ts3String.Unescape(value); break; + case "client_platform": ClientPlattform = Ts3String.Unescape(value); break; + case "client_input_hardware": InputHardwareEnabled = value.Length > 0 && value[0] != '0'; break; + case "client_output_hardware": OutputHardwareEnabled = value.Length > 0 && value[0] != '0'; break; + case "client_default_channel": DefaultChannel = Ts3String.Unescape(value); break; + case "client_default_channel_password": DefaultChannelPassword = Ts3String.Unescape(value); break; + case "client_server_password": ServerPassword = Ts3String.Unescape(value); break; + case "client_meta_data": Metadata = Ts3String.Unescape(value); break; + case "client_version_sign": ClientVersionSign = Ts3String.Unescape(value); break; + case "client_key_offset": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) ClientKeyOffset = oval; } break; + case "client_nickname_phonetic": PhoneticName = Ts3String.Unescape(value); break; + case "client_default_token": DefaultToken = Ts3String.Unescape(value); break; + case "hwid": HardwareId = Ts3String.Unescape(value); break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientInit[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "client_nickname": foreach(var toi in toc) { toi.Name = Name; } break; + case "client_version": foreach(var toi in toc) { toi.ClientVersion = ClientVersion; } break; + case "client_platform": foreach(var toi in toc) { toi.ClientPlattform = ClientPlattform; } break; + case "client_input_hardware": foreach(var toi in toc) { toi.InputHardwareEnabled = InputHardwareEnabled; } break; + case "client_output_hardware": foreach(var toi in toc) { toi.OutputHardwareEnabled = OutputHardwareEnabled; } break; + case "client_default_channel": foreach(var toi in toc) { toi.DefaultChannel = DefaultChannel; } break; + case "client_default_channel_password": foreach(var toi in toc) { toi.DefaultChannelPassword = DefaultChannelPassword; } break; + case "client_server_password": foreach(var toi in toc) { toi.ServerPassword = ServerPassword; } break; + case "client_meta_data": foreach(var toi in toc) { toi.Metadata = Metadata; } break; + case "client_version_sign": foreach(var toi in toc) { toi.ClientVersionSign = ClientVersionSign; } break; + case "client_key_offset": foreach(var toi in toc) { toi.ClientKeyOffset = ClientKeyOffset; } break; + case "client_nickname_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "client_default_token": foreach(var toi in toc) { toi.DefaultToken = DefaultToken; } break; + case "hwid": foreach(var toi in toc) { toi.HardwareId = HardwareId; } break; + } + } + + } + } + + public sealed class ClientInitIv : INotification + { + public NotificationType NotifyType { get; } = NotificationType.ClientInitIv; + + + public str Alpha { get; set; } + public str Omega { get; set; } + public str Ip { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "alpha": Alpha = Ts3String.Unescape(value); break; + case "omega": Omega = Ts3String.Unescape(value); break; + case "ip": Ip = Ts3String.Unescape(value); break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientInitIv[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "alpha": foreach(var toi in toc) { toi.Alpha = Alpha; } break; + case "omega": foreach(var toi in toc) { toi.Omega = Omega; } break; + case "ip": foreach(var toi in toc) { toi.Ip = Ip; } break; + } + } + + } } public sealed class ClientLeftView : INotification @@ -886,25 +1525,46 @@ public sealed class ClientLeftView : INotification public ClientId ClientId { get; set; } public ChannelId SourceChannelId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { case "reasonmsg": ReasonMessage = Ts3String.Unescape(value); break; - case "bantime": BanTime = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "reasonid": { if (!Enum.TryParse(value.NewString(), out Reason val)) throw new FormatException(); Reason = val; } break; - case "ctid": TargetChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "bantime": { if(Utf8Parser.TryParse(value, out double oval, out _)) BanTime = TimeSpan.FromSeconds(oval); } break; + case "reasonid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Reason = (Reason)oval; } break; + case "ctid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) TargetChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cfid": SourceChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "cfid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) SourceChannelId = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientLeftView[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "reasonmsg": foreach(var toi in toc) { toi.ReasonMessage = ReasonMessage; } break; + case "bantime": foreach(var toi in toc) { toi.BanTime = BanTime; } break; + case "reasonid": foreach(var toi in toc) { toi.Reason = Reason; } break; + case "ctid": foreach(var toi in toc) { toi.TargetChannelId = TargetChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "cfid": foreach(var toi in toc) { toi.SourceChannelId = SourceChannelId; } break; + } + } + + } } public sealed class ClientMoved : INotification @@ -919,41 +1579,115 @@ public sealed class ClientMoved : INotification public str InvokerName { get; set; } public Uid InvokerUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "reasonid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Reason = (Reason)oval; } break; + case "ctid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) TargetChannelId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; + case "invokername": InvokerName = Ts3String.Unescape(value); break; + case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientMoved[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "reasonid": foreach(var toi in toc) { toi.Reason = Reason; } break; + case "ctid": foreach(var toi in toc) { toi.TargetChannelId = TargetChannelId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + } + } + + } + } + + public sealed class ClientNeededPermissions : INotification + { + public NotificationType NotifyType { get; } = NotificationType.ClientNeededPermissions; + + + public PermissionId PermissionId { get; set; } + public i32 PermissionValue { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "permid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) PermissionId = (PermissionId)oval; } break; + case "permvalue": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) PermissionValue = oval; } break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientNeededPermissions[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "permid": foreach(var toi in toc) { toi.PermissionId = PermissionId; } break; + case "permvalue": foreach(var toi in toc) { toi.PermissionValue = PermissionValue; } break; + } + } + + } + } + + public sealed class ClientPoke : INotification + { + public NotificationType NotifyType { get; } = NotificationType.ClientPoke; + + + public ClientId InvokerId { get; set; } + public str InvokerName { get; set; } + public Uid InvokerUid { get; set; } + public str Message { get; set; } + + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "reasonid": { if (!Enum.TryParse(value.NewString(), out Reason val)) throw new FormatException(); Reason = val; } break; - case "ctid": TargetChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; + case "msg": Message = Ts3String.Unescape(value); break; } } - } - - public sealed class ClientNeededPermissions : INotification - { - public NotificationType NotifyType { get; } = NotificationType.ClientNeededPermissions; - - - public PermissionId PermissionId { get; set; } - public i32 PermissionValue { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void Expand(IMessage[] to, IEnumerable flds) { - - switch(name) + var toc = (ClientPoke[])to; + foreach (var fld in flds) { - - case "permid": PermissionId = (PermissionId)i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "permvalue": PermissionValue = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - + switch(fld) + { + + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "msg": foreach(var toi in toc) { toi.Message = Message; } break; + } } } @@ -968,19 +1702,34 @@ public sealed class ClientServerGroup : INotification, IResponse public ServerGroupId ServerGroupId { get; set; } public ClientDbId ClientDbId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { case "name": Name = Ts3String.Unescape(value); break; - case "sgid": ServerGroupId = ServerGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cldbid": ClientDbId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "sgid": { if(Utf8Parser.TryParse(value, out ServerGroupId oval, out _)) ServerGroupId = oval; } break; + case "cldbid": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) ClientDbId = oval; } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientServerGroup[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "sgid": foreach(var toi in toc) { toi.ServerGroupId = ServerGroupId; } break; + case "cldbid": foreach(var toi in toc) { toi.ClientDbId = ClientDbId; } break; + } + } + + } } public sealed class ClientServerGroupAdded : INotification @@ -996,23 +1745,42 @@ public sealed class ClientServerGroupAdded : INotification public ClientId ClientId { get; set; } public Uid ClientUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { case "name": Name = Ts3String.Unescape(value); break; - case "sgid": ServerGroupId = ServerGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "sgid": { if(Utf8Parser.TryParse(value, out ServerGroupId oval, out _)) ServerGroupId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; case "cluid": ClientUid = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ClientServerGroupAdded[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "sgid": foreach(var toi in toc) { toi.ServerGroupId = ServerGroupId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + } + } + + } } public sealed class CommandError : INotification @@ -1026,21 +1794,38 @@ public sealed class CommandError : INotification public str ReturnCode { get; set; } public str ExtraMessage { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "id": Id = (Ts3ErrorCode)u32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "id": { if(Utf8Parser.TryParse(value, out u32 oval, out _)) Id = (Ts3ErrorCode)oval; } break; case "msg": Message = Ts3String.Unescape(value); break; - case "failed_permid": MissingPermissionId = (PermissionId)i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "failed_permid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) MissingPermissionId = (PermissionId)oval; } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; case "extra_msg": ExtraMessage = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (CommandError[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "id": foreach(var toi in toc) { toi.Id = Id; } break; + case "msg": foreach(var toi in toc) { toi.Message = Message; } break; + case "failed_permid": foreach(var toi in toc) { toi.MissingPermissionId = MissingPermissionId; } break; + case "return_code": foreach(var toi in toc) { toi.ReturnCode = ReturnCode; } break; + case "extra_msg": foreach(var toi in toc) { toi.ExtraMessage = ExtraMessage; } break; + } + } + + } } public sealed class ConnectionInfo : INotification @@ -1090,57 +1875,110 @@ public sealed class ConnectionInfo : INotification public u64 FiletransferBandwidthReceived { get; set; } public DurationMilliseconds IdleTime { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_ping": Ping = TimeSpan.FromMilliseconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "connection_ping_deviation": PingDeviation = TimeSpan.FromMilliseconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "connection_connected_time": ConnectedTime = TimeSpan.FromMilliseconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "connection_ping": { if(Utf8Parser.TryParse(value, out double oval, out _)) Ping = TimeSpan.FromMilliseconds(oval); } break; + case "connection_ping_deviation": { if(Utf8Parser.TryParse(value, out double oval, out _)) PingDeviation = TimeSpan.FromMilliseconds(oval); } break; + case "connection_connected_time": { if(Utf8Parser.TryParse(value, out double oval, out _)) ConnectedTime = TimeSpan.FromMilliseconds(oval); } break; case "connection_client_ip": Ip = Ts3String.Unescape(value); break; - case "connection_client_port": Port = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_sent_speech": PacketsSentSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_sent_keepalive": PacketsSentKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_sent_control": PacketsSentControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_sent_speech": BytesSentSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_sent_keepalive": BytesSentKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_sent_control": BytesSentControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_received_speech": PacketsReceivedSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_received_keepalive": PacketsReceivedKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_packets_received_control": PacketsReceivedControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_received_speech": BytesReceivedSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_received_keepalive": BytesReceivedKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bytes_received_control": BytesReceivedControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_server2client_packetloss_speech": ServerToClientPacketlossSpeech = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_server2client_packetloss_keepalive": ServerToClientPacketlossKeepalive = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_server2client_packetloss_control": ServerToClientPacketlossControl = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_server2client_packetloss_total": ServerToClientPacketlossTotal = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_client2server_packetloss_speech": ClientToServerPacketlossSpeech = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_client2server_packetloss_keepalive": ClientToServerPacketlossKeepalive = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_client2server_packetloss_control": ClientToServerPacketlossControl = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_client2server_packetloss_total": ClientToServerPacketlossTotal = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_second_speech": BandwidthSentLastSecondSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_second_keepalive": BandwidthSentLastSecondKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_second_control": BandwidthSentLastSecondControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_minute_speech": BandwidthSentLastMinuteSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_minute_keepalive": BandwidthSentLastMinuteKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_sent_last_minute_control": BandwidthSentLastMinuteControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_second_speech": BandwidthReceivedLastSecondSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_second_keepalive": BandwidthReceivedLastSecondKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_second_control": BandwidthReceivedLastSecondControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_minute_speech": BandwidthReceivedLastMinuteSpeech = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_minute_keepalive": BandwidthReceivedLastMinuteKeepalive = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_bandwidth_received_last_minute_control": BandwidthReceivedLastMinuteControl = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_filetransfer_bandwidth_sent": FiletransferBandwidthSent = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_filetransfer_bandwidth_received": FiletransferBandwidthReceived = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "connection_idle_time": IdleTime = TimeSpan.FromMilliseconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "connection_client_port": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) Port = oval; } break; + case "connection_packets_sent_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsSentSpeech = oval; } break; + case "connection_packets_sent_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsSentKeepalive = oval; } break; + case "connection_packets_sent_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsSentControl = oval; } break; + case "connection_bytes_sent_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesSentSpeech = oval; } break; + case "connection_bytes_sent_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesSentKeepalive = oval; } break; + case "connection_bytes_sent_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesSentControl = oval; } break; + case "connection_packets_received_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsReceivedSpeech = oval; } break; + case "connection_packets_received_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsReceivedKeepalive = oval; } break; + case "connection_packets_received_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) PacketsReceivedControl = oval; } break; + case "connection_bytes_received_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesReceivedSpeech = oval; } break; + case "connection_bytes_received_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesReceivedKeepalive = oval; } break; + case "connection_bytes_received_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BytesReceivedControl = oval; } break; + case "connection_server2client_packetloss_speech": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ServerToClientPacketlossSpeech = oval; } break; + case "connection_server2client_packetloss_keepalive": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ServerToClientPacketlossKeepalive = oval; } break; + case "connection_server2client_packetloss_control": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ServerToClientPacketlossControl = oval; } break; + case "connection_server2client_packetloss_total": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ServerToClientPacketlossTotal = oval; } break; + case "connection_client2server_packetloss_speech": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ClientToServerPacketlossSpeech = oval; } break; + case "connection_client2server_packetloss_keepalive": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ClientToServerPacketlossKeepalive = oval; } break; + case "connection_client2server_packetloss_control": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ClientToServerPacketlossControl = oval; } break; + case "connection_client2server_packetloss_total": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) ClientToServerPacketlossTotal = oval; } break; + case "connection_bandwidth_sent_last_second_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastSecondSpeech = oval; } break; + case "connection_bandwidth_sent_last_second_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastSecondKeepalive = oval; } break; + case "connection_bandwidth_sent_last_second_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastSecondControl = oval; } break; + case "connection_bandwidth_sent_last_minute_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastMinuteSpeech = oval; } break; + case "connection_bandwidth_sent_last_minute_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastMinuteKeepalive = oval; } break; + case "connection_bandwidth_sent_last_minute_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthSentLastMinuteControl = oval; } break; + case "connection_bandwidth_received_last_second_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastSecondSpeech = oval; } break; + case "connection_bandwidth_received_last_second_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastSecondKeepalive = oval; } break; + case "connection_bandwidth_received_last_second_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastSecondControl = oval; } break; + case "connection_bandwidth_received_last_minute_speech": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastMinuteSpeech = oval; } break; + case "connection_bandwidth_received_last_minute_keepalive": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastMinuteKeepalive = oval; } break; + case "connection_bandwidth_received_last_minute_control": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) BandwidthReceivedLastMinuteControl = oval; } break; + case "connection_filetransfer_bandwidth_sent": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) FiletransferBandwidthSent = oval; } break; + case "connection_filetransfer_bandwidth_received": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) FiletransferBandwidthReceived = oval; } break; + case "connection_idle_time": { if(Utf8Parser.TryParse(value, out double oval, out _)) IdleTime = TimeSpan.FromMilliseconds(oval); } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ConnectionInfo[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "connection_ping": foreach(var toi in toc) { toi.Ping = Ping; } break; + case "connection_ping_deviation": foreach(var toi in toc) { toi.PingDeviation = PingDeviation; } break; + case "connection_connected_time": foreach(var toi in toc) { toi.ConnectedTime = ConnectedTime; } break; + case "connection_client_ip": foreach(var toi in toc) { toi.Ip = Ip; } break; + case "connection_client_port": foreach(var toi in toc) { toi.Port = Port; } break; + case "connection_packets_sent_speech": foreach(var toi in toc) { toi.PacketsSentSpeech = PacketsSentSpeech; } break; + case "connection_packets_sent_keepalive": foreach(var toi in toc) { toi.PacketsSentKeepalive = PacketsSentKeepalive; } break; + case "connection_packets_sent_control": foreach(var toi in toc) { toi.PacketsSentControl = PacketsSentControl; } break; + case "connection_bytes_sent_speech": foreach(var toi in toc) { toi.BytesSentSpeech = BytesSentSpeech; } break; + case "connection_bytes_sent_keepalive": foreach(var toi in toc) { toi.BytesSentKeepalive = BytesSentKeepalive; } break; + case "connection_bytes_sent_control": foreach(var toi in toc) { toi.BytesSentControl = BytesSentControl; } break; + case "connection_packets_received_speech": foreach(var toi in toc) { toi.PacketsReceivedSpeech = PacketsReceivedSpeech; } break; + case "connection_packets_received_keepalive": foreach(var toi in toc) { toi.PacketsReceivedKeepalive = PacketsReceivedKeepalive; } break; + case "connection_packets_received_control": foreach(var toi in toc) { toi.PacketsReceivedControl = PacketsReceivedControl; } break; + case "connection_bytes_received_speech": foreach(var toi in toc) { toi.BytesReceivedSpeech = BytesReceivedSpeech; } break; + case "connection_bytes_received_keepalive": foreach(var toi in toc) { toi.BytesReceivedKeepalive = BytesReceivedKeepalive; } break; + case "connection_bytes_received_control": foreach(var toi in toc) { toi.BytesReceivedControl = BytesReceivedControl; } break; + case "connection_server2client_packetloss_speech": foreach(var toi in toc) { toi.ServerToClientPacketlossSpeech = ServerToClientPacketlossSpeech; } break; + case "connection_server2client_packetloss_keepalive": foreach(var toi in toc) { toi.ServerToClientPacketlossKeepalive = ServerToClientPacketlossKeepalive; } break; + case "connection_server2client_packetloss_control": foreach(var toi in toc) { toi.ServerToClientPacketlossControl = ServerToClientPacketlossControl; } break; + case "connection_server2client_packetloss_total": foreach(var toi in toc) { toi.ServerToClientPacketlossTotal = ServerToClientPacketlossTotal; } break; + case "connection_client2server_packetloss_speech": foreach(var toi in toc) { toi.ClientToServerPacketlossSpeech = ClientToServerPacketlossSpeech; } break; + case "connection_client2server_packetloss_keepalive": foreach(var toi in toc) { toi.ClientToServerPacketlossKeepalive = ClientToServerPacketlossKeepalive; } break; + case "connection_client2server_packetloss_control": foreach(var toi in toc) { toi.ClientToServerPacketlossControl = ClientToServerPacketlossControl; } break; + case "connection_client2server_packetloss_total": foreach(var toi in toc) { toi.ClientToServerPacketlossTotal = ClientToServerPacketlossTotal; } break; + case "connection_bandwidth_sent_last_second_speech": foreach(var toi in toc) { toi.BandwidthSentLastSecondSpeech = BandwidthSentLastSecondSpeech; } break; + case "connection_bandwidth_sent_last_second_keepalive": foreach(var toi in toc) { toi.BandwidthSentLastSecondKeepalive = BandwidthSentLastSecondKeepalive; } break; + case "connection_bandwidth_sent_last_second_control": foreach(var toi in toc) { toi.BandwidthSentLastSecondControl = BandwidthSentLastSecondControl; } break; + case "connection_bandwidth_sent_last_minute_speech": foreach(var toi in toc) { toi.BandwidthSentLastMinuteSpeech = BandwidthSentLastMinuteSpeech; } break; + case "connection_bandwidth_sent_last_minute_keepalive": foreach(var toi in toc) { toi.BandwidthSentLastMinuteKeepalive = BandwidthSentLastMinuteKeepalive; } break; + case "connection_bandwidth_sent_last_minute_control": foreach(var toi in toc) { toi.BandwidthSentLastMinuteControl = BandwidthSentLastMinuteControl; } break; + case "connection_bandwidth_received_last_second_speech": foreach(var toi in toc) { toi.BandwidthReceivedLastSecondSpeech = BandwidthReceivedLastSecondSpeech; } break; + case "connection_bandwidth_received_last_second_keepalive": foreach(var toi in toc) { toi.BandwidthReceivedLastSecondKeepalive = BandwidthReceivedLastSecondKeepalive; } break; + case "connection_bandwidth_received_last_second_control": foreach(var toi in toc) { toi.BandwidthReceivedLastSecondControl = BandwidthReceivedLastSecondControl; } break; + case "connection_bandwidth_received_last_minute_speech": foreach(var toi in toc) { toi.BandwidthReceivedLastMinuteSpeech = BandwidthReceivedLastMinuteSpeech; } break; + case "connection_bandwidth_received_last_minute_keepalive": foreach(var toi in toc) { toi.BandwidthReceivedLastMinuteKeepalive = BandwidthReceivedLastMinuteKeepalive; } break; + case "connection_bandwidth_received_last_minute_control": foreach(var toi in toc) { toi.BandwidthReceivedLastMinuteControl = BandwidthReceivedLastMinuteControl; } break; + case "connection_filetransfer_bandwidth_sent": foreach(var toi in toc) { toi.FiletransferBandwidthSent = FiletransferBandwidthSent; } break; + case "connection_filetransfer_bandwidth_received": foreach(var toi in toc) { toi.FiletransferBandwidthReceived = FiletransferBandwidthReceived; } break; + case "connection_idle_time": foreach(var toi in toc) { toi.IdleTime = IdleTime; } break; + } + } + + } } public sealed class ConnectionInfoRequest : INotification @@ -1149,9 +1987,12 @@ public sealed class ConnectionInfoRequest : INotification - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { + } + public void Expand(IMessage[] to, IEnumerable flds) + { } } @@ -1167,22 +2008,40 @@ public sealed class FileDownload : INotification, IResponse public i64 Size { get; set; } public str Message { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clientftfid": ClientFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "serverftfid": ServerFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clientftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ClientFileTransferId = oval; } break; + case "serverftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ServerFileTransferId = oval; } break; case "ftkey": FileTransferKey = Ts3String.Unescape(value); break; - case "port": Port = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "size": Size = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "port": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) Port = oval; } break; + case "size": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) Size = oval; } break; case "msg": Message = Ts3String.Unescape(value); break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileDownload[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clientftfid": foreach(var toi in toc) { toi.ClientFileTransferId = ClientFileTransferId; } break; + case "serverftfid": foreach(var toi in toc) { toi.ServerFileTransferId = ServerFileTransferId; } break; + case "ftkey": foreach(var toi in toc) { toi.FileTransferKey = FileTransferKey; } break; + case "port": foreach(var toi in toc) { toi.Port = Port; } break; + case "size": foreach(var toi in toc) { toi.Size = Size; } break; + case "msg": foreach(var toi in toc) { toi.Message = Message; } break; + } + } + + } } public sealed class FileInfoTs : INotification, IResponse @@ -1196,21 +2055,38 @@ public sealed class FileInfoTs : INotification, IResponse public i64 Size { get; set; } public DateTime DateTime { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; case "path": Path = Ts3String.Unescape(value); break; case "name": Name = Ts3String.Unescape(value); break; - case "size": Size = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "datetime": DateTime = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "size": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) Size = oval; } break; + case "datetime": { if(Utf8Parser.TryParse(value, out double oval, out _)) DateTime = Util.UnixTimeStart.AddSeconds(oval); } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileInfoTs[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "path": foreach(var toi in toc) { toi.Path = Path; } break; + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "size": foreach(var toi in toc) { toi.Size = Size; } break; + case "datetime": foreach(var toi in toc) { toi.DateTime = DateTime; } break; + } + } + + } } public sealed class FileList : INotification, IResponse @@ -1225,22 +2101,40 @@ public sealed class FileList : INotification, IResponse public DateTime DateTime { get; set; } public bool IsFile { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; case "path": Path = Ts3String.Unescape(value); break; case "name": Name = Ts3String.Unescape(value); break; - case "size": Size = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "datetime": DateTime = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "size": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) Size = oval; } break; + case "datetime": { if(Utf8Parser.TryParse(value, out double oval, out _)) DateTime = Util.UnixTimeStart.AddSeconds(oval); } break; case "type": IsFile = value.Length > 0 && value[0] != '0'; break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileList[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "path": foreach(var toi in toc) { toi.Path = Path; } break; + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "size": foreach(var toi in toc) { toi.Size = Size; } break; + case "datetime": foreach(var toi in toc) { toi.DateTime = DateTime; } break; + case "type": foreach(var toi in toc) { toi.IsFile = IsFile; } break; + } + } + + } } public sealed class FileListFinished : INotification @@ -1251,18 +2145,32 @@ public sealed class FileListFinished : INotification public ChannelId ChannelId { get; set; } public str Path { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "cid": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "cid": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; case "path": Path = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileListFinished[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cid": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "path": foreach(var toi in toc) { toi.Path = Path; } break; + } + } + + } } public sealed class FileTransfer : INotification, IResponse @@ -1283,28 +2191,52 @@ public sealed class FileTransfer : INotification, IResponse public f32 AverageSpeed { get; set; } public DurationSeconds Runtime { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; case "path": Path = Ts3String.Unescape(value); break; case "name": Name = Ts3String.Unescape(value); break; - case "size": Size = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "sizedone": SizeDone = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "clientftfid": ClientFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "serverftfid": ServerFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "sender": Sender = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "status": Status = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "current_speed": CurrentSpeed = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "average_speed": AverageSpeed = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "runtime": Runtime = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "size": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) Size = oval; } break; + case "sizedone": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) SizeDone = oval; } break; + case "clientftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ClientFileTransferId = oval; } break; + case "serverftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ServerFileTransferId = oval; } break; + case "sender": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) Sender = oval; } break; + case "status": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Status = oval; } break; + case "current_speed": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) CurrentSpeed = oval; } break; + case "average_speed": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) AverageSpeed = oval; } break; + case "runtime": { if(Utf8Parser.TryParse(value, out double oval, out _)) Runtime = TimeSpan.FromSeconds(oval); } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileTransfer[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "path": foreach(var toi in toc) { toi.Path = Path; } break; + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "size": foreach(var toi in toc) { toi.Size = Size; } break; + case "sizedone": foreach(var toi in toc) { toi.SizeDone = SizeDone; } break; + case "clientftfid": foreach(var toi in toc) { toi.ClientFileTransferId = ClientFileTransferId; } break; + case "serverftfid": foreach(var toi in toc) { toi.ServerFileTransferId = ServerFileTransferId; } break; + case "sender": foreach(var toi in toc) { toi.Sender = Sender; } break; + case "status": foreach(var toi in toc) { toi.Status = Status; } break; + case "current_speed": foreach(var toi in toc) { toi.CurrentSpeed = CurrentSpeed; } break; + case "average_speed": foreach(var toi in toc) { toi.AverageSpeed = AverageSpeed; } break; + case "runtime": foreach(var toi in toc) { toi.Runtime = Runtime; } break; + } + } + + } } public sealed class FileTransferStatus : INotification @@ -1317,20 +2249,36 @@ public sealed class FileTransferStatus : INotification public str Message { get; set; } public i64 Size { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clientftfid": ClientFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "status": Status = (Ts3ErrorCode)u32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clientftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ClientFileTransferId = oval; } break; + case "status": { if(Utf8Parser.TryParse(value, out u32 oval, out _)) Status = (Ts3ErrorCode)oval; } break; case "msg": Message = Ts3String.Unescape(value); break; - case "size": Size = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "size": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) Size = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileTransferStatus[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clientftfid": foreach(var toi in toc) { toi.ClientFileTransferId = ClientFileTransferId; } break; + case "status": foreach(var toi in toc) { toi.Status = Status; } break; + case "msg": foreach(var toi in toc) { toi.Message = Message; } break; + case "size": foreach(var toi in toc) { toi.Size = Size; } break; + } + } + + } } public sealed class FileUpload : INotification, IResponse @@ -1345,22 +2293,106 @@ public sealed class FileUpload : INotification, IResponse public i64 SeekPosistion { get; set; } public str Message { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "clientftfid": ClientFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "serverftfid": ServerFileTransferId = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clientftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ClientFileTransferId = oval; } break; + case "serverftfid": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ServerFileTransferId = oval; } break; case "ftkey": FileTransferKey = Ts3String.Unescape(value); break; - case "port": Port = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "seekpos": SeekPosistion = i64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "port": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) Port = oval; } break; + case "seekpos": { if(Utf8Parser.TryParse(value, out i64 oval, out _)) SeekPosistion = oval; } break; case "msg": Message = Ts3String.Unescape(value); break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (FileUpload[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "clientftfid": foreach(var toi in toc) { toi.ClientFileTransferId = ClientFileTransferId; } break; + case "serverftfid": foreach(var toi in toc) { toi.ServerFileTransferId = ServerFileTransferId; } break; + case "ftkey": foreach(var toi in toc) { toi.FileTransferKey = FileTransferKey; } break; + case "port": foreach(var toi in toc) { toi.Port = Port; } break; + case "seekpos": foreach(var toi in toc) { toi.SeekPosistion = SeekPosistion; } break; + case "msg": foreach(var toi in toc) { toi.Message = Message; } break; + } + } + + } + } + + public sealed class GetClientDbIdFromUid : INotification + { + public NotificationType NotifyType { get; } = NotificationType.GetClientDbIdFromUid; + + + public Uid ClientUid { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "cluid": ClientUid = Ts3String.Unescape(value); break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (GetClientDbIdFromUid[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + } + } + + } + } + + public sealed class GetClientIds : INotification + { + public NotificationType NotifyType { get; } = NotificationType.GetClientIds; + + + public Uid ClientUid { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "cluid": ClientUid = Ts3String.Unescape(value); break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (GetClientIds[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + } + } + + } } public sealed class InitIvExpand : INotification @@ -1372,9 +2404,8 @@ public sealed class InitIvExpand : INotification public str Beta { get; set; } public str Omega { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { @@ -1385,6 +2416,22 @@ public void SetField(string name, ReadOnlySpan value) } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (InitIvExpand[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "alpha": foreach(var toi in toc) { toi.Alpha = Alpha; } break; + case "beta": foreach(var toi in toc) { toi.Beta = Beta; } break; + case "omega": foreach(var toi in toc) { toi.Omega = Omega; } break; + } + } + + } } public sealed class InitIvExpand2 : INotification @@ -1399,9 +2446,8 @@ public sealed class InitIvExpand2 : INotification public str Proof { get; set; } public str Tvd { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { @@ -1415,6 +2461,25 @@ public void SetField(string name, ReadOnlySpan value) } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (InitIvExpand2[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "l": foreach(var toi in toc) { toi.License = License; } break; + case "beta": foreach(var toi in toc) { toi.Beta = Beta; } break; + case "omega": foreach(var toi in toc) { toi.Omega = Omega; } break; + case "ot": foreach(var toi in toc) { toi.Ot = Ot; } break; + case "proof": foreach(var toi in toc) { toi.Proof = Proof; } break; + case "tvd": foreach(var toi in toc) { toi.Tvd = Tvd; } break; + } + } + + } } public sealed class InitServer : INotification @@ -1431,7 +2496,7 @@ public sealed class InitServer : INotification public HostMessageMode HostmessageMode { get; set; } public u64 VirtualServerId { get; set; } public str[] ServerIp { get; set; } - public bool AskForPrivilege { get; set; } + public bool AskForPrivilegekey { get; set; } public str ClientName { get; set; } public ClientId ClientId { get; set; } public u16 ProtocolVersion { get; set; } @@ -1454,47 +2519,90 @@ public sealed class InitServer : INotification public HostBannerMode HostbannerMode { get; set; } public DurationSeconds TempChannelDefaultDeleteDelay { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { case "virtualserver_welcomemessage": WelcomeMessage = Ts3String.Unescape(value); break; case "virtualserver_platform": ServerPlatform = Ts3String.Unescape(value); break; case "virtualserver_version": ServerVersion = Ts3String.Unescape(value); break; - case "virtualserver_maxclients": MaxClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_created": ServerCreated = Util.UnixTimeStart.AddSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "virtualserver_maxclients": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) MaxClients = oval; } break; + case "virtualserver_created": { if(Utf8Parser.TryParse(value, out double oval, out _)) ServerCreated = Util.UnixTimeStart.AddSeconds(oval); } break; case "virtualserver_hostmessage": Hostmessage = Ts3String.Unescape(value); break; - case "virtualserver_hostmessage_mode": { if (!Enum.TryParse(value.NewString(), out HostMessageMode val)) throw new FormatException(); HostmessageMode = val; } break; - case "virtualserver_id": VirtualServerId = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_ip": { if(value.Length == 0) ServerIp = Array.Empty(); else { var ss = new SpanSplitter(); ss.First(value, ','); int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++; ServerIp = new str[cnt + 1]; for(int i = 0; i < cnt + 1; i++) { ServerIp[i] = Ts3String.Unescape(ss.Trim(value)); if (i < cnt) value = ss.Next(value); } } } break; - case "virtualserver_ask_for_privilegekey": AskForPrivilege = value.Length > 0 && value[0] != '0'; break; + case "virtualserver_hostmessage_mode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) HostmessageMode = (HostMessageMode)oval; } break; + case "virtualserver_id": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) VirtualServerId = oval; } break; + case "virtualserver_ip": { if(value.Length == 0) ServerIp = Array.Empty(); else { var ss = new SpanSplitter(); ss.First(value, (byte)','); int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++; ServerIp = new str[cnt + 1]; for(int i = 0; i < cnt + 1; i++) { ServerIp[i] = Ts3String.Unescape(ss.Trim(value)); if (i < cnt) value = ss.Next(value); } } } break; + case "virtualserver_ask_for_privilegekey": AskForPrivilegekey = value.Length > 0 && value[0] != '0'; break; case "acn": ClientName = Ts3String.Unescape(value); break; - case "aclid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "pv": ProtocolVersion = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "lt": LicenseType = (LicenseType)u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_talk_power": TalkPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_needed_serverquery_view_power": NeededServerqueryViewPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "aclid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "pv": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) ProtocolVersion = oval; } break; + case "lt": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) LicenseType = (LicenseType)oval; } break; + case "client_talk_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) TalkPower = oval; } break; + case "client_needed_serverquery_view_power": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededServerqueryViewPower = oval; } break; case "virtualserver_name": Name = Ts3String.Unescape(value); break; - case "virtualserver_codec_encryption_mode": { if (!Enum.TryParse(value.NewString(), out CodecEncryptionMode val)) throw new FormatException(); CodecEncryptionMode = val; } break; - case "virtualserver_default_server_group": DefaultServerGroup = ServerGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_default_channel_group": DefaultChannelGroup = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_codec_encryption_mode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) CodecEncryptionMode = (CodecEncryptionMode)oval; } break; + case "virtualserver_default_server_group": { if(Utf8Parser.TryParse(value, out ServerGroupId oval, out _)) DefaultServerGroup = oval; } break; + case "virtualserver_default_channel_group": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) DefaultChannelGroup = oval; } break; case "virtualserver_hostbanner_url": HostbannerUrl = Ts3String.Unescape(value); break; case "virtualserver_hostbanner_gfx_url": HostbannerGfxUrl = Ts3String.Unescape(value); break; - case "virtualserver_hostbanner_gfx_interval": HostbannerGfxInterval = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "virtualserver_priority_speaker_dimm_modificator": PrioritySpeakerDimmModificator = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_hostbanner_gfx_interval": { if(Utf8Parser.TryParse(value, out double oval, out _)) HostbannerGfxInterval = TimeSpan.FromSeconds(oval); } break; + case "virtualserver_priority_speaker_dimm_modificator": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) PrioritySpeakerDimmModificator = oval; } break; case "virtualserver_hostbutton_tooltip": HostbuttonTooltip = Ts3String.Unescape(value); break; case "virtualserver_hostbutton_url": HostbuttonUrl = Ts3String.Unescape(value); break; case "virtualserver_hostbutton_gfx_url": HostbuttonGfxUrl = Ts3String.Unescape(value); break; case "virtualserver_name_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "virtualserver_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "virtualserver_hostbanner_mode": { if (!Enum.TryParse(value.NewString(), out HostBannerMode val)) throw new FormatException(); HostbannerMode = val; } break; - case "virtualserver_channel_temp_delete_delay_default": TempChannelDefaultDeleteDelay = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "virtualserver_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; + case "virtualserver_hostbanner_mode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) HostbannerMode = (HostBannerMode)oval; } break; + case "virtualserver_channel_temp_delete_delay_default": { if(Utf8Parser.TryParse(value, out double oval, out _)) TempChannelDefaultDeleteDelay = TimeSpan.FromSeconds(oval); } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (InitServer[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "virtualserver_welcomemessage": foreach(var toi in toc) { toi.WelcomeMessage = WelcomeMessage; } break; + case "virtualserver_platform": foreach(var toi in toc) { toi.ServerPlatform = ServerPlatform; } break; + case "virtualserver_version": foreach(var toi in toc) { toi.ServerVersion = ServerVersion; } break; + case "virtualserver_maxclients": foreach(var toi in toc) { toi.MaxClients = MaxClients; } break; + case "virtualserver_created": foreach(var toi in toc) { toi.ServerCreated = ServerCreated; } break; + case "virtualserver_hostmessage": foreach(var toi in toc) { toi.Hostmessage = Hostmessage; } break; + case "virtualserver_hostmessage_mode": foreach(var toi in toc) { toi.HostmessageMode = HostmessageMode; } break; + case "virtualserver_id": foreach(var toi in toc) { toi.VirtualServerId = VirtualServerId; } break; + case "virtualserver_ip": foreach(var toi in toc) { toi.ServerIp = ServerIp; } break; + case "virtualserver_ask_for_privilegekey": foreach(var toi in toc) { toi.AskForPrivilegekey = AskForPrivilegekey; } break; + case "acn": foreach(var toi in toc) { toi.ClientName = ClientName; } break; + case "aclid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "pv": foreach(var toi in toc) { toi.ProtocolVersion = ProtocolVersion; } break; + case "lt": foreach(var toi in toc) { toi.LicenseType = LicenseType; } break; + case "client_talk_power": foreach(var toi in toc) { toi.TalkPower = TalkPower; } break; + case "client_needed_serverquery_view_power": foreach(var toi in toc) { toi.NeededServerqueryViewPower = NeededServerqueryViewPower; } break; + case "virtualserver_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "virtualserver_codec_encryption_mode": foreach(var toi in toc) { toi.CodecEncryptionMode = CodecEncryptionMode; } break; + case "virtualserver_default_server_group": foreach(var toi in toc) { toi.DefaultServerGroup = DefaultServerGroup; } break; + case "virtualserver_default_channel_group": foreach(var toi in toc) { toi.DefaultChannelGroup = DefaultChannelGroup; } break; + case "virtualserver_hostbanner_url": foreach(var toi in toc) { toi.HostbannerUrl = HostbannerUrl; } break; + case "virtualserver_hostbanner_gfx_url": foreach(var toi in toc) { toi.HostbannerGfxUrl = HostbannerGfxUrl; } break; + case "virtualserver_hostbanner_gfx_interval": foreach(var toi in toc) { toi.HostbannerGfxInterval = HostbannerGfxInterval; } break; + case "virtualserver_priority_speaker_dimm_modificator": foreach(var toi in toc) { toi.PrioritySpeakerDimmModificator = PrioritySpeakerDimmModificator; } break; + case "virtualserver_hostbutton_tooltip": foreach(var toi in toc) { toi.HostbuttonTooltip = HostbuttonTooltip; } break; + case "virtualserver_hostbutton_url": foreach(var toi in toc) { toi.HostbuttonUrl = HostbuttonUrl; } break; + case "virtualserver_hostbutton_gfx_url": foreach(var toi in toc) { toi.HostbuttonGfxUrl = HostbuttonGfxUrl; } break; + case "virtualserver_name_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "virtualserver_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "virtualserver_hostbanner_mode": foreach(var toi in toc) { toi.HostbannerMode = HostbannerMode; } break; + case "virtualserver_channel_temp_delete_delay_default": foreach(var toi in toc) { toi.TempChannelDefaultDeleteDelay = TempChannelDefaultDeleteDelay; } break; + } + } + + } } public sealed class PluginCommand : INotification @@ -1505,18 +2613,71 @@ public sealed class PluginCommand : INotification public str Name { get; set; } public str Data { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) + { + switch(name) + { + + case "name": Name = Ts3String.Unescape(value); break; + case "data": Data = Ts3String.Unescape(value); break; + + } + + } + + public void Expand(IMessage[] to, IEnumerable flds) { + var toc = (PluginCommand[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "data": foreach(var toi in toc) { toi.Data = Data; } break; + } + } + + } + } + public sealed class PluginCommandRequest : INotification + { + public NotificationType NotifyType { get; } = NotificationType.PluginCommandRequest; + + + public str Name { get; set; } + public str Data { get; set; } + public i32 Target { get; set; } + + public void SetField(string name, ReadOnlySpan value) + { switch(name) { case "name": Name = Ts3String.Unescape(value); break; case "data": Data = Ts3String.Unescape(value); break; + case "targetmode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Target = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (PluginCommandRequest[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "data": foreach(var toi in toc) { toi.Data = Data; } break; + case "targetmode": foreach(var toi in toc) { toi.Target = Target; } break; + } + } + + } } public sealed class ServerData : IResponse @@ -1536,27 +2697,50 @@ public sealed class ServerData : IResponse public u16 VirtualServerPort { get; set; } public str VirtualServerStatus { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "virtualserver_clientsonline": ClientsOnline = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_queryclientsonline": QueriesOnline = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_maxclients": MaxClients = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_uptime": Uptime = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "virtualserver_clientsonline": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) ClientsOnline = oval; } break; + case "virtualserver_queryclientsonline": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) QueriesOnline = oval; } break; + case "virtualserver_maxclients": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) MaxClients = oval; } break; + case "virtualserver_uptime": { if(Utf8Parser.TryParse(value, out double oval, out _)) Uptime = TimeSpan.FromSeconds(oval); } break; case "virtualserver_autostart": Autostart = value.Length > 0 && value[0] != '0'; break; case "virtualserver_machine_id": MachineId = Ts3String.Unescape(value); break; case "virtualserver_name": Name = Ts3String.Unescape(value); break; - case "virtualserver_id": VirtualServerId = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_id": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) VirtualServerId = oval; } break; case "virtualserver_unique_identifier": VirtualServerUid = Ts3String.Unescape(value); break; - case "virtualserver_port": VirtualServerPort = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_port": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) VirtualServerPort = oval; } break; case "virtualserver_status": VirtualServerStatus = Ts3String.Unescape(value); break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ServerData[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "virtualserver_clientsonline": foreach(var toi in toc) { toi.ClientsOnline = ClientsOnline; } break; + case "virtualserver_queryclientsonline": foreach(var toi in toc) { toi.QueriesOnline = QueriesOnline; } break; + case "virtualserver_maxclients": foreach(var toi in toc) { toi.MaxClients = MaxClients; } break; + case "virtualserver_uptime": foreach(var toi in toc) { toi.Uptime = Uptime; } break; + case "virtualserver_autostart": foreach(var toi in toc) { toi.Autostart = Autostart; } break; + case "virtualserver_machine_id": foreach(var toi in toc) { toi.MachineId = MachineId; } break; + case "virtualserver_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "virtualserver_id": foreach(var toi in toc) { toi.VirtualServerId = VirtualServerId; } break; + case "virtualserver_unique_identifier": foreach(var toi in toc) { toi.VirtualServerUid = VirtualServerUid; } break; + case "virtualserver_port": foreach(var toi in toc) { toi.VirtualServerPort = VirtualServerPort; } break; + case "virtualserver_status": foreach(var toi in toc) { toi.VirtualServerStatus = VirtualServerStatus; } break; + } + } + + } } public sealed class ServerEdited : INotification @@ -1584,35 +2768,66 @@ public sealed class ServerEdited : INotification public HostBannerMode HostbannerMode { get; set; } public DurationSeconds TempChannelDefaultDeleteDelay { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; - case "reasonid": { if (!Enum.TryParse(value.NewString(), out Reason val)) throw new FormatException(); Reason = val; } break; + case "reasonid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Reason = (Reason)oval; } break; case "virtualserver_name": Name = Ts3String.Unescape(value); break; - case "virtualserver_codec_encryption_mode": { if (!Enum.TryParse(value.NewString(), out CodecEncryptionMode val)) throw new FormatException(); CodecEncryptionMode = val; } break; - case "virtualserver_default_server_group": DefaultServerGroup = ServerGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_default_channel_group": DefaultChannelGroup = ChannelGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_codec_encryption_mode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) CodecEncryptionMode = (CodecEncryptionMode)oval; } break; + case "virtualserver_default_server_group": { if(Utf8Parser.TryParse(value, out ServerGroupId oval, out _)) DefaultServerGroup = oval; } break; + case "virtualserver_default_channel_group": { if(Utf8Parser.TryParse(value, out ChannelGroupId oval, out _)) DefaultChannelGroup = oval; } break; case "virtualserver_hostbanner_url": HostbannerUrl = Ts3String.Unescape(value); break; case "virtualserver_hostbanner_gfx_url": HostbannerGfxUrl = Ts3String.Unescape(value); break; - case "virtualserver_hostbanner_gfx_interval": HostbannerGfxInterval = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "virtualserver_priority_speaker_dimm_modificator": PrioritySpeakerDimmModificator = f32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_hostbanner_gfx_interval": { if(Utf8Parser.TryParse(value, out double oval, out _)) HostbannerGfxInterval = TimeSpan.FromSeconds(oval); } break; + case "virtualserver_priority_speaker_dimm_modificator": { if(Utf8Parser.TryParse(value, out f32 oval, out _)) PrioritySpeakerDimmModificator = oval; } break; case "virtualserver_hostbutton_tooltip": HostbuttonTooltip = Ts3String.Unescape(value); break; case "virtualserver_hostbutton_url": HostbuttonUrl = Ts3String.Unescape(value); break; case "virtualserver_hostbutton_gfx_url": HostbuttonGfxUrl = Ts3String.Unescape(value); break; case "virtualserver_name_phonetic": PhoneticName = Ts3String.Unescape(value); break; - case "virtualserver_icon_id": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; - case "virtualserver_hostbanner_mode": { if (!Enum.TryParse(value.NewString(), out HostBannerMode val)) throw new FormatException(); HostbannerMode = val; } break; - case "virtualserver_channel_temp_delete_delay_default": TempChannelDefaultDeleteDelay = TimeSpan.FromSeconds(double.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "virtualserver_icon_id": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; + case "virtualserver_hostbanner_mode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) HostbannerMode = (HostBannerMode)oval; } break; + case "virtualserver_channel_temp_delete_delay_default": { if(Utf8Parser.TryParse(value, out double oval, out _)) TempChannelDefaultDeleteDelay = TimeSpan.FromSeconds(oval); } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ServerEdited[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + case "reasonid": foreach(var toi in toc) { toi.Reason = Reason; } break; + case "virtualserver_name": foreach(var toi in toc) { toi.Name = Name; } break; + case "virtualserver_codec_encryption_mode": foreach(var toi in toc) { toi.CodecEncryptionMode = CodecEncryptionMode; } break; + case "virtualserver_default_server_group": foreach(var toi in toc) { toi.DefaultServerGroup = DefaultServerGroup; } break; + case "virtualserver_default_channel_group": foreach(var toi in toc) { toi.DefaultChannelGroup = DefaultChannelGroup; } break; + case "virtualserver_hostbanner_url": foreach(var toi in toc) { toi.HostbannerUrl = HostbannerUrl; } break; + case "virtualserver_hostbanner_gfx_url": foreach(var toi in toc) { toi.HostbannerGfxUrl = HostbannerGfxUrl; } break; + case "virtualserver_hostbanner_gfx_interval": foreach(var toi in toc) { toi.HostbannerGfxInterval = HostbannerGfxInterval; } break; + case "virtualserver_priority_speaker_dimm_modificator": foreach(var toi in toc) { toi.PrioritySpeakerDimmModificator = PrioritySpeakerDimmModificator; } break; + case "virtualserver_hostbutton_tooltip": foreach(var toi in toc) { toi.HostbuttonTooltip = HostbuttonTooltip; } break; + case "virtualserver_hostbutton_url": foreach(var toi in toc) { toi.HostbuttonUrl = HostbuttonUrl; } break; + case "virtualserver_hostbutton_gfx_url": foreach(var toi in toc) { toi.HostbuttonGfxUrl = HostbuttonGfxUrl; } break; + case "virtualserver_name_phonetic": foreach(var toi in toc) { toi.PhoneticName = PhoneticName; } break; + case "virtualserver_icon_id": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "virtualserver_hostbanner_mode": foreach(var toi in toc) { toi.HostbannerMode = HostbannerMode; } break; + case "virtualserver_channel_temp_delete_delay_default": foreach(var toi in toc) { toi.TempChannelDefaultDeleteDelay = TempChannelDefaultDeleteDelay; } break; + } + } + + } } public sealed class ServerGroupAddResponse : IResponse @@ -1622,17 +2837,30 @@ public sealed class ServerGroupAddResponse : IResponse public ServerGroupId ServerGroupId { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "sgid": ServerGroupId = ServerGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "sgid": { if(Utf8Parser.TryParse(value, out ServerGroupId oval, out _)) ServerGroupId = oval; } break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ServerGroupAddResponse[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "sgid": foreach(var toi in toc) { toi.ServerGroupId = ServerGroupId; } break; + } + } + + } } public sealed class ServerGroupList : INotification @@ -1651,26 +2879,48 @@ public sealed class ServerGroupList : INotification public i32 NeededMemberAddPower { get; set; } public i32 NeededMemberRemovePower { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "sgid": ServerGroupId = ServerGroupId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "sgid": { if(Utf8Parser.TryParse(value, out ServerGroupId oval, out _)) ServerGroupId = oval; } break; case "name": Name = Ts3String.Unescape(value); break; - case "type": { if (!Enum.TryParse(value.NewString(), out GroupType val)) throw new FormatException(); GroupType = val; } break; - case "iconid": IconId = unchecked((int)long.Parse(value.NewString(), CultureInfo.InvariantCulture)); break; + case "type": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) GroupType = (GroupType)oval; } break; + case "iconid": { if(Utf8Parser.TryParse(value, out long oval, out _)) IconId = unchecked((int)oval); } break; case "savedb": IsPermanent = value.Length > 0 && value[0] != '0'; break; - case "sortid": SortId = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "namemode": { if (!Enum.TryParse(value.NewString(), out GroupNamingMode val)) throw new FormatException(); NamingMode = val; } break; - case "n_modifyp": NeededModifyPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "n_member_addp": NeededMemberAddPower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "n_member_remove_p": NeededMemberRemovePower = i32.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "sortid": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) SortId = oval; } break; + case "namemode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NamingMode = (GroupNamingMode)oval; } break; + case "n_modifyp": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededModifyPower = oval; } break; + case "n_member_addp": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededMemberAddPower = oval; } break; + case "n_member_remove_p": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) NeededMemberRemovePower = oval; } break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (ServerGroupList[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "sgid": foreach(var toi in toc) { toi.ServerGroupId = ServerGroupId; } break; + case "name": foreach(var toi in toc) { toi.Name = Name; } break; + case "type": foreach(var toi in toc) { toi.GroupType = GroupType; } break; + case "iconid": foreach(var toi in toc) { toi.IconId = IconId; } break; + case "savedb": foreach(var toi in toc) { toi.IsPermanent = IsPermanent; } break; + case "sortid": foreach(var toi in toc) { toi.SortId = SortId; } break; + case "namemode": foreach(var toi in toc) { toi.NamingMode = NamingMode; } break; + case "n_modifyp": foreach(var toi in toc) { toi.NeededModifyPower = NeededModifyPower; } break; + case "n_member_addp": foreach(var toi in toc) { toi.NeededMemberAddPower = NeededMemberAddPower; } break; + case "n_member_remove_p": foreach(var toi in toc) { toi.NeededMemberRemovePower = NeededMemberRemovePower; } break; + } + } + + } } public sealed class TextMessage : INotification @@ -1685,22 +2935,40 @@ public sealed class TextMessage : INotification public str InvokerName { get; set; } public Uid InvokerUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "targetmode": { if (!Enum.TryParse(value.NewString(), out TextMessageTargetMode val)) throw new FormatException(); Target = val; } break; + case "targetmode": { if(Utf8Parser.TryParse(value, out i32 oval, out _)) Target = (TextMessageTargetMode)oval; } break; case "msg": Message = Ts3String.Unescape(value); break; - case "target": TargetClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "invokerid": InvokerId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "target": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) TargetClientId = oval; } break; + case "invokerid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) InvokerId = oval; } break; case "invokername": InvokerName = Ts3String.Unescape(value); break; case "invokeruid": InvokerUid = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (TextMessage[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "targetmode": foreach(var toi in toc) { toi.Target = Target; } break; + case "msg": foreach(var toi in toc) { toi.Message = Message; } break; + case "target": foreach(var toi in toc) { toi.TargetClientId = TargetClientId; } break; + case "invokerid": foreach(var toi in toc) { toi.InvokerId = InvokerId; } break; + case "invokername": foreach(var toi in toc) { toi.InvokerName = InvokerName; } break; + case "invokeruid": foreach(var toi in toc) { toi.InvokerUid = InvokerUid; } break; + } + } + + } } public sealed class TokenUsed : INotification @@ -1716,9 +2984,8 @@ public sealed class TokenUsed : INotification public ClientDbId ClientDbId { get; set; } public Uid ClientUid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { @@ -1726,13 +2993,33 @@ public void SetField(string name, ReadOnlySpan value) case "tokencustomset": TokenCustomSet = Ts3String.Unescape(value); break; case "token1": Token1 = Ts3String.Unescape(value); break; case "token2": Token2 = Ts3String.Unescape(value); break; - case "clid": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "cldbid": ClientDbId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "clid": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "cldbid": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) ClientDbId = oval; } break; case "cluid": ClientUid = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (TokenUsed[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "token": foreach(var toi in toc) { toi.UsedToken = UsedToken; } break; + case "tokencustomset": foreach(var toi in toc) { toi.TokenCustomSet = TokenCustomSet; } break; + case "token1": foreach(var toi in toc) { toi.Token1 = Token1; } break; + case "token2": foreach(var toi in toc) { toi.Token2 = Token2; } break; + case "clid": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "cldbid": foreach(var toi in toc) { toi.ClientDbId = ClientDbId; } break; + case "cluid": foreach(var toi in toc) { toi.ClientUid = ClientUid; } break; + } + } + + } } public sealed class WhoAmI : IResponse @@ -1752,68 +3039,144 @@ public sealed class WhoAmI : IResponse public str VirtualServerStatus { get; set; } public Uid Uid { get; set; } - public void SetField(string name, ReadOnlySpan value) + public void SetField(string name, ReadOnlySpan value) { - switch(name) { - case "client_id": ClientId = ClientId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "client_channel_id": ChannelId = ChannelId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_id": { if(Utf8Parser.TryParse(value, out ClientId oval, out _)) ClientId = oval; } break; + case "client_channel_id": { if(Utf8Parser.TryParse(value, out ChannelId oval, out _)) ChannelId = oval; } break; case "client_nickname": Name = Ts3String.Unescape(value); break; - case "client_database_id": DatabaseId = ClientDbId.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_database_id": { if(Utf8Parser.TryParse(value, out ClientDbId oval, out _)) DatabaseId = oval; } break; case "client_login_name": LoginName = Ts3String.Unescape(value); break; - case "client_origin_server_id": OriginServerId = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; - case "virtualserver_id": VirtualServerId = u64.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "client_origin_server_id": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) OriginServerId = oval; } break; + case "virtualserver_id": { if(Utf8Parser.TryParse(value, out u64 oval, out _)) VirtualServerId = oval; } break; case "virtualserver_unique_identifier": VirtualServerUid = Ts3String.Unescape(value); break; - case "virtualserver_port": VirtualServerPort = u16.Parse(value.NewString(), CultureInfo.InvariantCulture); break; + case "virtualserver_port": { if(Utf8Parser.TryParse(value, out u16 oval, out _)) VirtualServerPort = oval; } break; case "virtualserver_status": VirtualServerStatus = Ts3String.Unescape(value); break; case "client_unique_identifier": Uid = Ts3String.Unescape(value); break; case "return_code": ReturnCode = Ts3String.Unescape(value); break; } } + + public void Expand(IMessage[] to, IEnumerable flds) + { + var toc = (WhoAmI[])to; + foreach (var fld in flds) + { + switch(fld) + { + + case "client_id": foreach(var toi in toc) { toi.ClientId = ClientId; } break; + case "client_channel_id": foreach(var toi in toc) { toi.ChannelId = ChannelId; } break; + case "client_nickname": foreach(var toi in toc) { toi.Name = Name; } break; + case "client_database_id": foreach(var toi in toc) { toi.DatabaseId = DatabaseId; } break; + case "client_login_name": foreach(var toi in toc) { toi.LoginName = LoginName; } break; + case "client_origin_server_id": foreach(var toi in toc) { toi.OriginServerId = OriginServerId; } break; + case "virtualserver_id": foreach(var toi in toc) { toi.VirtualServerId = VirtualServerId; } break; + case "virtualserver_unique_identifier": foreach(var toi in toc) { toi.VirtualServerUid = VirtualServerUid; } break; + case "virtualserver_port": foreach(var toi in toc) { toi.VirtualServerPort = VirtualServerPort; } break; + case "virtualserver_status": foreach(var toi in toc) { toi.VirtualServerStatus = VirtualServerStatus; } break; + case "client_unique_identifier": foreach(var toi in toc) { toi.Uid = Uid; } break; + } + } + + } } public enum NotificationType { Unknown, + ///[S2C] ntfy:notifychannelchanged ChannelChanged, + ///[S2C] ntfy:notifychannelcreated ChannelCreated, + ///[S2C] ntfy:notifychanneldeleted ChannelDeleted, + ///[S2C] ntfy:notifychanneledited ChannelEdited, + ///[S2C] ntfy:notifychannelgrouplist ChannelGroupList, + ///[S2C] ntfy:channellist ChannelList, + ///[S2C] ntfy:channellistfinished ChannelListFinished, + ///[S2C] ntfy:notifychannelmoved ChannelMoved, + ///[S2C] ntfy:notifychannelpasswordchanged ChannelPasswordChanged, + ///[S2C] ntfy:notifychannelsubscribed ChannelSubscribed, + ///[S2C] ntfy:notifychannelunsubscribed ChannelUnsubscribed, + ///[S2C] ntfy:notifyclientchannelgroupchanged ClientChannelGroupChanged, + ///[S2C] ntfy:notifyclientchatcomposing ClientChatComposing, + ///[S2C] ntfy:notifyclientdbidfromuid + ClientDbIdFromUid, + ///[S2C] ntfy:notifycliententerview ClientEnterView, + ///[S2C] ntfy:notifyclientids + ClientIds, + ///[C2S] ntfy:clientinit + ClientInit, + ///[C2S] ntfy:clientinitiv + ClientInitIv, + ///[S2C] ntfy:notifyclientleftview ClientLeftView, + ///[S2C] ntfy:notifyclientmoved ClientMoved, + ///[S2C] ntfy:notifyclientneededpermissions ClientNeededPermissions, + ///[S2C] ntfy:notifyclientpoke + ClientPoke, + ///[S2C] ntfy:notifyservergroupsbyclientid ClientServerGroup, + ///[S2C] ntfy:notifyservergroupclientadded ClientServerGroupAdded, + ///[S2C] ntfy:error CommandError, + ///[S2C] ntfy:notifyconnectioninfo ConnectionInfo, + ///[S2C] ntfy:notifyconnectioninforequest ConnectionInfoRequest, + ///[S2C] ntfy:notifystartdownload FileDownload, + ///[S2C] ntfy:notifyfileinfo FileInfoTs, + ///[S2C] ntfy:notifyfilelist FileList, + ///[S2C] ntfy:notifyfilelistfinished FileListFinished, + ///[S2C] ntfy:notifyfiletransferlist FileTransfer, + ///[S2C] ntfy:notifystatusfiletransfer FileTransferStatus, + ///[S2C] ntfy:notifystartupload FileUpload, + ///[C2S] ntfy:clientgetdbidfromuid + GetClientDbIdFromUid, + ///[C2S] ntfy:clientgetids + GetClientIds, + ///[S2C] ntfy:initivexpand InitIvExpand, + ///[S2C] ntfy:initivexpand2 InitIvExpand2, + ///[S2C] ntfy:initserver InitServer, + ///[S2C] ntfy:notifyplugincmd PluginCommand, + ///[C2S] ntfy:plugincmd + PluginCommandRequest, + ///[S2C] ntfy:notifyserveredited ServerEdited, + ///[S2C] ntfy:notifyservergrouplist ServerGroupList, + ///[S2C] ntfy:notifytextmessage TextMessage, + ///[S2C] ntfy:notifytokenused TokenUsed, } @@ -1836,10 +3199,15 @@ public static NotificationType GetNotificationType(string name) case "notifychannelunsubscribed": return NotificationType.ChannelUnsubscribed; case "notifyclientchannelgroupchanged": return NotificationType.ClientChannelGroupChanged; case "notifyclientchatcomposing": return NotificationType.ClientChatComposing; + case "notifyclientdbidfromuid": return NotificationType.ClientDbIdFromUid; case "notifycliententerview": return NotificationType.ClientEnterView; + case "notifyclientids": return NotificationType.ClientIds; + case "clientinit": return NotificationType.ClientInit; + case "clientinitiv": return NotificationType.ClientInitIv; case "notifyclientleftview": return NotificationType.ClientLeftView; case "notifyclientmoved": return NotificationType.ClientMoved; case "notifyclientneededpermissions": return NotificationType.ClientNeededPermissions; + case "notifyclientpoke": return NotificationType.ClientPoke; case "notifyservergroupsbyclientid": return NotificationType.ClientServerGroup; case "notifyservergroupclientadded": return NotificationType.ClientServerGroupAdded; case "error": return NotificationType.CommandError; @@ -1852,10 +3220,13 @@ public static NotificationType GetNotificationType(string name) case "notifyfiletransferlist": return NotificationType.FileTransfer; case "notifystatusfiletransfer": return NotificationType.FileTransferStatus; case "notifystartupload": return NotificationType.FileUpload; + case "clientgetdbidfromuid": return NotificationType.GetClientDbIdFromUid; + case "clientgetids": return NotificationType.GetClientIds; case "initivexpand": return NotificationType.InitIvExpand; case "initivexpand2": return NotificationType.InitIvExpand2; case "initserver": return NotificationType.InitServer; case "notifyplugincmd": return NotificationType.PluginCommand; + case "plugincmd": return NotificationType.PluginCommandRequest; case "notifyserveredited": return NotificationType.ServerEdited; case "notifyservergrouplist": return NotificationType.ServerGroupList; case "notifytextmessage": return NotificationType.TextMessage; @@ -1881,10 +3252,15 @@ public static INotification GenerateNotificationType(NotificationType name) case NotificationType.ChannelUnsubscribed: return new ChannelUnsubscribed(); case NotificationType.ClientChannelGroupChanged: return new ClientChannelGroupChanged(); case NotificationType.ClientChatComposing: return new ClientChatComposing(); + case NotificationType.ClientDbIdFromUid: return new ClientDbIdFromUid(); case NotificationType.ClientEnterView: return new ClientEnterView(); + case NotificationType.ClientIds: return new ClientIds(); + case NotificationType.ClientInit: return new ClientInit(); + case NotificationType.ClientInitIv: return new ClientInitIv(); case NotificationType.ClientLeftView: return new ClientLeftView(); case NotificationType.ClientMoved: return new ClientMoved(); case NotificationType.ClientNeededPermissions: return new ClientNeededPermissions(); + case NotificationType.ClientPoke: return new ClientPoke(); case NotificationType.ClientServerGroup: return new ClientServerGroup(); case NotificationType.ClientServerGroupAdded: return new ClientServerGroupAdded(); case NotificationType.CommandError: return new CommandError(); @@ -1897,10 +3273,13 @@ public static INotification GenerateNotificationType(NotificationType name) case NotificationType.FileTransfer: return new FileTransfer(); case NotificationType.FileTransferStatus: return new FileTransferStatus(); case NotificationType.FileUpload: return new FileUpload(); + case NotificationType.GetClientDbIdFromUid: return new GetClientDbIdFromUid(); + case NotificationType.GetClientIds: return new GetClientIds(); case NotificationType.InitIvExpand: return new InitIvExpand(); case NotificationType.InitIvExpand2: return new InitIvExpand2(); case NotificationType.InitServer: return new InitServer(); case NotificationType.PluginCommand: return new PluginCommand(); + case NotificationType.PluginCommandRequest: return new PluginCommandRequest(); case NotificationType.ServerEdited: return new ServerEdited(); case NotificationType.ServerGroupList: return new ServerGroupList(); case NotificationType.TextMessage: return new TextMessage(); @@ -1909,5 +3288,59 @@ public static INotification GenerateNotificationType(NotificationType name) default: throw Util.UnhandledDefault(name); } } + + public static INotification[] InstatiateNotificationArray(NotificationType name, int len) + { + switch(name) + { + case NotificationType.ChannelChanged: { var arr = new ChannelChanged[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelChanged(); return arr; } + case NotificationType.ChannelCreated: { var arr = new ChannelCreated[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelCreated(); return arr; } + case NotificationType.ChannelDeleted: { var arr = new ChannelDeleted[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelDeleted(); return arr; } + case NotificationType.ChannelEdited: { var arr = new ChannelEdited[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelEdited(); return arr; } + case NotificationType.ChannelGroupList: { var arr = new ChannelGroupList[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelGroupList(); return arr; } + case NotificationType.ChannelList: { var arr = new ChannelList[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelList(); return arr; } + case NotificationType.ChannelListFinished: { var arr = new ChannelListFinished[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelListFinished(); return arr; } + case NotificationType.ChannelMoved: { var arr = new ChannelMoved[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelMoved(); return arr; } + case NotificationType.ChannelPasswordChanged: { var arr = new ChannelPasswordChanged[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelPasswordChanged(); return arr; } + case NotificationType.ChannelSubscribed: { var arr = new ChannelSubscribed[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelSubscribed(); return arr; } + case NotificationType.ChannelUnsubscribed: { var arr = new ChannelUnsubscribed[len]; for (int i = 0; i < len; i++) arr[i] = new ChannelUnsubscribed(); return arr; } + case NotificationType.ClientChannelGroupChanged: { var arr = new ClientChannelGroupChanged[len]; for (int i = 0; i < len; i++) arr[i] = new ClientChannelGroupChanged(); return arr; } + case NotificationType.ClientChatComposing: { var arr = new ClientChatComposing[len]; for (int i = 0; i < len; i++) arr[i] = new ClientChatComposing(); return arr; } + case NotificationType.ClientDbIdFromUid: { var arr = new ClientDbIdFromUid[len]; for (int i = 0; i < len; i++) arr[i] = new ClientDbIdFromUid(); return arr; } + case NotificationType.ClientEnterView: { var arr = new ClientEnterView[len]; for (int i = 0; i < len; i++) arr[i] = new ClientEnterView(); return arr; } + case NotificationType.ClientIds: { var arr = new ClientIds[len]; for (int i = 0; i < len; i++) arr[i] = new ClientIds(); return arr; } + case NotificationType.ClientInit: { var arr = new ClientInit[len]; for (int i = 0; i < len; i++) arr[i] = new ClientInit(); return arr; } + case NotificationType.ClientInitIv: { var arr = new ClientInitIv[len]; for (int i = 0; i < len; i++) arr[i] = new ClientInitIv(); return arr; } + case NotificationType.ClientLeftView: { var arr = new ClientLeftView[len]; for (int i = 0; i < len; i++) arr[i] = new ClientLeftView(); return arr; } + case NotificationType.ClientMoved: { var arr = new ClientMoved[len]; for (int i = 0; i < len; i++) arr[i] = new ClientMoved(); return arr; } + case NotificationType.ClientNeededPermissions: { var arr = new ClientNeededPermissions[len]; for (int i = 0; i < len; i++) arr[i] = new ClientNeededPermissions(); return arr; } + case NotificationType.ClientPoke: { var arr = new ClientPoke[len]; for (int i = 0; i < len; i++) arr[i] = new ClientPoke(); return arr; } + case NotificationType.ClientServerGroup: { var arr = new ClientServerGroup[len]; for (int i = 0; i < len; i++) arr[i] = new ClientServerGroup(); return arr; } + case NotificationType.ClientServerGroupAdded: { var arr = new ClientServerGroupAdded[len]; for (int i = 0; i < len; i++) arr[i] = new ClientServerGroupAdded(); return arr; } + case NotificationType.CommandError: { var arr = new CommandError[len]; for (int i = 0; i < len; i++) arr[i] = new CommandError(); return arr; } + case NotificationType.ConnectionInfo: { var arr = new ConnectionInfo[len]; for (int i = 0; i < len; i++) arr[i] = new ConnectionInfo(); return arr; } + case NotificationType.ConnectionInfoRequest: { var arr = new ConnectionInfoRequest[len]; for (int i = 0; i < len; i++) arr[i] = new ConnectionInfoRequest(); return arr; } + case NotificationType.FileDownload: { var arr = new FileDownload[len]; for (int i = 0; i < len; i++) arr[i] = new FileDownload(); return arr; } + case NotificationType.FileInfoTs: { var arr = new FileInfoTs[len]; for (int i = 0; i < len; i++) arr[i] = new FileInfoTs(); return arr; } + case NotificationType.FileList: { var arr = new FileList[len]; for (int i = 0; i < len; i++) arr[i] = new FileList(); return arr; } + case NotificationType.FileListFinished: { var arr = new FileListFinished[len]; for (int i = 0; i < len; i++) arr[i] = new FileListFinished(); return arr; } + case NotificationType.FileTransfer: { var arr = new FileTransfer[len]; for (int i = 0; i < len; i++) arr[i] = new FileTransfer(); return arr; } + case NotificationType.FileTransferStatus: { var arr = new FileTransferStatus[len]; for (int i = 0; i < len; i++) arr[i] = new FileTransferStatus(); return arr; } + case NotificationType.FileUpload: { var arr = new FileUpload[len]; for (int i = 0; i < len; i++) arr[i] = new FileUpload(); return arr; } + case NotificationType.GetClientDbIdFromUid: { var arr = new GetClientDbIdFromUid[len]; for (int i = 0; i < len; i++) arr[i] = new GetClientDbIdFromUid(); return arr; } + case NotificationType.GetClientIds: { var arr = new GetClientIds[len]; for (int i = 0; i < len; i++) arr[i] = new GetClientIds(); return arr; } + case NotificationType.InitIvExpand: { var arr = new InitIvExpand[len]; for (int i = 0; i < len; i++) arr[i] = new InitIvExpand(); return arr; } + case NotificationType.InitIvExpand2: { var arr = new InitIvExpand2[len]; for (int i = 0; i < len; i++) arr[i] = new InitIvExpand2(); return arr; } + case NotificationType.InitServer: { var arr = new InitServer[len]; for (int i = 0; i < len; i++) arr[i] = new InitServer(); return arr; } + case NotificationType.PluginCommand: { var arr = new PluginCommand[len]; for (int i = 0; i < len; i++) arr[i] = new PluginCommand(); return arr; } + case NotificationType.PluginCommandRequest: { var arr = new PluginCommandRequest[len]; for (int i = 0; i < len; i++) arr[i] = new PluginCommandRequest(); return arr; } + case NotificationType.ServerEdited: { var arr = new ServerEdited[len]; for (int i = 0; i < len; i++) arr[i] = new ServerEdited(); return arr; } + case NotificationType.ServerGroupList: { var arr = new ServerGroupList[len]; for (int i = 0; i < len; i++) arr[i] = new ServerGroupList(); return arr; } + case NotificationType.TextMessage: { var arr = new TextMessage[len]; for (int i = 0; i < len; i++) arr[i] = new TextMessage(); return arr; } + case NotificationType.TokenUsed: { var arr = new TokenUsed[len]; for (int i = 0; i < len; i++) arr[i] = new TokenUsed(); return arr; } + case NotificationType.Unknown: + default: throw Util.UnhandledDefault(name); + } + } } } \ No newline at end of file diff --git a/TS3Client/Generated/Messages.tt b/TS3Client/Generated/Messages.tt index 0b49ac70..f44430ae 100644 --- a/TS3Client/Generated/Messages.tt +++ b/TS3Client/Generated/Messages.tt @@ -20,7 +20,9 @@ namespace TS3Client.Messages using Commands; using Helper; using System; + using System.Collections.Generic; using System.Globalization; + using System.Buffers.Text; <#= ConversionSet #> <# @@ -29,13 +31,13 @@ var gen = Messages.Parse(Host.ResolvePath("../Declarations/Messages.toml")); string GenerateDeserializer(Messages.Field fld) { if(fld.isArray) - return $"{{ if(value.Length == 0) {fld.prettyClean} = Array.Empty<{fld.type}>(); else {{" - + $" var ss = new SpanSplitter(); ss.First(value, ',');" + return $"{{ if(value.Length == 0) {fld.pretty} = Array.Empty<{fld.type}>(); else {{" + + $" var ss = new SpanSplitter(); ss.First(value, (byte)',');" + $" int cnt = 0; for (int i = 0; i < value.Length; i++) if (value[i] == ',') cnt++;" - + $" {fld.prettyClean} = new {fld.type}[cnt + 1];" - + $" for(int i = 0; i < cnt + 1; i++) {{ {GenerateSingleDeserializer(fld, "ss.Trim(value)", fld.prettyClean + "[i]")} if (i < cnt) value = ss.Next(value); }} }} }}"; + + $" {fld.pretty} = new {fld.type}[cnt + 1];" + + $" for(int i = 0; i < cnt + 1; i++) {{ {GenerateSingleDeserializer(fld, "ss.Trim(value)", fld.pretty + "[i]")} if (i < cnt) value = ss.Next(value); }} }} }}"; else - return GenerateSingleDeserializer(fld, "value", fld.prettyClean); + return GenerateSingleDeserializer(fld, "value", fld.pretty); } Dictionary BackingTypes = new Dictionary() { @@ -65,13 +67,13 @@ string GenerateSingleDeserializer(Messages.Field fld, string input, string outpu case "ChannelId": case "ServerGroupId": case "ChannelGroupId": - return $"{output} = {fld.type}.Parse({input}.NewString(), CultureInfo.InvariantCulture);"; + return $"{{ if(Utf8Parser.TryParse({input}, out {fld.type} oval, out _)) {output} = oval; }}"; case "DurationSeconds": - return $"{output} = TimeSpan.FromSeconds(double.Parse({input}.NewString(), CultureInfo.InvariantCulture));"; + return $"{{ if(Utf8Parser.TryParse({input}, out double oval, out _)) {output} = TimeSpan.FromSeconds(oval); }}"; case "DurationMilliseconds": - return $"{output} = TimeSpan.FromMilliseconds(double.Parse({input}.NewString(), CultureInfo.InvariantCulture));"; + return $"{{ if(Utf8Parser.TryParse({input}, out double oval, out _)) {output} = TimeSpan.FromMilliseconds(oval); }}"; case "DateTime": - return $"{output} = Util.UnixTimeStart.AddSeconds(double.Parse({input}.NewString(), CultureInfo.InvariantCulture));"; + return $"{{ if(Utf8Parser.TryParse({input}, out double oval, out _)) {output} = Util.UnixTimeStart.AddSeconds(oval); }}"; case "str": case "Uid": return $"{output} = Ts3String.Unescape({input});"; @@ -83,16 +85,15 @@ string GenerateSingleDeserializer(Messages.Field fld, string input, string outpu case "TextMessageTargetMode": case "GroupType": case "GroupNamingMode": - return $"{{ if (!Enum.TryParse({input}.NewString(), out {fld.type} val)) throw new FormatException(); {output} = val; }}"; case "Codec": case "Ts3ErrorCode": case "LicenseType": case "PermissionId": if(!BackingTypes.TryGetValue(fld.type, out var backType)) backType = "i32"; - return $"{output} = ({fld.type}){backType}.Parse({input}.NewString(), CultureInfo.InvariantCulture);"; + return $"{{ if(Utf8Parser.TryParse({input}, out {backType} oval, out _)) {output} = ({fld.type})oval; }}"; case "IconHash": - return $"{output} = unchecked((int)long.Parse({input}.NewString(), CultureInfo.InvariantCulture));"; + return $"{{ if(Utf8Parser.TryParse({input}, out long oval, out _)) {output} = unchecked((int)oval); }}"; default: Warn($"Missing deserializer for {fld.type}"); return ""; @@ -101,7 +102,7 @@ string GenerateSingleDeserializer(Messages.Field fld, string input, string outpu foreach(var msg in gen.GetOrderedMsg()) { - if(!msg.s2c.Value) continue; + //if(!msg.s2c.Value) continue; #> public sealed class <#= msg.name #><# bool isNotify = msg.notify != null; @@ -117,9 +118,8 @@ foreach(var msg in gen.GetOrderedMsg()) foreach (var (genField, optional) in msg.attributes.Select(f => gen.GetField(f))) { #> public <#= genField.typeFin #> <#= genField.pretty #> { get; set; }<# } #> - public void SetField(string name, ReadOnlySpan value) - { -<# + public void SetField(string name, ReadOnlySpan value) + {<# if (msg.attributes.Length > 0) { #> switch(name) { @@ -131,6 +131,26 @@ foreach(var msg in gen.GetOrderedMsg()) #> <#= isResponse ? ("case \"return_code\": " + GenerateDeserializer(gen.GetField("return_code").fld) + " break;") : "" #> } +<# + } #> + } + + public void Expand(IMessage[] to, IEnumerable flds) + {<# + if (msg.attributes.Length > 0) { #> + var toc = (<#= msg.name #>[])to; + foreach (var fld in flds) + { + switch(fld) + { +<# + foreach (var (genField, optional) in msg.attributes.Select(f => gen.GetField(f))) { +#> + case "<#= genField.ts #>": foreach(var toi in toc) { toi.<#= genField.pretty #> = <#= genField.pretty #>; } break;<# + } +#> + } + } <# } #> } @@ -140,7 +160,8 @@ foreach(var msg in gen.GetOrderedMsg()) { Unknown,<# foreach(var ntfy in gen.NotifiesSorted) { - if(!ntfy.s2c.Value) continue; #> + /*if(!ntfy.s2c.Value) continue;*/ #> + ///<#= ntfy.s2c.Value ? "[S2C] " : "" #><#= ntfy.c2s.Value ? "[C2S] " : "" #>ntfy:<#= ntfy.notify #> <#= ntfy.name #>,<# } #> @@ -153,7 +174,7 @@ foreach(var msg in gen.GetOrderedMsg()) switch(name) {<# foreach(var ntfy in gen.NotifiesSorted) { - if(!ntfy.s2c.Value) continue; #> + /*if(!ntfy.s2c.Value) continue;*/ #> case "<#= ntfy.notify #>": return NotificationType.<#= ntfy.name #>;<# } #> @@ -167,10 +188,26 @@ foreach(var msg in gen.GetOrderedMsg()) {<# foreach(var ntfy in gen.NotifiesSorted) { - if(!ntfy.s2c.Value) continue; + /*if(!ntfy.s2c.Value) continue;*/ #> case NotificationType.<#= ntfy.name #>: return new <#= ntfy.name #>();<# } +#> + case NotificationType.Unknown: + default: throw Util.UnhandledDefault(name); + } + } + + public static INotification[] InstatiateNotificationArray(NotificationType name, int len) + { + switch(name) + {<# + foreach(var ntfy in gen.NotifiesSorted) + { + /*if(!ntfy.s2c.Value) continue;*/ +#> + case NotificationType.<#= ntfy.name #>: { var arr = new <#= ntfy.name #>[len]; for (int i = 0; i < len; i++) arr[i] = new <#= ntfy.name #>(); return arr; }<# + } #> case NotificationType.Unknown: default: throw Util.UnhandledDefault(name); diff --git a/TS3Client/Generated/Ts3FullEvents.cs b/TS3Client/Generated/Ts3FullEvents.cs new file mode 100644 index 00000000..95a324f2 --- /dev/null +++ b/TS3Client/Generated/Ts3FullEvents.cs @@ -0,0 +1,744 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + + + + + + + + + + + + + + + + +namespace TS3Client.Full +{ + using Helper; + using Messages; + using System; + + public sealed partial class Ts3FullClient + { + + public event NotifyEventHandler OnChannelChanged; + public event EventHandler OnEachChannelChanged; + public event NotifyEventHandler OnChannelCreated; + public event EventHandler OnEachChannelCreated; + public event NotifyEventHandler OnChannelDeleted; + public event EventHandler OnEachChannelDeleted; + public event NotifyEventHandler OnChannelEdited; + public event EventHandler OnEachChannelEdited; + public event NotifyEventHandler OnChannelGroupList; + public event EventHandler OnEachChannelGroupList; + public event NotifyEventHandler OnChannelList; + public event EventHandler OnEachChannelList; + public event NotifyEventHandler OnChannelListFinished; + public event EventHandler OnEachChannelListFinished; + public event NotifyEventHandler OnChannelMoved; + public event EventHandler OnEachChannelMoved; + public event NotifyEventHandler OnChannelPasswordChanged; + public event EventHandler OnEachChannelPasswordChanged; + public event NotifyEventHandler OnChannelSubscribed; + public event EventHandler OnEachChannelSubscribed; + public event NotifyEventHandler OnChannelUnsubscribed; + public event EventHandler OnEachChannelUnsubscribed; + public event NotifyEventHandler OnClientChannelGroupChanged; + public event EventHandler OnEachClientChannelGroupChanged; + public event NotifyEventHandler OnClientChatComposing; + public event EventHandler OnEachClientChatComposing; + public event NotifyEventHandler OnClientDbIdFromUid; + public event EventHandler OnEachClientDbIdFromUid; + public override event NotifyEventHandler OnClientEnterView; + public event EventHandler OnEachClientEnterView; + public event NotifyEventHandler OnClientIds; + public event EventHandler OnEachClientIds; + public override event NotifyEventHandler OnClientLeftView; + public event EventHandler OnEachClientLeftView; + public event NotifyEventHandler OnClientMoved; + public event EventHandler OnEachClientMoved; + public event NotifyEventHandler OnClientNeededPermissions; + public event EventHandler OnEachClientNeededPermissions; + public event NotifyEventHandler OnClientPoke; + public event EventHandler OnEachClientPoke; + public event NotifyEventHandler OnClientServerGroup; + public event EventHandler OnEachClientServerGroup; + public event NotifyEventHandler OnClientServerGroupAdded; + public event EventHandler OnEachClientServerGroupAdded; + public event NotifyEventHandler OnCommandError; + public event EventHandler OnEachCommandError; + public event NotifyEventHandler OnConnectionInfo; + public event EventHandler OnEachConnectionInfo; + public event NotifyEventHandler OnConnectionInfoRequest; + public event EventHandler OnEachConnectionInfoRequest; + public event NotifyEventHandler OnFileDownload; + public event EventHandler OnEachFileDownload; + public event NotifyEventHandler OnFileInfoTs; + public event EventHandler OnEachFileInfoTs; + public event NotifyEventHandler OnFileList; + public event EventHandler OnEachFileList; + public event NotifyEventHandler OnFileListFinished; + public event EventHandler OnEachFileListFinished; + public event NotifyEventHandler OnFileTransfer; + public event EventHandler OnEachFileTransfer; + public event NotifyEventHandler OnFileTransferStatus; + public event EventHandler OnEachFileTransferStatus; + public event NotifyEventHandler OnFileUpload; + public event EventHandler OnEachFileUpload; + public event NotifyEventHandler OnInitIvExpand; + public event EventHandler OnEachInitIvExpand; + public event NotifyEventHandler OnInitIvExpand2; + public event EventHandler OnEachInitIvExpand2; + public event NotifyEventHandler OnInitServer; + public event EventHandler OnEachInitServer; + public event NotifyEventHandler OnPluginCommand; + public event EventHandler OnEachPluginCommand; + public event NotifyEventHandler OnServerEdited; + public event EventHandler OnEachServerEdited; + public event NotifyEventHandler OnServerGroupList; + public event EventHandler OnEachServerGroupList; + public override event NotifyEventHandler OnTextMessage; + public event EventHandler OnEachTextMessage; + public event NotifyEventHandler OnTokenUsed; + public event EventHandler OnEachTokenUsed; + + + private void InvokeEvent(LazyNotification lazyNotification) + { + var ntf = lazyNotification.Notifications; + switch (lazyNotification.NotifyType) + { + + case NotificationType.ChannelChanged: { + var ntfc = (ChannelChanged[])ntf; + ProcessChannelChanged(ntfc); + OnChannelChanged?.Invoke(this, ntfc); + var ev = OnEachChannelChanged; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelChanged(that); + } + break; + } + + case NotificationType.ChannelCreated: { + var ntfc = (ChannelCreated[])ntf; + ProcessChannelCreated(ntfc); + OnChannelCreated?.Invoke(this, ntfc); + var ev = OnEachChannelCreated; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelCreated(that); + book?.UpdateChannelCreated(that); + } + break; + } + + case NotificationType.ChannelDeleted: { + var ntfc = (ChannelDeleted[])ntf; + ProcessChannelDeleted(ntfc); + OnChannelDeleted?.Invoke(this, ntfc); + var ev = OnEachChannelDeleted; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelDeleted(that); + book?.UpdateChannelDeleted(that); + } + break; + } + + case NotificationType.ChannelEdited: { + var ntfc = (ChannelEdited[])ntf; + ProcessChannelEdited(ntfc); + OnChannelEdited?.Invoke(this, ntfc); + var ev = OnEachChannelEdited; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelEdited(that); + book?.UpdateChannelEdited(that); + } + break; + } + + case NotificationType.ChannelGroupList: { + var ntfc = (ChannelGroupList[])ntf; + ProcessChannelGroupList(ntfc); + OnChannelGroupList?.Invoke(this, ntfc); + var ev = OnEachChannelGroupList; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelGroupList(that); + } + break; + } + + case NotificationType.ChannelList: { + var ntfc = (ChannelList[])ntf; + ProcessChannelList(ntfc); + OnChannelList?.Invoke(this, ntfc); + var ev = OnEachChannelList; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelList(that); + book?.UpdateChannelList(that); + } + break; + } + + case NotificationType.ChannelListFinished: { + var ntfc = (ChannelListFinished[])ntf; + ProcessChannelListFinished(ntfc); + OnChannelListFinished?.Invoke(this, ntfc); + var ev = OnEachChannelListFinished; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelListFinished(that); + } + break; + } + + case NotificationType.ChannelMoved: { + var ntfc = (ChannelMoved[])ntf; + ProcessChannelMoved(ntfc); + OnChannelMoved?.Invoke(this, ntfc); + var ev = OnEachChannelMoved; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelMoved(that); + book?.UpdateChannelMoved(that); + } + break; + } + + case NotificationType.ChannelPasswordChanged: { + var ntfc = (ChannelPasswordChanged[])ntf; + ProcessChannelPasswordChanged(ntfc); + OnChannelPasswordChanged?.Invoke(this, ntfc); + var ev = OnEachChannelPasswordChanged; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelPasswordChanged(that); + } + break; + } + + case NotificationType.ChannelSubscribed: { + var ntfc = (ChannelSubscribed[])ntf; + ProcessChannelSubscribed(ntfc); + OnChannelSubscribed?.Invoke(this, ntfc); + var ev = OnEachChannelSubscribed; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelSubscribed(that); + } + break; + } + + case NotificationType.ChannelUnsubscribed: { + var ntfc = (ChannelUnsubscribed[])ntf; + ProcessChannelUnsubscribed(ntfc); + OnChannelUnsubscribed?.Invoke(this, ntfc); + var ev = OnEachChannelUnsubscribed; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachChannelUnsubscribed(that); + } + break; + } + + case NotificationType.ClientChannelGroupChanged: { + var ntfc = (ClientChannelGroupChanged[])ntf; + ProcessClientChannelGroupChanged(ntfc); + OnClientChannelGroupChanged?.Invoke(this, ntfc); + var ev = OnEachClientChannelGroupChanged; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientChannelGroupChanged(that); + book?.UpdateClientChannelGroupChanged(that); + } + break; + } + + case NotificationType.ClientChatComposing: { + var ntfc = (ClientChatComposing[])ntf; + ProcessClientChatComposing(ntfc); + OnClientChatComposing?.Invoke(this, ntfc); + var ev = OnEachClientChatComposing; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientChatComposing(that); + } + break; + } + + case NotificationType.ClientDbIdFromUid: { + var ntfc = (ClientDbIdFromUid[])ntf; + ProcessClientDbIdFromUid(ntfc); + OnClientDbIdFromUid?.Invoke(this, ntfc); + var ev = OnEachClientDbIdFromUid; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientDbIdFromUid(that); + } + break; + } + + case NotificationType.ClientEnterView: { + var ntfc = (ClientEnterView[])ntf; + ProcessClientEnterView(ntfc); + OnClientEnterView?.Invoke(this, ntfc); + var ev = OnEachClientEnterView; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientEnterView(that); + book?.UpdateClientEnterView(that); + } + break; + } + + case NotificationType.ClientIds: { + var ntfc = (ClientIds[])ntf; + ProcessClientIds(ntfc); + OnClientIds?.Invoke(this, ntfc); + var ev = OnEachClientIds; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientIds(that); + } + break; + } + + case NotificationType.ClientLeftView: { + var ntfc = (ClientLeftView[])ntf; + ProcessClientLeftView(ntfc); + OnClientLeftView?.Invoke(this, ntfc); + var ev = OnEachClientLeftView; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientLeftView(that); + book?.UpdateClientLeftView(that); + } + break; + } + + case NotificationType.ClientMoved: { + var ntfc = (ClientMoved[])ntf; + ProcessClientMoved(ntfc); + OnClientMoved?.Invoke(this, ntfc); + var ev = OnEachClientMoved; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientMoved(that); + book?.UpdateClientMoved(that); + } + break; + } + + case NotificationType.ClientNeededPermissions: { + var ntfc = (ClientNeededPermissions[])ntf; + ProcessClientNeededPermissions(ntfc); + OnClientNeededPermissions?.Invoke(this, ntfc); + var ev = OnEachClientNeededPermissions; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientNeededPermissions(that); + } + break; + } + + case NotificationType.ClientPoke: { + var ntfc = (ClientPoke[])ntf; + ProcessClientPoke(ntfc); + OnClientPoke?.Invoke(this, ntfc); + var ev = OnEachClientPoke; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientPoke(that); + } + break; + } + + case NotificationType.ClientServerGroup: { + var ntfc = (ClientServerGroup[])ntf; + ProcessClientServerGroup(ntfc); + OnClientServerGroup?.Invoke(this, ntfc); + var ev = OnEachClientServerGroup; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientServerGroup(that); + } + break; + } + + case NotificationType.ClientServerGroupAdded: { + var ntfc = (ClientServerGroupAdded[])ntf; + ProcessClientServerGroupAdded(ntfc); + OnClientServerGroupAdded?.Invoke(this, ntfc); + var ev = OnEachClientServerGroupAdded; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachClientServerGroupAdded(that); + book?.UpdateClientServerGroupAdded(that); + } + break; + } + + case NotificationType.CommandError: { + var ntfc = (CommandError[])ntf; + ProcessCommandError(ntfc); + OnCommandError?.Invoke(this, ntfc); + var ev = OnEachCommandError; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachCommandError(that); + } + break; + } + + case NotificationType.ConnectionInfo: { + var ntfc = (ConnectionInfo[])ntf; + ProcessConnectionInfo(ntfc); + OnConnectionInfo?.Invoke(this, ntfc); + var ev = OnEachConnectionInfo; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachConnectionInfo(that); + book?.UpdateConnectionInfo(that); + } + break; + } + + case NotificationType.ConnectionInfoRequest: { + var ntfc = (ConnectionInfoRequest[])ntf; + ProcessConnectionInfoRequest(ntfc); + OnConnectionInfoRequest?.Invoke(this, ntfc); + var ev = OnEachConnectionInfoRequest; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachConnectionInfoRequest(that); + } + break; + } + + case NotificationType.FileDownload: { + var ntfc = (FileDownload[])ntf; + ProcessFileDownload(ntfc); + OnFileDownload?.Invoke(this, ntfc); + var ev = OnEachFileDownload; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileDownload(that); + } + break; + } + + case NotificationType.FileInfoTs: { + var ntfc = (FileInfoTs[])ntf; + ProcessFileInfoTs(ntfc); + OnFileInfoTs?.Invoke(this, ntfc); + var ev = OnEachFileInfoTs; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileInfoTs(that); + } + break; + } + + case NotificationType.FileList: { + var ntfc = (FileList[])ntf; + ProcessFileList(ntfc); + OnFileList?.Invoke(this, ntfc); + var ev = OnEachFileList; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileList(that); + } + break; + } + + case NotificationType.FileListFinished: { + var ntfc = (FileListFinished[])ntf; + ProcessFileListFinished(ntfc); + OnFileListFinished?.Invoke(this, ntfc); + var ev = OnEachFileListFinished; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileListFinished(that); + } + break; + } + + case NotificationType.FileTransfer: { + var ntfc = (FileTransfer[])ntf; + ProcessFileTransfer(ntfc); + OnFileTransfer?.Invoke(this, ntfc); + var ev = OnEachFileTransfer; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileTransfer(that); + } + break; + } + + case NotificationType.FileTransferStatus: { + var ntfc = (FileTransferStatus[])ntf; + ProcessFileTransferStatus(ntfc); + OnFileTransferStatus?.Invoke(this, ntfc); + var ev = OnEachFileTransferStatus; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileTransferStatus(that); + } + break; + } + + case NotificationType.FileUpload: { + var ntfc = (FileUpload[])ntf; + ProcessFileUpload(ntfc); + OnFileUpload?.Invoke(this, ntfc); + var ev = OnEachFileUpload; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachFileUpload(that); + } + break; + } + + case NotificationType.InitIvExpand: { + var ntfc = (InitIvExpand[])ntf; + ProcessInitIvExpand(ntfc); + OnInitIvExpand?.Invoke(this, ntfc); + var ev = OnEachInitIvExpand; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachInitIvExpand(that); + } + break; + } + + case NotificationType.InitIvExpand2: { + var ntfc = (InitIvExpand2[])ntf; + ProcessInitIvExpand2(ntfc); + OnInitIvExpand2?.Invoke(this, ntfc); + var ev = OnEachInitIvExpand2; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachInitIvExpand2(that); + } + break; + } + + case NotificationType.InitServer: { + var ntfc = (InitServer[])ntf; + ProcessInitServer(ntfc); + OnInitServer?.Invoke(this, ntfc); + var ev = OnEachInitServer; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachInitServer(that); + book?.UpdateInitServer(that); + } + break; + } + + case NotificationType.PluginCommand: { + var ntfc = (PluginCommand[])ntf; + ProcessPluginCommand(ntfc); + OnPluginCommand?.Invoke(this, ntfc); + var ev = OnEachPluginCommand; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachPluginCommand(that); + } + break; + } + + case NotificationType.ServerEdited: { + var ntfc = (ServerEdited[])ntf; + ProcessServerEdited(ntfc); + OnServerEdited?.Invoke(this, ntfc); + var ev = OnEachServerEdited; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachServerEdited(that); + book?.UpdateServerEdited(that); + } + break; + } + + case NotificationType.ServerGroupList: { + var ntfc = (ServerGroupList[])ntf; + ProcessServerGroupList(ntfc); + OnServerGroupList?.Invoke(this, ntfc); + var ev = OnEachServerGroupList; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachServerGroupList(that); + book?.UpdateServerGroupList(that); + } + break; + } + + case NotificationType.TextMessage: { + var ntfc = (TextMessage[])ntf; + ProcessTextMessage(ntfc); + OnTextMessage?.Invoke(this, ntfc); + var ev = OnEachTextMessage; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachTextMessage(that); + } + break; + } + + case NotificationType.TokenUsed: { + var ntfc = (TokenUsed[])ntf; + ProcessTokenUsed(ntfc); + OnTokenUsed?.Invoke(this, ntfc); + var ev = OnEachTokenUsed; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEachTokenUsed(that); + } + break; + } + + case NotificationType.Unknown: + default: + throw Util.UnhandledDefault(lazyNotification.NotifyType); + } + } + + partial void ProcessChannelChanged(ChannelChanged[] notifies); + partial void ProcessEachChannelChanged(ChannelChanged notifies); + partial void ProcessChannelCreated(ChannelCreated[] notifies); + partial void ProcessEachChannelCreated(ChannelCreated notifies); + partial void ProcessChannelDeleted(ChannelDeleted[] notifies); + partial void ProcessEachChannelDeleted(ChannelDeleted notifies); + partial void ProcessChannelEdited(ChannelEdited[] notifies); + partial void ProcessEachChannelEdited(ChannelEdited notifies); + partial void ProcessChannelGroupList(ChannelGroupList[] notifies); + partial void ProcessEachChannelGroupList(ChannelGroupList notifies); + partial void ProcessChannelList(ChannelList[] notifies); + partial void ProcessEachChannelList(ChannelList notifies); + partial void ProcessChannelListFinished(ChannelListFinished[] notifies); + partial void ProcessEachChannelListFinished(ChannelListFinished notifies); + partial void ProcessChannelMoved(ChannelMoved[] notifies); + partial void ProcessEachChannelMoved(ChannelMoved notifies); + partial void ProcessChannelPasswordChanged(ChannelPasswordChanged[] notifies); + partial void ProcessEachChannelPasswordChanged(ChannelPasswordChanged notifies); + partial void ProcessChannelSubscribed(ChannelSubscribed[] notifies); + partial void ProcessEachChannelSubscribed(ChannelSubscribed notifies); + partial void ProcessChannelUnsubscribed(ChannelUnsubscribed[] notifies); + partial void ProcessEachChannelUnsubscribed(ChannelUnsubscribed notifies); + partial void ProcessClientChannelGroupChanged(ClientChannelGroupChanged[] notifies); + partial void ProcessEachClientChannelGroupChanged(ClientChannelGroupChanged notifies); + partial void ProcessClientChatComposing(ClientChatComposing[] notifies); + partial void ProcessEachClientChatComposing(ClientChatComposing notifies); + partial void ProcessClientDbIdFromUid(ClientDbIdFromUid[] notifies); + partial void ProcessEachClientDbIdFromUid(ClientDbIdFromUid notifies); + partial void ProcessClientEnterView(ClientEnterView[] notifies); + partial void ProcessEachClientEnterView(ClientEnterView notifies); + partial void ProcessClientIds(ClientIds[] notifies); + partial void ProcessEachClientIds(ClientIds notifies); + partial void ProcessClientLeftView(ClientLeftView[] notifies); + partial void ProcessEachClientLeftView(ClientLeftView notifies); + partial void ProcessClientMoved(ClientMoved[] notifies); + partial void ProcessEachClientMoved(ClientMoved notifies); + partial void ProcessClientNeededPermissions(ClientNeededPermissions[] notifies); + partial void ProcessEachClientNeededPermissions(ClientNeededPermissions notifies); + partial void ProcessClientPoke(ClientPoke[] notifies); + partial void ProcessEachClientPoke(ClientPoke notifies); + partial void ProcessClientServerGroup(ClientServerGroup[] notifies); + partial void ProcessEachClientServerGroup(ClientServerGroup notifies); + partial void ProcessClientServerGroupAdded(ClientServerGroupAdded[] notifies); + partial void ProcessEachClientServerGroupAdded(ClientServerGroupAdded notifies); + partial void ProcessCommandError(CommandError[] notifies); + partial void ProcessEachCommandError(CommandError notifies); + partial void ProcessConnectionInfo(ConnectionInfo[] notifies); + partial void ProcessEachConnectionInfo(ConnectionInfo notifies); + partial void ProcessConnectionInfoRequest(ConnectionInfoRequest[] notifies); + partial void ProcessEachConnectionInfoRequest(ConnectionInfoRequest notifies); + partial void ProcessFileDownload(FileDownload[] notifies); + partial void ProcessEachFileDownload(FileDownload notifies); + partial void ProcessFileInfoTs(FileInfoTs[] notifies); + partial void ProcessEachFileInfoTs(FileInfoTs notifies); + partial void ProcessFileList(FileList[] notifies); + partial void ProcessEachFileList(FileList notifies); + partial void ProcessFileListFinished(FileListFinished[] notifies); + partial void ProcessEachFileListFinished(FileListFinished notifies); + partial void ProcessFileTransfer(FileTransfer[] notifies); + partial void ProcessEachFileTransfer(FileTransfer notifies); + partial void ProcessFileTransferStatus(FileTransferStatus[] notifies); + partial void ProcessEachFileTransferStatus(FileTransferStatus notifies); + partial void ProcessFileUpload(FileUpload[] notifies); + partial void ProcessEachFileUpload(FileUpload notifies); + partial void ProcessInitIvExpand(InitIvExpand[] notifies); + partial void ProcessEachInitIvExpand(InitIvExpand notifies); + partial void ProcessInitIvExpand2(InitIvExpand2[] notifies); + partial void ProcessEachInitIvExpand2(InitIvExpand2 notifies); + partial void ProcessInitServer(InitServer[] notifies); + partial void ProcessEachInitServer(InitServer notifies); + partial void ProcessPluginCommand(PluginCommand[] notifies); + partial void ProcessEachPluginCommand(PluginCommand notifies); + partial void ProcessServerEdited(ServerEdited[] notifies); + partial void ProcessEachServerEdited(ServerEdited notifies); + partial void ProcessServerGroupList(ServerGroupList[] notifies); + partial void ProcessEachServerGroupList(ServerGroupList notifies); + partial void ProcessTextMessage(TextMessage[] notifies); + partial void ProcessEachTextMessage(TextMessage notifies); + partial void ProcessTokenUsed(TokenUsed[] notifies); + partial void ProcessEachTokenUsed(TokenUsed notifies); + + } +} \ No newline at end of file diff --git a/TS3Client/Generated/Ts3FullEvents.tt b/TS3Client/Generated/Ts3FullEvents.tt new file mode 100644 index 00000000..5cc585fa --- /dev/null +++ b/TS3Client/Generated/Ts3FullEvents.tt @@ -0,0 +1,96 @@ +// TS3Client - A free TeamSpeak3 client implementation +// Copyright (C) 2017 TS3Client contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the Open Software License v. 3.0 +// +// You should have received a copy of the Open Software License along with this +// program. If not, see . + +<#@ template debug="true" hostSpecific="true" language="C#" #> +<#@ include file="M2BParser.ttinclude" once="true" #> +<#@ include file="MessageParser.ttinclude" once="true" #> +<#@ include file="BookParser.ttinclude" once="true" #> +<#@ output extension=".cs" #> +<#@ import namespace="System.Collections.Generic" #> + +namespace TS3Client.Full +{ + using Helper; + using Messages; + using System; + + public sealed partial class Ts3FullClient + { + <# + var genbook = BookDeclarations.Parse(Host.ResolvePath("../Declarations/BookDeclarations.toml")); + var genmsg = Messages.Parse(Host.ResolvePath("../Declarations/Messages.toml")); + var genm2b = M2BDeclarations.Parse(Host.ResolvePath("../Declarations/MessagesToBook.toml"), genmsg, genbook); + var shared = new HashSet { + "TextMessage", + "ClientEnterView", + "ClientLeftView", + }; + + PushIndent("\t\t"); + WriteLine(""); + + foreach(var ntfy in OnlyS2C(genmsg.NotifiesSorted)) + { + Write("public"); + if(shared.Contains(ntfy.name)) + Write(" override"); + WriteLine($" event NotifyEventHandler<{ntfy.name}> On{ntfy.name};"); + + Write("public"); + //if(shared.Contains(ntfy.name)) + // Write(" override"); + WriteLine($" event EventHandler<{ntfy.name}> OnEach{ntfy.name};"); + } + PopIndent(); + #> + + private void InvokeEvent(LazyNotification lazyNotification) + { + var ntf = lazyNotification.Notifications; + switch (lazyNotification.NotifyType) + { + <# + foreach(var ntfy in OnlyS2C(genmsg.NotifiesSorted)) + { + #> + case NotificationType.<#= ntfy.name #>: { + var ntfc = (<#=ntfy.name #>[])ntf; + Process<#= ntfy.name #>(ntfc); + On<#= ntfy.name #>?.Invoke(this, ntfc); + var ev = OnEach<#= ntfy.name #>; + var book = Book; + foreach(var that in ntfc) { + ev?.Invoke(this, that); + ProcessEach<#= ntfy.name #>(that);<# + var bookitem = genm2b.rule.FirstOrDefault(x => x.from == ntfy.name); + if(bookitem != null) { + #> + book?.Update<#= ntfy.name #>(that);<# } #> + } + break; + } + <# + } + #> + case NotificationType.Unknown: + default: + throw Util.UnhandledDefault(lazyNotification.NotifyType); + } + } + + <# + PushIndent("\t\t"); + foreach(var ntfy in OnlyS2C(genmsg.NotifiesSorted)) + { + WriteLine($"partial void Process{ntfy.name}({ntfy.name}[] notifies);"); + WriteLine($"partial void ProcessEach{ntfy.name}({ntfy.name} notifies);"); + } + #> + } +} \ No newline at end of file diff --git a/TS3Client/Generated/Util.ttinclude b/TS3Client/Generated/Util.ttinclude index 1d2c4e0f..975f3c70 100644 --- a/TS3Client/Generated/Util.ttinclude +++ b/TS3Client/Generated/Util.ttinclude @@ -1,16 +1,12 @@ <#@ assembly name="System.Core" #> -<#@ import namespace="System.Linq" #> -<#@ import namespace="System.IO" #> -<#@ import namespace="System.Text" #> -<#@ import namespace="System.Text.RegularExpressions" #> -<#@ import namespace="System.Collections.Generic" #> -<#@ assembly name="$(SolutionDir)\packages\Nett.0.9.0\lib\Net40\Nett.dll" #> +<#@ assembly name="%userprofile%/.nuget/packages/nett/0.9.0/lib/Net40/Nett.dll" #> <# void Warn(string _warn) { WriteLine($"#warning {_warn}"); } const string ConversionSet = - @"using i8 = System.Byte; - using u8 = System.SByte; +@"#pragma warning disable CS8019 // Ignore unused imports + using i8 = System.SByte; + using u8 = System.Byte; using i16 = System.Int16; using u16 = System.UInt16; using i32 = System.Int32; @@ -33,5 +29,6 @@ const string ConversionSet = using ServerGroupId = System.UInt64; using ChannelGroupId = System.UInt64; using IconHash = System.Int32; - using ConnectionId = System.UInt32;"; + using ConnectionId = System.UInt32; +#pragma warning restore CS8019"; #> \ No newline at end of file diff --git a/TS3Client/Generated/Versions.cs b/TS3Client/Generated/Versions.cs index a5338d5a..cdb9d982 100644 --- a/TS3Client/Generated/Versions.cs +++ b/TS3Client/Generated/Versions.cs @@ -25,7 +25,7 @@ namespace TS3Client.Full /// Describes a triple of version, platform and a cryptographical signature (usually distributed by "TeamSpeak Systems"). /// Each triple has to match and is not interchangeable with other triple parts. /// - public class VersionSign + public sealed class VersionSign { private static readonly string[] Plattforms = { null, "Windows", "Linux", "OS X", "Android", "iOS" }; @@ -52,28 +52,59 @@ public VersionSign(string name, string plattform, string sign) PlattformName = plattform; } - // Many ids implemented from here: https://r4p3.net/threads/client-builds.499/ + public bool CheckValid() => Ts3Crypt.EdCheck(this); // ReSharper disable InconsistentNaming, UnusedMember.Global - - public static VersionSign VER_WIN_3_0_11 { get; } = new VersionSign("3.0.11 [Build: 1375083581]", ClientPlattform.Windows, "54wPDkfv0kT56UE0lv/LFkFJObH+Q4Irmo4Brfz1EcvjVhj8hJ+RCHcVTZsdKU2XvVvh+VLJpURulEHsAOsyBw=="); - public static VersionSign VER_WIN_3_0_19_3 { get; } = new VersionSign("3.0.19.3 [Build: 1466672534]", ClientPlattform.Windows, "a1OYzvM18mrmfUQBUgxYBxYz2DUU6y5k3/mEL6FurzU0y97Bd1FL7+PRpcHyPkg4R+kKAFZ1nhyzbgkGphDWDg=="); - public static VersionSign VER_WIN_3_0_19_4 { get; } = new VersionSign("3.0.19.4 [Build: 1468491418]", ClientPlattform.Windows, "ldWL49uDKC3N9uxdgWRMTOzUuiG1nBqUiOa+Nal5HvdxJiN4fsTnmmPo5tvglN7WqoVoFfuuKuYq1LzodtEtCg=="); - public static VersionSign VER_LIN_3_0_19_4 { get; } = new VersionSign("3.0.19.4 [Build: 1468491418]", ClientPlattform.Linux, "jvhhk75EV3nCGeewx4Y5zZmiZSN07q5ByKZ9Wlmg85aAbnw7c1jKq5/Iq0zY6dfGwCEwuKod0I5lQcVLf2NTCg=="); - public static VersionSign VER_OSX_3_0_19_4 { get; } = new VersionSign("3.0.19.4 [Build: 1468491418]", ClientPlattform.Osx, "Pvcizdk3HRQMzTLt7goUYBmmS5nbAS1g2E6HIypLU+9eXTqGTBLim0UUtKc0s867TFHbK91GroDrTtv0aMUGAw=="); - public static VersionSign VER_WIN_3_0_20 { get; } = new VersionSign("3.0.20 [Build: 1465542546]", ClientPlattform.Windows, "vDK31sOwOvDpTXgqAJzmR1NzeUeSDG9dLMgIz5LCX+KpDSVD/qU60mzScz9tuc9AsLyrL8DxHpDDO3eQD+hYCA=="); - public static VersionSign VER_AND_3_0_23 { get; } = new VersionSign("3.0.23 [Build: 1463662487]", ClientPlattform.Android, "RN+cwFI+jSHJEhggucIuUyEteWNVFy4iw0QDp3qn2UzfopypFVE9BPZqJjBUGeoCN7Q/SfYL4RNIRzJEQaZUCA=="); - public static VersionSign VER_WIN_3_1 { get; } = new VersionSign("3.1 [Build: 1471417187]", ClientPlattform.Windows, "Vr9F7kbVorcrkV5b/Iw+feH9qmDGvfsW8tpa737zhc1fDpK5uaEo6M5l2DzgaGqqOr3GKl5A7PF9Sj6eTM26Aw=="); - public static VersionSign VER_WIN_3_1_6 { get; } = new VersionSign("3.1.6 [Build: 1502873983]", ClientPlattform.Windows, "73fB82Jt1lmIRHKBFaE8h1JKPGFbnt6/yrXOHwTS93Oo7Adx1usY5TzNg+8BKy9nmmA2FEBnRmz5cRfXDghnBA=="); - public static VersionSign VER_LIN_3_1_6 { get; } = new VersionSign("3.1.6 [Build: 1502873983]", ClientPlattform.Linux, "o+l92HKfiUF+THx2rBsuNjj/S1QpxG1fd5o3Q7qtWxkviR3LI3JeWyc26eTmoQoMTgI3jjHV7dCwHsK1BVu6Aw=="); - public static VersionSign VER_WIN_3_1_7 { get; } = new VersionSign("3.1.7 [Build: 1507896705]", ClientPlattform.Windows, "Iks42KIMcmFv5vzPLhziqahcPD2AHygkepr8xHNCbqx+li5n7Htbq5LE9e1YYhRhLoS4e2HqOpKkt+/+LC8EDA=="); - public static VersionSign VER_OSX_3_1_7 { get; } = new VersionSign("3.1.7 [Build: 1507896705]", ClientPlattform.Osx, "iM0IyUpaH9ak0gTtrHlRT0VGZa4rC51iZwSFwifK6iFqciSba/WkIQDWk9GUJN0OCCfatoc/fmlq8TPBnE5XCA=="); - public static VersionSign VER_LIN_3_1_7 { get; } = new VersionSign("3.1.7 [Build: 1513163251]", ClientPlattform.Linux, "/j5TZqPuOU8yMYPdGehvijYvU74KefRrKO5sgTUrkpeslNFiy4XfU7quKW0diLHQoPQn1t3KArdfzOAMk8dlAg=="); + public static VersionSign VER_IOS_3_X_X { get; } = new VersionSign("3.?.? [Build: 5680278000]", ClientPlattform.Ios, "XrAf+Buq6Eb0ehEW/niFp06YX+nGGOS0Ke4MoUBzn+cX9q6G5C0A/d5XtgcNMe8r9jJgV/adIYVpsGS3pVlSAA=="); + public static VersionSign VER_AND_3_X_X { get; } = new VersionSign("3.?.? [Build: 5680278000]", ClientPlattform.Android, "AWb948BY32Z7bpIyoAlQguSmxOGcmjESPceQe1DpW5IZ4+AW1KfTk2VUIYNfUPsxReDJMCtlhVKslzhR2lf0AA=="); + public static VersionSign VER_WIN_3_X_X { get; } = new VersionSign("3.?.? [Build: 5680278000]", ClientPlattform.Windows, "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="); + public static VersionSign VER_WIN_3_2_0 { get; } = new VersionSign("3.2.0 [Build: 1530183207]", ClientPlattform.Windows, "6LYlvxFIPCVnYmmFd64H8SRmxsX+y3IZOe9ju8PbZLI5j2K7ni6Z0BfEyhVprYP9VA5o4bXVrV+uNbdGSKnjCQ=="); + public static VersionSign VER_WIN_3_1_10 { get; } = new VersionSign("3.1.10 [Build: 1528537615]", ClientPlattform.Windows, "+/BWvueikGg4YkO1v2uuZB5vtJJgUZ5bL8cRfxAstfnCVdro2ja+4a+8rGUzDx8/vvTZOUVD6U95hnWb638MCQ=="); + public static VersionSign VER_LIN_3_1_10 { get; } = new VersionSign("3.1.10 [Build: 1528537615]", ClientPlattform.Linux, "jEfjYy09JfbJPZ+W3fwqygOu8uuc5raYTGpbJ5F8dHLHpqUfvmCyJVKoXRieMNkmPzeiylsUc9/HiV+8bt8tDw=="); + public static VersionSign VER_WIN_3_1_9 { get; } = new VersionSign("3.1.9 [Build: 1525442084]", ClientPlattform.Windows, "2SLjPTFXM9hQyNkeEGYIzs0fkBffyhsh5z+ZuaCcZdDfM8vgRM5lrAU6KNspFjLddcvw8cXw6gxRY73ZHsRVBg=="); + public static VersionSign VER_OSX_3_1_9 { get; } = new VersionSign("3.1.9 [Build: 1525442084]", ClientPlattform.Osx, "WVaMmYPig4eG2JUM8cMMW2MA7+IoRoPUSr74CPe7oS8TLHGjYxPr1FP88op6YsFFQrPJysWmIsnGR7BiFXjHCQ=="); + public static VersionSign VER_LIN_3_1_9 { get; } = new VersionSign("3.1.9 [Build: 1525442084]", ClientPlattform.Linux, "wBcnfNU7FA0CvFeisKhywZWzmUqD6IBFbYQTveMvxWowXUjWwNHTg9tbRLQ1YgBFDdlOwV36VMX7aAMXMX2rAA=="); + public static VersionSign VER_AND_3_1_8 { get; } = new VersionSign("3.1.8 [Build: 1516865456]", ClientPlattform.Android, "sG/qsKb9iZpBRXFSYY2Tuq7ZLUKHcmgA/6Qe/cx35L3risqoH4aGkPkDicuKtaQi8Ikh4IrQz6xe7V49M+8VBg=="); + public static VersionSign VER_IOS_3_1_8 { get; } = new VersionSign("3.1.8 [Build: 1516887927]", ClientPlattform.Ios, "pdWyIOpTWECIdA2NExrjqY1a7Q0alFyU7MgiDJYdiUXAspusOHwMIcfKm7oAh+Ty2gcgVgOh8wAPyZcKFKYXBA=="); public static VersionSign VER_WIN_3_1_8 { get; } = new VersionSign("3.1.8 [Build: 1516614607]", ClientPlattform.Windows, "gDEgQf/BiOQZdAheKccM1XWcMUj2OUQqt75oFuvF2c0MQMXyv88cZQdUuckKbcBRp7RpmLInto4PIgd7mPO7BQ=="); public static VersionSign VER_LIN_3_1_8 { get; } = new VersionSign("3.1.8 [Build: 1516614607]", ClientPlattform.Linux, "LJ5q+KWT4KwBX7oR/9j9A12hBrq5ds5ony99f9kepNmqFskhT7gfB51bAJNgAMOzXVCeaItNmc10F2wUNktqCw=="); - public static VersionSign VER_WIN_3_X_X { get; } = new VersionSign("3.?.? [Build: 5680278000]", ClientPlattform.Windows, "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="); - public static VersionSign VER_AND_3_X_X { get; } = new VersionSign("3.?.? [Build: 5680278000]", ClientPlattform.Android, "AWb948BY32Z7bpIyoAlQguSmxOGcmjESPceQe1DpW5IZ4+AW1KfTk2VUIYNfUPsxReDJMCtlhVKslzhR2lf0AA=="); - public static VersionSign VER_IOS_3_X_X { get; } = new VersionSign("3.?.? [Build: 5680278000]", ClientPlattform.Ios, "XrAf+Buq6Eb0ehEW/niFp06YX+nGGOS0Ke4MoUBzn+cX9q6G5C0A/d5XtgcNMe8r9jJgV/adIYVpsGS3pVlSAA=="); + public static VersionSign VER_LIN_3_1_7 { get; } = new VersionSign("3.1.7 [Build: 1513163251]", ClientPlattform.Linux, "/j5TZqPuOU8yMYPdGehvijYvU74KefRrKO5sgTUrkpeslNFiy4XfU7quKW0diLHQoPQn1t3KArdfzOAMk8dlAg=="); + public static VersionSign VER_OSX_3_1_7 { get; } = new VersionSign("3.1.7 [Build: 1512141423]", ClientPlattform.Osx, "PP+/cBUDtSyV0k7lm8aYvYWAs28KL+KmXa+f0pUpDqjQDKy8dnDzJp16F4YGJxJ+2ODGPkp5YQYwts3m8T7+CA=="); + public static VersionSign VER_WIN_3_1_7 { get; } = new VersionSign("3.1.7 [Build: 1513163251]", ClientPlattform.Windows, "tdNngCAZ1ImAf7BxJzO4RXv5nBRsUERsrSOnMKVUFNQg6BS4Bzag0RFgLVzs2DRj19AC8+q5cXgH+5Ms50mTCA=="); + public static VersionSign VER_WIN_3_1_6 { get; } = new VersionSign("3.1.6 [Build: 1502873983]", ClientPlattform.Windows, "73fB82Jt1lmIRHKBFaE8h1JKPGFbnt6/yrXOHwTS93Oo7Adx1usY5TzNg+8BKy9nmmA2FEBnRmz5cRfXDghnBA=="); + public static VersionSign VER_LIN_3_1_6 { get; } = new VersionSign("3.1.6 [Build: 1502873983]", ClientPlattform.Linux, "o+l92HKfiUF+THx2rBsuNjj/S1QpxG1fd5o3Q7qtWxkviR3LI3JeWyc26eTmoQoMTgI3jjHV7dCwHsK1BVu6Aw=="); + public static VersionSign VER_WIN_3_1_5 { get; } = new VersionSign("3.1.5 [Build: 1500537355]", ClientPlattform.Windows, "O9WqHB9oX0qe9AXIYmJm0+mzl6VLxNvrGF0lGlovLaig5MXUIwd6T00NkCj62OkBbzM3eECs9FUuJk7N8V0dCg=="); + public static VersionSign VER_WIN_3_1_4_2 { get; } = new VersionSign("3.1.4.2 [Build: 1498644101]", ClientPlattform.Windows, "WtscrpvJG13kbF6aoVzsGwQuE/WwR1b8++ydDc8IpmiXLw+zFC6zFUvLinOeE0zZgh2Hs5Amp3DZoPJSynOWBg=="); + public static VersionSign VER_WIN_3_1_4 { get; } = new VersionSign("3.1.4 [Build: 1491993378]", ClientPlattform.Windows, "rwdyEwnJCzbVfNCqbxMrRyhL5BSYqYSzKQkeZ6m5KImc1F8VB8wEkwwwyxoG7SimC/sxIyy4h27CjBFP6rcgBQ=="); + public static VersionSign VER_WIN_3_1_3 { get; } = new VersionSign("3.1.3 [Build: 1490279472]", ClientPlattform.Windows, "7RPY2bzJmMdgVX24VuKD3lTnYYb6yHWqfn2x21tFOjXL9q+2t7tU9Vy8Bh5/IpeiqklUHTWc23mWpYOCoW9eCA=="); + public static VersionSign VER_WIN_3_1_2 { get; } = new VersionSign("3.1.2 [Build: 1489662774]", ClientPlattform.Windows, "5Aaj21gGFtrjW9424ezfLa1SMQBpZvgQgcJLZmrLoNMe4XebBPV2s8rxEDAIodfFpruLxLFbFpH63A/BGnJyDw=="); + public static VersionSign VER_WIN_3_1_1_1 { get; } = new VersionSign("3.1.1.1 [Build: 1487668590]", ClientPlattform.Windows, "CchjMitGiVGfRlGph0D1mDjOCJCnkVxR/WuYvNHdPyeQUCncRWML8jYxYfnhRF6CzViwYRnsmZkN+W5oenB2CQ=="); + public static VersionSign VER_WIN_3_1_1 { get; } = new VersionSign("3.1.1 [Build: 1486712038]", ClientPlattform.Windows, "sryyx++NhRWKDAo+Tnwv9N+IrOaQBP0XjjDszY0BBv0YIMr4jmdHtgrwzWkUqhU7kfql7qBWIhlb/r0l1ZHeBw=="); + public static VersionSign VER_WIN_3_1_0_1 { get; } = new VersionSign("3.1.0.1 [Build: 1484223040]", ClientPlattform.Windows, "oaaorJ4co/sS2m5JT5oRiu9AieW6kfFY+RENqPfp26iP4pbWbf9GcZj+JhDA+/JyLpfueCcSulZSRRbash2JCw=="); + public static VersionSign VER_WIN_3_1 { get; } = new VersionSign("3.1 [Build: 1481795005]", ClientPlattform.Windows, "3TpZZM0V+PKHELFnsfRPoKjEFfvfHUL/6mUP5LHbI3nvmdOjRqEEKi4ndXZG6OpWOKQ3VeadHDH0KBfD8EI2Cg=="); + public static VersionSign VER_AND_3_0_23 { get; } = new VersionSign("3.0.23 [Build: 1463662487]", ClientPlattform.Android, "RN+cwFI+jSHJEhggucIuUyEteWNVFy4iw0QDp3qn2UzfopypFVE9BPZqJjBUGeoCN7Q/SfYL4RNIRzJEQaZUCA=="); + public static VersionSign VER_WIN_3_0_20 { get; } = new VersionSign("3.0.20 [Build: 1465542546]", ClientPlattform.Windows, "vDK31sOwOvDpTXgqAJzmR1NzeUeSDG9dLMgIz5LCX+KpDSVD/qU60mzScz9tuc9AsLyrL8DxHpDDO3eQD+hYCA=="); + public static VersionSign VER_WIN_3_0_19_4 { get; } = new VersionSign("3.0.19.4 [Build: 1468491418]", ClientPlattform.Windows, "ldWL49uDKC3N9uxdgWRMTOzUuiG1nBqUiOa+Nal5HvdxJiN4fsTnmmPo5tvglN7WqoVoFfuuKuYq1LzodtEtCg=="); + public static VersionSign VER_OSX_3_0_19_4 { get; } = new VersionSign("3.0.19.4 [Build: 1468491418]", ClientPlattform.Osx, "Pvcizdk3HRQMzTLt7goUYBmmS5nbAS1g2E6HIypLU+9eXTqGTBLim0UUtKc0s867TFHbK91GroDrTtv0aMUGAw=="); + public static VersionSign VER_LIN_3_0_19_4 { get; } = new VersionSign("3.0.19.4 [Build: 1468491418]", ClientPlattform.Linux, "jvhhk75EV3nCGeewx4Y5zZmiZSN07q5ByKZ9Wlmg85aAbnw7c1jKq5/Iq0zY6dfGwCEwuKod0I5lQcVLf2NTCg=="); + public static VersionSign VER_WIN_3_0_19_3 { get; } = new VersionSign("3.0.19.3 [Build: 1466672534]", ClientPlattform.Windows, "a1OYzvM18mrmfUQBUgxYBxYz2DUU6y5k3/mEL6FurzU0y97Bd1FL7+PRpcHyPkg4R+kKAFZ1nhyzbgkGphDWDg=="); + public static VersionSign VER_WIN_3_0_19_2 { get; } = new VersionSign("3.0.19.2 [Build: 1466597785]", ClientPlattform.Windows, "sDOzu7rCGb7kBID2WbBk35DjPijKkXzujnsAtLhXxhkQ+am0JlDOpuU1ISHhq9gCl/Qo0dzc723o0AIPI+yoCQ=="); + public static VersionSign VER_WIN_3_0_19_1 { get; } = new VersionSign("3.0.19.1 [Build: 1461588969]", ClientPlattform.Windows, "KYo52MA89dowkYpFU1KixgHngjbJ6F2Yi++5tbaqBlBpz9YikX2gI3sqmU1kP1ghsKCLKM7o0patDH1hv9bmAg=="); + public static VersionSign VER_WIN_3_0_19 { get; } = new VersionSign("3.0.19 [Build: 1459504131]", ClientPlattform.Windows, "JoHyZHF4k/a3+QH1zPNSEzc40487fzbpssyRZtoWB5kbQorAJgwlpcScA08J4vjGoUbdaTZsT0vCw56wo/Q9Ag=="); + public static VersionSign VER_WIN_3_0_18_2 { get; } = new VersionSign("3.0.18.2 [Build: 1445512488]", ClientPlattform.Windows, "F0hY25Dtja0wcU6dzC39rNuYbhnDAbIwPHC3VO9Oicf13kUY2I2g6scPZ3p195Cw9gUYdBIRYm8ucHEhtSeWCw=="); + public static VersionSign VER_WIN_3_0_18_1 { get; } = new VersionSign("3.0.18.1 [Build: 1444491275]", ClientPlattform.Windows, "xqfa3CUd2GFiTqjJWYzcu9ZbxVVLng8qIMKlVxMqWdiM8JrTRiXBAaTBDd8Xc+flVe+rGSIOZTkXRsz1rqjiAA=="); + public static VersionSign VER_WIN_3_0_18 { get; } = new VersionSign("3.0.18 [Build: 1442998335]", ClientPlattform.Windows, "vUgm8mJoeVLBG6qB2HcYF7YNG4D+H/4edILaZbHze2Unua6mrBvNmbtRkRtmRyDZSd7sVQHMApinRDgGT1mUBw=="); + public static VersionSign VER_WIN_3_0_17 { get; } = new VersionSign("3.0.17 [Build: 1438673913]", ClientPlattform.Windows, "znDjHvCgmQF/jQKTK49X8tnXqF7AGXfS2XYcogww4XxNTBxp2tf1aFc/jgboKco9EuVa0ku2cf/xg9wW3Cm7AQ=="); + public static VersionSign VER_WIN_3_0_16 { get; } = new VersionSign("3.0.16 [Build: 1407159763]", ClientPlattform.Windows, "Y1DuQGXo/8/rYznEGyeQHgpvZMuiCH4FYm4QVyAgLYyMpNpc/LM7XetVWhDQxGsNejkN/2olI7GVJkt4X+ooDg=="); + public static VersionSign VER_LIN_3_0_16 { get; } = new VersionSign("3.0.16 [Build: 1407159763]", ClientPlattform.Linux, "8776GitHAgkFPfOLxEh5x+Luuh4NrYPEJUdsUzNKndcAuWMYjwQTZkmeZOeG/swdn/p2Cg2pRfZfsIFSOAUWCQ=="); + public static VersionSign VER_WIN_3_0_15_1 { get; } = new VersionSign("3.0.15.1 [Build: 1405341092]", ClientPlattform.Windows, "b+hr0KQWOVW2WEn49BmNb08R9zimsJcThm2gEeF7EAgRUeUDYzeplh5HrHmda0ftbbnrzWV33U/GOo2LAs/rAg=="); + public static VersionSign VER_WIN_3_0_15 { get; } = new VersionSign("3.0.15 [Build: 1403250090]", ClientPlattform.Windows, "FKKAHPwV1swKwH6mqHqdcGuYm8o5mZw4WreBxJrQjOprC3NXXcJviPe0p7EZPI810HOWMfmQRUgFpggoRL8kAQ=="); + public static VersionSign VER_WIN_3_0_14 { get; } = new VersionSign("3.0.14 [Build: 1394624943]", ClientPlattform.Windows, "F0WIO9sBVzG893AtX2Jfd98cH6yZPAnfMBNvBlQbAIfvfyiq+cbjZ31AUngEjq7UPIYdnYSsdRX9hczwdBrKAQ=="); + public static VersionSign VER_WIN_3_0_13_1 { get; } = new VersionSign("3.0.13.1 [Build: 1382530211]", ClientPlattform.Windows, "bCIfLPUgTM6C0kNkesvhcxaDPvV9h6qLbYVy9cQVSP5lzaYebZaeDzAOOHsdjKcRTa6LU1oHEdz9D/d+2gxJCw=="); + public static VersionSign VER_WIN_3_0_13 { get; } = new VersionSign("3.0.13 [Build: 1380283653]", ClientPlattform.Windows, "7dA+6EbVyMevol4gE3/Cu1WonRjqu1C6pTWF+txApbaiTgKtZ/ky+NVxluPkSDnCxXN1pOR4uGdF6B7LUqQgDQ=="); + public static VersionSign VER_WIN_3_0_12 { get; } = new VersionSign("3.0.12 [Build: 1378715177]", ClientPlattform.Windows, "x6wFA5xqjenf6kbAh36IC4CkrbT8/uSBpgjM9juSt9oxGCXLqHOC2oaYlB1zZSJZjT4sOrnp0M+uOdVjYCzLCg=="); + public static VersionSign VER_WIN_3_0_11_1 { get; } = new VersionSign("3.0.11.1 [Build: 1375773286]", ClientPlattform.Windows, "Qfvcn4uQmKETDsD4LbtdbZR8rDetJ26Z/bVbu5SZJjMjGlYEMSbJnR4PtOBshdMSEwEsAJf1G+5tjx+onm2fDA=="); + public static VersionSign VER_WIN_3_0_11 { get; } = new VersionSign("3.0.11 [Build: 1375083581]", ClientPlattform.Windows, "54wPDkfv0kT56UE0lv/LFkFJObH+Q4Irmo4Brfz1EcvjVhj8hJ+RCHcVTZsdKU2XvVvh+VLJpURulEHsAOsyBw=="); // ReSharper restore InconsistentNaming, UnusedMember.Global } diff --git a/TS3Client/Generated/Versions.tt b/TS3Client/Generated/Versions.tt index 2abdb0d8..3cd17aea 100644 --- a/TS3Client/Generated/Versions.tt +++ b/TS3Client/Generated/Versions.tt @@ -48,7 +48,7 @@ namespace TS3Client.Full /// Describes a triple of version, platform and a cryptographical signature (usually distributed by "TeamSpeak Systems"). /// Each triple has to match and is not interchangeable with other triple parts. /// - public class VersionSign + public sealed class VersionSign { private static readonly string[] Plattforms = { null, "Windows", "Linux", "OS X", "Android", "iOS" }; @@ -75,11 +75,24 @@ namespace TS3Client.Full PlattformName = plattform; } - // Many ids implemented from here: https://r4p3.net/threads/client-builds.499/ + public bool CheckValid() => Ts3Crypt.EdCheck(this); - // ReSharper disable InconsistentNaming, UnusedMember.Global - <# foreach (var line in data.Skip(1)) { var ver = dict[line[1]]; #> - public static VersionSign VER_<#= ver.plat #>_<#= BuildToFld(line[0]) #> { get; } = new VersionSign("<#= line[0] #>", ClientPlattform.<#= ver.enu #>, "<#= line[2] #>");<# } #> + // ReSharper disable InconsistentNaming, UnusedMember.Global<# + var header = data[0]; + int ichan = Array.IndexOf(header, "channel"); + int iname = Array.IndexOf(header, "version"); + int iplat = Array.IndexOf(header, "platform"); + int ihash = Array.IndexOf(header, "hash"); + var duplicates = new HashSet(); + + foreach (var line in data.Skip(1).Reverse()) { + var ver = dict[line[iplat]]; + var fldName = $"VER_{ver.plat}_{BuildToFld(line[iname])}"; + if (duplicates.Contains(fldName)) + continue; + duplicates.Add(fldName); + #> + public static VersionSign <#= fldName #> { get; } = new VersionSign("<#= line[iname] #>", ClientPlattform.<#= ver.enu #>, "<#= line[ihash] #>");<# } #> // ReSharper restore InconsistentNaming, UnusedMember.Global } diff --git a/TS3Client/Helper/Extensions.cs b/TS3Client/Helper/Extensions.cs index e9332142..a176443a 100644 --- a/TS3Client/Helper/Extensions.cs +++ b/TS3Client/Helper/Extensions.cs @@ -25,7 +25,7 @@ public static string ErrorFormat(this CommandError error) return $"{error.Id}: the command failed to execute: {error.Message}"; } - public static R WrapSingle(this R, CommandError> result) where T : class + public static R WrapSingle(in this R result) where T : class { if (result.Ok) return WrapSingle(result.Value); @@ -40,13 +40,40 @@ internal static R WrapSingle(this IEnumerable enu) where return R.Err(Util.NoResultCommandError); } - internal static R, CommandError> UnwrapNotification(this R result) where T : class + internal static R UnwrapNotification(in this R result) where T : class { if (!result.Ok) return result.Error; - return R, CommandError>.OkR(result.Value.Notifications.Cast()); + return R.OkR((T[])result.Value.Notifications); } - internal static string NewString(this ReadOnlySpan span) => new string(span.ToArray()); + internal static string NewString(in this ReadOnlySpan span) => span.ToString(); + + // TODO add optional improvement when nc2.1 is available + internal static string NewUtf8String(this ReadOnlySpan span) => System.Text.Encoding.UTF8.GetString(span.ToArray()); + + internal static ReadOnlySpan Trim(this ReadOnlySpan span, byte elem) => span.TrimStart(elem).TrimEnd(elem); + + internal static ReadOnlySpan TrimStart(this ReadOnlySpan span, byte elem) + { + int start = 0; + for (; start < span.Length; start++) + { + if (span[start] != elem) + break; + } + return span.Slice(start); + } + + internal static ReadOnlySpan TrimEnd(this ReadOnlySpan span, byte elem) + { + int end = span.Length - 1; + for (; end >= 0; end--) + { + if (span[end] != elem) + break; + } + return span.Slice(0, end + 1); + } } } diff --git a/TS3Client/Helper/NativeWinDllLoader.cs b/TS3Client/Helper/NativeLibraryLoader.cs similarity index 55% rename from TS3Client/Helper/NativeWinDllLoader.cs rename to TS3Client/Helper/NativeLibraryLoader.cs index 8d700800..2377c29f 100644 --- a/TS3Client/Helper/NativeWinDllLoader.cs +++ b/TS3Client/Helper/NativeLibraryLoader.cs @@ -13,23 +13,42 @@ namespace TS3Client.Helper using System.IO; using System.Runtime.InteropServices; - internal static class NativeWinDllLoader + internal static class NativeLibraryLoader { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LoadLibrary(string dllToLoad); - public static void DirectLoadLibrary(string lib) + public static bool DirectLoadLibrary(string lib, Action dummyLoad = null) { if (Util.IsLinux) - return; - - var libPath = Path.Combine(ArchFolder, lib); - Log.Debug("Loading \"{0}\" from \"{1}\"", lib, libPath); - var handle = LoadLibrary(libPath); - if (handle == IntPtr.Zero) - Log.Error("Failed to load library \"{0}\" from \"{1}\", error: {2}", lib, libPath, Marshal.GetLastWin32Error()); + { + if (dummyLoad != null) + { + try + { + dummyLoad.Invoke(); + } + catch (DllNotFoundException ex) + { + Log.Error(ex, "Failed to load library \"{0}\".", lib); + return false; + } + } + } + else + { + var libPath = Path.Combine(ArchFolder, lib); + Log.Debug("Loading \"{0}\" from \"{1}\"", lib, libPath); + var handle = LoadLibrary(libPath); + if (handle == IntPtr.Zero) + { + Log.Error("Failed to load library \"{0}\" from \"{1}\", error: {2}", lib, libPath, Marshal.GetLastWin32Error()); + return false; + } + } + return true; } public static string ArchFolder diff --git a/TS3Client/Helper/R.cs b/TS3Client/Helper/R.cs index 25183c69..82a394b8 100644 --- a/TS3Client/Helper/R.cs +++ b/TS3Client/Helper/R.cs @@ -7,154 +7,132 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -using System; - -// ReSharper disable CheckNamespace -#pragma warning disable IDE0016 - -/// -/// Provides a safe alternative to Exceptions for error and result wrapping. -/// This type represents either success or an error + message. -/// -public struct R +namespace System { - public static readonly R OkR = new R(); - - public bool Ok => Error == null; - public string Error { get; } + using System.Diagnostics; - private R(string error) { Error = error ?? throw new ArgumentNullException(nameof(error), "Error must not be null."); } - /// Creates a new failed result with a message - /// The message - public static R Err(string error) => new R(error); - - public static implicit operator bool(R result) => result.Ok; - public static implicit operator string(R result) => result.Error; - - public static implicit operator R(string error) => new R(error); + /// + /// Provides a safe alternative to Exceptions for error and result wrapping. + /// This type represents either success or an error + message. + /// + public static class R + { + public static readonly _Ok Ok = new _Ok(); + public static readonly _Error Err = new _Error(); + } - public override string ToString() => Error; -} + public readonly struct _Ok { } + public readonly struct _Error { } + + /// + /// Provides a safe alternative to Exceptions for error and result wrapping. + /// This type represents either success + value or an error + message. + /// The value is guaranteed to be non-null when successful. + /// + /// The type of the success value. + [DebuggerDisplay("{Ok ? (\"Ok : \" + typeof(TSuccess).Name) : \"Err\", nq}")] + public readonly struct R + { + public static readonly R ErrR = new R(); -/// -/// Provides a safe alternative to Exceptions for error and result wrapping. -/// This type represents either success + value or an error + message. -/// The value is guaranteed to be non-null when successful. -/// -/// The type of the success value. -public struct R -{ - private readonly bool isError; - public bool Ok => !isError; - public string Error { get; } - public TSuccess Value { get; } + private readonly bool isOk; + public bool Ok => isOk; + public TSuccess Value { get; } - private R(TSuccess value) { isError = false; Error = null; if (value == null) throw new ArgumentNullException(nameof(value), "Return of ok must not be null."); Value = value; } - private R(string error) { isError = true; Error = error ?? throw new ArgumentNullException(nameof(error), "Error must not be null."); Value = default; } - //internal R(bool isError, TSuccess value) + private R(TSuccess value) { isOk = true; if (value == null) throw new ArgumentNullException(nameof(value), "Return of ok must not be null."); Value = value; } - /// Creates a new failed result with a message - /// The message - public static R Err(string error) => new R(error); - /// Creates a new successful result with a value - /// The value - public static R OkR(TSuccess value) => new R(value); + /// Creates a new successful result with a value + /// The value + public static R OkR(TSuccess value) => new R(value); - public static implicit operator bool(R result) => result.Ok; - public static implicit operator string(R result) => result.Error; + public static implicit operator bool(R result) => result.Ok; - public static implicit operator R(TSuccess result) => new R(result); - public static implicit operator R(string error) => new R(error); + public static implicit operator R(TSuccess result) => new R(result); - // Unwrapping - public TSuccess OkOr(TSuccess alt) => Ok ? Value : alt; - public TSuccess Unwrap() => Ok ? Value : throw new InvalidOperationException("Called upwrap on error"); + // Convenience casting + public static implicit operator R(_Error _) => ErrR; - public override string ToString() => Error; -} + // Unwrapping + public TSuccess OkOr(TSuccess alt) => Ok ? Value : alt; + public TSuccess Unwrap() => Ok ? Value : throw new InvalidOperationException("Called upwrap on error"); + } -/// -/// Provides a safe alternative to Exceptions for error and result wrapping. -/// This type represents either success + value or an error + error-object. -/// The value is guaranteed to be non-null when successful. -/// -/// The type of the success value. -/// The error type. -public struct R -{ - private readonly bool isError; - public bool Ok => !isError; - public TError Error { get; } - public TSuccess Value { get; } - - private R(TSuccess value) { isError = false; Error = default; if (value == null) throw new ArgumentNullException(nameof(value), "Return of ok must not be null."); Value = value; } - private R(TError error) { isError = true; Value = default; if (error == null) throw new ArgumentNullException(nameof(error), "Error must not be null."); Error = error; } - internal R(bool isError, TSuccess value, TError error) { this.isError = isError; Value = value; Error = error; } - - /// Creates a new failed result with an error object - /// The error - public static R Err(TError error) => new R(error); - /// Creates a new successful result with a value - /// The value - public static R OkR(TSuccess value) => new R(value); - - public static implicit operator bool(R result) => result.Ok; - public static implicit operator TError(R result) => result.Error; - - public static implicit operator R(TSuccess result) => new R(result); - public static implicit operator R(TError error) => new R(error); - - // Unwrapping - public TSuccess OkOr(TSuccess alt) => Ok ? Value : alt; - public TSuccess Unwrap() => Ok ? Value : throw new InvalidOperationException("Called upwrap on error"); - - // Downwrapping - public E OnlyError() => new E(isError, Error); - public static implicit operator E(R result) => result.OnlyError(); -} + /// + /// Provides a safe alternative to Exceptions for error and result wrapping. + /// This type represents either success + value or an error + error-object. + /// The value is guaranteed to be non-null when successful. + /// + /// The type of the success value. + /// The error type. + [DebuggerDisplay("{Ok ? (\"Ok : \" + typeof(TSuccess).Name) : (\"Err : \" + typeof(TError).Name), nq}")] + public readonly struct R + { + private readonly bool isError; + public bool Ok => !isError; + public TError Error { get; } + public TSuccess Value { get; } + + private R(TSuccess value) { isError = false; Error = default; if (value == null) throw new ArgumentNullException(nameof(value), "Return of ok must not be null."); Value = value; } + private R(TError error) { isError = true; Value = default; if (error == null) throw new ArgumentNullException(nameof(error), "Error must not be null."); Error = error; } + internal R(bool isError, TSuccess value, TError error) { this.isError = isError; Value = value; Error = error; } + + /// Creates a new failed result with an error object + /// The error + public static R Err(TError error) => new R(error); + /// Creates a new successful result with a value + /// The value + public static R OkR(TSuccess value) => new R(value); + + public static implicit operator bool(R result) => result.Ok; + public static implicit operator TError(R result) => result.Error; + + public static implicit operator R(TSuccess result) => new R(result); + public static implicit operator R(TError error) => new R(error); + + // Unwrapping + public TSuccess OkOr(TSuccess alt) => Ok ? Value : alt; + public TSuccess Unwrap() => Ok ? Value : throw new InvalidOperationException("Called upwrap on error"); + + // Downwrapping + public E OnlyError() => new E(isError, Error); + public static implicit operator E(R result) => result.OnlyError(); + } -/// -/// Provides a safe alternative to Exceptions for error and result wrapping. -/// This type represents either success or an error + error object. -/// -/// The type of the error value. -public struct E -{ - /// Represents a successful state. - public static E OkR { get; } = new E(); + /// + /// Provides a safe alternative to Exceptions for error and result wrapping. + /// This type represents either success or an error + error object. + /// + /// The type of the error value. + [DebuggerDisplay("{Ok ? \"Ok\" : (\"Err : \" + typeof(TError).Name), nq}")] + public readonly struct E + { + /// Represents a successful state. + public static E OkR { get; } = new E(); - private readonly bool isError; - public bool Ok => !isError; - public TError Error { get; } + private readonly bool isError; + public bool Ok => !isError; + public TError Error { get; } - private E(TError error) { isError = true; if (error == null) throw new ArgumentNullException(nameof(error), "Error must not be null."); Error = error; } - internal E(bool isError, TError error) { this.isError = isError; Error = error; } // No null check here, we already check cosistently. + private E(TError error) { isError = true; if (error == null) throw new ArgumentNullException(nameof(error), "Error must not be null."); Error = error; } + internal E(bool isError, TError error) { this.isError = isError; Error = error; } // No null check here, we already check cosistently. - /// Creates a new failed result with a message - /// The message - public static E Err(TError error) => new E(error); + /// Creates a new failed result with a error object. + /// The error object. + public static E Err(TError error) => new E(error); - public static implicit operator bool(E result) => result.Ok; - public static implicit operator TError(E result) => result.Error; + public static implicit operator bool(E result) => result.Ok; + public static implicit operator TError(E result) => result.Error; - public static implicit operator E(TError result) => new E(result); + public static implicit operator E(TError result) => new E(result); - // Upwrapping - public R WithValue(TSuccess value) - { - if (!isError && value == null) throw new ArgumentNullException(nameof(value), "Value must not be null."); - return new R(isError, value, Error); - } -} + // Convenience casting + public static implicit operator E(_Ok _) => OkR; -public static class RExtensions -{ - public static R ToR(this T obj) where T : class - { - if (obj != null) - return R.OkR(obj); - return R.Err("Result is empty"); + // Upwrapping + public R WithValue(TSuccess value) + { + if (!isError && value == null) throw new ArgumentNullException(nameof(value), "Value must not be null."); + return new R(isError, value, Error); + } } } - -#pragma warning restore IDE0016 diff --git a/TS3Client/Helper/SpanSplitter.cs b/TS3Client/Helper/SpanSplitter.cs index c7b6942c..59c32a04 100644 --- a/TS3Client/Helper/SpanSplitter.cs +++ b/TS3Client/Helper/SpanSplitter.cs @@ -12,30 +12,21 @@ namespace TS3Client.Helper using System; using System.Runtime.CompilerServices; - internal class SpanSplitter + internal struct SpanSplitter where T: IEquatable { public bool HasNext => NextIndex >= 0; public int NextIndex { get; private set; } - private char splitchar; + private T splitchar; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan First(string str, char split) + public void First(in ReadOnlySpan span, T split) { splitchar = split; - var span = str.AsReadOnlySpan(); NextIndex = span.IndexOf(split); - return span; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void First(ReadOnlySpan span, char split) - { - splitchar = split; - NextIndex = span.IndexOf(split); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan Next(ReadOnlySpan current) + public ReadOnlySpan Next(in ReadOnlySpan current) { if(!HasNext) throw new InvalidOperationException("No next element in span split"); @@ -45,6 +36,6 @@ public ReadOnlySpan Next(ReadOnlySpan current) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan Trim(ReadOnlySpan current) => HasNext ? current.Slice(0, NextIndex) : current; + public ReadOnlySpan Trim(in ReadOnlySpan current) => HasNext ? current.Slice(0, NextIndex) : current; } } diff --git a/TS3Client/Helper/Util.cs b/TS3Client/Helper/Util.cs index f568a86f..6892997f 100644 --- a/TS3Client/Helper/Util.cs +++ b/TS3Client/Helper/Util.cs @@ -30,7 +30,7 @@ public static bool IsLinux public static void Init(out T fld) where T : new() => fld = new T(); - public static Encoding Encoder { get; } = new UTF8Encoding(false); + public static Encoding Encoder { get; } = new UTF8Encoding(false, false); public static readonly DateTime UnixTimeStart = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); @@ -49,6 +49,8 @@ public static bool IsLinux public static CommandError NoResultCommandError { get; } = CustomError("Result is empty"); + public static CommandError ParserCommandError { get; } = CustomError("Result could not be parsed"); + public static CommandError CustomError(string message) => new CommandError { Id = Ts3ErrorCode.custom_error, Message = message }; } @@ -57,7 +59,7 @@ internal sealed class MissingEnumCaseException : Exception public MissingEnumCaseException(string enumTypeName, string valueName) : base($"The the switch does not handle the value \"{valueName}\" from \"{enumTypeName}\".") { } public MissingEnumCaseException(string message, Exception inner) : base(message, inner) { } } - + internal static class DebugUtil { public static string DebugToHex(byte[] bytes) => bytes == null ? "" : DebugToHex(bytes.AsSpan()); @@ -76,9 +78,9 @@ public static string DebugToHex(ReadOnlySpan bytes) } return new string(c); } - + public static byte[] DebugFromHex(string hex) - => hex.Split(new []{' '}, StringSplitOptions.RemoveEmptyEntries) + => hex.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => Convert.ToByte(x, 16)).ToArray(); } } diff --git a/TS3Client/LazyNotification.cs b/TS3Client/LazyNotification.cs index f59a8a6d..11aaf729 100644 --- a/TS3Client/LazyNotification.cs +++ b/TS3Client/LazyNotification.cs @@ -10,27 +10,26 @@ namespace TS3Client { using Messages; - using Helper; - using System.Collections.Generic; + using System; using System.Linq; - public struct LazyNotification + public readonly struct LazyNotification { - public readonly IEnumerable Notifications; + public readonly INotification[] Notifications; public readonly NotificationType NotifyType; - public LazyNotification(IEnumerable notifications, NotificationType notifyType) + public LazyNotification(INotification[] notifications, NotificationType notifyType) { Notifications = notifications; NotifyType = notifyType; } - public R WrapSingle() where T : INotification + public R WrapSingle() where T : INotification { var first = Notifications.FirstOrDefault(); if (first == null) - return R.Err(Util.NoResultCommandError); - return R.OkR((T)first); + return R.ErrR; + return R.OkR((T)first); } } } diff --git a/TS3Client/MessageProcessor.cs b/TS3Client/MessageProcessor.cs index c7781157..285d15a0 100644 --- a/TS3Client/MessageProcessor.cs +++ b/TS3Client/MessageProcessor.cs @@ -20,22 +20,25 @@ internal abstract class BaseMessageProcessor protected static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); protected readonly List[] dependingBlocks; - protected string cmdLineBuffer; + protected ReadOnlyMemory cmdLineBuffer; protected readonly object waitBlockLock = new object(); + const byte AsciiSpace = (byte)' '; + public BaseMessageProcessor() { dependingBlocks = new List[Enum.GetValues(typeof(NotificationType)).Length]; } - public LazyNotification? PushMessage(string message) + public LazyNotification? PushMessage(ReadOnlyMemory message) { + var msgSpan = message.Span; string notifyname; - int splitindex = message.IndexOf(' '); + int splitindex = msgSpan.IndexOf(AsciiSpace); if (splitindex < 0) - notifyname = message.TrimEnd(); + notifyname = msgSpan.TrimEnd(AsciiSpace).NewUtf8String(); else - notifyname = message.Substring(0, splitindex); + notifyname = msgSpan.Slice(0, splitindex).NewUtf8String(); bool hasEqual; NotificationType ntfyType; @@ -48,13 +51,19 @@ public BaseMessageProcessor() return null; } - var lineDataPart = splitindex < 0 ? "" : message.Substring(splitindex); + var lineDataPart = splitindex < 0 ? ReadOnlySpan.Empty : msgSpan.Slice(splitindex); // if it's not an error it is a notification if (ntfyType != NotificationType.CommandError) { var notification = Deserializer.GenerateNotification(lineDataPart, ntfyType); - var lazyNotification = new LazyNotification(notification, ntfyType); + if (!notification.Ok) + { + Log.Warn("Got unparsable message. ({0})", msgSpan.NewUtf8String()); + return null; + } + + var lazyNotification = new LazyNotification(notification.Value, ntfyType); lock (waitBlockLock) { var dependantList = dependingBlocks[(int)ntfyType]; diff --git a/TS3Client/Messages/BaseTypes.cs b/TS3Client/Messages/BaseTypes.cs index 0d420815..33e166f8 100644 --- a/TS3Client/Messages/BaseTypes.cs +++ b/TS3Client/Messages/BaseTypes.cs @@ -10,18 +10,20 @@ namespace TS3Client.Messages { using System; + using System.Collections.Generic; - public interface IQueryMessage + public interface IMessage { - void SetField(string name, ReadOnlySpan value); + void SetField(string name, ReadOnlySpan value); + void Expand(IMessage[] to, IEnumerable flds); } - public interface INotification : IQueryMessage + public interface INotification : IMessage { NotificationType NotifyType { get; } } - public interface IResponse : IQueryMessage + public interface IResponse : IMessage { string ReturnCode { get; set; } } diff --git a/TS3Client/Messages/Deserializer.cs b/TS3Client/Messages/Deserializer.cs index 2bc462c4..ca0c6053 100644 --- a/TS3Client/Messages/Deserializer.cs +++ b/TS3Client/Messages/Deserializer.cs @@ -12,73 +12,143 @@ namespace TS3Client.Messages using Helper; using System; using System.Collections.Generic; - using System.Linq; public static class Deserializer { public static event EventHandler OnError; + private const byte AsciiSpace = (byte)' '; + private const byte AsciiPipe = (byte)'|'; + private const byte AsciiEquals = (byte)'='; + // data to notification - internal static IEnumerable GenerateNotification(string lineDataPart, NotificationType ntfyType) + internal static R GenerateNotification(ReadOnlySpan line, NotificationType ntfyType) { if (ntfyType == NotificationType.Unknown) - throw new ArgumentException("The NotificationType must not be unknown", nameof(lineDataPart)); + throw new ArgumentException("The NotificationType must not be unknown", nameof(ntfyType)); - if (lineDataPart == null) - throw new ArgumentNullException(nameof(lineDataPart)); + var pipes = PipeList(line); + var arr = MessageHelper.InstatiateNotificationArray(ntfyType, (pipes?.Count ?? 0) + 1); + return Dersialize(arr, line, pipes); + } - return lineDataPart.TrimStart().Split('|').Select(msg => GenerateSingleNotification(msg, ntfyType)).Where(x => x.Ok).Select(x => x.Value); + private static List PipeList(ReadOnlySpan line) + { + List pipes = null; + for (int i = 0; i < line.Length; i++) + if (line[i] == AsciiPipe) + (pipes = pipes ?? new List()).Add(i); + return pipes; } - internal static R GenerateSingleNotification(string lineDataPart, NotificationType ntfyType) + private static R Dersialize(T[] arr, ReadOnlySpan line, List pipes) where T : IMessage + { + if (pipes == null || pipes.Count == 0) + { + if (!ParseKeyValueLine(arr[0], line, null, null)) + return R.Err; + return arr; + } + + var arrItems = new HashSet(); + var single = new List(); + + // index using the last one + if (!ParseKeyValueLine(arr[arr.Length - 1], line.Slice(pipes[pipes.Count - 1] + 1).Trim(AsciiSpace), arrItems, null)) + return R.Err; + + for (int i = 0; i < pipes.Count - 1; i++) + { + if (!ParseKeyValueLine(arr[i + 1], line.Slice(pipes[i] + 1, pipes[i + 1] - pipes[i] - 1), null, null)) + return R.Err; + } + + // trim with the first one + if (!ParseKeyValueLine(arr[0], line.Slice(0, pipes[0]), arrItems, single)) + return R.Err; + + if (arrItems.Count > 0) + { + arr[0].Expand((IMessage[])(object)arr, single); + } + return arr; + } + + internal static R GenerateSingleNotification(ReadOnlySpan line, NotificationType ntfyType) { if (ntfyType == NotificationType.Unknown) - throw new ArgumentException("The NotificationType must not be unknown", nameof(lineDataPart)); + throw new ArgumentException("The NotificationType must not be unknown", nameof(ntfyType)); - if (lineDataPart == null) - throw new ArgumentNullException(nameof(lineDataPart)); + if (line.IsEmpty) + throw new ArgumentNullException(nameof(line)); - var notification = MessageHelper.GenerateNotificationType(ntfyType); - return ParseKeyValueLine(notification, lineDataPart); + var result = GenerateNotification(line, ntfyType); + if (!result.Ok || result.Value.Length == 0) + return R.Err; + return R.OkR(result.Value[0]); } // data to response - internal static IEnumerable GenerateResponse(string line) where T : IResponse, new() + internal static R GenerateResponse(ReadOnlySpan line) where T : IResponse, new() { - if (string.IsNullOrWhiteSpace(line)) + if (line.IsEmpty) return Array.Empty(); - var messageList = line.Split('|'); - return messageList.Select(msg => ParseKeyValueLine(new T(), msg)).Where(x => x.Ok).Select(x => x.Value); + + var pipes = PipeList(line); + var arr = new T[(pipes?.Count ?? 0) + 1]; + for (int i = 0; i < arr.Length; i++) + arr[i] = new T(); + return Dersialize(arr, line, pipes); } - private static R ParseKeyValueLine(T qm, string line) where T : IQueryMessage + private static bool ParseKeyValueLine(IMessage qm, ReadOnlySpan line, HashSet indexing, List single) { - if (string.IsNullOrWhiteSpace(line)) - return R.Err("Empty"); + if (line.IsEmpty) + return true; - var ss = new SpanSplitter(); - var lineSpan = ss.First(line, ' '); - var key = ReadOnlySpan.Empty; - var value = ReadOnlySpan.Empty; + var ss = new SpanSplitter(); + ss.First(line, AsciiSpace); + var key = ReadOnlySpan.Empty; + var value = ReadOnlySpan.Empty; try { do { - var param = ss.Trim(lineSpan); - var kvpSplitIndex = param.IndexOf('='); - var skey = kvpSplitIndex >= 0 ? param.Slice(0, kvpSplitIndex) : ReadOnlySpan.Empty; - value = kvpSplitIndex <= param.Length - 1 ? param.Slice(kvpSplitIndex + 1) : ReadOnlySpan.Empty; - - qm.SetField(skey.NewString(), value); + var param = ss.Trim(line); + var kvpSplitIndex = param.IndexOf(AsciiEquals); + key = kvpSplitIndex >= 0 ? param.Slice(0, kvpSplitIndex) : ReadOnlySpan.Empty; + value = kvpSplitIndex <= param.Length - 1 ? param.Slice(kvpSplitIndex + 1) : ReadOnlySpan.Empty; + + if (!key.IsEmpty) + { + var keyStr = key.NewUtf8String(); + qm.SetField(keyStr, value); + if (indexing != null) + { + if (single == null) + { + indexing.Add(keyStr); + } + else if (!indexing.Contains(keyStr)) + { + single.Add(keyStr); + } + else + { + indexing = null; + single = null; + } + } + } if (!ss.HasNext) break; - lineSpan = ss.Next(lineSpan); - } while (lineSpan.Length > 0); - return R.OkR(qm); + line = ss.Next(line); + } while (line.Length > 0); + return true; } - catch (Exception ex) { OnError?.Invoke(null, new Error(qm.GetType().Name, line, key.NewString(), value.NewString(), ex)); } - return R.Err("Error"); + catch (Exception ex) { OnError?.Invoke(null, new Error(qm.GetType().Name, line.NewUtf8String(), key.NewUtf8String(), value.NewUtf8String(), ex)); } + return false; } public class Error : EventArgs diff --git a/TS3Client/Messages/ResponseDictionary.cs b/TS3Client/Messages/ResponseDictionary.cs index 43acb072..070973c2 100644 --- a/TS3Client/Messages/ResponseDictionary.cs +++ b/TS3Client/Messages/ResponseDictionary.cs @@ -44,7 +44,20 @@ public ValueType this[KeyType key] public bool TryGetValue(KeyType key, out ValueType value) => data.TryGetValue(key, out value); IEnumerator IEnumerable.GetEnumerator() => data.GetEnumerator(); - public void SetField(string name, ReadOnlySpan value) => data[name] = value.NewString(); + public void SetField(string name, ReadOnlySpan value) => data[name] = value.NewUtf8String(); + public void Expand(IMessage[] to, IEnumerable flds) + { + foreach (var fld in flds) + { + if (TryGetValue(fld, out var fldval)) + { + foreach (var toi in (ResponseDictionary[])to) + { + toi.data[fld] = fldval; + } + } + } + } public string ReturnCode { get => data.ContainsKey("return_code") ? data["return_code"] : string.Empty; @@ -55,6 +68,7 @@ public string ReturnCode public sealed class ResponseVoid : IResponse { public string ReturnCode { get; set; } - public void SetField(string name, ReadOnlySpan value) { } + public void SetField(string name, ReadOnlySpan value) { } + public void Expand(IMessage[] to, IEnumerable flds) { } } } diff --git a/TS3Client/Properties.cs b/TS3Client/Properties.cs new file mode 100644 index 00000000..1b68d558 --- /dev/null +++ b/TS3Client/Properties.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("TS3ABotUnitTests")] diff --git a/TS3Client/Properties/AssemblyInfo.cs b/TS3Client/Properties/AssemblyInfo.cs deleted file mode 100644 index 60ec5cdb..00000000 --- a/TS3Client/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TS3Client")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TS3Client")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("0eb99e9d-87e5-4534-a100-55d231c2b6a6")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TS3Client/Query/Ts3QueryClient.cs b/TS3Client/Query/Ts3QueryClient.cs index 45dcff22..f9d65b18 100644 --- a/TS3Client/Query/Ts3QueryClient.cs +++ b/TS3Client/Query/Ts3QueryClient.cs @@ -13,17 +13,16 @@ namespace TS3Client.Query using Helper; using Messages; using System; - using System.Linq; + using System.Buffers; using System.Collections.Generic; using System.IO; + using System.IO.Pipelines; + using System.Linq; using System.Net.Sockets; - - using ClientUidT = System.String; - using ClientDbIdT = System.UInt64; - using ClientIdT = System.UInt16; + using System.Threading.Tasks; using ChannelIdT = System.UInt64; - using ServerGroupIdT = System.UInt64; - using ChannelGroupIdT = System.UInt64; + using CmdR = System.E; + using Uid = System.String; public sealed class Ts3QueryClient : Ts3BaseFunctions { @@ -34,13 +33,14 @@ public sealed class Ts3QueryClient : Ts3BaseFunctions private StreamWriter tcpWriter; private readonly SyncMessageProcessor msgProc; private readonly IEventDispatcher dispatcher; + private Pipe dataPipe = new Pipe(); public override ClientType ClientType => ClientType.Query; public override bool Connected => tcpClient.Connected; private bool connecting; public override bool Connecting => connecting && !Connected; - public override event NotifyEventHandler OnTextMessageReceived; + public override event NotifyEventHandler OnTextMessage; public override event NotifyEventHandler OnClientEnterView; public override event NotifyEventHandler OnClientLeftView; public override event EventHandler OnConnected; @@ -56,7 +56,7 @@ public Ts3QueryClient(EventDispatchType dispatcherType) public override void Connect(ConnectionData conData) { - if (!TsDnsResolver.TryResolve(conData.Address, out remoteAddress)) + if (!TsDnsResolver.TryResolve(conData.Address, out remoteAddress, TsDnsResolver.Ts3QueryDefaultPort)) throw new Ts3Exception("Could not read or resolve address."); try @@ -88,24 +88,85 @@ public override void Disconnect() { SendRaw("quit"); if (tcpClient.Connected) - ((IDisposable)tcpClient)?.Dispose(); + tcpClient?.Dispose(); } } private void NetworkLoop(object ctx) { + Task.WhenAll(ReadLoopAsync(tcpStream, dataPipe.Writer), WriteLoopAsync(tcpStream, dataPipe.Reader)).ConfigureAwait(false).GetAwaiter().GetResult(); + OnDisconnected?.Invoke(this, new DisconnectEventArgs(Reason.LeftServer)); + } + + private async Task ReadLoopAsync(NetworkStream stream, PipeWriter writer) + { + const int minimumBufferSize = 4096; + var dataReadBuffer = new byte[4096]; + while (true) { - string line; - try { line = tcpReader.ReadLine(); } - catch (IOException) { line = null; } - if (line == null) break; - if (string.IsNullOrWhiteSpace(line)) continue; - - var message = line.Trim(); - msgProc.PushMessage(message); + try + { + var mem = writer.GetMemory(minimumBufferSize); + int bytesRead = await stream.ReadAsync(dataReadBuffer, 0, dataReadBuffer.Length); + if (bytesRead == 0) + { + break; + } + + dataReadBuffer.CopyTo(mem); + //await writer.WriteAsync(dataReadBuffer.AsMemory(0, bytesRead)); + //await writer.FlushAsync(); + writer.Advance(bytesRead); + } + catch (IOException) { break; } + + FlushResult result = await writer.FlushAsync(); + + if (result.IsCompleted) + { + break; + } } - OnDisconnected?.Invoke(this, new DisconnectEventArgs(Reason.LeftServer)); + writer.Complete(); + } + + private async Task WriteLoopAsync(NetworkStream stream, PipeReader reader) + { + var dataWriteBuffer = new byte[4096]; + while (true) + { + var result = await reader.ReadAsync(); + + ReadOnlySequence buffer = result.Buffer; + SequencePosition? position = null; + + do + { + position = buffer.PositionOf((byte)'\n'); + + if (position != null) + { + var notif = msgProc.PushMessage(buffer.Slice(0, position.Value).ToArray()); + if (notif.HasValue) + { + dispatcher.Invoke(notif.Value); + } + + // +2 = skipping \n\r + buffer = buffer.Slice(buffer.GetPosition(2, position.Value)); + } + } while (position != null); + + reader.AdvanceTo(buffer.Start, buffer.End); + + if (result.IsCompleted) + { + break; + } + } + + reader.Complete(); } private void InvokeEvent(LazyNotification lazyNotification) @@ -123,7 +184,7 @@ private void InvokeEvent(LazyNotification lazyNotification) case NotificationType.ClientLeftView: OnClientLeftView?.Invoke(this, notification.Cast()); break; case NotificationType.ClientMoved: break; case NotificationType.ServerEdited: break; - case NotificationType.TextMessage: OnTextMessageReceived?.Invoke(this, notification.Cast()); break; + case NotificationType.TextMessage: OnTextMessage?.Invoke(this, notification.Cast()); break; case NotificationType.TokenUsed: break; // special case NotificationType.CommandError: break; @@ -132,7 +193,7 @@ private void InvokeEvent(LazyNotification lazyNotification) } } - public override R, CommandError> SendCommand(Ts3Command com) // Synchronous + public override R SendCommand(Ts3Command com) // Synchronous { using (var wb = new WaitBlock(false)) { @@ -158,28 +219,28 @@ private void SendRaw(string data) private static readonly string[] TargetTypeString = { "textprivate", "textchannel", "textserver", "channel", "server" }; - public void RegisterNotification(TextMessageTargetMode target, ChannelIdT channel) + public CmdR RegisterNotification(TextMessageTargetMode target, ChannelIdT channel) => RegisterNotification(TargetTypeString[(int)target], channel); - public void RegisterNotification(ReasonIdentifier target, ChannelIdT channel) + public CmdR RegisterNotification(ReasonIdentifier target, ChannelIdT channel) => RegisterNotification(TargetTypeString[(int)target], channel); - private void RegisterNotification(string target, ChannelIdT channel) + private CmdR RegisterNotification(string target, ChannelIdT channel) { var ev = new CommandParameter("event", target.ToLowerInvariant()); if (target == "channel") - Send("servernotifyregister", ev, new CommandParameter("id", channel)); + return Send("servernotifyregister", ev, new CommandParameter("id", channel)); else - Send("servernotifyregister", ev); + return Send("servernotifyregister", ev); } - public void Login(string username, string password) - => Send("login", + public CmdR Login(string username, string password) + => Send("login", new CommandParameter("client_login_name", username), new CommandParameter("client_login_password", password)); - public void UseServer(int serverId) - => Send("use", + public CmdR UseServer(int serverId) + => Send("use", new CommandParameter("sid", serverId)); // Splitted base commands @@ -190,7 +251,7 @@ public override R ServerGroupAdd(string na ? new List { new CommandParameter("name", name), new CommandParameter("type", (int)type.Value) } : new List { new CommandParameter("name", name) }).WrapSingle(); - public override R, CommandError> ServerGroupsByClientDbId(ulong clDbId) + public override R ServerGroupsByClientDbId(ulong clDbId) => Send("servergroupsbyclientid", new CommandParameter("cldbid", clDbId)); @@ -214,21 +275,29 @@ public override R FileTransferInitDownload(ChannelId new CommandParameter("clientftfid", clientTransferId), new CommandParameter("seekpos", seek)).WrapSingle(); - public override R, CommandError> FileTransferList() + public override R FileTransferList() => Send("ftlist"); - public override R, CommandError> FileTransferGetFileList(ChannelIdT channelId, string path, string channelPassword = "") + public override R FileTransferGetFileList(ChannelIdT channelId, string path, string channelPassword = "") => Send("ftgetfilelist", new CommandParameter("cid", channelId), new CommandParameter("path", path), new CommandParameter("cpw", channelPassword)); - public override R, CommandError> FileTransferGetFileInfo(ChannelIdT channelId, string[] path, string channelPassword = "") + public override R FileTransferGetFileInfo(ChannelIdT channelId, string[] path, string channelPassword = "") => Send("ftgetfileinfo", new CommandParameter("cid", channelId), new CommandParameter("cpw", channelPassword), new CommandMultiParameter("name", path)); + public override R ClientGetDbIdFromUid(Uid clientUid) + => Send("clientgetdbidfromuid", + new CommandParameter("cluid", clientUid)).WrapSingle(); + + public override R GetClientIds(Uid clientUid) + => Send("clientgetids", + new CommandParameter("cluid", clientUid)); + #endregion public override void Dispose() diff --git a/TS3Client/TS3Client.csproj b/TS3Client/TS3Client.csproj index 64867e24..3a2ee0cd 100644 --- a/TS3Client/TS3Client.csproj +++ b/TS3Client/TS3Client.csproj @@ -1,212 +1,101 @@ - - - + - Debug - AnyCPU - {0EB99E9D-87E5-4534-A100-55D231C2B6A6} Library - Properties + net46;netstandard2.0 + 7.2 TS3Client TS3Client - v4.6 - 512 - 7 - - - - - - true - full - false - bin\Debug\ - TRACE;DEBUG - prompt - 4 - false - true - Auto - TS3Client.ruleset - 7.2 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - TS3Client.ruleset - 7.2 - - - - true + AnyCPU + false + ../TS3AudioBot.ruleset - - ..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll - True - - - ..\packages\Splamy.Ed25519.Toolkit.1.0.2-alpha\lib\net46\Chaos.NaCl.dll - - - ..\packages\Heijden.Dns.2.0.0\lib\net35\Heijden.Dns.dll - - - ..\packages\Nett.0.9.0\lib\Net40\Nett.dll - - - ..\packages\NLog.4.5.2\lib\net45\NLog.dll - - - - ..\packages\System.Memory.4.5.0-preview1-26216-02\lib\netstandard1.1\System.Memory.dll - - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0-preview1-26216-02\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + True True + Ts3FullEvents.tt + + + + TextTemplatingFileGenerator + Book.cs + + + TextTemplatingFileGenerator + Errors.cs + + + TextTemplatingFileGenerator + M2B.cs + + + TextTemplatingFileGenerator + Messages.cs + + + TextTemplatingFileGenerator + Permissions.cs + + + TextTemplatingFileGenerator + Ts3FullEvents.cs + + + TextTemplatingFileGenerator + Versions.cs + + + + True + True Book.tt - - True + True + True Errors.tt - - True + True - Permissions.tt + True + M2B.tt - + + True True + Messages.tt + + True - Versions.tt + True + Permissions.tt - - - - - - - - - - - - - - - - - - - - - - - - - - - + + True True + Ts3FullEvents.tt + + True - Messages.tt + True + Versions.tt - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - - - - - - TextTemplatingFileGenerator - Book.cs - - - TextTemplatingFileGenerator - Errors.cs - - - TextTemplatingFileGenerator - Messages.cs - - - TextTemplatingFileGenerator - Permissions.cs - - - TextTemplatingFileGenerator - Versions.cs - - + - - \ No newline at end of file diff --git a/TS3Client/TS3Client.csproj.DotSettings b/TS3Client/TS3Client.csproj.DotSettings deleted file mode 100644 index d46bfc51..00000000 --- a/TS3Client/TS3Client.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/TS3Client/Ts3BaseClient.cs b/TS3Client/Ts3BaseClient.cs index b344ec2c..39c1c72b 100644 --- a/TS3Client/Ts3BaseClient.cs +++ b/TS3Client/Ts3BaseClient.cs @@ -14,18 +14,14 @@ namespace TS3Client using Messages; using System; using System.Collections.Generic; - using System.Diagnostics; using System.Linq; using System.Net; - - using CmdR = E; - - using ClientUidT = System.String; + using ChannelIdT = System.UInt64; using ClientDbIdT = System.UInt64; using ClientIdT = System.UInt16; - using ChannelIdT = System.UInt64; + using CmdR = System.E; using ServerGroupIdT = System.UInt64; - using ChannelGroupIdT = System.UInt64; + using Uid = System.String; public delegate void NotifyEventHandler(object sender, IEnumerable e) where TEventArgs : INotification; @@ -34,7 +30,7 @@ public abstract class Ts3BaseFunctions : IDisposable { protected static readonly NLog.Logger LogCmd = NLog.LogManager.GetLogger("TS3Client.Cmd"); /// When this client receives any visible message. - public abstract event NotifyEventHandler OnTextMessageReceived; + public abstract event NotifyEventHandler OnTextMessage; /// When another client enters visiblility. public abstract event NotifyEventHandler OnClientEnterView; /// When another client leaves visiblility. @@ -65,24 +61,21 @@ public abstract class Ts3BaseFunctions : IDisposable /// Creates a new command. /// The command name. - [DebuggerStepThrough] - public R, CommandError> Send(string command) + public R Send(string command) => SendCommand(new Ts3Command(command)); /// Creates a new command. /// The command name. /// The parameters to be added to this command. /// See , or for more information. - [DebuggerStepThrough] - public R, CommandError> Send(string command, params ICommandPart[] parameter) + public R Send(string command, params ICommandPart[] parameter) => SendCommand(new Ts3Command(command, parameter.ToList())); /// Creates a new command. /// The type to deserialize the response to. /// The command name. /// Returns an enumeration of the deserialized and split up in objects data. - [DebuggerStepThrough] - public R, CommandError> Send(string command) where T : IResponse, new() + public R Send(string command) where T : IResponse, new() => SendCommand(new Ts3Command(command)); /// Creates a new command. @@ -90,8 +83,7 @@ public R, CommandError> Send(string command, par /// The command name. /// The parameters to be added to this command. /// Returns an enumeration of the deserialized and split up in objects data. - [DebuggerStepThrough] - public R, CommandError> Send(string command, params ICommandPart[] parameter) where T : IResponse, new() + public R Send(string command, params ICommandPart[] parameter) where T : IResponse, new() => Send(command, parameter.ToList()); /// Creates a new command. @@ -99,11 +91,9 @@ public R, CommandError> Send(string command, par /// The command name. /// The parameters to be added to this command. /// Returns an enumeration of the deserialized and split up in objects data. - [DebuggerStepThrough] - public R, CommandError> Send(string command, List parameter) where T : IResponse, new() + public R Send(string command, List parameter) where T : IResponse, new() => SendCommand(new Ts3Command(command, parameter)); - [DebuggerStepThrough] protected CmdR SendNoResponsed(Ts3Command command) => SendCommand(command.ExpectsResponse(false)); @@ -111,7 +101,7 @@ protected CmdR SendNoResponsed(Ts3Command command) /// The type to deserialize the response to. Use for unknown response data. /// The raw command to send. /// Returns an enumeration of the deserialized and split up in objects data. - public abstract R, CommandError> SendCommand(Ts3Command com) where T : IResponse, new(); + public abstract R SendCommand(Ts3Command com) where T : IResponse, new(); #endregion @@ -179,7 +169,7 @@ public CmdR KickClient(ClientIdT[] clientIds, ReasonIdentifier reasonId, string /// Displays a list of clients online on a virtual server including their ID, nickname, status flags, etc. /// The output can be modified using several command options. /// Please note that the output will only contain clients which are currently in channels you're able to subscribe to. - public R, CommandError> ClientList(ClientListOptions options = 0) + public R ClientList(ClientListOptions options = 0) => Send("clientlist", new CommandOption(options)); @@ -269,17 +259,14 @@ public CmdR FileTransferRenameFile(ChannelIdT channelId, string oldName, string public CmdR UploadAvatar(System.IO.Stream image) { - var token = FileTransferManager.UploadFile(image, 0, "/avatar", true); + var token = FileTransferManager.UploadFile(image, 0, "/avatar", overwrite: true, createMd5: true); if (!token.Ok) return token.Error; token.Value.Wait(); - image.Seek(0, System.IO.SeekOrigin.Begin); - using (var md5Dig = System.Security.Cryptography.MD5.Create()) - { - var md5Bytes = md5Dig.ComputeHash(image); - var md5 = string.Join("", md5Bytes.Select(x => x.ToString("x2"))); - return Send("clientupdate", new CommandParameter("client_flag_avatar", md5)); - } + if (token.Value.Status != TransferStatus.Done) + return Util.CustomError("Avatar upload failed"); + var md5 = string.Concat(token.Value.Md5Sum.Select(x => x.ToString("x2"))); + return Send("clientupdate", new CommandParameter("client_flag_avatar", md5)); } public CmdR ClientMove(ClientIdT clientId, ChannelIdT channelId, string channelPassword = null) @@ -301,7 +288,7 @@ public CmdR ClientMove(ClientIdT clientId, ChannelIdT channelId, string channelP public abstract R ServerGroupAdd(string name, GroupType? type = null); /// Displays all server groups the client specified with is currently residing in. - public abstract R, CommandError> ServerGroupsByClientDbId(ClientDbIdT clDbId); + public abstract R ServerGroupsByClientDbId(ClientDbIdT clDbId); public abstract R FileTransferInitUpload(ChannelIdT channelId, string path, string channelPassword, ushort clientTransferId, long fileSize, bool overwrite, bool resume); @@ -309,12 +296,15 @@ public abstract R FileTransferInitUpload(ChannelIdT ch public abstract R FileTransferInitDownload(ChannelIdT channelId, string path, string channelPassword, ushort clientTransferId, long seek); - public abstract R, CommandError> FileTransferList(); + public abstract R FileTransferList(); + + public abstract R FileTransferGetFileList(ChannelIdT channelId, string path, string channelPassword = ""); - public abstract R, CommandError> FileTransferGetFileList(ChannelIdT channelId, string path, string channelPassword = ""); + public abstract R FileTransferGetFileInfo(ChannelIdT channelId, string[] path, string channelPassword = ""); - public abstract R, CommandError> FileTransferGetFileInfo(ChannelIdT channelId, string[] path, string channelPassword = ""); + public abstract R ClientGetDbIdFromUid(Uid clientUid); + public abstract R GetClientIds(Uid clientUid); #endregion } } diff --git a/TS3Client/TsDnsResolver.cs b/TS3Client/TsDnsResolver.cs index 651d04b3..b181d97b 100644 --- a/TS3Client/TsDnsResolver.cs +++ b/TS3Client/TsDnsResolver.cs @@ -7,22 +7,23 @@ // You should have received a copy of the Open Software License along with this // program. If not, see . -using System.Collections.Generic; -using System.Net.Sockets; -using System.Text; - namespace TS3Client { + using Heijden.Dns.Portable; using Heijden.DNS; using System; + using System.Collections.Generic; using System.Net; + using System.Net.Sockets; + using System.Text; using System.Text.RegularExpressions; /// Provides methods to resolve TSDNS, SRV redirects and nicknames public static class TsDnsResolver { private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); - private const ushort Ts3DefaultPort = 9987; + public const ushort Ts3VoiceDefaultPort = 9987; + public const ushort Ts3QueryDefaultPort = 10011; private const ushort TsDnsDefaultPort = 41144; private const string DnsPrefixTcp = "_tsdns._tcp."; private const string DnsPrefixUdp = "_ts3._udp."; @@ -33,7 +34,7 @@ public static class TsDnsResolver /// The address, nickname, etc. to resolve. /// The ip address if successfully resolved. Otherwise a dummy. /// Whether the resolve was succesful. - public static bool TryResolve(string address, out IPEndPoint endPoint) + public static bool TryResolve(string address, out IPEndPoint endPoint, ushort defaultPort = Ts3VoiceDefaultPort) { Log.Debug("Trying to look up '{0}'", address); @@ -49,9 +50,8 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) } } - // host is specified as an IP (+ Port) - if ((endPoint = ParseIpEndPoint(address)) != null) + if ((endPoint = ParseIpEndPoint(address, defaultPort)) != null) { Log.Trace("Address is an ip: '{0}'", endPoint); return true; @@ -67,9 +67,9 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) { Recursion = true, Retries = 3, - TimeOut = (int)LookupTimeout.TotalMilliseconds, + //TimeOut = 1000, XXX UseCache = true, - DnsServer = "8.8.8.8", + //DnsServer = "8.8.8.8", XXX TransportType = Heijden.DNS.TransportType.Udp, }; @@ -95,7 +95,7 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) return false; var domainList = new List(); for (int i = 1; i < Math.Min(domainSplit.Length, 4); i++) - domainList.Add(string.Join(".", domainSplit, (domainSplit.Length - (i + 1)), i + 1)); + domainList.Add(string.Join(".", domainSplit, domainSplit.Length - (i + 1), i + 1)); // Try resolve tcp prefix // Under this address we'll get the tsdns server @@ -105,7 +105,7 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) if (srvEndPoint == null) continue; - endPoint = ResolveTsDns(srvEndPoint, uri.Host); + endPoint = ResolveTsDns(srvEndPoint, uri.Host, defaultPort); if (endPoint != null) return true; } @@ -113,7 +113,7 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) // Try resolve to the tsdns service directly foreach (var domain in domainList) { - endPoint = ResolveTsDns(domain, TsDnsDefaultPort, uri.Host); + endPoint = ResolveTsDns(domain, TsDnsDefaultPort, uri.Host, defaultPort); if (endPoint != null) return true; } @@ -124,7 +124,7 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) return false; var port = string.IsNullOrEmpty(uri.GetComponents(UriComponents.Port, UriFormat.Unescaped)) - ? Ts3DefaultPort + ? defaultPort : uri.Port; endPoint = new IPEndPoint(hostAddress, port); @@ -134,7 +134,7 @@ public static bool TryResolve(string address, out IPEndPoint endPoint) private static IPEndPoint ResolveSrv(Resolver resolver, string domain) { Log.Trace("Resolving srv record '{0}'", domain); - var response = resolver.Query(domain, QType.SRV, QClass.IN); + var response = resolver.Query(domain, QType.SRV, QClass.IN).ConfigureAwait(false).GetAwaiter().GetResult(); if (response.RecordsSRV.Length > 0) { @@ -147,17 +147,17 @@ private static IPEndPoint ResolveSrv(Resolver resolver, string domain) return null; } - private static IPEndPoint ResolveTsDns(string tsDnsAddress, ushort port, string resolveAddress) + private static IPEndPoint ResolveTsDns(string tsDnsAddress, ushort port, string resolveAddress, ushort defaultPort) { Log.Trace("Looking for the tsdns under '{0}'", tsDnsAddress); var hostAddress = ResolveDns(tsDnsAddress); if (hostAddress == null) return null; - return ResolveTsDns(new IPEndPoint(hostAddress, port), resolveAddress); + return ResolveTsDns(new IPEndPoint(hostAddress, port), resolveAddress, defaultPort); } - private static IPEndPoint ResolveTsDns(IPEndPoint tsDnsAddress, string resolveAddress) + private static IPEndPoint ResolveTsDns(IPEndPoint tsDnsAddress, string resolveAddress, ushort defaultPort) { Log.Trace("Looking up tsdns address '{0}'", resolveAddress); string returnString; @@ -188,7 +188,7 @@ private static IPEndPoint ResolveTsDns(IPEndPoint tsDnsAddress, string resolveAd return null; } - return ParseIpEndPoint(returnString); + return ParseIpEndPoint(returnString, defaultPort); } private static IPAddress ResolveDns(string hostOrNameAddress) @@ -206,7 +206,7 @@ private static IPAddress ResolveDns(string hostOrNameAddress) private static readonly Regex IpRegex = new Regex(@"(?(?:\d{1,3}\.){3}\d{1,3}|\[[0-9a-fA-F:]+\]|localhost)(?::(?\d{1,5}))?", RegexOptions.ECMAScript | RegexOptions.Compiled); - private static IPEndPoint ParseIpEndPoint(string address) + private static IPEndPoint ParseIpEndPoint(string address, ushort defaultPort) { var match = IpRegex.Match(address); if (!match.Success) @@ -219,7 +219,7 @@ private static IPEndPoint ParseIpEndPoint(string address) return null; if (!match.Groups["port"].Success) - return new IPEndPoint(ipAddr, Ts3DefaultPort); + return new IPEndPoint(ipAddr, defaultPort); if (!ushort.TryParse(match.Groups["port"].Value, out ushort port)) return null; diff --git a/TS3Client/WaitBlock.cs b/TS3Client/WaitBlock.cs index b7ac5d7a..55b10137 100644 --- a/TS3Client/WaitBlock.cs +++ b/TS3Client/WaitBlock.cs @@ -12,7 +12,6 @@ namespace TS3Client using Helper; using Messages; using System; - using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -22,7 +21,7 @@ internal sealed class WaitBlock : IDisposable private readonly ManualResetEvent answerWaiter; private readonly ManualResetEvent notificationWaiter; private CommandError commandError; - private string commandLine; + private ReadOnlyMemory? commandLine; public NotificationType[] DependsOn { get; } private LazyNotification notification; private bool isDisposed; @@ -46,7 +45,7 @@ public WaitBlock(bool async, NotificationType[] dependsOn = null) } } - public R, CommandError> WaitForMessage() where T : IResponse, new() + public R WaitForMessage() where T : IResponse, new() { if (isDisposed) throw new ObjectDisposedException(nameof(WaitBlock)); @@ -55,21 +54,29 @@ public WaitBlock(bool async, NotificationType[] dependsOn = null) if (commandError.Id != Ts3ErrorCode.ok) return commandError; - return R, CommandError>.OkR(Deserializer.GenerateResponse(commandLine)); + var result = Deserializer.GenerateResponse(commandLine.Value.Span); + if (result.Ok) + return result.Value; + else + return Util.ParserCommandError; } - public async Task, CommandError>> WaitForMessageAsync() where T : IResponse, new() + public async Task> WaitForMessageAsync() where T : IResponse, new() { if (isDisposed) throw new ObjectDisposedException(nameof(WaitBlock)); var timeOut = Task.Delay(CommandTimeout); - var res = await Task.WhenAny(answerWaiterAsync.Task, timeOut); + var res = await Task.WhenAny(answerWaiterAsync.Task, timeOut).ConfigureAwait(false); if (res == timeOut) return Util.TimeOutCommandError; if (commandError.Id != Ts3ErrorCode.ok) return commandError; - return R, CommandError>.OkR(Deserializer.GenerateResponse(commandLine)); + var result = Deserializer.GenerateResponse(commandLine.Value.Span); + if (result.Ok) + return result.Value; + else + return Util.ParserCommandError; } public R WaitForNotification() @@ -88,7 +95,7 @@ public R WaitForNotification() return notification; } - public void SetAnswer(CommandError commandError, string commandLine = null) + public void SetAnswer(CommandError commandError, ReadOnlyMemory? commandLine = null) { if (isDisposed) return; diff --git a/TS3Client/packages.config b/TS3Client/packages.config deleted file mode 100644 index 818976bf..00000000 --- a/TS3Client/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Ts3ClientTests/Program.cs b/Ts3ClientTests/Program.cs index ad559873..ed715309 100644 --- a/Ts3ClientTests/Program.cs +++ b/Ts3ClientTests/Program.cs @@ -6,19 +6,216 @@ using TS3Client.Helper; using TS3Client.Full; using TS3Client.Messages; +using TS3Client.Audio; +using System.Net; +using System.Diagnostics; +using System.Threading; +using TS3Client.Commands; // ReSharper disable All namespace Ts3ClientTests { static class Program { + #region versions + static string[] vers = new string[] { + "1326378143", + "1326794905", + "1326886516", + "1327056547", + "1327583302", + "1327924980", + "1328110407", + "1328254851", + "1328701600", + "1328791207", + "1328874614", + "1329129765", + "1329301801", + "1331895694", + "1332929507", + "1333537992", + "1334822484", + "1334902755", + "1334913258", + "1337926405", + "1337934326", + "1337956928", + "1339508041", + "1339686180", + "1340260499", + "1341992313", + "1342099233", + "1342421813", + "1343649206", + "1343657352", + "1349851829", + "1350549414", + "1350973218", + "1351090895", + "1351504843", + "1354873317", + "1357824174", + "1361527354", + "1361977727", + "1363937305", + "1363960354", + "1365064384", + "1374563791", + "1374830986", + "1375083581", + "1375367141", + "1375773286", + "1378199876", + "1378301061", + "1378461722", + "1378715177", + "1380008864", + "1380283653", + "1382530211", + }; + + static string[] vers_new = new string[] { + "1375083581", + "1375773286", + "1378199876", + "1378301061", + "1378461722", + "1378715177", + "1380008864", + "1380283653", + "1382530211", + "1387444094", + "1392643117", + "1393597517", + "1394114560", + "1394624943", + "1401808190", + "1402646489", + "1403250090", + "1405341092", + "1406898538", + "1407159763", + "1437491062", + "1437730067", + "1438160323", + "1438246387", + "1438673913", + "1441371697", + "1442498553", + "1442913547", + "1442998335", + "1444491275", + "1445263695", + "1445512488", + "1455611032", + "1457598290", + "1459504131", + "1461588969", + "1466597785", + "1466672534", + "1468491418", + "1471417187", + "1472203002", + "1475158080", + "1476372595", + "1476720122", + "1478701553", + "1480583762", + "1481795005", + "1484223040", + "1486051051", + "1486485240", + "1486712038", + "1487668590", + "1489662774", + "1490279472", + "1491993378", + "1496989945", + "1497432760", + "1498644101", + "1498740787", + "1499699899", + "1500360741", + "1500537355", + "1502264952", + "1502873983", + "1512391926", + "1513163251", + "1516099541", + "1516349129", + "1516614607", + }; + #endregion + static ConnectionDataFull con; public static string ToHex(this IEnumerable seq) => string.Join(" ", seq.Select(x => x.ToString("X2"))); public static byte[] FromHex(this string hex) => hex.Split(' ').Select(x => Convert.ToByte(x, 16)).ToArray(); - static void Main() + static void Main2() { + /* + foreach (var ver in vers.Skip(0)) + { + Ts3Server serv = null; + Process ts3 = null; + try + { + serv = new Ts3Server(); + serv.Listen(new IPEndPoint(IPAddress.Any, 9987)); + // http://ftp.4players.de/pub/hosted/ts3/updater-images/client/ + // .\CAnydate.exe C:\TS\Any http://files.teamspeak-services.com/updater-images/client/1516349129 win32 + + Process anyd = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = @"C:\TS\CAnydate.exe", + Arguments = $@"C:\TS\Any http://ftp.4players.de/pub/hosted/ts3/updater-images/client/{ver} win32" + } + }; + anyd.Start(); + anyd.WaitForExit(); + + ts3 = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = @"C:\TS\Any\ts3client_win32.exe", + } + }; + ts3.Start(); + + for (int i = 0; i < 240; i++) + { + if (serv.Init) + break; + Thread.Sleep(1000); + } + + if (!serv.Init) + { + Console.WriteLine("ERR! {0}", ver); + File.WriteAllText("sign.out", $"ERR! {ver}"); + continue; + } + } + catch (Exception) + { + + } + finally + { + try + { + ts3?.Kill(); + } + catch { } + serv?.Dispose(); + } + } + */ //var crypt = new Ts3Crypt(); //crypt.Test(); //return; @@ -31,7 +228,7 @@ static void Main() client.OnConnected += Client_OnConnected; client.OnDisconnected += Client_OnDisconnected; client.OnErrorEvent += Client_OnErrorEvent; - client.OnTextMessageReceived += Client_OnTextMessageReceived; + client.OnEachTextMessage += Client_OnTextMessageReceived; var data = Ts3Crypt.LoadIdentity("MCkDAgbAAgEgAiBPKKMIrHtAH/FBKchbm4iRWZybdRTk/ZiehtH0gQRg+A==", 64, 0); con = new ConnectionDataFull() { Address = "127.0.0.1", Username = "TestClient", Identity = data.Unwrap(), ServerPassword = "123", VersionSign = VersionSign.VER_WIN_3_1_8 }; client.Connect(con); @@ -42,6 +239,74 @@ static void Main() Console.ReadLine(); } + static void Main3(string[] args) + { + // Initialize client + var client = new Ts3FullClient(EventDispatchType.AutoThreadPooled); + var data = Ts3Crypt.LoadIdentity("MCkDAgbAAgEgAiBPKKMIrHtAH/FBKchbm4iRWZybdRTk/ZiehtH0gQRg+A==", 64, 0).Unwrap(); + //var data = Ts3Crypt.GenerateNewIdentity(); + con = new ConnectionDataFull() { Address = "pow.splamy.de", Username = "TestClient", Identity = data }; + + // Setup audio + client + // Save cpu by not processing the rest of the pipe when the + // output is not read. + .Chain() + // This reads the packet meta data, checks for packet order + // and manages packet merging. + .Chain() + // Teamspeak sends audio encoded. This pipe will decode it to + // simple PCM. + .Chain() + // This will merge multiple clients talking into one audio stream + .Chain() + // Reads from the ClientMixdown buffer with a fixed timing + .Into(x => x.Initialize(new SampleInfo(48_000, 2, 16))) + // Reencode to the codec of our choice + .Chain(new EncoderPipe(Codec.OpusMusic)) + // Define where to send to. + .Chain(x => x.SetVoice()) + // Send it with our client. + .Chain(client); + + // Connect + client.Connect(con); + } + + static void Main(string[] args) + { + var query = new TS3Client.Query.Ts3QueryClient(EventDispatchType.DoubleThread); + var con = new ConnectionData() { Address = "127.0.0.1" }; + query.Connect(con); + var use = query.UseServer(1); + Console.WriteLine("Use: {0}", use.Ok); + var who = query.WhoAmI(); + Console.WriteLine("Who: {0}", who.Ok ? (object)who.Value : who.Error.ErrorFormat()); + + while (true) + { + var line = Console.ReadLine(); + if (string.IsNullOrEmpty(line)) + break; + var dict = query.SendCommand(new Ts3RawCommand(line)); + if (dict.Ok) + { + foreach (var item in dict.Value) + { + foreach (var val in item) + { + Console.Write("{0}={1}", val.Key, val.Value); + } + Console.WriteLine(); + } + } + else + { + Console.WriteLine(dict.Error.ErrorFormat()); + } + } + } + private static void Client_OnDisconnected(object sender, DisconnectEventArgs e) { var client = (Ts3FullClient)sender; @@ -56,44 +321,70 @@ private static void Client_OnConnected(object sender, EventArgs e) Console.WriteLine("Connected id {0}", client.ClientId); var data = client.ClientInfo(client.ClientId); - /*var sw = System.Diagnostics.Stopwatch.StartNew(); + //var channel = client. + + var folder = client.FileTransferGetFileList(1, "/"); + var resultDlX = client.FileTransferManager.DownloadFile(new FileInfo("test.toml"), 1, "/conf.toml"); + + folder = client.FileTransferGetFileList(0, "/icons"); + + var result = client.SendNotifyCommand(new TS3Client.Commands.Ts3Command("servergrouplist"), NotificationType.ServerGroupList).Unwrap(); + foreach (var group in result.Notifications.Cast()) + { + var icon = group.IconId; + string fileName = "icon_" + icon; + using (var fs = new FileInfo(fileName).Open(FileMode.OpenOrCreate, FileAccess.ReadWrite)) + { + var resultDl = client.FileTransferManager.DownloadFile(fs, 0, "/" + fileName); + if (resultDl.Ok) + { + var token = resultDl.Value; + token.Wait(); + } + } + } + + // warmup + /*for (int i = 0; i < 100; i++) + { + var err = client.SendChannelMessage("Hi" + i); + } + + var sw = Stopwatch.StartNew(); const int amnt = 1000; for (int i = 0; i < amnt; i++) { - client.SendChannelMessage("Hi" + i); + var err = client.SendChannelMessage("Hi" + i); } sw.Start(); - var elap = (sw.ElapsedTicks / (float)System.Diagnostics.Stopwatch.Frequency); + var elap = (sw.ElapsedTicks / (float)Stopwatch.Frequency); Console.WriteLine("{0} messages in {1}s", amnt, elap); - Console.WriteLine("{0:0.000}ms per message", elap / amnt * 1000);*/ + Console.WriteLine("{0:0.000}ms per message", (elap / amnt) * 1000);*/ //client.Disconnect(); //client.Connect(con); } - private static void Client_OnTextMessageReceived(object sender, IEnumerable e) + private static void Client_OnTextMessageReceived(object sender, TextMessage msg) { - foreach (var msg in e) + if (msg.Message == "Hi") + Console.WriteLine("Hi" + msg.InvokerName); + else if (msg.Message == "Exit") { - if (msg.Message == "Hi") - Console.WriteLine("Hi" + msg.InvokerName); - else if (msg.Message == "Exit") - { - var client = (Ts3FullClient)sender; - var id = client.ClientId; - Console.WriteLine("Exiting... {0}", id); - client.Disconnect(); - Console.WriteLine("Exited... {0}", id); - } - else if (msg.Message == "upl") - { - var client = (Ts3FullClient)sender; + var client = (Ts3FullClient)sender; + var id = client.ClientId; + Console.WriteLine("Exiting... {0}", id); + client.Disconnect(); + Console.WriteLine("Exited... {0}", id); + } + else if (msg.Message == "upl") + { + var client = (Ts3FullClient)sender; - var token = client.FileTransferManager.UploadFile(new FileInfo("img.png"), 0, "/avatar", true); - if (!token.Ok) - return; - token.Value.Wait(); - } + var token = client.FileTransferManager.UploadFile(new FileInfo("img.png"), 0, "/avatar", overwrite: true); + if (!token.Ok) + return; + token.Value.Wait(); } } diff --git a/Ts3ClientTests/Properties/AssemblyInfo.cs b/Ts3ClientTests/Properties/AssemblyInfo.cs deleted file mode 100644 index eabac85e..00000000 --- a/Ts3ClientTests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Ts3ClientTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Ts3ClientTests")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3f6f11f0-c0de-4c24-b39f-4a5b5b150376")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Ts3ClientTests/TomlTest.cs b/Ts3ClientTests/TomlTest.cs deleted file mode 100644 index 5ca540ad..00000000 --- a/Ts3ClientTests/TomlTest.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Nett; - -namespace Ts3ClientTests -{ - class TomlTest - { - static void Main(string[] args) - { - var toml = Toml.ReadFile("conf.toml"); - - var struc = toml.Get(); - - Toml.WriteFile(toml, "conf_out.toml"); - } - } - - class TStruc - { - public TKey main { get; set; } - public TKey second { get; set; } - } - - class TKey - { - public string key { get; set; } - } -} diff --git a/Ts3ClientTests/Ts3ClientTests.csproj b/Ts3ClientTests/Ts3ClientTests.csproj index de727f1b..b68f8e98 100644 --- a/Ts3ClientTests/Ts3ClientTests.csproj +++ b/Ts3ClientTests/Ts3ClientTests.csproj @@ -1,78 +1,19 @@ - - - + + - Debug - AnyCPU - {3F6F11F0-C0DE-4C24-B39F-4A5B5B150376} Exe - Ts3ClientTests - Ts3ClientTests - v4.6 - 512 - true - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - + net46;netcoreapp2.0 + 7.2 - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 false - - - - Ts3ClientTests.TomlTest + Ts3ClientTests.Program + - - ..\packages\Nett.0.9.0\lib\Net40\Nett.dll - - - - - - ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - - - - - - - - - - - - - - - - - - - {0eb99e9d-87e5-4534-a100-55d231c2b6a6} - TS3Client - - - - + + + - + \ No newline at end of file diff --git a/Ts3ClientTests/packages.config b/Ts3ClientTests/packages.config deleted file mode 100644 index b79c6bc7..00000000 --- a/Ts3ClientTests/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/WebInterface/openapi/favicon-16x16.png b/WebInterface/openapi/favicon-16x16.png new file mode 100644 index 00000000..0f7e13b0 Binary files /dev/null and b/WebInterface/openapi/favicon-16x16.png differ diff --git a/WebInterface/openapi/favicon-32x32.png b/WebInterface/openapi/favicon-32x32.png new file mode 100644 index 00000000..b0a3352f Binary files /dev/null and b/WebInterface/openapi/favicon-32x32.png differ diff --git a/WebInterface/openapi/index.html b/WebInterface/openapi/index.html new file mode 100644 index 00000000..09564548 --- /dev/null +++ b/WebInterface/openapi/index.html @@ -0,0 +1,60 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/WebInterface/openapi/oauth2-redirect.html b/WebInterface/openapi/oauth2-redirect.html new file mode 100644 index 00000000..fb68399d --- /dev/null +++ b/WebInterface/openapi/oauth2-redirect.html @@ -0,0 +1,67 @@ + + + + + + diff --git a/WebInterface/openapi/swagger-ui-bundle.js b/WebInterface/openapi/swagger-ui-bundle.js new file mode 100644 index 00000000..6236eb5d --- /dev/null +++ b/WebInterface/openapi/swagger-ui-bundle.js @@ -0,0 +1,104 @@ +!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIBundle=t():e.SwaggerUIBundle=t()}(this,function(){return function(e){var t={};function __webpack_require__(r){if(t[r])return t[r].exports;var n=t[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,__webpack_require__),n.l=!0,n.exports}return __webpack_require__.m=e,__webpack_require__.c=t,__webpack_require__.d=function(e,t,r){__webpack_require__.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},__webpack_require__.n=function(e){var t=e&&e.__esModule?function getDefault(){return e.default}:function getModuleExports(){return e};return __webpack_require__.d(t,"a",t),t},__webpack_require__.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},__webpack_require__.p="/dist",__webpack_require__(__webpack_require__.s=491)}([function(e,t,r){"use strict";e.exports=r(77)},function(e,t,r){e.exports=r(898)()},function(e,t,r){"use strict";t.__esModule=!0,t.default=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t,r){"use strict";t.__esModule=!0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(285));t.default=function(){function defineProperties(e,t){for(var r=0;r>>0;if(""+r!==t||4294967295===r)return NaN;t=r}return t<0?ensureSize(e)+t:t}function returnTrue(){return!0}function wholeSlice(e,t,r){return(0===e||void 0!==r&&e<=-r)&&(void 0===t||void 0!==r&&t>=r)}function resolveBegin(e,t){return resolveIndex(e,t,0)}function resolveEnd(e,t){return resolveIndex(e,t,t)}function resolveIndex(e,t,r){return void 0===e?r:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var p=0,f=1,d=2,h="function"==typeof Symbol&&Symbol.iterator,m="@@iterator",v=h||m;function Iterator(e){this.next=e}function iteratorValue(e,t,r,n){var i=0===e?t:1===e?r:[t,r];return n?n.value=i:n={value:i,done:!1},n}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(e){return!!getIteratorFn(e)}function isIterator(e){return e&&"function"==typeof e.next}function getIterator(e){var t=getIteratorFn(e);return t&&t.call(e)}function getIteratorFn(e){var t=e&&(h&&e[h]||e[m]);if("function"==typeof t)return t}function isArrayLike(e){return e&&"number"==typeof e.length}function Seq(e){return null===e||void 0===e?emptySequence():isIterable(e)?e.toSeq():function seqFromValue(e){var t=maybeIndexedSeqFromValue(e)||"object"==typeof e&&new ObjectSeq(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}(e)}function KeyedSeq(e){return null===e||void 0===e?emptySequence().toKeyedSeq():isIterable(e)?isKeyed(e)?e.toSeq():e.fromEntrySeq():keyedSeqFromValue(e)}function IndexedSeq(e){return null===e||void 0===e?emptySequence():isIterable(e)?isKeyed(e)?e.entrySeq():e.toIndexedSeq():indexedSeqFromValue(e)}function SetSeq(e){return(null===e||void 0===e?emptySequence():isIterable(e)?isKeyed(e)?e.entrySeq():e:indexedSeqFromValue(e)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=p,Iterator.VALUES=f,Iterator.ENTRIES=d,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[v]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(e,t){return seqIterate(this,e,t,!0)},Seq.prototype.__iterator=function(e,t){return seqIterator(this,e,t,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(e,t){return seqIterate(this,e,t,!1)},IndexedSeq.prototype.__iterator=function(e,t){return seqIterator(this,e,t,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var g,y,_,b="@@__IMMUTABLE_SEQ__@@";function ArraySeq(e){this._array=e,this.size=e.length}function ObjectSeq(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function IterableSeq(e){this._iterable=e,this.size=e.length||e.size}function IteratorSeq(e){this._iterator=e,this._iteratorCache=[]}function isSeq(e){return!(!e||!e[b])}function emptySequence(){return g||(g=new ArraySeq([]))}function keyedSeqFromValue(e){var t=Array.isArray(e)?new ArraySeq(e).fromEntrySeq():isIterator(e)?new IteratorSeq(e).fromEntrySeq():hasIterator(e)?new IterableSeq(e).fromEntrySeq():"object"==typeof e?new ObjectSeq(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function indexedSeqFromValue(e){var t=maybeIndexedSeqFromValue(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function maybeIndexedSeqFromValue(e){return isArrayLike(e)?new ArraySeq(e):isIterator(e)?new IteratorSeq(e):hasIterator(e)?new IterableSeq(e):void 0}function seqIterate(e,t,r,n){var i=e._cache;if(i){for(var o=i.length-1,a=0;a<=o;a++){var s=i[r?o-a:a];if(!1===t(s[1],n?s[0]:a,e))return a+1}return a}return e.__iterateUncached(t,r)}function seqIterator(e,t,r,n){var i=e._cache;if(i){var o=i.length-1,a=0;return new Iterator(function(){var e=i[r?o-a:a];return a++>o?{value:void 0,done:!0}:iteratorValue(t,n?e[0]:a-1,e[1])})}return e.__iteratorUncached(t,r)}function fromJS(e,t){return t?function fromJSWith(e,t,r,n){if(Array.isArray(t))return e.call(n,r,IndexedSeq(t).map(function(r,n){return fromJSWith(e,r,n,t)}));if(isPlainObj(t))return e.call(n,r,KeyedSeq(t).map(function(r,n){return fromJSWith(e,r,n,t)}));return t}(t,e,"",{"":e}):fromJSDefault(e)}function fromJSDefault(e){return Array.isArray(e)?IndexedSeq(e).map(fromJSDefault).toList():isPlainObj(e)?KeyedSeq(e).map(fromJSDefault).toMap():e}function isPlainObj(e){return e&&(e.constructor===Object||void 0===e.constructor)}function is(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function deepEqual(e,t){if(e===t)return!0;if(!isIterable(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||isKeyed(e)!==isKeyed(t)||isIndexed(e)!==isIndexed(t)||isOrdered(e)!==isOrdered(t))return!1;if(0===e.size&&0===t.size)return!0;var r=!isAssociative(e);if(isOrdered(e)){var n=e.entries();return t.every(function(e,t){var i=n.next().value;return i&&is(i[1],e)&&(r||is(i[0],t))})&&n.next().done}var i=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{i=!0;var o=e;e=t,t=o}var a=!0,s=t.__iterate(function(t,n){if(r?!e.has(t):i?!is(t,e.get(n,u)):!is(e.get(n,u),t))return a=!1,!1});return a&&e.size===s}function Repeat(e,t){if(!(this instanceof Repeat))return new Repeat(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if(y)return y;y=this}}function invariant(e,t){if(!e)throw new Error(t)}function Range(e,t,r){if(!(this instanceof Range))return new Range(e,t,r);if(invariant(0!==r,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),r=void 0===r?1:Math.abs(r),tn?{value:void 0,done:!0}:iteratorValue(e,i,r[t?n-i++:i++])})},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},ObjectSeq.prototype.has=function(e){return this._object.hasOwnProperty(e)},ObjectSeq.prototype.__iterate=function(e,t){for(var r=this._object,n=this._keys,i=n.length-1,o=0;o<=i;o++){var a=n[t?i-o:o];if(!1===e(r[a],a,this))return o+1}return o},ObjectSeq.prototype.__iterator=function(e,t){var r=this._object,n=this._keys,i=n.length-1,o=0;return new Iterator(function(){var a=n[t?i-o:o];return o++>i?{value:void 0,done:!0}:iteratorValue(e,a,r[a])})},ObjectSeq.prototype[i]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var r=getIterator(this._iterable),n=0;if(isIterator(r))for(var i;!(i=r.next()).done&&!1!==e(i.value,n++,this););return n},IterableSeq.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var r=getIterator(this._iterable);if(!isIterator(r))return new Iterator(iteratorDone);var n=0;return new Iterator(function(){var t=r.next();return t.done?t:iteratorValue(e,n++,t.value)})},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var r,n=this._iterator,i=this._iteratorCache,o=0;o=n.length){var t=r.next();if(t.done)return t;n[i]=t.value}return iteratorValue(e,i,n[i++])})},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(e,t){return this.has(e)?this._value:t},Repeat.prototype.includes=function(e){return is(this._value,e)},Repeat.prototype.slice=function(e,t){var r=this.size;return wholeSlice(e,t,r)?this:new Repeat(this._value,resolveEnd(t,r)-resolveBegin(e,r))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(e){return is(this._value,e)?0:-1},Repeat.prototype.lastIndexOf=function(e){return is(this._value,e)?this.size:-1},Repeat.prototype.__iterate=function(e,t){for(var r=0;r=0&&t=0&&rr?{value:void 0,done:!0}:iteratorValue(e,o++,a)})},Range.prototype.equals=function(e){return e instanceof Range?this._start===e._start&&this._end===e._end&&this._step===e._step:deepEqual(this,e)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var S="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(e,t){var r=65535&(e|=0),n=65535&(t|=0);return r*n+((e>>>16)*n+r*(t>>>16)<<16>>>0)|0};function smi(e){return e>>>1&1073741824|3221225471&e}function hash(e){if(!1===e||null===e||void 0===e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null===e||void 0===e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var r=0|e;for(r!==e&&(r^=4294967295*e);e>4294967295;)r^=e/=4294967295;return smi(r)}if("string"===t)return e.length>A?function cachedHashString(e){var t=T[e];void 0===t&&(t=hashString(e),M===R&&(M=0,T={}),M++,T[e]=t);return t}(e):hashString(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return function hashJSObj(e){var t;if(C&&void 0!==(t=E.get(e)))return t;if(void 0!==(t=e[D]))return t;if(!x){if(void 0!==(t=e.propertyIsEnumerable&&e.propertyIsEnumerable[D]))return t;if(void 0!==(t=function getIENodeHash(e){if(e&&e.nodeType>0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}(e)))return t}t=++w,1073741824&w&&(w=0);if(C)E.set(e,t);else{if(void 0!==k&&!1===k(e))throw new Error("Non-extensible objects are not allowed as keys.");if(x)Object.defineProperty(e,D,{enumerable:!1,configurable:!1,writable:!1,value:t});else if(void 0!==e.propertyIsEnumerable&&e.propertyIsEnumerable===e.constructor.prototype.propertyIsEnumerable)e.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},e.propertyIsEnumerable[D]=t;else{if(void 0===e.nodeType)throw new Error("Unable to set a non-enumerable property on object.");e[D]=t}}return t}(e);if("function"==typeof e.toString)return hashString(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function hashString(e){for(var t=0,r=0;r=t.length)throw new Error("Missing value for key: "+t[r]);e.set(t[r],t[r+1])}})},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},Map.prototype.set=function(e,t){return updateMap(this,e,t)},Map.prototype.setIn=function(e,t){return this.updateIn(e,u,function(){return t})},Map.prototype.remove=function(e){return updateMap(this,e,u)},Map.prototype.deleteIn=function(e){return this.updateIn(e,function(){return u})},Map.prototype.update=function(e,t,r){return 1===arguments.length?e(this):this.updateIn([e],t,r)},Map.prototype.updateIn=function(e,t,r){r||(r=t,t=void 0);var n=function updateInDeepMap(e,t,r,n){var i=e===u;var o=t.next();if(o.done){var a=i?r:e,s=n(a);return s===a?e:s}invariant(i||e&&e.set,"invalid keyPath");var l=o.value;var c=i?u:e.get(l,u);var p=updateInDeepMap(c,t,r,n);return p===c?e:p===u?e.remove(l):(i?emptyMap():e).set(l,p)}(this,forceIterator(e),t,r);return n===u?void 0:n},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(t){return mergeIntoMapWith(this,t,e.call(arguments,1))},Map.prototype.mergeIn=function(t){var r=e.call(arguments,1);return this.updateIn(t,emptyMap(),function(e){return"function"==typeof e.merge?e.merge.apply(e,r):r[r.length-1]})},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(t){var r=e.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(t),r)},Map.prototype.mergeDeepIn=function(t){var r=e.call(arguments,1);return this.updateIn(t,emptyMap(),function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,r):r[r.length-1]})},Map.prototype.sort=function(e){return OrderedMap(sortFactory(this,e))},Map.prototype.sortBy=function(e,t){return OrderedMap(sortFactory(this,t,e))},Map.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(e,t){return new MapIterator(this,e,t)},Map.prototype.__iterate=function(e,t){var r=this,n=0;return this._root&&this._root.iterate(function(t){return n++,e(t[1],t[0],r)},t),n},Map.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?makeMap(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Map.isMap=isMap;var O,P="@@__IMMUTABLE_MAP__@@",I=Map.prototype;function ArrayMapNode(e,t){this.ownerID=e,this.entries=t}function BitmapIndexedNode(e,t,r){this.ownerID=e,this.bitmap=t,this.nodes=r}function HashArrayMapNode(e,t,r){this.ownerID=e,this.count=t,this.nodes=r}function HashCollisionNode(e,t,r){this.ownerID=e,this.keyHash=t,this.entries=r}function ValueNode(e,t,r){this.ownerID=e,this.keyHash=t,this.entry=r}function MapIterator(e,t,r){this._type=t,this._reverse=r,this._stack=e._root&&mapIteratorFrame(e._root)}function mapIteratorValue(e,t){return iteratorValue(e,t[0],t[1])}function mapIteratorFrame(e,t){return{node:e,index:0,__prev:t}}function makeMap(e,t,r,n){var i=Object.create(I);return i.size=e,i._root=t,i.__ownerID=r,i.__hash=n,i.__altered=!1,i}function emptyMap(){return O||(O=makeMap(0))}function updateMap(e,t,r){var n,i;if(e._root){var o=MakeRef(l),a=MakeRef(c);if(n=updateNode(e._root,e.__ownerID,0,void 0,t,r,o,a),!a.value)return e;i=e.size+(o.value?r===u?-1:1:0)}else{if(r===u)return e;i=1,n=new ArrayMapNode(e.__ownerID,[[t,r]])}return e.__ownerID?(e.size=i,e._root=n,e.__hash=void 0,e.__altered=!0,e):n?makeMap(i,n):emptyMap()}function updateNode(e,t,r,n,i,o,a,s){return e?e.update(t,r,n,i,o,a,s):o===u?e:(SetRef(s),SetRef(a),new ValueNode(t,n,[i,o]))}function isLeafNode(e){return e.constructor===ValueNode||e.constructor===HashCollisionNode}function mergeIntoNode(e,t,r,n,i){if(e.keyHash===n)return new HashCollisionNode(t,n,[e.entry,i]);var a,u=(0===r?e.keyHash:e.keyHash>>>r)&s,l=(0===r?n:n>>>r)&s;return new BitmapIndexedNode(t,1<>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function setIn(e,t,r,n){var i=n?e:arrCopy(e);return i[t]=r,i}I[P]=!0,I.delete=I.remove,I.removeIn=I.deleteIn,ArrayMapNode.prototype.get=function(e,t,r,n){for(var i=this.entries,o=0,a=i.length;o=q)return function createNodes(e,t,r,n){e||(e=new OwnerID);for(var i=new ValueNode(e,hash(r),[r,n]),o=0;o>>e)&s),a=this.bitmap;return 0==(a&i)?n:this.nodes[popCount(a&i-1)].get(e+o,t,r,n)},BitmapIndexedNode.prototype.update=function(e,t,r,n,i,l,c){void 0===r&&(r=hash(n));var p=(0===t?r:r>>>t)&s,f=1<=F)return function expandNodes(e,t,r,n,i){for(var o=0,s=new Array(a),u=0;0!==r;u++,r>>>=1)s[u]=1&r?t[o++]:void 0;return s[n]=i,new HashArrayMapNode(e,o+1,s)}(e,v,d,p,y);if(h&&!y&&2===v.length&&isLeafNode(v[1^m]))return v[1^m];if(h&&y&&1===v.length&&isLeafNode(y))return y;var _=e&&e===this.ownerID,b=h?y?d:d^f:d|f,S=h?y?setIn(v,m,y,_):function spliceOut(e,t,r){var n=e.length-1;if(r&&t===n)return e.pop(),e;for(var i=new Array(n),o=0,a=0;a>>e)&s,a=this.nodes[i];return a?a.get(e+o,t,r,n):n},HashArrayMapNode.prototype.update=function(e,t,r,n,i,a,l){void 0===r&&(r=hash(n));var c=(0===t?r:r>>>t)&s,p=i===u,f=this.nodes,d=f[c];if(p&&!d)return this;var h=updateNode(d,e,t+o,r,n,i,a,l);if(h===d)return this;var m=this.count;if(d){if(!h&&--m0&&n=0&&e=e.size||t<0)return e.withMutations(function(e){t<0?setListBounds(e,t).set(0,r):setListBounds(e,0,t+1).set(t,r)});t+=e._origin;var n=e._tail,i=e._root,o=MakeRef(c);t>=getTailOffset(e._capacity)?n=updateVNode(n,e.__ownerID,0,t,r,o):i=updateVNode(i,e.__ownerID,e._level,t,r,o);if(!o.value)return e;if(e.__ownerID)return e._root=i,e._tail=n,e.__hash=void 0,e.__altered=!0,e;return makeList(e._origin,e._capacity,e._level,i,n)}(this,e,t)},List.prototype.remove=function(e){return this.has(e)?0===e?this.shift():e===this.size-1?this.pop():this.splice(e,1):this},List.prototype.insert=function(e,t){return this.splice(e,0,t)},List.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=o,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):emptyList()},List.prototype.push=function(){var e=arguments,t=this.size;return this.withMutations(function(r){setListBounds(r,0,t+e.length);for(var n=0;n>>t&s;if(n>=this.array.length)return new VNode([],e);var i,a=0===n;if(t>0){var u=this.array[n];if((i=u&&u.removeBefore(e,t-o,r))===u&&a)return this}if(a&&!i)return this;var l=editableVNode(this,e);if(!a)for(var c=0;c>>t&s;if(i>=this.array.length)return this;if(t>0){var a=this.array[i];if((n=a&&a.removeAfter(e,t-o,r))===a&&i===this.array.length-1)return this}var u=editableVNode(this,e);return u.array.splice(i+1),n&&(u.array[i]=n),u};var L,z,U={};function iterateList(e,t){var r=e._origin,n=e._capacity,i=getTailOffset(n),s=e._tail;return iterateNodeOrLeaf(e._root,e._level,0);function iterateNodeOrLeaf(e,u,l){return 0===u?function iterateLeaf(e,o){var u=o===i?s&&s.array:e&&e.array,l=o>r?0:r-o,c=n-o;c>a&&(c=a);return function(){if(l===c)return U;var e=t?--c:l++;return u&&u[e]}}(e,l):function iterateNode(e,i,s){var u,l=e&&e.array,c=s>r?0:r-s>>i,p=1+(n-s>>i);p>a&&(p=a);return function(){for(;;){if(u){var e=u();if(e!==U)return e;u=null}if(c===p)return U;var r=t?--p:c++;u=iterateNodeOrLeaf(l&&l[r],i-o,s+(r<>>r&s,c=e&&l0){var p=e&&e.array[l],f=updateVNode(p,t,r-o,n,i,a);return f===p?e:((u=editableVNode(e,t)).array[l]=f,u)}return c&&e.array[l]===i?e:(SetRef(a),u=editableVNode(e,t),void 0===i&&l===u.array.length-1?u.array.pop():u.array[l]=i,u)}function editableVNode(e,t){return t&&e&&t===e.ownerID?e:new VNode(e?e.array.slice():[],t)}function listNodeFor(e,t){if(t>=getTailOffset(e._capacity))return e._tail;if(t<1<0;)r=r.array[t>>>n&s],n-=o;return r}}function setListBounds(e,t,r){void 0!==t&&(t|=0),void 0!==r&&(r|=0);var n=e.__ownerID||new OwnerID,i=e._origin,a=e._capacity,u=i+t,l=void 0===r?a:r<0?a+r:i+r;if(u===i&&l===a)return e;if(u>=l)return e.clear();for(var c=e._level,p=e._root,f=0;u+f<0;)p=new VNode(p&&p.array.length?[void 0,p]:[],n),f+=1<<(c+=o);f&&(u+=f,i+=f,l+=f,a+=f);for(var d=getTailOffset(a),h=getTailOffset(l);h>=1<d?new VNode([],n):m;if(m&&h>d&&uo;y-=o){var _=d>>>y&s;g=g.array[_]=editableVNode(g.array[_],n)}g.array[d>>>o&s]=m}if(l=h)u-=h,l-=h,c=o,p=null,v=v&&v.removeBefore(n,0,u);else if(u>i||h>>c&s;if(b!==h>>>c&s)break;b&&(f+=(1<i&&(p=p.removeBefore(n,c,u-f)),p&&hi&&(i=s.size),isIterable(a)||(s=s.map(function(e){return fromJS(e)})),n.push(s)}return i>e.size&&(e=e.setSize(i)),mergeIntoCollectionWith(e,t,n)}function getTailOffset(e){return e>>o<=a&&s.size>=2*o.size?(n=(i=s.filter(function(e,t){return void 0!==e&&l!==t})).toKeyedSeq().map(function(e){return e[0]}).flip().toMap(),e.__ownerID&&(n.__ownerID=i.__ownerID=e.__ownerID)):(n=o.remove(t),i=l===s.size-1?s.pop():s.set(l,void 0))}else if(c){if(r===s.get(l)[1])return e;n=o,i=s.set(l,[t,r])}else n=o.set(t,s.size),i=s.set(s.size,[t,r]);return e.__ownerID?(e.size=n.size,e._map=n,e._list=i,e.__hash=void 0,e):makeOrderedMap(n,i)}function ToKeyedSequence(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function ToIndexedSequence(e){this._iter=e,this.size=e.size}function ToSetSequence(e){this._iter=e,this.size=e.size}function FromEntriesSequence(e){this._iter=e,this.size=e.size}function flipFactory(e){var t=makeSequence(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=cacheResultThrough,t.__iterateUncached=function(t,r){var n=this;return e.__iterate(function(e,r){return!1!==t(r,e,n)},r)},t.__iteratorUncached=function(t,r){if(t===d){var n=e.__iterator(t,r);return new Iterator(function(){var e=n.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e})}return e.__iterator(t===f?p:f,r)},t}function mapFactory(e,t,r){var n=makeSequence(e);return n.size=e.size,n.has=function(t){return e.has(t)},n.get=function(n,i){var o=e.get(n,u);return o===u?i:t.call(r,o,n,e)},n.__iterateUncached=function(n,i){var o=this;return e.__iterate(function(e,i,a){return!1!==n(t.call(r,e,i,a),i,o)},i)},n.__iteratorUncached=function(n,i){var o=e.__iterator(d,i);return new Iterator(function(){var i=o.next();if(i.done)return i;var a=i.value,s=a[0];return iteratorValue(n,s,t.call(r,a[1],s,e),i)})},n}function reverseFactory(e,t){var r=makeSequence(e);return r._iter=e,r.size=e.size,r.reverse=function(){return e},e.flip&&(r.flip=function(){var t=flipFactory(e);return t.reverse=function(){return e.flip()},t}),r.get=function(r,n){return e.get(t?r:-1-r,n)},r.has=function(r){return e.has(t?r:-1-r)},r.includes=function(t){return e.includes(t)},r.cacheResult=cacheResultThrough,r.__iterate=function(t,r){var n=this;return e.__iterate(function(e,r){return t(e,r,n)},!r)},r.__iterator=function(t,r){return e.__iterator(t,!r)},r}function filterFactory(e,t,r,n){var i=makeSequence(e);return n&&(i.has=function(n){var i=e.get(n,u);return i!==u&&!!t.call(r,i,n,e)},i.get=function(n,i){var o=e.get(n,u);return o!==u&&t.call(r,o,n,e)?o:i}),i.__iterateUncached=function(i,o){var a=this,s=0;return e.__iterate(function(e,o,u){if(t.call(r,e,o,u))return s++,i(e,n?o:s-1,a)},o),s},i.__iteratorUncached=function(i,o){var a=e.__iterator(d,o),s=0;return new Iterator(function(){for(;;){var o=a.next();if(o.done)return o;var u=o.value,l=u[0],c=u[1];if(t.call(r,c,l,e))return iteratorValue(i,n?l:s++,c,o)}})},i}function sliceFactory(e,t,r,n){var i=e.size;if(void 0!==t&&(t|=0),void 0!==r&&(r===1/0?r=i:r|=0),wholeSlice(t,r,i))return e;var o=resolveBegin(t,i),a=resolveEnd(r,i);if(o!=o||a!=a)return sliceFactory(e.toSeq().cacheResult(),t,r,n);var s,u=a-o;u==u&&(s=u<0?0:u);var l=makeSequence(e);return l.size=0===s?s:e.size&&s||void 0,!n&&isSeq(e)&&s>=0&&(l.get=function(t,r){return(t=wrapIndex(this,t))>=0&&ts)return{value:void 0,done:!0};var e=i.next();return n||t===f?e:iteratorValue(t,u-1,t===p?void 0:e.value[1],e)})},l}function skipWhileFactory(e,t,r,n){var i=makeSequence(e);return i.__iterateUncached=function(i,o){var a=this;if(o)return this.cacheResult().__iterate(i,o);var s=!0,u=0;return e.__iterate(function(e,o,l){if(!s||!(s=t.call(r,e,o,l)))return u++,i(e,n?o:u-1,a)}),u},i.__iteratorUncached=function(i,o){var a=this;if(o)return this.cacheResult().__iterator(i,o);var s=e.__iterator(d,o),u=!0,l=0;return new Iterator(function(){var e,o,c;do{if((e=s.next()).done)return n||i===f?e:iteratorValue(i,l++,i===p?void 0:e.value[1],e);var h=e.value;o=h[0],c=h[1],u&&(u=t.call(r,c,o,a))}while(u);return i===d?e:iteratorValue(i,o,c,e)})},i}function flattenFactory(e,t,r){var n=makeSequence(e);return n.__iterateUncached=function(n,i){var o=0,a=!1;return function flatDeep(e,s){var u=this;e.__iterate(function(e,i){return(!t||s0}function zipWithFactory(e,t,r){var n=makeSequence(e);return n.size=new ArraySeq(r).map(function(e){return e.size}).min(),n.__iterate=function(e,t){for(var r,n=this.__iterator(f,t),i=0;!(r=n.next()).done&&!1!==e(r.value,i++,this););return i},n.__iteratorUncached=function(e,n){var i=r.map(function(e){return e=Iterable(e),getIterator(n?e.reverse():e)}),o=0,a=!1;return new Iterator(function(){var r;return a||(r=i.map(function(e){return e.next()}),a=r.some(function(e){return e.done})),a?{value:void 0,done:!0}:iteratorValue(e,o++,t.apply(null,r.map(function(e){return e.value})))})},n}function reify(e,t){return isSeq(e)?t:e.constructor(t)}function validateEntry(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function resolveSize(e){return assertNotInfinite(e.size),ensureSize(e)}function iterableClass(e){return isKeyed(e)?KeyedIterable:isIndexed(e)?IndexedIterable:SetIterable}function makeSequence(e){return Object.create((isKeyed(e)?KeyedSeq:isIndexed(e)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(e,t){return e>t?1:e=0;r--)t={value:arguments[r],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):makeStack(e,t)},Stack.prototype.pushAll=function(e){if(0===(e=IndexedIterable(e)).size)return this;assertNotInfinite(e.size);var t=this.size,r=this._head;return e.reverse().forEach(function(e){t++,r={value:e,next:r}}),this.__ownerID?(this.size=t,this._head=r,this.__hash=void 0,this.__altered=!0,this):makeStack(t,r)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(e){return this.pushAll(e)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(e,t){if(wholeSlice(e,t,this.size))return this;var r=resolveBegin(e,this.size);if(resolveEnd(t,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,e,t);for(var n=this.size-r,i=this._head;r--;)i=i.next;return this.__ownerID?(this.size=n,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(n,i)},Stack.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?makeStack(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Stack.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var r=0,n=this._head;n&&!1!==e(n.value,r++,this);)n=n.next;return r},Stack.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var r=0,n=this._head;return new Iterator(function(){if(n){var t=n.value;return n=n.next,iteratorValue(e,r++,t)}return{value:void 0,done:!0}})},Stack.isStack=isStack;var X,Y="@@__IMMUTABLE_STACK__@@",$=Stack.prototype;function makeStack(e,t,r,n){var i=Object.create($);return i.size=e,i._head=t,i.__ownerID=r,i.__hash=n,i.__altered=!1,i}function emptyStack(){return X||(X=makeStack(0))}function mixin(e,t){var r=function(r){e.prototype[r]=t[r]};return Object.keys(t).forEach(r),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(r),e}$[Y]=!0,$.withMutations=I.withMutations,$.asMutable=I.asMutable,$.asImmutable=I.asImmutable,$.wasAltered=I.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate(function(t,r){e[r]=t}),e},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJS?e.toJS():e}).__toJS()},toJSON:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e}).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var e={};return this.__iterate(function(t,r){e[r]=t}),e},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){return reify(this,function concatFactory(e,t){var r=isKeyed(e),n=[e].concat(t).map(function(e){return isIterable(e)?r&&(e=KeyedIterable(e)):e=r?keyedSeqFromValue(e):indexedSeqFromValue(Array.isArray(e)?e:[e]),e}).filter(function(e){return 0!==e.size});if(0===n.length)return e;if(1===n.length){var i=n[0];if(i===e||r&&isKeyed(i)||isIndexed(e)&&isIndexed(i))return i}var o=new ArraySeq(n);return r?o=o.toKeyedSeq():isIndexed(e)||(o=o.toSetSeq()),(o=o.flatten(!0)).size=n.reduce(function(e,t){if(void 0!==e){var r=t.size;if(void 0!==r)return e+r}},0),o}(this,e.call(arguments,0)))},includes:function(e){return this.some(function(t){return is(t,e)})},entries:function(){return this.__iterator(d)},every:function(e,t){assertNotInfinite(this.size);var r=!0;return this.__iterate(function(n,i,o){if(!e.call(t,n,i,o))return r=!1,!1}),r},filter:function(e,t){return reify(this,filterFactory(this,e,t,!0))},find:function(e,t,r){var n=this.findEntry(e,t);return n?n[1]:r},forEach:function(e,t){return assertNotInfinite(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){assertNotInfinite(this.size),e=void 0!==e?""+e:",";var t="",r=!0;return this.__iterate(function(n){r?r=!1:t+=e,t+=null!==n&&void 0!==n?n.toString():""}),t},keys:function(){return this.__iterator(p)},map:function(e,t){return reify(this,mapFactory(this,e,t))},reduce:function(e,t,r){var n,i;return assertNotInfinite(this.size),arguments.length<2?i=!0:n=t,this.__iterate(function(t,o,a){i?(i=!1,n=t):n=e.call(r,n,t,o,a)}),n},reduceRight:function(e,t,r){var n=this.toKeyedSeq().reverse();return n.reduce.apply(n,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(e,t){return reify(this,sliceFactory(this,e,t,!0))},some:function(e,t){return!this.every(not(e),t)},sort:function(e){return reify(this,sortFactory(this,e))},values:function(){return this.__iterator(f)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(e,t){return ensureSize(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return function countByFactory(e,t,r){var n=Map().asMutable();return e.__iterate(function(i,o){n.update(t.call(r,i,o,e),0,function(e){return e+1})}),n.asImmutable()}(this,e,t)},equals:function(e){return deepEqual(this,e)},entrySeq:function(){var e=this;if(e._cache)return new ArraySeq(e._cache);var t=e.toSeq().map(entryMapper).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(not(e),t)},findEntry:function(e,t,r){var n=r;return this.__iterate(function(r,i,o){if(e.call(t,r,i,o))return n=[i,r],!1}),n},findKey:function(e,t){var r=this.findEntry(e,t);return r&&r[0]},findLast:function(e,t,r){return this.toKeyedSeq().reverse().find(e,t,r)},findLastEntry:function(e,t,r){return this.toKeyedSeq().reverse().findEntry(e,t,r)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(returnTrue)},flatMap:function(e,t){return reify(this,function flatMapFactory(e,t,r){var n=iterableClass(e);return e.toSeq().map(function(i,o){return n(t.call(r,i,o,e))}).flatten(!0)}(this,e,t))},flatten:function(e){return reify(this,flattenFactory(this,e,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(e,t){return this.find(function(t,r){return is(r,e)},void 0,t)},getIn:function(e,t){for(var r,n=this,i=forceIterator(e);!(r=i.next()).done;){var o=r.value;if((n=n&&n.get?n.get(o,u):u)===u)return t}return n},groupBy:function(e,t){return function groupByFactory(e,t,r){var n=isKeyed(e),i=(isOrdered(e)?OrderedMap():Map()).asMutable();e.__iterate(function(o,a){i.update(t.call(r,o,a,e),function(e){return(e=e||[]).push(n?[a,o]:o),e})});var o=iterableClass(e);return i.map(function(t){return reify(e,o(t))})}(this,e,t)},has:function(e){return this.get(e,u)!==u},hasIn:function(e){return this.getIn(e,u)!==u},isSubset:function(e){return e="function"==typeof e.includes?e:Iterable(e),this.every(function(t){return e.includes(t)})},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:Iterable(e)).isSubset(this)},keyOf:function(e){return this.findKey(function(t){return is(t,e)})},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return maxFactory(this,e)},maxBy:function(e,t){return maxFactory(this,t,e)},min:function(e){return maxFactory(this,e?neg(e):defaultNegComparator)},minBy:function(e,t){return maxFactory(this,t?neg(t):defaultNegComparator,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return reify(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return reify(this,skipWhileFactory(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(not(e),t)},sortBy:function(e,t){return reify(this,sortFactory(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return reify(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return reify(this,function takeWhileFactory(e,t,r){var n=makeSequence(e);return n.__iterateUncached=function(n,i){var o=this;if(i)return this.cacheResult().__iterate(n,i);var a=0;return e.__iterate(function(e,i,s){return t.call(r,e,i,s)&&++a&&n(e,i,o)}),a},n.__iteratorUncached=function(n,i){var o=this;if(i)return this.cacheResult().__iterator(n,i);var a=e.__iterator(d,i),s=!0;return new Iterator(function(){if(!s)return{value:void 0,done:!0};var e=a.next();if(e.done)return e;var i=e.value,u=i[0],l=i[1];return t.call(r,l,u,o)?n===d?e:iteratorValue(n,u,l,e):(s=!1,{value:void 0,done:!0})})},n}(this,e,t))},takeUntil:function(e,t){return this.takeWhile(not(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function hashIterable(e){if(e.size===1/0)return 0;var t=isOrdered(e),r=isKeyed(e),n=t?1:0;return function murmurHashOfSize(e,t){return t=S(t,3432918353),t=S(t<<15|t>>>-15,461845907),t=S(t<<13|t>>>-13,5),t=S((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=smi((t=S(t^t>>>13,3266489909))^t>>>16)}(e.__iterate(r?t?function(e,t){n=31*n+hashMerge(hash(e),hash(t))|0}:function(e,t){n=n+hashMerge(hash(e),hash(t))|0}:t?function(e){n=31*n+hash(e)|0}:function(e){n=n+hash(e)|0}),n)}(this))}});var Z=Iterable.prototype;Z[t]=!0,Z[v]=Z.values,Z.__toJS=Z.toArray,Z.__toStringMapper=quoteString,Z.inspect=Z.toSource=function(){return this.toString()},Z.chain=Z.flatMap,Z.contains=Z.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(e,t){var r=this,n=0;return reify(this,this.toSeq().map(function(i,o){return e.call(t,[o,i],n++,r)}).fromEntrySeq())},mapKeys:function(e,t){var r=this;return reify(this,this.toSeq().flip().map(function(n,i){return e.call(t,n,i,r)}).flip())}});var Q=KeyedIterable.prototype;function keyMapper(e,t){return t}function entryMapper(e,t){return[t,e]}function not(e){return function(){return!e.apply(this,arguments)}}function neg(e){return function(){return-e.apply(this,arguments)}}function quoteString(e){return"string"==typeof e?JSON.stringify(e):String(e)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(e,t){return et?-1:0}function hashMerge(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return Q[r]=!0,Q[v]=Z.entries,Q.__toJS=Z.toObject,Q.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+quoteString(e)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(e,t){return reify(this,filterFactory(this,e,t,!1))},findIndex:function(e,t){var r=this.findEntry(e,t);return r?r[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(e,t){return reify(this,sliceFactory(this,e,t,!1))},splice:function(e,t){var r=arguments.length;if(t=Math.max(0|t,0),0===r||2===r&&!t)return this;e=resolveBegin(e,e<0?this.count():this.size);var n=this.slice(0,e);return reify(this,1===r?n:n.concat(arrCopy(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var r=this.findLastEntry(e,t);return r?r[0]:-1},first:function(){return this.get(0)},flatten:function(e){return reify(this,flattenFactory(this,e,!1))},get:function(e,t){return(e=wrapIndex(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find(function(t,r){return r===e},void 0,t)},has:function(e){return(e=wrapIndex(this,e))>=0&&(void 0!==this.size?this.size===1/0||e5e3)return e.textContent;return function reset(e){for(var r,n,i,o,a,s=e.textContent,u=0,l=s[0],c=1,p=e.innerHTML="",f=0;n=r,r=f<7&&"\\"==r?1:c;){if(c=l,l=s[++u],o=p.length>1,!c||f>8&&"\n"==c||[/\S/.test(c),1,1,!/[$\w]/.test(c),("/"==r||"\n"==r)&&o,'"'==r&&o,"'"==r&&o,s[u-4]+n+r=="--\x3e",n+r=="*/"][f])for(p&&(e.appendChild(a=t.createElement("span")).setAttribute("style",["color: #555; font-weight: bold;","","","color: #555;",""][f?f<3?2:f>6?4:f>3?3:+/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/.test(p):0]),a.appendChild(t.createTextNode(p))),i=f&&f<7?f:i,p="",f=11;![1,/[\/{}[(\-+*=<>:;|\\.,?!&@~]/.test(c),/[\])]/.test(c),/[$\w]/.test(c),"/"==c&&i<2&&"<"!=r,'"'==c,"'"==c,c+l+s[u+1]+s[u+2]=="\x3c!--",c+l=="/*",c+l=="//","#"==c][--f];);p+=c}}(e)},t.mapToList=function mapToList(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"key";var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:l.default.Map();if(!l.default.Map.isMap(e)||!e.size)return l.default.List();Array.isArray(t)||(t=[t]);if(t.length<1)return e.merge(r);var n=l.default.List();var a=t[0];var s=!0;var u=!1;var c=void 0;try{for(var p,f=(0,o.default)(e.entries());!(s=(p=f.next()).done);s=!0){var d=p.value,h=(0,i.default)(d,2),m=h[0],v=h[1],g=mapToList(v,t.slice(1),r.set(a,m));n=l.default.List.isList(g)?n.concat(g):n.push(g)}}catch(e){u=!0,c=e}finally{try{!s&&f.return&&f.return()}finally{if(u)throw c}}return n},t.extractFileNameFromContentDispositionHeader=function extractFileNameFromContentDispositionHeader(e){var t=/filename="([^;]*);?"/i.exec(e);null===t&&(t=/filename=([^;]*);?/i.exec(e));if(null!==t&&t.length>1)return t[1];return null},t.pascalCase=pascalCase,t.pascalCaseFilename=function pascalCaseFilename(e){return pascalCase(e.replace(/\.[^./]*$/,""))},t.sanitizeUrl=function sanitizeUrl(e){if("string"!=typeof e||""===e)return"";return(0,c.sanitizeUrl)(e)},t.getAcceptControllingResponse=function getAcceptControllingResponse(e){if(!l.default.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=e.find(function(e,t){return t.startsWith("2")&&(0,s.default)(e.get("content")||{}).length>0}),r=e.get("default")||l.default.OrderedMap(),n=(r.get("content")||l.default.OrderedMap()).keySeq().toJS().length?r:null;return t||n},t.deeplyStripKey=function deeplyStripKey(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==(void 0===e?"undefined":(0,u.default)(e))||Array.isArray(e)||null===e||!t)return e;var n=(0,a.default)({},e);(0,s.default)(n).forEach(function(e){e===t&&r(n[e],e)?delete n[e]:n[e]=deeplyStripKey(n[e],t,r)});return n},t.stringify=function stringify(e){if("string"==typeof e)return e;e.toJS&&(e=e.toJS());if("object"===(void 0===e?"undefined":(0,u.default)(e))&&null!==e)try{return(0,n.default)(e,null,2)}catch(t){return String(e)}return e.toString()};var l=_interopRequireDefault(r(7)),c=r(617),p=_interopRequireDefault(r(618)),f=_interopRequireDefault(r(303)),d=_interopRequireDefault(r(308)),h=_interopRequireDefault(r(193)),m=_interopRequireDefault(r(695)),v=_interopRequireDefault(r(111)),g=r(204),y=_interopRequireDefault(r(32)),_=_interopRequireDefault(r(768));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var b="default",S=t.isImmutable=function isImmutable(e){return l.default.Iterable.isIterable(e)};function normalizeArray(e){return Array.isArray(e)?e:[e]}function isObject(e){return!!e&&"object"===(void 0===e?"undefined":(0,u.default)(e))}t.memoize=d.default;function pascalCase(e){return(0,f.default)((0,p.default)(e))}t.propChecker=function propChecker(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:[];return(0,s.default)(e).length!==(0,s.default)(t).length||((0,m.default)(e,function(e,r){if(n.includes(r))return!1;var i=t[r];return l.default.Iterable.isIterable(e)?!l.default.is(e,i):("object"!==(void 0===e?"undefined":(0,u.default)(e))||"object"!==(void 0===i?"undefined":(0,u.default)(i)))&&e!==i})||r.some(function(r){return!(0,v.default)(e[r],t[r])}))};var k=t.validateMaximum=function validateMaximum(e,t){if(e>t)return"Value must be less than Maximum"},x=t.validateMinimum=function validateMinimum(e,t){if(et)return"Value must be less than MaxLength"},O=t.validateMinLength=function validateMinLength(e,t){if(e.length2&&void 0!==arguments[2]&&arguments[2],n=[],i=t&&"body"===e.get("in")?e.get("value_xml"):e.get("value"),o=e.get("required"),a=r?e.get("schema"):e;if(!a)return n;var s=a.get("maximum"),c=a.get("minimum"),p=a.get("type"),f=a.get("format"),d=a.get("maxLength"),h=a.get("minLength"),m=a.get("pattern");if(p&&(o||i)){var v="string"===p&&i,g="array"===p&&Array.isArray(i)&&i.length,_="array"===p&&l.default.List.isList(i)&&i.count(),b="file"===p&&i instanceof y.default.File,S="boolean"===p&&(i||!1===i),I="number"===p&&(i||0===i),q="integer"===p&&(i||0===i),F=!1;if(r&&"object"===p)if("object"===(void 0===i?"undefined":(0,u.default)(i)))F=!0;else if("string"==typeof i)try{JSON.parse(i),F=!0}catch(e){return n.push("Parameter string value must be valid JSON"),n}var B=[v,g,_,b,S,I,q,F].some(function(e){return!!e});if(o&&!B)return n.push("Required field is not provided"),n;if(m){var N=P(i,m);N&&n.push(N)}if(d||0===d){var j=T(i,d);j&&n.push(j)}if(h){var L=O(i,h);L&&n.push(L)}if(s||0===s){var z=k(i,s);z&&n.push(z)}if(c||0===c){var U=x(i,c);U&&n.push(U)}if("string"===p){var W=void 0;if(!(W="date-time"===f?R(i):"uuid"===f?M(i):A(i)))return n;n.push(W)}else if("boolean"===p){var V=D(i);if(!V)return n;n.push(V)}else if("number"===p){var H=E(i);if(!H)return n;n.push(H)}else if("integer"===p){var J=C(i);if(!J)return n;n.push(J)}else if("array"===p){var K;if(!_||!i.count())return n;K=a.getIn(["items","type"]),i.forEach(function(e,t){var r=void 0;"number"===K?r=E(e):"integer"===K?r=C(e):"string"===K&&(r=A(e)),r&&n.push({index:t,error:r})})}else if("file"===p){var G=w(i);if(!G)return n;n.push(G)}}return n},t.getSampleSchema=function getSampleSchema(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(/xml/.test(t)){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated --\x3e':null;var i=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=i[1]}return(0,g.memoizedCreateXMLExample)(e,r)}var o=(0,g.memoizedSampleFromSchema)(e,r);return"object"===(void 0===o?"undefined":(0,u.default)(o))?(0,n.default)(o,null,2):o},t.parseSearch=function parseSearch(){var e={},t=y.default.location.search;if(!t)return{};if(""!=t){var r=t.substr(1).split("&");for(var n in r)r.hasOwnProperty(n)&&(n=r[n].split("="),e[decodeURIComponent(n[0])]=n[1]&&decodeURIComponent(n[1])||"")}return e},t.serializeSearch=function serializeSearch(e){return(0,s.default)(e).map(function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])}).join("&")},t.btoa=function btoa(t){return(t instanceof e?t:new e(t.toString(),"utf-8")).toString("base64")},t.sorters={operationsSorter:{alpha:function alpha(e,t){return e.get("path").localeCompare(t.get("path"))},method:function method(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function alpha(e,t){return e.localeCompare(t)}}},t.buildFormData=function buildFormData(e){var t=[];for(var r in e){var n=e[r];void 0!==n&&""!==n&&t.push([r,"=",encodeURIComponent(n).replace(/%20/g,"+")].join(""))}return t.join("&")},t.shallowEqualKeys=function shallowEqualKeys(e,t,r){return!!(0,h.default)(r,function(r){return(0,v.default)(e[r],t[r])})};var I=t.createDeepLinkPath=function createDeepLinkPath(e){return"string"==typeof e||e instanceof String?e.trim().replace(/\s/g,"_"):""};t.escapeDeepLinkPath=function escapeDeepLinkPath(e){return(0,_.default)(I(e))},t.getExtensions=function getExtensions(e){return e.filter(function(e,t){return/^x-/.test(t)})},t.getCommonExtensions=function getCommonExtensions(e){return e.filter(function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)})}}).call(t,r(48).Buffer)},function(e,t,r){"use strict";e.exports=function reactProdInvariant(e){for(var t=arguments.length-1,r="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,n=0;n>",o={listOf:function createListOfTypeChecker(e){return createIterableTypeChecker(e,"List",n.List.isList)},mapOf:function createMapOfTypeChecker(e,t){return createMapOfTypeCheckerFactory(e,t,"Map",n.Map.isMap)},orderedMapOf:function createOrderedMapOfTypeChecker(e,t){return createMapOfTypeCheckerFactory(e,t,"OrderedMap",n.OrderedMap.isOrderedMap)},setOf:function createSetOfTypeChecker(e){return createIterableTypeChecker(e,"Set",n.Set.isSet)},orderedSetOf:function createOrderedSetOfTypeChecker(e){return createIterableTypeChecker(e,"OrderedSet",n.OrderedSet.isOrderedSet)},stackOf:function createStackOfTypeChecker(e){return createIterableTypeChecker(e,"Stack",n.Stack.isStack)},iterableOf:function createIterableOfTypeChecker(e){return createIterableTypeChecker(e,"Iterable",n.Iterable.isIterable)},recordOf:function createRecordOfTypeChecker(e){return createChainableTypeChecker(function validate(t,r,i,o,a){for(var s=arguments.length,u=Array(s>5?s-5:0),l=5;l6?u-6:0),c=6;c5?u-5:0),c=5;c5?a-5:0),u=5;u key("+c[p]+")"].concat(s));if(d instanceof Error)return d}})}(t).apply(void 0,o)})}function createShapeTypeChecker(e){var t=void 0===arguments[1]?"Iterable":arguments[1],r=void 0===arguments[2]?n.Iterable.isIterable:arguments[2];return createChainableTypeChecker(function validate(n,i,o,a,s){for(var u=arguments.length,l=Array(u>5?u-5:0),c=5;c?@[\]^_`{|}~-])/g;function isValidEntityCode(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function fromCodePoint(e){if(e>65535){var t=55296+((e-=65536)>>10),r=56320+(1023&e);return String.fromCharCode(t,r)}return String.fromCharCode(e)}var o=/&([a-z#][a-z0-9]{1,31});/gi,a=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,s=r(456);function replaceEntityPattern(e,t){var r=0;return has(s,t)?s[t]:35===t.charCodeAt(0)&&a.test(t)&&isValidEntityCode(r="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10))?fromCodePoint(r):e}var u=/[&<>"]/,l=/[&<>"]/g,c={"&":"&","<":"<",">":">",'"':"""};function replaceUnsafeChar(e){return c[e]}t.assign=function assign(e){return[].slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(r){e[r]=t[r]})}}),e},t.isString=function isString(e){return"[object String]"===function typeOf(e){return Object.prototype.toString.call(e)}(e)},t.has=has,t.unescapeMd=function unescapeMd(e){return e.indexOf("\\")<0?e:e.replace(i,"$1")},t.isValidEntityCode=isValidEntityCode,t.fromCodePoint=fromCodePoint,t.replaceEntities=function replaceEntities(e){return e.indexOf("&")<0?e:e.replace(o,replaceEntityPattern)},t.escapeHtml=function escapeHtml(e){return u.test(e)?e.replace(l,replaceUnsafeChar):e}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,r){var n=r(33),i=r(63),o=r(61),a=r(75),s=r(127),u=function(e,t,r){var l,c,p,f,d=e&u.F,h=e&u.G,m=e&u.S,v=e&u.P,g=e&u.B,y=h?n:m?n[t]||(n[t]={}):(n[t]||{}).prototype,_=h?i:i[t]||(i[t]={}),b=_.prototype||(_.prototype={});for(l in h&&(r=t),r)p=((c=!d&&y&&void 0!==y[l])?y:r)[l],f=g&&c?s(p,n):v&&"function"==typeof p?s(Function.call,p):p,y&&a(y,l,p,e&u.U),_[l]!=p&&o(_,l,f),v&&b[l]!=p&&(b[l]=p)};n.core=i,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t,r){var n=r(30),i=r(107),o=r(57),a=/"/g,s=function(e,t,r,n){var i=String(o(e)),s="<"+t;return""!==r&&(s+=" "+r+'="'+String(n).replace(a,""")+'"'),s+">"+i+""};e.exports=function(e,t){var r={};r[e]=t(s),n(n.P+n.F*i(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",r)}},function(e,t,r){"use strict";var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(97));e.exports=function makeWindow(){var e={location:{},history:{},open:function open(){},close:function close(){},File:function File(){}};if("undefined"==typeof window)return e;try{e=window;var t=!0,r=!1,i=void 0;try{for(var o,a=(0,n.default)(["File","Blob","FormData"]);!(t=(o=a.next()).done);t=!0){var s=o.value;s in window&&(e[s]=window[s])}}catch(e){r=!0,i=e}finally{try{!t&&a.return&&a.return()}finally{if(r)throw i}}}catch(e){console.error(e)}return e}()},function(e,t){var r=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=r)},function(e,t,r){"use strict";function makeEmptyFunction(e){return function(){return e}}var n=function emptyFunction(){};n.thatReturns=makeEmptyFunction,n.thatReturnsFalse=makeEmptyFunction(!1),n.thatReturnsTrue=makeEmptyFunction(!0),n.thatReturnsNull=makeEmptyFunction(null),n.thatReturnsThis=function(){return this},n.thatReturnsArgument=function(e){return e},e.exports=n},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(26));t.isOAS3=isOAS3,t.isSwagger2=function isSwagger2(e){var t=e.get("swagger");if("string"!=typeof t)return!1;return t.startsWith("2.0")},t.OAS3ComponentWrapFactory=function OAS3ComponentWrapFactory(e){return function(t,r){return function(o){if(r&&r.specSelectors&&r.specSelectors.specJson){var a=r.specSelectors.specJson();return isOAS3(a)?i.default.createElement(e,(0,n.default)({},o,r,{Ori:t})):i.default.createElement(t,o)}return console.warn("OAS3 wrapper: couldn't get spec"),null}}};var i=_interopRequireDefault(r(0));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}function isOAS3(e){var t=e.get("openapi");return"string"==typeof t&&(t.startsWith("3.0.")&&t.length>4)}},function(e,t,r){var n=r(29);e.exports=function(e){if(!n(e))throw TypeError(e+" is not an object!");return e}},function(e,t,r){var n=r(301),i="object"==typeof self&&self&&self.Object===Object&&self,o=n||i||Function("return this")();e.exports=o},function(e,t){e.exports=function isObject(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t){var r,n,i=e.exports={};function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}function runTimeout(e){if(r===setTimeout)return setTimeout(e,0);if((r===defaultSetTimout||!r)&&setTimeout)return r=setTimeout,setTimeout(e,0);try{return r(e,0)}catch(t){try{return r.call(null,e,0)}catch(t){return r.call(this,e,0)}}}!function(){try{r="function"==typeof setTimeout?setTimeout:defaultSetTimout}catch(e){r=defaultSetTimout}try{n="function"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(e){n=defaultClearTimeout}}();var o,a=[],s=!1,u=-1;function cleanUpNextTick(){s&&o&&(s=!1,o.length?a=o.concat(a):u=-1,a.length&&drainQueue())}function drainQueue(){if(!s){var e=runTimeout(cleanUpNextTick);s=!0;for(var t=a.length;t;){for(o=a,a=[];++u1)for(var r=1;r0&&(o=this.buffer[s-1],e.call("\0\r\n…\u2028\u2029",o)<0);)if(s--,this.pointer-s>r/2-1){i=" ... ",s+=5;break}for(u="",n=this.pointer;nr/2-1){u=" ... ",n-=5;break}return""+new Array(t).join(" ")+i+this.buffer.slice(s,n)+u+"\n"+new Array(t+this.pointer-s+i.length).join(" ")+"^"},Mark.prototype.toString=function(){var e,t;return e=this.get_snippet(),t=" on line "+(this.line+1)+", column "+(this.column+1),e?t:t+":\n"+e},Mark}(),this.YAMLError=function(e){function YAMLError(e){this.message=e,YAMLError.__super__.constructor.call(this),this.stack=this.toString()+"\n"+(new Error).stack.split("\n").slice(1).join("\n")}return t(YAMLError,e),YAMLError.prototype.toString=function(){return this.message},YAMLError}(Error),this.MarkedYAMLError=function(e){function MarkedYAMLError(e,t,r,n,i){this.context=e,this.context_mark=t,this.problem=r,this.problem_mark=n,this.note=i,MarkedYAMLError.__super__.constructor.call(this)}return t(MarkedYAMLError,e),MarkedYAMLError.prototype.toString=function(){var e;return e=[],null!=this.context&&e.push(this.context),null==this.context_mark||null!=this.problem&&null!=this.problem_mark&&this.context_mark.line===this.problem_mark.line&&this.context_mark.column===this.problem_mark.column||e.push(this.context_mark.toString()),null!=this.problem&&e.push(this.problem),null!=this.problem_mark&&e.push(this.problem_mark.toString()),null!=this.note&&e.push(this.note),e.join("\n")},MarkedYAMLError}(this.YAMLError)}).call(this)},function(e,t,r){e.exports=!r(55)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,r){"use strict";(function(e){ +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ +var n=r(574),i=r(575),o=r(284);function kMaxLength(){return Buffer.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function createBuffer(e,t){if(kMaxLength()=kMaxLength())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+kMaxLength().toString(16)+" bytes");return 0|e}function byteLength(e,t){if(Buffer.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var r=e.length;if(0===r)return 0;for(var n=!1;;)switch(t){case"ascii":case"latin1":case"binary":return r;case"utf8":case"utf-8":case void 0:return utf8ToBytes(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*r;case"hex":return r>>>1;case"base64":return base64ToBytes(e).length;default:if(n)return utf8ToBytes(e).length;t=(""+t).toLowerCase(),n=!0}}function swap(e,t,r){var n=e[t];e[t]=e[r],e[r]=n}function bidirectionalIndexOf(e,t,r,n,i){if(0===e.length)return-1;if("string"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),r=+r,isNaN(r)&&(r=i?0:e.length-1),r<0&&(r=e.length+r),r>=e.length){if(i)return-1;r=e.length-1}else if(r<0){if(!i)return-1;r=0}if("string"==typeof t&&(t=Buffer.from(t,n)),Buffer.isBuffer(t))return 0===t.length?-1:arrayIndexOf(e,t,r,n,i);if("number"==typeof t)return t&=255,Buffer.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,r):Uint8Array.prototype.lastIndexOf.call(e,t,r):arrayIndexOf(e,[t],r,n,i);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(e,t,r,n,i){var o,a=1,s=e.length,u=t.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,r/=2}function read(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(i){var l=-1;for(o=r;os&&(r=s-u),o=r;o>=0;o--){for(var c=!0,p=0;pi&&(n=i):n=i;var o=t.length;if(o%2!=0)throw new TypeError("Invalid hex string");n>o/2&&(n=o/2);for(var a=0;a>8,i=r%256,o.push(i),o.push(n);return o}(t,e.length-r),e,r,n)}function base64Slice(e,t,r){return 0===t&&r===e.length?n.fromByteArray(e):n.fromByteArray(e.slice(t,r))}function utf8Slice(e,t,r){r=Math.min(e.length,r);for(var n=[],i=t;i239?4:c>223?3:c>191?2:1;if(i+f<=r)switch(f){case 1:c<128&&(p=c);break;case 2:128==(192&(o=e[i+1]))&&(l=(31&c)<<6|63&o)>127&&(p=l);break;case 3:o=e[i+1],s=e[i+2],128==(192&o)&&128==(192&s)&&(l=(15&c)<<12|(63&o)<<6|63&s)>2047&&(l<55296||l>57343)&&(p=l);break;case 4:o=e[i+1],s=e[i+2],u=e[i+3],128==(192&o)&&128==(192&s)&&128==(192&u)&&(l=(15&c)<<18|(63&o)<<12|(63&s)<<6|63&u)>65535&&l<1114112&&(p=l)}null===p?(p=65533,f=1):p>65535&&(p-=65536,n.push(p>>>10&1023|55296),p=56320|1023&p),n.push(p),i+=f}return function decodeCodePointsArray(e){var t=e.length;if(t<=a)return String.fromCharCode.apply(String,e);var r="",n=0;for(;nthis.length)return"";if((void 0===r||r>this.length)&&(r=this.length),r<=0)return"";if((r>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return hexSlice(this,t,r);case"utf8":case"utf-8":return utf8Slice(this,t,r);case"ascii":return asciiSlice(this,t,r);case"latin1":case"binary":return latin1Slice(this,t,r);case"base64":return base64Slice(this,t,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,t,r);default:if(n)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),n=!0}}.apply(this,arguments)},Buffer.prototype.equals=function equals(e){if(!Buffer.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===Buffer.compare(this,e)},Buffer.prototype.inspect=function inspect(){var e="",r=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,r).match(/.{2}/g).join(" "),this.length>r&&(e+=" ... ")),""},Buffer.prototype.compare=function compare(e,t,r,n,i){if(!Buffer.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===r&&(r=e?e.length:0),void 0===n&&(n=0),void 0===i&&(i=this.length),t<0||r>e.length||n<0||i>this.length)throw new RangeError("out of range index");if(n>=i&&t>=r)return 0;if(n>=i)return-1;if(t>=r)return 1;if(t>>>=0,r>>>=0,n>>>=0,i>>>=0,this===e)return 0;for(var o=i-n,a=r-t,s=Math.min(o,a),u=this.slice(n,i),l=e.slice(t,r),c=0;ci)&&(r=i),e.length>0&&(r<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var o=!1;;)switch(n){case"hex":return hexWrite(this,e,t,r);case"utf8":case"utf-8":return utf8Write(this,e,t,r);case"ascii":return asciiWrite(this,e,t,r);case"latin1":case"binary":return latin1Write(this,e,t,r);case"base64":return base64Write(this,e,t,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,e,t,r);default:if(o)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),o=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var a=4096;function asciiSlice(e,t,r){var n="";r=Math.min(e.length,r);for(var i=t;in)&&(r=n);for(var i="",o=t;or)throw new RangeError("Trying to access beyond buffer length")}function checkInt(e,t,r,n,i,o){if(!Buffer.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||te.length)throw new RangeError("Index out of range")}function objectWriteUInt16(e,t,r,n){t<0&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-r,2);i>>8*(n?i:1-i)}function objectWriteUInt32(e,t,r,n){t<0&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-r,4);i>>8*(n?i:3-i)&255}function checkIEEE754(e,t,r,n,i,o){if(r+n>e.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("Index out of range")}function writeFloat(e,t,r,n,o){return o||checkIEEE754(e,0,r,4),i.write(e,t,r,n,23,4),r+4}function writeDouble(e,t,r,n,o){return o||checkIEEE754(e,0,r,8),i.write(e,t,r,n,52,8),r+8}Buffer.prototype.slice=function slice(e,t){var r,n=this.length;if(e=~~e,t=void 0===t?n:~~t,e<0?(e+=n)<0&&(e=0):e>n&&(e=n),t<0?(t+=n)<0&&(t=0):t>n&&(t=n),t0&&(i*=256);)n+=this[e+--t]*i;return n},Buffer.prototype.readUInt8=function readUInt8(e,t){return t||checkOffset(e,1,this.length),this[e]},Buffer.prototype.readUInt16LE=function readUInt16LE(e,t){return t||checkOffset(e,2,this.length),this[e]|this[e+1]<<8},Buffer.prototype.readUInt16BE=function readUInt16BE(e,t){return t||checkOffset(e,2,this.length),this[e]<<8|this[e+1]},Buffer.prototype.readUInt32LE=function readUInt32LE(e,t){return t||checkOffset(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},Buffer.prototype.readUInt32BE=function readUInt32BE(e,t){return t||checkOffset(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},Buffer.prototype.readIntLE=function readIntLE(e,t,r){e|=0,t|=0,r||checkOffset(e,t,this.length);for(var n=this[e],i=1,o=0;++o=(i*=128)&&(n-=Math.pow(2,8*t)),n},Buffer.prototype.readIntBE=function readIntBE(e,t,r){e|=0,t|=0,r||checkOffset(e,t,this.length);for(var n=t,i=1,o=this[e+--n];n>0&&(i*=256);)o+=this[e+--n]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*t)),o},Buffer.prototype.readInt8=function readInt8(e,t){return t||checkOffset(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},Buffer.prototype.readInt16LE=function readInt16LE(e,t){t||checkOffset(e,2,this.length);var r=this[e]|this[e+1]<<8;return 32768&r?4294901760|r:r},Buffer.prototype.readInt16BE=function readInt16BE(e,t){t||checkOffset(e,2,this.length);var r=this[e+1]|this[e]<<8;return 32768&r?4294901760|r:r},Buffer.prototype.readInt32LE=function readInt32LE(e,t){return t||checkOffset(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(e,t){return t||checkOffset(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},Buffer.prototype.readFloatLE=function readFloatLE(e,t){return t||checkOffset(e,4,this.length),i.read(this,e,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(e,t){return t||checkOffset(e,4,this.length),i.read(this,e,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(e,t){return t||checkOffset(e,8,this.length),i.read(this,e,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(e,t){return t||checkOffset(e,8,this.length),i.read(this,e,!1,52,8)},Buffer.prototype.writeUIntLE=function writeUIntLE(e,t,r,n){(e=+e,t|=0,r|=0,n)||checkInt(this,e,t,r,Math.pow(2,8*r)-1,0);var i=1,o=0;for(this[t]=255&e;++o=0&&(o*=256);)this[t+i]=e/o&255;return t+r},Buffer.prototype.writeUInt8=function writeUInt8(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,1,255,0),Buffer.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},Buffer.prototype.writeUInt16LE=function writeUInt16LE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,2,65535,0),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):objectWriteUInt16(this,e,t,!0),t+2},Buffer.prototype.writeUInt16BE=function writeUInt16BE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,2,65535,0),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):objectWriteUInt16(this,e,t,!1),t+2},Buffer.prototype.writeUInt32LE=function writeUInt32LE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,4,4294967295,0),Buffer.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):objectWriteUInt32(this,e,t,!0),t+4},Buffer.prototype.writeUInt32BE=function writeUInt32BE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,4,4294967295,0),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):objectWriteUInt32(this,e,t,!1),t+4},Buffer.prototype.writeIntLE=function writeIntLE(e,t,r,n){if(e=+e,t|=0,!n){var i=Math.pow(2,8*r-1);checkInt(this,e,t,r,i-1,-i)}var o=0,a=1,s=0;for(this[t]=255&e;++o>0)-s&255;return t+r},Buffer.prototype.writeIntBE=function writeIntBE(e,t,r,n){if(e=+e,t|=0,!n){var i=Math.pow(2,8*r-1);checkInt(this,e,t,r,i-1,-i)}var o=r-1,a=1,s=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)e<0&&0===s&&0!==this[t+o+1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+r},Buffer.prototype.writeInt8=function writeInt8(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,1,127,-128),Buffer.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},Buffer.prototype.writeInt16LE=function writeInt16LE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,2,32767,-32768),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):objectWriteUInt16(this,e,t,!0),t+2},Buffer.prototype.writeInt16BE=function writeInt16BE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,2,32767,-32768),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):objectWriteUInt16(this,e,t,!1),t+2},Buffer.prototype.writeInt32LE=function writeInt32LE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,4,2147483647,-2147483648),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):objectWriteUInt32(this,e,t,!0),t+4},Buffer.prototype.writeInt32BE=function writeInt32BE(e,t,r){return e=+e,t|=0,r||checkInt(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),Buffer.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):objectWriteUInt32(this,e,t,!1),t+4},Buffer.prototype.writeFloatLE=function writeFloatLE(e,t,r){return writeFloat(this,e,t,!0,r)},Buffer.prototype.writeFloatBE=function writeFloatBE(e,t,r){return writeFloat(this,e,t,!1,r)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(e,t,r){return writeDouble(this,e,t,!0,r)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(e,t,r){return writeDouble(this,e,t,!1,r)},Buffer.prototype.copy=function copy(e,t,r,n){if(r||(r=0),n||0===n||(n=this.length),t>=e.length&&(t=e.length),t||(t=0),n>0&&n=this.length)throw new RangeError("sourceStart out of bounds");if(n<0)throw new RangeError("sourceEnd out of bounds");n>this.length&&(n=this.length),e.length-t=0;--i)e[i+t]=this[i+r];else if(o<1e3||!Buffer.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,r=void 0===r?this.length:r>>>0,e||(e=0),"number"==typeof e)for(o=t;o55295&&r<57344){if(!i){if(r>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===n){(t-=3)>-1&&o.push(239,191,189);continue}i=r;continue}if(r<56320){(t-=3)>-1&&o.push(239,191,189),i=r;continue}r=65536+(i-55296<<10|r-56320)}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,r<128){if((t-=1)<0)break;o.push(r)}else if(r<2048){if((t-=2)<0)break;o.push(r>>6|192,63&r|128)}else if(r<65536){if((t-=3)<0)break;o.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return o}function base64ToBytes(e){return n.toByteArray(function base64clean(e){if((e=function stringtrim(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(s,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function blitBuffer(e,t,r,n){for(var i=0;i=t.length||i>=e.length);++i)t[i+r]=e[i];return i}}).call(t,r(18))},function(e,t,r){"use strict";e.exports={current:null}},function(e,t){e.exports=function isObjectLike(e){return null!=e&&"object"==typeof e}},function(e,t,r){"use strict";var n=r(13),i=r(71),o=r(34),a=(r(9),["dispatchConfig","_targetInst","nativeEvent","isDefaultPrevented","isPropagationStopped","_dispatchListeners","_dispatchInstances"]),s={type:null,target:null,currentTarget:o.thatReturnsNull,eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null};function SyntheticEvent(e,t,r,n){this.dispatchConfig=e,this._targetInst=t,this.nativeEvent=r;var i=this.constructor.Interface;for(var a in i)if(i.hasOwnProperty(a)){0;var s=i[a];s?this[a]=s(r):"target"===a?this.target=n:this[a]=r[a]}var u=null!=r.defaultPrevented?r.defaultPrevented:!1===r.returnValue;return this.isDefaultPrevented=u?o.thatReturnsTrue:o.thatReturnsFalse,this.isPropagationStopped=o.thatReturnsFalse,this}n(SyntheticEvent.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=o.thatReturnsTrue)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=o.thatReturnsTrue)},persist:function(){this.isPersistent=o.thatReturnsTrue},isPersistent:o.thatReturnsFalse,destructor:function(){var e=this.constructor.Interface;for(var t in e)this[t]=null;for(var r=0;r1?t-1:0),n=1;n2?r-2:0),i=2;i=r?e:e.length+1===r?""+t+e:""+new Array(r-e.length+1).join(t)+e},this.to_hex=function(e){return"string"==typeof e&&(e=e.charCodeAt(0)),e.toString(16)}}).call(this)}).call(t,r(18))},function(e,t,r){var n=r(124),i=r(266);e.exports=r(106)?function(e,t,r){return n.f(e,t,i(1,r))}:function(e,t,r){return e[t]=r,e}},function(e,t,r){var n=r(76);e.exports=function(e){if(!n(e))throw TypeError(e+" is not an object!");return e}},function(e,t){var r=e.exports={version:"2.5.5"};"number"==typeof __e&&(__e=r)},function(e,t,r){var n=r(80),i=r(620),o=r(621),a="[object Null]",s="[object Undefined]",u=n?n.toStringTag:void 0;e.exports=function baseGetTag(e){return null==e?void 0===e?s:a:u&&u in Object(e)?i(e):o(e)}},function(e,t,r){var n=r(637),i=r(640);e.exports=function getNative(e,t){var r=i(e,t);return n(r)?r:void 0}},function(e,t,r){var n=r(318),i=r(677),o=r(81);e.exports=function keys(e){return o(e)?n(e):i(e)}},function(e,t,r){"use strict";var n=r(147),i=Object.keys||function(e){var t=[];for(var r in e)t.push(r);return t};e.exports=Duplex;var o=r(112);o.inherits=r(84);var a=r(328),s=r(207);o.inherits(Duplex,a);for(var u=i(s.prototype),l=0;l1){for(var f=Array(p),d=0;d1){for(var m=Array(h),v=0;v=0||Object.prototype.hasOwnProperty.call(e,n)&&(r[n]=e[n]);return r}},function(e,t,r){"use strict";function isNothing(e){return void 0===e||null===e}e.exports.isNothing=isNothing,e.exports.isObject=function isObject(e){return"object"==typeof e&&null!==e},e.exports.toArray=function toArray(e){return Array.isArray(e)?e:isNothing(e)?[]:[e]},e.exports.repeat=function repeat(e,t){var r,n="";for(r=0;r`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>",u="]",l=new RegExp("^(?:<[A-Za-z][A-Za-z0-9-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>|]|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|[<][?].*?[?][>]|]*>|)","i"),c=/[\\&]/,p="[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]",f=new RegExp("\\\\"+p+"|"+a,"gi"),d=new RegExp('[&<>"]',"g"),h=new RegExp(a+'|[&<>"]',"gi"),m=function(e){return 92===e.charCodeAt(0)?e.charAt(1):o(e)},v=function(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";default:return e}};e.exports={unescapeString:function(e){return c.test(e)?e.replace(f,m):e},normalizeURI:function(e){try{return n(i(e))}catch(t){return e}},escapeXml:function(e,t){return d.test(e)?t?e.replace(h,v):e.replace(d,v):e},reHtmlTag:l,OPENTAG:s,CLOSETAG:u,ENTITY:a,ESCAPABLE:p}},function(e,t,r){"use strict";var n=r(476),i=r(477),o=r(164).decodeHTML,a="&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});",s="<[A-Za-z][A-Za-z0-9-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>",u="]",l=new RegExp("^(?:<[A-Za-z][A-Za-z0-9-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>|]|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|[<][?].*?[?][>]|]*>|)","i"),c=/[\\&]/,p="[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]",f=new RegExp("\\\\"+p+"|"+a,"gi"),d=new RegExp('[&<>"]',"g"),h=new RegExp(a+'|[&<>"]',"gi"),m=function(e){return 92===e.charCodeAt(0)?e.charAt(1):o(e)},v=function(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";default:return e}};e.exports={unescapeString:function(e){return c.test(e)?e.replace(f,m):e},normalizeURI:function(e){try{return n(i(e))}catch(t){return e}},escapeXml:function(e,t){return d.test(e)?t?e.replace(h,v):e.replace(d,v):e},reHtmlTag:l,OPENTAG:s,CLOSETAG:u,ENTITY:a,ESCAPABLE:p}},function(e,t,r){e.exports={default:r(493),__esModule:!0}},function(e,t,r){r(494);for(var n=r(23),i=r(54),o=r(72),a=r(20)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),u=0;u=t.length?{value:void 0,done:!0}:(e=n(t,r),this._i+=e.length,{value:e,done:!1})})},function(e,t){var r={}.toString;e.exports=function(e){return r.call(e).slice(8,-1)}},function(e,t,r){e.exports=!r(107)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports={}},function(e,t,r){var n=r(126),i=Math.min;e.exports=function(e){return e>0?i(n(e),9007199254740991):0}},function(e,t,r){"use strict";e.exports=function reactProdInvariant(e){for(var t=arguments.length-1,r="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,n=0;n0?i(n(e),9007199254740991):0}},function(e,t){var r=0,n=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++r+n).toString(36))}},function(e,t,r){var n=r(62),i=r(505),o=r(506),a=Object.defineProperty;t.f=r(106)?Object.defineProperty:function defineProperty(e,t,r){if(n(e),t=o(t,!0),n(r),i)try{return a(e,t,r)}catch(e){}if("get"in r||"set"in r)throw TypeError("Accessors not supported!");return"value"in r&&(e[t]=r.value),e}},function(e,t){var r={}.hasOwnProperty;e.exports=function(e,t){return r.call(e,t)}},function(e,t){var r=Math.ceil,n=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?n:r)(e)}},function(e,t,r){var n=r(128);e.exports=function(e,t,r){if(n(e),void 0===t)return e;switch(r){case 1:return function(r){return e.call(t,r)};case 2:return function(r,n){return e.call(t,r,n)};case 3:return function(r,n,i){return e.call(t,r,n,i)}}return function(){return e.apply(t,arguments)}}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,r){var n=r(511),i=r(57);e.exports=function(e){return n(i(e))}},function(e,t,r){"use strict";var n=r(61),i=r(75),o=r(107),a=r(57),s=r(17);e.exports=function(e,t,r){var u=s(e),l=r(a,u,""[e]),c=l[0],p=l[1];o(function(){var t={};return t[u]=function(){return 7},7!=""[e](t)})&&(i(String.prototype,e,c),n(RegExp.prototype,u,2==t?function(e,t){return p.call(e,this,t)}:function(e){return p.call(e,this)}))}},function(e,t,r){var n=r(123)("meta"),i=r(29),o=r(56),a=r(41).f,s=0,u=Object.isExtensible||function(){return!0},l=!r(55)(function(){return u(Object.preventExtensions({}))}),c=function(e){a(e,n,{value:{i:"O"+ ++s,w:{}}})},p=e.exports={KEY:n,NEED:!1,fastKey:function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,n)){if(!u(e))return"F";if(!t)return"E";c(e)}return e[n].i},getWeak:function(e,t){if(!o(e,n)){if(!u(e))return!0;if(!t)return!1;c(e)}return e[n].w},onFreeze:function(e){return l&&p.NEED&&u(e)&&!o(e,n)&&c(e),e}}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t,r){"use strict";var n={};e.exports=n},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CLEAR_BY=t.CLEAR=t.NEW_AUTH_ERR=t.NEW_SPEC_ERR_BATCH=t.NEW_SPEC_ERR=t.NEW_THROWN_ERR_BATCH=t.NEW_THROWN_ERR=void 0,t.newThrownErr=function newThrownErr(e){return{type:i,payload:(0,n.default)(e)}},t.newThrownErrBatch=function newThrownErrBatch(e){return{type:o,payload:e}},t.newSpecErr=function newSpecErr(e){return{type:a,payload:e}},t.newSpecErrBatch=function newSpecErrBatch(e){return{type:s,payload:e}},t.newAuthErr=function newAuthErr(e){return{type:u,payload:e}},t.clear=function clear(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:l,payload:e}},t.clearBy=function clearBy(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:c,payload:e}};var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(190));var i=t.NEW_THROWN_ERR="err_new_thrown_err",o=t.NEW_THROWN_ERR_BATCH="err_new_thrown_err_batch",a=t.NEW_SPEC_ERR="err_new_spec_err",s=t.NEW_SPEC_ERR_BATCH="err_new_spec_err_batch",u=t.NEW_AUTH_ERR="err_new_auth_err",l=t.CLEAR="err_clear",c=t.CLEAR_BY="err_clear_by"},function(e,t,r){var n=r(64),i=r(50),o="[object Symbol]";e.exports=function isSymbol(e){return"symbol"==typeof e||i(e)&&n(e)==o}},function(e,t,r){var n=r(65)(Object,"create");e.exports=n},function(e,t,r){var n=r(645),i=r(646),o=r(647),a=r(648),s=r(649);function ListCache(e){var t=-1,r=null==e?0:e.length;for(this.clear();++t-1&&e%1==0&&e_;_++)if((v=t?y(a(h=e[_])[0],h[1]):y(e[_]))===l||v===c)return v}else for(m=g.call(e);!(h=m.next()).done;)if((v=i(m,y,h.value,t))===l||v===c)return v}).BREAK=l,t.RETURN=c},function(e,t,r){"use strict";var n=r(89);e.exports=n.DEFAULT=new n({include:[r(114)],explicit:[r(802),r(803),r(804)]})},function(e,t,r){var n=r(367),i=r(111),o=Object.prototype.hasOwnProperty;e.exports=function assignValue(e,t,r){var a=e[t];o.call(e,t)&&i(a,r)&&(void 0!==r||t in e)||n(e,t,r)}},function(e,t,r){"use strict";var n=r(11),i=(r(8),{}),o={reinitializeTransaction:function(){this.transactionWrappers=this.getTransactionWrappers(),this.wrapperInitData?this.wrapperInitData.length=0:this.wrapperInitData=[],this._isInTransaction=!1},_isInTransaction:!1,getTransactionWrappers:null,isInTransaction:function(){return!!this._isInTransaction},perform:function(e,t,r,i,o,a,s,u){var l,c;this.isInTransaction()&&n("27");try{this._isInTransaction=!0,l=!0,this.initializeAll(0),c=e.call(t,r,i,o,a,s,u),l=!1}finally{try{if(l)try{this.closeAll(0)}catch(e){}else this.closeAll(0)}finally{this._isInTransaction=!1}}return c},initializeAll:function(e){for(var t=this.transactionWrappers,r=e;r]/,u=r(229)(function(e,t){if(e.namespaceURI!==o.svg||"innerHTML"in e)e.innerHTML=t;else{(n=n||document.createElement("div")).innerHTML=""+t+"";for(var r=n.firstChild;r.firstChild;)e.appendChild(r.firstChild)}});if(i.canUseDOM){var l=document.createElement("div");l.innerHTML=" ",""===l.innerHTML&&(u=function(e,t){if(e.parentNode&&e.parentNode.replaceChild(e,e),a.test(t)||"<"===t[0]&&s.test(t)){e.innerHTML=String.fromCharCode(65279)+t;var r=e.firstChild;1===r.data.length?e.removeChild(r):r.deleteData(0,1)}else e.innerHTML=t}),l=null}e.exports=u},function(e,t,r){"use strict";var n=/["'&<>]/;e.exports=function escapeTextContentForBrowser(e){return"boolean"==typeof e||"number"==typeof e?""+e:function escapeHtml(e){var t,r=""+e,i=n.exec(r);if(!i)return r;var o="",a=0,s=0;for(a=i.index;adocument.F=Object<\/script>"),e.close(),u=e.F;n--;)delete u.prototype[o[n]];return u()};e.exports=Object.create||function create(e,t){var r;return null!==e?(s.prototype=n(e),r=new s,s.prototype=null,r[a]=e):r=u(),void 0===t?r:i(r,t)}},function(e,t){var r=Math.ceil,n=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?n:r)(e)}},function(e,t,r){var n=r(173)("keys"),i=r(123);e.exports=function(e){return n[e]||(n[e]=i(e))}},function(e,t,r){var n=r(23),i=n["__core-js_shared__"]||(n["__core-js_shared__"]={});e.exports=function(e){return i[e]||(i[e]={})}},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,r){var n=r(176),i=r(20)("iterator"),o=r(72);e.exports=r(15).getIteratorMethod=function(e){if(void 0!=e)return e[i]||e["@@iterator"]||o[n(e)]}},function(e,t,r){var n=r(99),i=r(20)("toStringTag"),o="Arguments"==n(function(){return arguments}());e.exports=function(e){var t,r,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?r:o?n(t):"Object"==(a=n(t))&&"function"==typeof t.callee?"Arguments":a}},function(e,t,r){var n=r(105),i=r(17)("toStringTag"),o="Arguments"==n(function(){return arguments}());e.exports=function(e){var t,r,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?r:o?n(t):"Object"==(a=n(t))&&"function"==typeof t.callee?"Arguments":a}},function(e,t){var r=0,n=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++r+n).toString(36))}},function(e,t,r){var n=r(76),i=r(33).document,o=n(i)&&n(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},function(e,t,r){var n=r(265)("keys"),i=r(178);e.exports=function(e){return n[e]||(n[e]=i(e))}},function(e,t,r){var n=r(124).f,i=r(125),o=r(17)("toStringTag");e.exports=function(e,t,r){e&&!i(e=r?e:e.prototype,o)&&n(e,o,{configurable:!0,value:t})}},function(e,t,r){"use strict";var n=r(128);e.exports.f=function(e){return new function PromiseCapability(e){var t,r;this.promise=new e(function(e,n){if(void 0!==t||void 0!==r)throw TypeError("Bad Promise constructor");t=e,r=n}),this.resolve=n(t),this.reject=n(r)}(e)}},function(e,t,r){var n=r(279),i=r(57);e.exports=function(e,t,r){if(n(t))throw TypeError("String#"+r+" doesn't accept regex!");return String(i(e))}},function(e,t,r){var n=r(17)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(r){try{return t[n]=!1,!"/./"[e](t)}catch(e){}}return!0}},function(e,t,r){t.f=r(20)},function(e,t,r){var n=r(23),i=r(15),o=r(121),a=r(185),s=r(41).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=o?{}:n.Symbol||{});"_"==e.charAt(0)||e in t||s(t,e,{value:a.f(e)})}},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t){},function(e,t,r){"use strict";(function(t){ +/*! + * @description Recursive object extending + * @author Viacheslav Lotsmanov + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2018 Viacheslav Lotsmanov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +function isSpecificValue(e){return e instanceof t||e instanceof Date||e instanceof RegExp}function cloneSpecificValue(e){if(e instanceof t){var r=t.alloc?t.alloc(e.length):new t(e.length);return e.copy(r),r}if(e instanceof Date)return new Date(e.getTime());if(e instanceof RegExp)return new RegExp(e);throw new Error("Unexpected situation")}function safeGetProperty(e,t){return"__proto__"===t?void 0:e[t]}var r=e.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var e,t,n=arguments[0];return Array.prototype.slice.call(arguments,1).forEach(function(i){"object"!=typeof i||null===i||Array.isArray(i)||Object.keys(i).forEach(function(o){return t=safeGetProperty(n,o),(e=safeGetProperty(i,o))===n?void 0:"object"!=typeof e||null===e?void(n[o]=e):Array.isArray(e)?void(n[o]=function deepCloneArray(e){var t=[];return e.forEach(function(e,n){"object"==typeof e&&null!==e?Array.isArray(e)?t[n]=deepCloneArray(e):isSpecificValue(e)?t[n]=cloneSpecificValue(e):t[n]=r({},e):t[n]=e}),t}(e)):isSpecificValue(e)?void(n[o]=cloneSpecificValue(e)):"object"!=typeof t||null===t||Array.isArray(t)?void(n[o]=r({},e)):void(n[o]=r(t,e))})}),n}}).call(t,r(48).Buffer)},function(e,t,r){"use strict";e.exports=function(e){return"object"==typeof e?function destroyCircular(e,t){var r;r=Array.isArray(e)?[]:{};t.push(e);Object.keys(e).forEach(function(n){var i=e[n];"function"!=typeof i&&(i&&"object"==typeof i?-1!==t.indexOf(e[n])?r[n]="[Circular]":r[n]=destroyCircular(e[n],t.slice(0)):r[n]=i)});"string"==typeof e.name&&(r.name=e.name);"string"==typeof e.message&&(r.message=e.message);"string"==typeof e.stack&&(r.stack=e.stack);return r}(e,[]):"function"==typeof e?"[Function: "+(e.name||"anonymous")+"]":e}},function(e,t,r){var n=r(634),i=r(650),o=r(652),a=r(653),s=r(654);function MapCache(e){var t=-1,r=null==e?0:e.length;for(this.clear();++t-1&&e%1==0&&e<=r}},function(e,t){e.exports=function baseUnary(e){return function(t){return e(t)}}},function(e,t,r){(function(e){var n=r(301),i="object"==typeof t&&t&&!t.nodeType&&t,o=i&&"object"==typeof e&&e&&!e.nodeType&&e,a=o&&o.exports===i&&n.process,s=function(){try{var e=o&&o.require&&o.require("util").types;return e||a&&a.binding&&a.binding("util")}catch(e){}}();e.exports=s}).call(t,r(141)(e))},function(e,t,r){var n=r(21),i=r(135),o=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,a=/^\w*$/;e.exports=function isKey(e,t){if(n(e))return!1;var r=typeof e;return!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=e&&!i(e))||a.test(e)||!o.test(e)||null!=t&&e in Object(t)}},function(e,t){e.exports=function identity(e){return e}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.memoizedSampleFromSchema=t.memoizedCreateXMLExample=t.sampleXmlFromSchema=t.inferSchema=t.sampleFromSchema=void 0,t.createXMLExample=createXMLExample;var n=r(10),i=_interopRequireDefault(r(701)),o=_interopRequireDefault(r(714));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var a={string:function string(){return"string"},string_email:function string_email(){return"user@example.com"},"string_date-time":function string_dateTime(){return(new Date).toISOString()},number:function number(){return 0},number_float:function number_float(){return 0},integer:function integer(){return 0},boolean:function boolean(e){return"boolean"!=typeof e.default||e.default}},s=function primitive(e){var t=e=(0,n.objectify)(e),r=t.type,i=t.format,o=a[r+"_"+i]||a[r];return(0,n.isFunc)(o)?o(e):"Unknown Type: "+e.type},u=t.sampleFromSchema=function sampleFromSchema(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=(0,n.objectify)(e),i=r.type,o=r.example,a=r.properties,u=r.additionalProperties,l=r.items,c=t.includeReadOnly,p=t.includeWriteOnly;if(void 0!==o)return(0,n.deeplyStripKey)(o,"$$ref",function(e){return"string"==typeof e&&e.indexOf("#")>-1});if(!i)if(a)i="object";else{if(!l)return;i="array"}if("object"===i){var f=(0,n.objectify)(a),d={};for(var h in f)f[h]&&f[h].readOnly&&!c||f[h]&&f[h].writeOnly&&!p||(d[h]=sampleFromSchema(f[h],t));if(!0===u)d.additionalProp1={};else if(u)for(var m=(0,n.objectify)(u),v=sampleFromSchema(m,t),g=1;g<4;g++)d["additionalProp"+g]=v;return d}return"array"===i?Array.isArray(l.anyOf)?l.anyOf.map(function(e){return sampleFromSchema(e,t)}):Array.isArray(l.oneOf)?l.oneOf.map(function(e){return sampleFromSchema(e,t)}):[sampleFromSchema(l,t)]:e.enum?e.default?e.default:(0,n.normalizeArray)(e.enum)[0]:"file"!==i?s(e):void 0},l=(t.inferSchema=function inferSchema(e){return e.schema&&(e=e.schema),e.properties&&(e.type="object"),e},t.sampleXmlFromSchema=function sampleXmlFromSchema(e){var t,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=(0,n.objectify)(e),o=i.type,a=i.properties,u=i.additionalProperties,l=i.items,c=i.example,p=r.includeReadOnly,f=r.includeWriteOnly,d=i.default,h={},m={},v=e.xml,g=v.name,y=v.prefix,_=v.namespace,b=i.enum,S=void 0;if(!o)if(a||u)o="object";else{if(!l)return;o="array"}(g=g||"notagname",t=(y?y+":":"")+g,_)&&(m[y?"xmlns:"+y:"xmlns"]=_);if("array"===o&&l){if(l.xml=l.xml||v||{},l.xml.name=l.xml.name||v.name,v.wrapped)return h[t]=[],Array.isArray(c)?c.forEach(function(e){l.example=e,h[t].push(sampleXmlFromSchema(l,r))}):Array.isArray(d)?d.forEach(function(e){l.default=e,h[t].push(sampleXmlFromSchema(l,r))}):h[t]=[sampleXmlFromSchema(l,r)],m&&h[t].push({_attr:m}),h;var k=[];return Array.isArray(c)?(c.forEach(function(e){l.example=e,k.push(sampleXmlFromSchema(l,r))}),k):Array.isArray(d)?(d.forEach(function(e){l.default=e,k.push(sampleXmlFromSchema(l,r))}),k):sampleXmlFromSchema(l,r)}if("object"===o){var x=(0,n.objectify)(a);for(var E in h[t]=[],c=c||{},x)if(x.hasOwnProperty(E)&&(!x[E].readOnly||p)&&(!x[E].writeOnly||f))if(x[E].xml=x[E].xml||{},x[E].xml.attribute){var C=Array.isArray(x[E].enum)&&x[E].enum[0],w=x[E].example,D=x[E].default;m[x[E].xml.name||E]=void 0!==w&&w||void 0!==c[E]&&c[E]||void 0!==D&&D||C||s(x[E])}else{x[E].xml.name=x[E].xml.name||E,void 0===x[E].example&&void 0!==c[E]&&(x[E].example=c[E]);var A=sampleXmlFromSchema(x[E]);Array.isArray(A)?h[t]=h[t].concat(A):h[t].push(A)}return!0===u?h[t].push({additionalProp:"Anything can be here"}):u&&h[t].push({additionalProp:s(u)}),m&&h[t].push({_attr:m}),h}return S=void 0!==c?c:void 0!==d?d:Array.isArray(b)?b[0]:s(e),h[t]=m?[{_attr:m},S]:S,h});function createXMLExample(e,t){var r=l(e,t);if(r)return(0,i.default)(r,{declaration:!0,indent:"\t"})}t.memoizedCreateXMLExample=(0,o.default)(createXMLExample),t.memoizedSampleFromSchema=(0,o.default)(u)},function(e,t){function EventEmitter(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function isFunction(e){return"function"==typeof e}function isObject(e){return"object"==typeof e&&null!==e}function isUndefined(e){return void 0===e}e.exports=EventEmitter,EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._maxListeners=void 0,EventEmitter.defaultMaxListeners=10,EventEmitter.prototype.setMaxListeners=function(e){if(!function isNumber(e){return"number"==typeof e}(e)||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},EventEmitter.prototype.emit=function(e){var t,r,n,i,o,a;if(this._events||(this._events={}),"error"===e&&(!this._events.error||isObject(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var s=new Error('Uncaught, unspecified "error" event. ('+t+")");throw s.context=t,s}if(isUndefined(r=this._events[e]))return!1;if(isFunction(r))switch(arguments.length){case 1:r.call(this);break;case 2:r.call(this,arguments[1]);break;case 3:r.call(this,arguments[1],arguments[2]);break;default:i=Array.prototype.slice.call(arguments,1),r.apply(this,i)}else if(isObject(r))for(i=Array.prototype.slice.call(arguments,1),n=(a=r.slice()).length,o=0;o0&&this._events[e].length>r&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},EventEmitter.prototype.on=EventEmitter.prototype.addListener,EventEmitter.prototype.once=function(e,t){if(!isFunction(t))throw TypeError("listener must be a function");var r=!1;function g(){this.removeListener(e,g),r||(r=!0,t.apply(this,arguments))}return g.listener=t,this.on(e,g),this},EventEmitter.prototype.removeListener=function(e,t){var r,n,i,o;if(!isFunction(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(i=(r=this._events[e]).length,n=-1,r===t||isFunction(r.listener)&&r.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(isObject(r)){for(o=i;o-- >0;)if(r[o]===t||r[o].listener&&r[o].listener===t){n=o;break}if(n<0)return this;1===r.length?(r.length=0,delete this._events[e]):r.splice(n,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},EventEmitter.prototype.removeAllListeners=function(e){var t,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(isFunction(r=this._events[e]))this.removeListener(e,r);else if(r)for(;r.length;)this.removeListener(e,r[r.length-1]);return delete this._events[e],this},EventEmitter.prototype.listeners=function(e){return this._events&&this._events[e]?isFunction(this._events[e])?[this._events[e]]:this._events[e].slice():[]},EventEmitter.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(isFunction(t))return 1;if(t)return t.length}return 0},EventEmitter.listenerCount=function(e,t){return e.listenerCount(t)}},function(e,t,r){(t=e.exports=r(328)).Stream=t,t.Readable=t,t.Writable=r(207),t.Duplex=r(67),t.Transform=r(333),t.PassThrough=r(709)},function(e,t,r){"use strict";(function(t,n,i){var o=r(147);function CorkedRequest(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function onCorkedFinish(e,t,r){var n=e.entry;e.entry=null;for(;n;){var i=n.callback;t.pendingcb--,i(r),n=n.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=Writable;var a,s=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?n:o.nextTick;Writable.WritableState=WritableState;var u=r(112);u.inherits=r(84);var l={deprecate:r(708)},c=r(329),p=r(148).Buffer,f=i.Uint8Array||function(){};var d,h=r(330);function nop(){}function WritableState(e,t){a=a||r(67),e=e||{};var n=t instanceof a;this.objectMode=!!e.objectMode,n&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var i=e.highWaterMark,u=e.writableHighWaterMark,l=this.objectMode?16:16384;this.highWaterMark=i||0===i?i:n&&(u||0===u)?u:l,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var c=!1===e.decodeStrings;this.decodeStrings=!c,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function onwrite(e,t){var r=e._writableState,n=r.sync,i=r.writecb;if(function onwriteStateUpdate(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(r),t)!function onwriteError(e,t,r,n,i){--t.pendingcb,r?(o.nextTick(i,n),o.nextTick(finishMaybe,e,t),e._writableState.errorEmitted=!0,e.emit("error",n)):(i(n),e._writableState.errorEmitted=!0,e.emit("error",n),finishMaybe(e,t))}(e,r,n,t,i);else{var a=needFinish(r);a||r.corked||r.bufferProcessing||!r.bufferedRequest||clearBuffer(e,r),n?s(afterWrite,e,r,a,i):afterWrite(e,r,a,i)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new CorkedRequest(this)}function Writable(e){if(a=a||r(67),!(d.call(Writable,this)||this instanceof a))return new Writable(e);this._writableState=new WritableState(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),c.call(this)}function doWrite(e,t,r,n,i,o,a){t.writelen=n,t.writecb=a,t.writing=!0,t.sync=!0,r?e._writev(i,t.onwrite):e._write(i,o,t.onwrite),t.sync=!1}function afterWrite(e,t,r,n){r||function onwriteDrain(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,n(),finishMaybe(e,t)}function clearBuffer(e,t){t.bufferProcessing=!0;var r=t.bufferedRequest;if(e._writev&&r&&r.next){var n=t.bufferedRequestCount,i=new Array(n),o=t.corkedRequestsFree;o.entry=r;for(var a=0,s=!0;r;)i[a]=r,r.isBuf||(s=!1),r=r.next,a+=1;i.allBuffers=s,doWrite(e,t,!0,t.length,i,"",o.finish),t.pendingcb++,t.lastBufferedRequest=null,o.next?(t.corkedRequestsFree=o.next,o.next=null):t.corkedRequestsFree=new CorkedRequest(t),t.bufferedRequestCount=0}else{for(;r;){var u=r.chunk,l=r.encoding,c=r.callback;if(doWrite(e,t,!1,t.objectMode?1:u.length,u,l,c),r=r.next,t.bufferedRequestCount--,t.writing)break}null===r&&(t.lastBufferedRequest=null)}t.bufferedRequest=r,t.bufferProcessing=!1}function needFinish(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function callFinal(e,t){e._final(function(r){t.pendingcb--,r&&e.emit("error",r),t.prefinished=!0,e.emit("prefinish"),finishMaybe(e,t)})}function finishMaybe(e,t){var r=needFinish(t);return r&&(!function prefinish(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,o.nextTick(callFinal,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),r}u.inherits(Writable,c),WritableState.prototype.getBuffer=function getBuffer(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(WritableState.prototype,"buffer",{get:l.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(d=Function.prototype[Symbol.hasInstance],Object.defineProperty(Writable,Symbol.hasInstance,{value:function(e){return!!d.call(this,e)||this===Writable&&(e&&e._writableState instanceof WritableState)}})):d=function(e){return e instanceof this},Writable.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},Writable.prototype.write=function(e,t,r){var n=this._writableState,i=!1,a=!n.objectMode&&function _isUint8Array(e){return p.isBuffer(e)||e instanceof f}(e);return a&&!p.isBuffer(e)&&(e=function _uint8ArrayToBuffer(e){return p.from(e)}(e)),"function"==typeof t&&(r=t,t=null),a?t="buffer":t||(t=n.defaultEncoding),"function"!=typeof r&&(r=nop),n.ended?function writeAfterEnd(e,t){var r=new Error("write after end");e.emit("error",r),o.nextTick(t,r)}(this,r):(a||function validChunk(e,t,r,n){var i=!0,a=!1;return null===r?a=new TypeError("May not write null values to stream"):"string"==typeof r||void 0===r||t.objectMode||(a=new TypeError("Invalid non-string/buffer chunk")),a&&(e.emit("error",a),o.nextTick(n,a),i=!1),i}(this,n,e,r))&&(n.pendingcb++,i=function writeOrBuffer(e,t,r,n,i,o){if(!r){var a=function decodeChunk(e,t,r){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=p.from(t,r));return t}(t,n,i);n!==a&&(r=!0,i="buffer",n=a)}var s=t.objectMode?1:n.length;t.length+=s;var u=t.length-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(Writable.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),Writable.prototype._write=function(e,t,r){r(new Error("_write() is not implemented"))},Writable.prototype._writev=null,Writable.prototype.end=function(e,t,r){var n=this._writableState;"function"==typeof e?(r=e,e=null,t=null):"function"==typeof t&&(r=t,t=null),null!==e&&void 0!==e&&this.write(e,t),n.corked&&(n.corked=1,this.uncork()),n.ending||n.finished||function endWritable(e,t,r){t.ending=!0,finishMaybe(e,t),r&&(t.finished?o.nextTick(r):e.once("finish",r));t.ended=!0,e.writable=!1}(this,n,r)},Object.defineProperty(Writable.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),Writable.prototype.destroy=h.destroy,Writable.prototype._undestroy=h.undestroy,Writable.prototype._destroy=function(e,t){this.end(),t(e)}}).call(t,r(39),r(331).setImmediate,r(18))},function(e,t,r){"use strict";e.exports=function(e){return"function"==typeof e}},function(e,t,r){"use strict";e.exports=r(735)()?Array.from:r(736)},function(e,t,r){"use strict";var n=r(749),i=r(69),o=r(85),a=Array.prototype.indexOf,s=Object.prototype.hasOwnProperty,u=Math.abs,l=Math.floor;e.exports=function(e){var t,r,c,p;if(!n(e))return a.apply(this,arguments);for(r=i(o(this).length),c=arguments[1],t=c=isNaN(c)?0:c>=0?l(c):i(this.length)-l(u(c));t1&&void 0!==arguments[1])||arguments[1];return e=(0,n.normalizeArray)(e),{type:s,payload:{thing:e,shown:t}}},t.changeMode=function changeMode(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=(0,n.normalizeArray)(e),{type:a,payload:{thing:e,mode:t}}};var n=r(10),i=t.UPDATE_LAYOUT="layout_update_layout",o=t.UPDATE_FILTER="layout_update_filter",a=t.UPDATE_MODE="layout_update_mode",s=t.SHOW="layout_show"},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.validateBeforeExecute=t.canExecuteScheme=t.operationScheme=t.hasHost=t.operationWithMeta=t.parameterWithMeta=t.parameterWithMetaByIdentity=t.allowTryItOutFor=t.mutatedRequestFor=t.requestFor=t.responseFor=t.mutatedRequests=t.requests=t.responses=t.taggedOperations=t.operationsWithTags=t.tagDetails=t.tags=t.operationsWithRootInherited=t.schemes=t.host=t.basePath=t.definitions=t.findDefinition=t.securityDefinitions=t.security=t.produces=t.consumes=t.operations=t.paths=t.semver=t.version=t.externalDocs=t.info=t.isOAS3=t.spec=t.specJsonWithResolvedSubtrees=t.specResolvedSubtree=t.specResolved=t.specJson=t.specSource=t.specStr=t.url=t.lastError=void 0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(86));t.getParameter=function getParameter(e,t,r,i){return t=t||[],e.getIn(["meta","paths"].concat((0,n.default)(t),["parameters"]),(0,a.fromJS)([])).find(function(e){return a.Map.isMap(e)&&e.get("name")===r&&e.get("in")===i})||(0,a.Map)()},t.parameterValues=function parameterValues(e,t,r){return t=t||[],D.apply(void 0,[e].concat((0,n.default)(t))).get("parameters",(0,a.List)()).reduce(function(e,t){var n=r&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(t.get("in")+"."+t.get("name"),n)},(0,a.fromJS)({}))},t.parametersIncludeIn=function parametersIncludeIn(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(a.List.isList(e))return e.some(function(e){return a.Map.isMap(e)&&e.get("in")===t})},t.parametersIncludeType=parametersIncludeType,t.contentTypeValues=function contentTypeValues(e,t){t=t||[];var r=p(e).getIn(["paths"].concat((0,n.default)(t)),(0,a.fromJS)({})),i=e.getIn(["meta","paths"].concat((0,n.default)(t)),(0,a.fromJS)({})),o=currentProducesFor(e,t),s=r.get("parameters")||new a.List,u=i.get("consumes_value")?i.get("consumes_value"):parametersIncludeType(s,"file")?"multipart/form-data":parametersIncludeType(s,"formData")?"application/x-www-form-urlencoded":void 0;return(0,a.fromJS)({requestContentType:u,responseContentType:o})},t.operationConsumes=function operationConsumes(e,t){return t=t||[],p(e).getIn(["paths"].concat((0,n.default)(t),["consumes"]),(0,a.fromJS)({}))},t.currentProducesFor=currentProducesFor;var i=r(59),o=r(10),a=r(7);var s=["get","put","post","delete","options","head","patch","trace"],u=function state(e){return e||(0,a.Map)()},l=(t.lastError=(0,i.createSelector)(u,function(e){return e.get("lastError")}),t.url=(0,i.createSelector)(u,function(e){return e.get("url")}),t.specStr=(0,i.createSelector)(u,function(e){return e.get("spec")||""}),t.specSource=(0,i.createSelector)(u,function(e){return e.get("specSource")||"not-editor"}),t.specJson=(0,i.createSelector)(u,function(e){return e.get("json",(0,a.Map)())})),c=(t.specResolved=(0,i.createSelector)(u,function(e){return e.get("resolved",(0,a.Map)())}),t.specResolvedSubtree=function specResolvedSubtree(e,t){return e.getIn(["resolvedSubtrees"].concat((0,n.default)(t)),void 0)},function mergerFn(e,t){return a.Map.isMap(e)&&a.Map.isMap(t)?t.get("$$ref")?t:(0,a.OrderedMap)().mergeWith(mergerFn,e,t):t}),p=t.specJsonWithResolvedSubtrees=(0,i.createSelector)(u,function(e){return(0,a.OrderedMap)().mergeWith(c,e.get("json"),e.get("resolvedSubtrees"))}),f=t.spec=function spec(e){return l(e)},d=(t.isOAS3=(0,i.createSelector)(f,function(){return!1}),t.info=(0,i.createSelector)(f,function(e){return returnSelfOrNewMap(e&&e.get("info"))})),h=(t.externalDocs=(0,i.createSelector)(f,function(e){return returnSelfOrNewMap(e&&e.get("externalDocs"))}),t.version=(0,i.createSelector)(d,function(e){return e&&e.get("version")})),m=(t.semver=(0,i.createSelector)(h,function(e){return/v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(e).slice(1)}),t.paths=(0,i.createSelector)(p,function(e){return e.get("paths")})),v=t.operations=(0,i.createSelector)(m,function(e){if(!e||e.size<1)return(0,a.List)();var t=(0,a.List)();return e&&e.forEach?(e.forEach(function(e,r){if(!e||!e.forEach)return{};e.forEach(function(e,n){s.indexOf(n)<0||(t=t.push((0,a.fromJS)({path:r,method:n,operation:e,id:n+"-"+r})))})}),t):(0,a.List)()}),g=t.consumes=(0,i.createSelector)(f,function(e){return(0,a.Set)(e.get("consumes"))}),y=t.produces=(0,i.createSelector)(f,function(e){return(0,a.Set)(e.get("produces"))}),_=(t.security=(0,i.createSelector)(f,function(e){return e.get("security",(0,a.List)())}),t.securityDefinitions=(0,i.createSelector)(f,function(e){return e.get("securityDefinitions")}),t.findDefinition=function findDefinition(e,t){var r=e.getIn(["resolvedSubtrees","definitions",t],null),n=e.getIn(["json","definitions",t],null);return r||n||null},t.definitions=(0,i.createSelector)(f,function(e){return e.get("definitions")||(0,a.Map)()}),t.basePath=(0,i.createSelector)(f,function(e){return e.get("basePath")}),t.host=(0,i.createSelector)(f,function(e){return e.get("host")}),t.schemes=(0,i.createSelector)(f,function(e){return e.get("schemes",(0,a.Map)())}),t.operationsWithRootInherited=(0,i.createSelector)(v,g,y,function(e,t,r){return e.map(function(e){return e.update("operation",function(e){if(e){if(!a.Map.isMap(e))return;return e.withMutations(function(e){return e.get("consumes")||e.update("consumes",function(e){return(0,a.Set)(e).merge(t)}),e.get("produces")||e.update("produces",function(e){return(0,a.Set)(e).merge(r)}),e})}return(0,a.Map)()})})})),b=t.tags=(0,i.createSelector)(f,function(e){return e.get("tags",(0,a.List)())}),S=t.tagDetails=function tagDetails(e,t){return(b(e)||(0,a.List)()).filter(a.Map.isMap).find(function(e){return e.get("name")===t},(0,a.Map)())},k=t.operationsWithTags=(0,i.createSelector)(_,b,function(e,t){return e.reduce(function(e,t){var r=(0,a.Set)(t.getIn(["operation","tags"]));return r.count()<1?e.update("default",(0,a.List)(),function(e){return e.push(t)}):r.reduce(function(e,r){return e.update(r,(0,a.List)(),function(e){return e.push(t)})},e)},t.reduce(function(e,t){return e.set(t.get("name"),(0,a.List)())},(0,a.OrderedMap)()))}),x=(t.taggedOperations=function taggedOperations(e){return function(t){var r=(0,t.getConfigs)(),n=r.tagsSorter,i=r.operationsSorter;return k(e).sortBy(function(e,t){return t},function(e,t){var r="function"==typeof n?n:o.sorters.tagsSorter[n];return r?r(e,t):null}).map(function(t,r){var n="function"==typeof i?i:o.sorters.operationsSorter[i],s=n?t.sort(n):t;return(0,a.Map)({tagDetails:S(e,r),operations:s})})}},t.responses=(0,i.createSelector)(u,function(e){return e.get("responses",(0,a.Map)())})),E=t.requests=(0,i.createSelector)(u,function(e){return e.get("requests",(0,a.Map)())}),C=t.mutatedRequests=(0,i.createSelector)(u,function(e){return e.get("mutatedRequests",(0,a.Map)())}),w=(t.responseFor=function responseFor(e,t,r){return x(e).getIn([t,r],null)},t.requestFor=function requestFor(e,t,r){return E(e).getIn([t,r],null)},t.mutatedRequestFor=function mutatedRequestFor(e,t,r){return C(e).getIn([t,r],null)},t.allowTryItOutFor=function allowTryItOutFor(){return!0},t.parameterWithMetaByIdentity=function parameterWithMetaByIdentity(e,t,r){var i=p(e).getIn(["paths"].concat((0,n.default)(t),["parameters"]),(0,a.OrderedMap)()),o=e.getIn(["meta","paths"].concat((0,n.default)(t),["parameters"]),(0,a.OrderedMap)());return i.map(function(e){var t=o.get(r.get("name")+"."+r.get("in")),n=o.get(r.get("name")+"."+r.get("in")+".hash-"+r.hashCode());return(0,a.OrderedMap)().merge(e,t,n)}).find(function(e){return e.get("in")===r.get("in")&&e.get("name")===r.get("name")},(0,a.OrderedMap)())}),D=(t.parameterWithMeta=function parameterWithMeta(e,t,r,i){var o=p(e).getIn(["paths"].concat((0,n.default)(t),["parameters"]),(0,a.OrderedMap)()).find(function(e){return e.get("in")===i&&e.get("name")===r},(0,a.OrderedMap)());return w(e,t,o)},t.operationWithMeta=function operationWithMeta(e,t,r){var n=p(e).getIn(["paths",t,r],(0,a.OrderedMap)()),i=e.getIn(["meta","paths",t,r],(0,a.OrderedMap)()),o=n.get("parameters",(0,a.List)()).map(function(n){return w(e,[t,r],n)});return(0,a.OrderedMap)().merge(n,i).set("parameters",o)});t.hasHost=(0,i.createSelector)(f,function(e){var t=e.get("host");return"string"==typeof t&&t.length>0&&"/"!==t[0]});function parametersIncludeType(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(a.List.isList(e))return e.some(function(e){return a.Map.isMap(e)&&e.get("type")===t})}function currentProducesFor(e,t){t=t||[];var r=p(e).getIn(["paths"].concat((0,n.default)(t)),null);if(null!==r){var i=e.getIn(["meta","paths"].concat((0,n.default)(t),["produces_value"]),null),o=r.getIn(["produces",0],null);return i||o||"application/json"}}var A=t.operationScheme=function operationScheme(e,t,r){var n=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),i=Array.isArray(n)?n[1]:null;return e.getIn(["scheme",t,r])||e.getIn(["scheme","_defaultScheme"])||i||""};t.canExecuteScheme=function canExecuteScheme(e,t,r){return["http","https"].indexOf(A(e,t,r))>-1},t.validateBeforeExecute=function validateBeforeExecute(e,t){t=t||[];var r=!0;return e.getIn(["meta","paths"].concat((0,n.default)(t),["parameters"]),(0,a.fromJS)([])).forEach(function(e){var t=e.get("errors");t&&t.count()&&(r=!1)}),r};function returnSelfOrNewMap(e){return a.Map.isMap(e)?e:new a.Map}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.execute=t.executeRequest=t.logRequest=t.setMutatedRequest=t.setRequest=t.setResponse=t.validateParams=t.invalidateResolvedSubtreeCache=t.updateResolvedSubtree=t.requestResolvedSubtree=t.resolveSpec=t.parseToJson=t.SET_SCHEME=t.UPDATE_RESOLVED_SUBTREE=t.UPDATE_RESOLVED=t.UPDATE_OPERATION_META_VALUE=t.CLEAR_VALIDATE_PARAMS=t.CLEAR_REQUEST=t.CLEAR_RESPONSE=t.LOG_REQUEST=t.SET_MUTATED_REQUEST=t.SET_REQUEST=t.SET_RESPONSE=t.VALIDATE_PARAMS=t.UPDATE_PARAM=t.UPDATE_JSON=t.UPDATE_URL=t.UPDATE_SPEC=void 0;var n=_interopRequireDefault(r(26)),i=_interopRequireDefault(r(87)),o=_interopRequireDefault(r(25)),a=_interopRequireDefault(r(43)),s=_interopRequireDefault(r(151)),u=_interopRequireDefault(r(361)),l=_interopRequireDefault(r(362)),c=_interopRequireDefault(r(44));t.updateSpec=function updateSpec(e){var t=q(e).replace(/\t/g," ");if("string"==typeof e)return{type:_,payload:t}},t.updateResolved=function updateResolved(e){return{type:O,payload:e}},t.updateUrl=function updateUrl(e){return{type:b,payload:e}},t.updateJsonSpec=function updateJsonSpec(e){return{type:S,payload:e}},t.changeParam=function changeParam(e,t,r,n,i){return{type:k,payload:{path:e,value:n,paramName:t,paramIn:r,isXml:i}}},t.changeParamByIdentity=function changeParamByIdentity(e,t,r,n){return{type:k,payload:{path:e,param:t,value:r,isXml:n}}},t.clearValidateParams=function clearValidateParams(e){return{type:M,payload:{pathMethod:e}}},t.changeConsumesValue=function changeConsumesValue(e,t){return{type:T,payload:{path:e,value:t,key:"consumes_value"}}},t.changeProducesValue=function changeProducesValue(e,t){return{type:T,payload:{path:e,value:t,key:"produces_value"}}},t.clearResponse=function clearResponse(e,t){return{type:A,payload:{path:e,method:t}}},t.clearRequest=function clearRequest(e,t){return{type:R,payload:{path:e,method:t}}},t.setScheme=function setScheme(e,t,r){return{type:I,payload:{scheme:e,path:t,method:r}}};var p=_interopRequireDefault(r(218)),f=r(7),d=_interopRequireDefault(r(220)),h=_interopRequireDefault(r(190)),m=_interopRequireDefault(r(365)),v=_interopRequireDefault(r(809)),g=_interopRequireDefault(r(811)),y=r(10);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var _=t.UPDATE_SPEC="spec_update_spec",b=t.UPDATE_URL="spec_update_url",S=t.UPDATE_JSON="spec_update_json",k=t.UPDATE_PARAM="spec_update_param",x=t.VALIDATE_PARAMS="spec_validate_param",E=t.SET_RESPONSE="spec_set_response",C=t.SET_REQUEST="spec_set_request",w=t.SET_MUTATED_REQUEST="spec_set_mutated_request",D=t.LOG_REQUEST="spec_log_request",A=t.CLEAR_RESPONSE="spec_clear_response",R=t.CLEAR_REQUEST="spec_clear_request",M=t.CLEAR_VALIDATE_PARAMS="spec_clear_validate_param",T=t.UPDATE_OPERATION_META_VALUE="spec_update_operation_meta_value",O=t.UPDATE_RESOLVED="spec_update_resolved",P=t.UPDATE_RESOLVED_SUBTREE="spec_update_resolved_subtree",I=t.SET_SCHEME="set_scheme",q=function toStr(e){return(0,m.default)(e)?e:""};t.parseToJson=function parseToJson(e){return function(t){var r=t.specActions,n=t.specSelectors,i=t.errActions,o=n.specStr,a=null;try{e=e||o(),i.clear({source:"parser"}),a=p.default.safeLoad(e)}catch(e){return console.error(e),i.newSpecErr({source:"parser",level:"error",message:e.reason,line:e.mark&&e.mark.line?e.mark.line+1:void 0})}return a&&"object"===(void 0===a?"undefined":(0,c.default)(a))?r.updateJsonSpec(a):{}}};var F=!1,B=(t.resolveSpec=function resolveSpec(e,t){return function(r){var n=r.specActions,i=r.specSelectors,o=r.errActions,a=r.fn,s=a.fetch,u=a.resolve,l=a.AST,c=r.getConfigs;F||(console.warn("specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!"),F=!0);var p=c(),f=p.modelPropertyMacro,d=p.parameterMacro,h=p.requestInterceptor,m=p.responseInterceptor;void 0===e&&(e=i.specJson()),void 0===t&&(t=i.url());var v=l.getLineNumberForPath,g=i.specStr();return u({fetch:s,spec:e,baseDoc:t,modelPropertyMacro:f,parameterMacro:d,requestInterceptor:h,responseInterceptor:m}).then(function(e){var t=e.spec,r=e.errors;if(o.clear({type:"thrown"}),Array.isArray(r)&&r.length>0){var i=r.map(function(e){return console.error(e),e.line=e.fullPath?v(g,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",Object.defineProperty(e,"message",{enumerable:!0,value:e.message}),e});o.newThrownErrBatch(i)}return n.updateResolved(t)})}},[]),N=(0,v.default)((0,l.default)(u.default.mark(function _callee2(){var e,t,r,n,i,o,a,c,p,d,h,m,v,y,_;return u.default.wrap(function _callee2$(b){for(;;)switch(b.prev=b.next){case 0:if(e=B.system){b.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),b.abrupt("return");case 4:if(t=e.errActions,r=e.errSelectors,n=e.fn,i=n.resolveSubtree,o=n.AST.getLineNumberForPath,a=e.specSelectors,c=e.specActions,i){b.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),b.abrupt("return");case 8:return p=a.specStr(),d=e.getConfigs(),h=d.modelPropertyMacro,m=d.parameterMacro,v=d.requestInterceptor,y=d.responseInterceptor,b.prev=10,b.next=13,B.reduce(function(){var e=(0,l.default)(u.default.mark(function _callee(e,n){var s,l,c,f,d,_,b;return u.default.wrap(function _callee$(u){for(;;)switch(u.prev=u.next){case 0:return u.next=2,e;case 2:return s=u.sent,l=s.resultMap,c=s.specWithCurrentSubtrees,u.next=7,i(c,n,{baseDoc:a.url(),modelPropertyMacro:h,parameterMacro:m,requestInterceptor:v,responseInterceptor:y});case 7:return f=u.sent,d=f.errors,_=f.spec,r.allErrors().size&&t.clear({type:"thrown"}),Array.isArray(d)&&d.length>0&&(b=d.map(function(e){return e.line=e.fullPath?o(p,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",Object.defineProperty(e,"message",{enumerable:!0,value:e.message}),e}),t.newThrownErrBatch(b)),(0,g.default)(l,n,_),(0,g.default)(c,n,_),u.abrupt("return",{resultMap:l,specWithCurrentSubtrees:c});case 15:case"end":return u.stop()}},_callee,void 0)}));return function(t,r){return e.apply(this,arguments)}}(),s.default.resolve({resultMap:(a.specResolvedSubtree([])||(0,f.Map)()).toJS(),specWithCurrentSubtrees:a.specJson().toJS()}));case 13:_=b.sent,delete B.system,B=[],b.next=21;break;case 18:b.prev=18,b.t0=b.catch(10),console.error(b.t0);case 21:c.updateResolvedSubtree([],_.resultMap);case 22:case"end":return b.stop()}},_callee2,void 0,[[10,18]])})),35);t.requestResolvedSubtree=function requestResolvedSubtree(e){return function(t){B.push(e),B.system=t,N()}};t.updateResolvedSubtree=function updateResolvedSubtree(e,t){return{type:P,payload:{path:e,value:t}}},t.invalidateResolvedSubtreeCache=function invalidateResolvedSubtreeCache(){return{type:P,payload:{path:[],value:(0,f.Map)()}}},t.validateParams=function validateParams(e,t){return{type:x,payload:{pathMethod:e,isOAS3:t}}};t.setResponse=function setResponse(e,t,r){return{payload:{path:e,method:t,res:r},type:E}},t.setRequest=function setRequest(e,t,r){return{payload:{path:e,method:t,req:r},type:C}},t.setMutatedRequest=function setMutatedRequest(e,t,r){return{payload:{path:e,method:t,req:r},type:w}},t.logRequest=function logRequest(e){return{payload:e,type:D}},t.executeRequest=function executeRequest(e){return function(t){var r=t.fn,n=t.specActions,i=t.specSelectors,s=t.getConfigs,u=t.oas3Selectors,l=e.pathName,c=e.method,p=e.operation,f=s(),m=f.requestInterceptor,v=f.responseInterceptor,g=p.toJS();if(e.contextUrl=(0,d.default)(i.url()).toString(),g&&g.operationId?e.operationId=g.operationId:g&&l&&c&&(e.operationId=r.opId(g,l,c)),i.isOAS3()){var _=l+":"+c;e.server=u.selectedServer(_)||u.selectedServer();var b=u.serverVariables({server:e.server,namespace:_}).toJS(),S=u.serverVariables({server:e.server}).toJS();e.serverVariables=(0,a.default)(b).length?b:S,e.requestContentType=u.requestContentType(l,c),e.responseContentType=u.responseContentType(l,c)||"*/*";var k=u.requestBodyValue(l,c);(0,y.isJSONObject)(k)?e.requestBody=JSON.parse(k):k&&k.toJS?e.requestBody=k.toJS():e.requestBody=k}var x=(0,o.default)({},e);x=r.buildRequest(x),n.setRequest(e.pathName,e.method,x);e.requestInterceptor=function requestInterceptorWrapper(t){var r=m.apply(this,[t]),i=(0,o.default)({},r);return n.setMutatedRequest(e.pathName,e.method,i),r},e.responseInterceptor=v;var E=Date.now();return r.execute(e).then(function(t){t.duration=Date.now()-E,n.setResponse(e.pathName,e.method,t)}).catch(function(t){return n.setResponse(e.pathName,e.method,{error:!0,err:(0,h.default)(t)})})}};t.execute=function execute(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,r=e.method,o=(0,i.default)(e,["path","method"]);return function(e){var i=e.fn.fetch,a=e.specSelectors,s=e.specActions,u=a.specJsonWithResolvedSubtrees().toJS(),l=a.operationScheme(t,r),c=a.contentTypeValues([t,r]).toJS(),p=c.requestContentType,f=c.responseContentType,d=/xml/i.test(p),h=a.parameterValues([t,r],d).toJS();return s.executeRequest((0,n.default)({},o,{fetch:i,spec:u,pathName:t,method:r,parameters:h,requestContentType:p,scheme:l,responseContentType:f}))}}},function(e,t){e.exports=function(e,t,r,n){if(!(e instanceof t)||void 0!==n&&n in e)throw TypeError(r+": incorrect invocation!");return e}},function(e,t,r){"use strict";var n=r(100);e.exports.f=function(e){return new function PromiseCapability(e){var t,r;this.promise=new e(function(e,n){if(void 0!==t||void 0!==r)throw TypeError("Bad Promise constructor");t=e,r=n}),this.resolve=n(t),this.reject=n(r)}(e)}},function(e,t,r){var n=r(54);e.exports=function(e,t,r){for(var i in t)r&&e[i]?e[i]=t[i]:n(e,i,t[i]);return e}},function(e,t,r){"use strict";var n=r(786);e.exports=n},function(e,t,r){"use strict";var n=r(89);e.exports=new n({explicit:[r(789),r(790),r(791)]})},function(e,t,r){"use strict";(function(t){var n=r(807),i=r(808),o=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i,a=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,s=[["#","hash"],["?","query"],["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d+)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],u={hash:1,query:1};function lolcation(e){var r,n={},i=typeof(e=e||t.location||{});if("blob:"===e.protocol)n=new URL(unescape(e.pathname),{});else if("string"===i)for(r in n=new URL(e,{}),u)delete n[r];else if("object"===i){for(r in e)r in u||(n[r]=e[r]);void 0===n.slashes&&(n.slashes=a.test(e.href))}return n}function extractProtocol(e){var t=o.exec(e);return{protocol:t[1]?t[1].toLowerCase():"",slashes:!!t[2],rest:t[3]}}function URL(e,t,r){if(!(this instanceof URL))return new URL(e,t,r);var o,a,u,l,c,p,f=s.slice(),d=typeof t,h=this,m=0;for("object"!==d&&"string"!==d&&(r=t,t=null),r&&"function"!=typeof r&&(r=i.parse),t=lolcation(t),o=!(a=extractProtocol(e||"")).protocol&&!a.slashes,h.slashes=a.slashes||o&&t.slashes,h.protocol=a.protocol||t.protocol||"",e=a.rest,a.slashes||(f[2]=[/(.*)/,"pathname"]);m-1||n("96",e),!a.plugins[r]){t.extractEvents||n("97",e),a.plugins[r]=t;var s=t.eventTypes;for(var u in s)publishEventForPlugin(s[u],t,u)||n("98",u,e)}}}function publishEventForPlugin(e,t,r){a.eventNameDispatchConfigs.hasOwnProperty(r)&&n("99",r),a.eventNameDispatchConfigs[r]=e;var i=e.phasedRegistrationNames;if(i){for(var o in i){if(i.hasOwnProperty(o))publishRegistrationName(i[o],t,r)}return!0}return!!e.registrationName&&(publishRegistrationName(e.registrationName,t,r),!0)}function publishRegistrationName(e,t,r){a.registrationNameModules[e]&&n("100",e),a.registrationNameModules[e]=t,a.registrationNameDependencies[e]=t.eventTypes[r].dependencies}var a={plugins:[],eventNameDispatchConfigs:{},registrationNameModules:{},registrationNameDependencies:{},possibleRegistrationNames:null,injectEventPluginOrder:function(e){i&&n("101"),i=Array.prototype.slice.call(e),recomputePluginOrdering()},injectEventPluginsByName:function(e){var t=!1;for(var r in e)if(e.hasOwnProperty(r)){var i=e[r];o.hasOwnProperty(r)&&o[r]===i||(o[r]&&n("102",r),o[r]=i,t=!0)}t&&recomputePluginOrdering()},getPluginModuleForEvent:function(e){var t=e.dispatchConfig;if(t.registrationName)return a.registrationNameModules[t.registrationName]||null;if(void 0!==t.phasedRegistrationNames){var r=t.phasedRegistrationNames;for(var n in r)if(r.hasOwnProperty(n)){var i=a.registrationNameModules[r[n]];if(i)return i}}return null},_resetEventPlugins:function(){for(var e in i=null,o)o.hasOwnProperty(e)&&delete o[e];a.plugins.length=0;var t=a.eventNameDispatchConfigs;for(var r in t)t.hasOwnProperty(r)&&delete t[r];var n=a.registrationNameModules;for(var s in n)n.hasOwnProperty(s)&&delete n[s]}};e.exports=a},function(e,t,r){"use strict";var n,i,o=r(11),a=r(223);r(8),r(9);function executeDispatch(e,t,r,n){var i=e.type||"unknown-event";e.currentTarget=s.getNodeFromInstance(n),t?a.invokeGuardedCallbackWithCatch(i,r,e):a.invokeGuardedCallback(i,r,e),e.currentTarget=null}var s={isEndish:function isEndish(e){return"topMouseUp"===e||"topTouchEnd"===e||"topTouchCancel"===e},isMoveish:function isMoveish(e){return"topMouseMove"===e||"topTouchMove"===e},isStartish:function isStartish(e){return"topMouseDown"===e||"topTouchStart"===e},executeDirectDispatch:function executeDirectDispatch(e){var t=e._dispatchListeners,r=e._dispatchInstances;Array.isArray(t)&&o("103"),e.currentTarget=t?s.getNodeFromInstance(r):null;var n=t?t(e):null;return e.currentTarget=null,e._dispatchListeners=null,e._dispatchInstances=null,n},executeDispatchesInOrder:function executeDispatchesInOrder(e,t){var r=e._dispatchListeners,n=e._dispatchInstances;if(Array.isArray(r))for(var i=0;i0&&n.length<20?r+" (keys: "+n.join(", ")+")":r}function getInternalInstanceReadyForUpdate(e,t){var r=i.get(e);return r||null}var a={isMounted:function(e){var t=i.get(e);return!!t&&!!t._renderedComponent},enqueueCallback:function(e,t,r){a.validateCallback(t,r);var n=getInternalInstanceReadyForUpdate(e);if(!n)return null;n._pendingCallbacks?n._pendingCallbacks.push(t):n._pendingCallbacks=[t],enqueueUpdate(n)},enqueueCallbackInternal:function(e,t){e._pendingCallbacks?e._pendingCallbacks.push(t):e._pendingCallbacks=[t],enqueueUpdate(e)},enqueueForceUpdate:function(e){var t=getInternalInstanceReadyForUpdate(e);t&&(t._pendingForceUpdate=!0,enqueueUpdate(t))},enqueueReplaceState:function(e,t,r){var n=getInternalInstanceReadyForUpdate(e);n&&(n._pendingStateQueue=[t],n._pendingReplaceState=!0,void 0!==r&&null!==r&&(a.validateCallback(r,"replaceState"),n._pendingCallbacks?n._pendingCallbacks.push(r):n._pendingCallbacks=[r]),enqueueUpdate(n))},enqueueSetState:function(e,t){var r=getInternalInstanceReadyForUpdate(e);r&&((r._pendingStateQueue||(r._pendingStateQueue=[])).push(t),enqueueUpdate(r))},enqueueElementInternal:function(e,t,r){e._pendingElement=t,e._context=r,enqueueUpdate(e)},validateCallback:function(e,t){e&&"function"!=typeof e&&n("122",t,formatUnexpectedArgument(e))}};e.exports=a},function(e,t,r){"use strict";r(13);var n=r(34),i=(r(9),n);e.exports=i},function(e,t,r){"use strict";e.exports=function getEventCharCode(e){var t,r=e.keyCode;return"charCode"in e?0===(t=e.charCode)&&13===r&&(t=13):t=r,t>=32||13===t?t:0}},function(e,t,r){var n=r(64),i=r(239),o=r(50),a="[object Object]",s=Function.prototype,u=Object.prototype,l=s.toString,c=u.hasOwnProperty,p=l.call(Object);e.exports=function isPlainObject(e){if(!o(e)||n(e)!=a)return!1;var t=i(e);if(null===t)return!0;var r=c.call(t,"constructor")&&t.constructor;return"function"==typeof r&&r instanceof r&&l.call(r)==p}},function(e,t,r){var n=r(320)(Object.getPrototypeOf,Object);e.exports=n},function(e,t,r){var n=r(314);e.exports=function cloneArrayBuffer(e){var t=new e.constructor(e.byteLength);return new n(t).set(new n(e)),t}},function(e,t,r){(function(){var e,t,n,i=function(e,t){for(var r in t)o.call(t,r)&&(e[r]=t[r]);function ctor(){this.constructor=e}return ctor.prototype=t.prototype,e.prototype=new ctor,e.__super__=t.prototype,e},o={}.hasOwnProperty,a=[].indexOf||function(e){for(var t=0,r=this.length;tr?p.push([c,s]):i[s]=this.yaml_path_resolvers[c][s]);else for(a=0,l=(h=this.yaml_path_resolvers).length;a=0)return o[e];if(a.call(o,null)>=0)return o.null}return e===t.ScalarNode?"tag:yaml.org,2002:str":e===t.SequenceNode?"tag:yaml.org,2002:seq":e===t.MappingNode?"tag:yaml.org,2002:map":void 0},BaseResolver}(),this.Resolver=function(e){function Resolver(){return Resolver.__super__.constructor.apply(this,arguments)}return i(Resolver,e),Resolver}(this.BaseResolver),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:bool",/^(?:yes|Yes|YES|true|True|TRUE|on|On|ON|no|No|NO|false|False|FALSE|off|Off|OFF)$/,"yYnNtTfFoO"),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:float",/^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9_]+(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/,"-+0123456789."),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:int",/^(?:[-+]?0b[01_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?0o[0-7_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$/,"-+0123456789"),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:merge",/^(?:<<)$/,"<"),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:null",/^(?:~|null|Null|NULL|)$/,["~","n","N",""]),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:timestamp",/^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:[Tt]|[\x20\t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\.[0-9]*)?(?:[\x20\t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$/,"0123456789"),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:value",/^(?:=)$/,"="),this.Resolver.add_implicit_resolver("tag:yaml.org,2002:yaml",/^(?:!|&|\*)$/,"!&*")}).call(this)},function(e,t){(function(){var e=function(e,r){for(var n in r)t.call(r,n)&&(e[n]=r[n]);function ctor(){this.constructor=e}return ctor.prototype=r.prototype,e.prototype=new ctor,e.__super__=r.prototype,e},t={}.hasOwnProperty;this.Token=function(){return function Token(e,t){this.start_mark=e,this.end_mark=t}}(),this.DirectiveToken=function(t){function DirectiveToken(e,t,r,n){this.name=e,this.value=t,this.start_mark=r,this.end_mark=n}return e(DirectiveToken,t),DirectiveToken.prototype.id="",DirectiveToken}(this.Token),this.DocumentStartToken=function(t){function DocumentStartToken(){return DocumentStartToken.__super__.constructor.apply(this,arguments)}return e(DocumentStartToken,t),DocumentStartToken.prototype.id="",DocumentStartToken}(this.Token),this.DocumentEndToken=function(t){function DocumentEndToken(){return DocumentEndToken.__super__.constructor.apply(this,arguments)}return e(DocumentEndToken,t),DocumentEndToken.prototype.id="",DocumentEndToken}(this.Token),this.StreamStartToken=function(t){function StreamStartToken(e,t,r){this.start_mark=e,this.end_mark=t,this.encoding=r}return e(StreamStartToken,t),StreamStartToken.prototype.id="",StreamStartToken}(this.Token),this.StreamEndToken=function(t){function StreamEndToken(){return StreamEndToken.__super__.constructor.apply(this,arguments)}return e(StreamEndToken,t),StreamEndToken.prototype.id="",StreamEndToken}(this.Token),this.BlockSequenceStartToken=function(t){function BlockSequenceStartToken(){return BlockSequenceStartToken.__super__.constructor.apply(this,arguments)}return e(BlockSequenceStartToken,t),BlockSequenceStartToken.prototype.id="",BlockSequenceStartToken}(this.Token),this.BlockMappingStartToken=function(t){function BlockMappingStartToken(){return BlockMappingStartToken.__super__.constructor.apply(this,arguments)}return e(BlockMappingStartToken,t),BlockMappingStartToken.prototype.id="",BlockMappingStartToken}(this.Token),this.BlockEndToken=function(t){function BlockEndToken(){return BlockEndToken.__super__.constructor.apply(this,arguments)}return e(BlockEndToken,t),BlockEndToken.prototype.id="",BlockEndToken}(this.Token),this.FlowSequenceStartToken=function(t){function FlowSequenceStartToken(){return FlowSequenceStartToken.__super__.constructor.apply(this,arguments)}return e(FlowSequenceStartToken,t),FlowSequenceStartToken.prototype.id="[",FlowSequenceStartToken}(this.Token),this.FlowMappingStartToken=function(t){function FlowMappingStartToken(){return FlowMappingStartToken.__super__.constructor.apply(this,arguments)}return e(FlowMappingStartToken,t),FlowMappingStartToken.prototype.id="{",FlowMappingStartToken}(this.Token),this.FlowSequenceEndToken=function(t){function FlowSequenceEndToken(){return FlowSequenceEndToken.__super__.constructor.apply(this,arguments)}return e(FlowSequenceEndToken,t),FlowSequenceEndToken.prototype.id="]",FlowSequenceEndToken}(this.Token),this.FlowMappingEndToken=function(t){function FlowMappingEndToken(){return FlowMappingEndToken.__super__.constructor.apply(this,arguments)}return e(FlowMappingEndToken,t),FlowMappingEndToken.prototype.id="}",FlowMappingEndToken}(this.Token),this.KeyToken=function(t){function KeyToken(){return KeyToken.__super__.constructor.apply(this,arguments)}return e(KeyToken,t),KeyToken.prototype.id="?",KeyToken}(this.Token),this.ValueToken=function(t){function ValueToken(){return ValueToken.__super__.constructor.apply(this,arguments)}return e(ValueToken,t),ValueToken.prototype.id=":",ValueToken}(this.Token),this.BlockEntryToken=function(t){function BlockEntryToken(){return BlockEntryToken.__super__.constructor.apply(this,arguments)}return e(BlockEntryToken,t),BlockEntryToken.prototype.id="-",BlockEntryToken}(this.Token),this.FlowEntryToken=function(t){function FlowEntryToken(){return FlowEntryToken.__super__.constructor.apply(this,arguments)}return e(FlowEntryToken,t),FlowEntryToken.prototype.id=",",FlowEntryToken}(this.Token),this.AliasToken=function(t){function AliasToken(e,t,r){this.value=e,this.start_mark=t,this.end_mark=r}return e(AliasToken,t),AliasToken.prototype.id="",AliasToken}(this.Token),this.AnchorToken=function(t){function AnchorToken(e,t,r){this.value=e,this.start_mark=t,this.end_mark=r}return e(AnchorToken,t),AnchorToken.prototype.id="",AnchorToken}(this.Token),this.TagToken=function(t){function TagToken(e,t,r){this.value=e,this.start_mark=t,this.end_mark=r}return e(TagToken,t),TagToken.prototype.id="",TagToken}(this.Token),this.ScalarToken=function(t){function ScalarToken(e,t,r,n,i){this.value=e,this.plain=t,this.start_mark=r,this.end_mark=n,this.style=i}return e(ScalarToken,t),ScalarToken.prototype.id="",ScalarToken}(this.Token)}).call(this)},function(e,t){var r=this&&this.__extends||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r]);function __(){this.constructor=e}e.prototype=null===t?Object.create(t):(__.prototype=t.prototype,new __)},n=Object.prototype.hasOwnProperty; +/*! + * https://github.com/Starcounter-Jack/JSON-Patch + * (c) 2017 Joachim Wester + * MIT license + */function hasOwnProperty(e,t){return n.call(e,t)}function _objectKeys(e){if(Array.isArray(e)){for(var t=new Array(e.length),r=0;r=48&&t<=57))return!1;r++}return!0},t.escapePathComponent=escapePathComponent,t.unescapePathComponent=function unescapePathComponent(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")},t._getPathRecursive=_getPathRecursive,t.getPath=function getPath(e,t){if(e===t)return"/";var r=_getPathRecursive(e,t);if(""===r)throw new Error("Object not found in root");return"/"+r},t.hasUndefined=function hasUndefined(e){if(void 0===e)return!0;if(e)if(Array.isArray(e)){for(var t=0,r=e.length;tS;S++)if((f||S in y)&&(v=_(m=y[S],S,g),e))if(r)k[S]=v;else if(v)switch(e){case 3:return!0;case 5:return m;case 6:return S;case 2:k.push(m)}else if(c)return!1;return p?-1:l||c?c:k}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.authorizeRequest=t.authorizeAccessCodeWithBasicAuthentication=t.authorizeAccessCodeWithFormParams=t.authorizeApplication=t.authorizePassword=t.preAuthorizeImplicit=t.CONFIGURE_AUTH=t.VALIDATE=t.AUTHORIZE_OAUTH2=t.PRE_AUTHORIZE_OAUTH2=t.LOGOUT=t.AUTHORIZE=t.SHOW_AUTH_POPUP=void 0;var n=_interopRequireDefault(r(44)),i=_interopRequireDefault(r(25)),o=_interopRequireDefault(r(42));t.showDefinitions=function showDefinitions(e){return{type:l,payload:e}},t.authorize=function authorize(e){return{type:c,payload:e}},t.logout=function logout(e){return{type:p,payload:e}},t.authorizeOauth2=function authorizeOauth2(e){return{type:f,payload:e}},t.configureAuth=function configureAuth(e){return{type:d,payload:e}};var a=_interopRequireDefault(r(220)),s=_interopRequireDefault(r(32)),u=r(10);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var l=t.SHOW_AUTH_POPUP="show_popup",c=t.AUTHORIZE="authorize",p=t.LOGOUT="logout",f=(t.PRE_AUTHORIZE_OAUTH2="pre_authorize_oauth2",t.AUTHORIZE_OAUTH2="authorize_oauth2"),d=(t.VALIDATE="validate",t.CONFIGURE_AUTH="configure_auth");t.preAuthorizeImplicit=function preAuthorizeImplicit(e){return function(t){var r=t.authActions,n=t.errActions,i=e.auth,a=e.token,u=e.isValid,l=i.schema,c=i.name,p=l.get("flow");delete s.default.swaggerUIRedirectOauth2,"accessCode"===p||u||n.newAuthErr({authId:c,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),a.error?n.newAuthErr({authId:c,source:"auth",level:"error",message:(0,o.default)(a)}):r.authorizeOauth2({auth:i,token:a})}};t.authorizePassword=function authorizePassword(e){return function(t){var r=t.authActions,n=e.schema,o=e.name,a=e.username,s=e.password,l=e.passwordType,c=e.clientId,p=e.clientSecret,f={grant_type:"password",scope:e.scopes.join(" ")},d={},h={};return"basic"===l?h.Authorization="Basic "+(0,u.btoa)(a+":"+s):((0,i.default)(f,{username:a},{password:s}),"query"===l?(c&&(d.client_id=c),p&&(d.client_secret=p)):h.Authorization="Basic "+(0,u.btoa)(c+":"+p)),r.authorizeRequest({body:(0,u.buildFormData)(f),url:n.get("tokenUrl"),name:o,headers:h,query:d,auth:e})}},t.authorizeApplication=function authorizeApplication(e){return function(t){var r=t.authActions,n=e.schema,i=e.scopes,o=e.name,a=e.clientId,s=e.clientSecret,l={Authorization:"Basic "+(0,u.btoa)(a+":"+s)},c={grant_type:"client_credentials",scope:i.join(" ")};return r.authorizeRequest({body:(0,u.buildFormData)(c),name:o,url:n.get("tokenUrl"),auth:e,headers:l})}},t.authorizeAccessCodeWithFormParams=function authorizeAccessCodeWithFormParams(e){var t=e.auth,r=e.redirectUrl;return function(e){var n=e.authActions,i=t.schema,o=t.name,a=t.clientId,s=t.clientSecret,l={grant_type:"authorization_code",code:t.code,client_id:a,client_secret:s,redirect_uri:r};return n.authorizeRequest({body:(0,u.buildFormData)(l),name:o,url:i.get("tokenUrl"),auth:t})}},t.authorizeAccessCodeWithBasicAuthentication=function authorizeAccessCodeWithBasicAuthentication(e){var t=e.auth,r=e.redirectUrl;return function(e){var n=e.authActions,i=t.schema,o=t.name,a=t.clientId,s=t.clientSecret,l={Authorization:"Basic "+(0,u.btoa)(a+":"+s)},c={grant_type:"authorization_code",code:t.code,client_id:a,redirect_uri:r};return n.authorizeRequest({body:(0,u.buildFormData)(c),name:o,url:i.get("tokenUrl"),auth:t,headers:l})}},t.authorizeRequest=function authorizeRequest(e){return function(t){var r=t.fn,s=t.getConfigs,u=t.authActions,l=t.errActions,c=t.oas3Selectors,p=t.specSelectors,f=t.authSelectors,d=e.body,h=e.query,m=void 0===h?{}:h,v=e.headers,g=void 0===v?{}:v,y=e.name,_=e.url,b=e.auth,S=(f.getConfigs()||{}).additionalQueryStringParams,k=void 0;k=p.isOAS3()?(0,a.default)(_,c.selectedServer(),!0):(0,a.default)(_,p.url(),!0),"object"===(void 0===S?"undefined":(0,n.default)(S))&&(k.query=(0,i.default)({},k.query,S));var x=k.toString(),E=(0,i.default)({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded"},g);r.fetch({url:x,method:"post",headers:E,query:m,body:d,requestInterceptor:s().requestInterceptor,responseInterceptor:s().responseInterceptor}).then(function(e){var t=JSON.parse(e.data),r=t&&(t.error||""),n=t&&(t.parseError||"");e.ok?r||n?l.newAuthErr({authId:y,level:"error",source:"auth",message:(0,o.default)(t)}):u.authorizeOauth2({auth:b,token:t}):l.newAuthErr({authId:y,level:"error",source:"auth",message:e.statusText})}).catch(function(e){var t=new Error(e);l.newAuthErr({authId:y,level:"error",source:"auth",message:t.message})})}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(991)),i=_interopRequireDefault(r(996)),o=_interopRequireDefault(r(997)),a=_interopRequireDefault(r(998)),s=_interopRequireDefault(r(999)),u=_interopRequireDefault(r(1e3)),l=_interopRequireDefault(r(1001)),c=_interopRequireDefault(r(1002)),p=_interopRequireDefault(r(1003)),f=_interopRequireDefault(r(1004)),d=_interopRequireDefault(r(1005)),h=_interopRequireDefault(r(1007)),m=_interopRequireDefault(r(1021));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var v=[o.default,i.default,a.default,u.default,l.default,c.default,p.default,f.default,d.default,s.default],g=(0,n.default)({prefixMap:m.default.prefixMap,plugins:v},h.default);t.default=g,e.exports=t.default},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function capitalizeString(e){return e.charAt(0).toUpperCase()+e.slice(1)},e.exports=t.default},function(e,t,r){var n=r(1022),i=r(1);e.exports=function(e,t,r){var i=e[t];if(i){var o=[];if(Object.keys(i).forEach(function(e){-1===n.indexOf(e)&&o.push(e)}),o.length)throw new Error("Prop "+t+" passed to "+r+". Has invalid keys "+o.join(", "))}},e.exports.isRequired=function(t,r,n){if(!t[r])throw new Error("Prop "+r+" passed to "+n+" is required");return e.exports(t,r,n)},e.exports.supportingArrays=i.oneOfType([i.arrayOf(e.exports),e.exports])},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseYamlConfig=void 0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(218));t.parseYamlConfig=function parseYamlConfig(e,t){try{return n.default.safeLoad(e)}catch(e){return t&&t.errActions.newThrownErr(new Error(e)),{}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.loaded=t.TOGGLE_CONFIGS=t.UPDATE_CONFIGS=void 0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(24));t.update=function update(e,t){return{type:i,payload:(0,n.default)({},e,t)}},t.toggle=function toggle(e){return{type:o,payload:e}};var i=t.UPDATE_CONFIGS="configs_update",o=t.TOGGLE_CONFIGS="configs_toggle";t.loaded=function loaded(){return function(){}}},function(e,t,r){"use strict";t.__esModule=!0,t.default=function mapToZero(e){var t={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=0);return t},e.exports=t.default},function(e,t,r){"use strict";t.__esModule=!0,t.default=function stepper(e,t,r,i,o,a,s){var u=r+(-o*(t-i)+-a*r)*e,l=t+u*e;if(Math.abs(u)u;)n(s,r=t[u++])&&(~o(l,r)||l.push(r));return l}},function(e,t,r){var n=r(23).document;e.exports=n&&n.documentElement},function(e,t,r){var n=r(56),i=r(74),o=r(172)("IE_PROTO"),a=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=i(e),n(e,o)?e[o]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?a:null}},function(e,t,r){var n=r(33),i=n["__core-js_shared__"]||(n["__core-js_shared__"]={});e.exports=function(e){return i[e]||(i[e]={})}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,r){"use strict";var n=r(268)(!0);r(269)(String,"String",function(e){this._t=String(e),this._i=0},function(){var e,t=this._t,r=this._i;return r>=t.length?{value:void 0,done:!0}:(e=n(t,r),this._i+=e.length,{value:e,done:!1})})},function(e,t,r){var n=r(126),i=r(57);e.exports=function(e){return function(t,r){var o,a,s=String(i(t)),u=n(r),l=s.length;return u<0||u>=l?e?"":void 0:(o=s.charCodeAt(u))<55296||o>56319||u+1===l||(a=s.charCodeAt(u+1))<56320||a>57343?e?s.charAt(u):o:e?s.slice(u,u+2):a-56320+(o-55296<<10)+65536}}},function(e,t,r){"use strict";var n=r(270),i=r(30),o=r(75),a=r(61),s=r(108),u=r(507),l=r(181),c=r(513),p=r(17)("iterator"),f=!([].keys&&"next"in[].keys()),d=function(){return this};e.exports=function(e,t,r,h,m,v,g){u(r,t,h);var y,_,b,S=function(e){if(!f&&e in C)return C[e];switch(e){case"keys":return function keys(){return new r(this,e)};case"values":return function values(){return new r(this,e)}}return function entries(){return new r(this,e)}},k=t+" Iterator",x="values"==m,E=!1,C=e.prototype,w=C[p]||C["@@iterator"]||m&&C[m],D=w||S(m),A=m?x?S("entries"):D:void 0,R="Array"==t&&C.entries||w;if(R&&(b=c(R.call(new e)))!==Object.prototype&&b.next&&(l(b,k,!0),n||"function"==typeof b[p]||a(b,p,d)),x&&w&&"values"!==w.name&&(E=!0,D=function values(){return w.call(this)}),n&&!g||!f&&!E&&C[p]||a(C,p,D),s[t]=D,s[k]=d,m)if(y={values:x?D:S("values"),keys:v?D:S("keys"),entries:A},g)for(_ in y)_ in C||o(C,_,y[_]);else i(i.P+i.F*(f||E),t,y);return y}},function(e,t){e.exports=!1},function(e,t,r){var n=r(510),i=r(273);e.exports=Object.keys||function keys(e){return n(e,i)}},function(e,t,r){var n=r(126),i=Math.max,o=Math.min;e.exports=function(e,t){return(e=n(e))<0?i(e+t,0):o(e,t)}},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,r){var n=r(33).document;e.exports=n&&n.documentElement},function(e,t,r){var n=r(62),i=r(128),o=r(17)("species");e.exports=function(e,t){var r,a=n(e).constructor;return void 0===a||void 0==(r=n(a)[o])?t:i(r)}},function(e,t,r){var n,i,o,a=r(127),s=r(525),u=r(274),l=r(179),c=r(33),p=c.process,f=c.setImmediate,d=c.clearImmediate,h=c.MessageChannel,m=c.Dispatch,v=0,g={},y=function(){var e=+this;if(g.hasOwnProperty(e)){var t=g[e];delete g[e],t()}},_=function(e){y.call(e.data)};f&&d||(f=function setImmediate(e){for(var t=[],r=1;arguments.length>r;)t.push(arguments[r++]);return g[++v]=function(){s("function"==typeof e?e:Function(e),t)},n(v),v},d=function clearImmediate(e){delete g[e]},"process"==r(105)(p)?n=function(e){p.nextTick(a(y,e,1))}:m&&m.now?n=function(e){m.now(a(y,e,1))}:h?(o=(i=new h).port2,i.port1.onmessage=_,n=a(o.postMessage,o,1)):c.addEventListener&&"function"==typeof postMessage&&!c.importScripts?(n=function(e){c.postMessage(e+"","*")},c.addEventListener("message",_,!1)):n="onreadystatechange"in l("script")?function(e){u.appendChild(l("script")).onreadystatechange=function(){u.removeChild(this),y.call(e)}}:function(e){setTimeout(a(y,e,1),0)}),e.exports={set:f,clear:d}},function(e,t){e.exports=function(e){try{return{e:!1,v:e()}}catch(e){return{e:!0,v:e}}}},function(e,t,r){var n=r(62),i=r(76),o=r(182);e.exports=function(e,t){if(n(e),i(t)&&t.constructor===e)return t;var r=o.f(e);return(0,r.resolve)(t),r.promise}},function(e,t,r){var n=r(76),i=r(105),o=r(17)("match");e.exports=function(e){var t;return n(e)&&(void 0!==(t=e[o])?!!t:"RegExp"==i(e))}},function(e,t,r){var n=r(22),i=r(15),o=r(55);e.exports=function(e,t){var r=(i.Object||{})[e]||Object[e],a={};a[e]=t(r),n(n.S+n.F*o(function(){r(1)}),"Object",a)}},function(e,t,r){var n=r(99);e.exports=Array.isArray||function isArray(e){return"Array"==n(e)}},function(e,t,r){var n=r(262),i=r(174).concat("length","prototype");t.f=Object.getOwnPropertyNames||function getOwnPropertyNames(e){return n(e,i)}},function(e,t,r){var n=r(132),i=r(101),o=r(73),a=r(168),s=r(56),u=r(261),l=Object.getOwnPropertyDescriptor;t.f=r(47)?l:function getOwnPropertyDescriptor(e,t){if(e=o(e),t=a(t,!0),u)try{return l(e,t)}catch(e){}if(s(e,t))return i(!n.f.call(e,t),e[t])}},function(e,t){var r={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==r.call(e)}},function(e,t,r){e.exports={default:r(577),__esModule:!0}},function(e,t,r){"use strict";var n=r(102),i=r(187),o=r(132),a=r(74),s=r(165),u=Object.assign;e.exports=!u||r(55)(function(){var e={},t={},r=Symbol(),n="abcdefghijklmnopqrst";return e[r]=7,n.split("").forEach(function(e){t[e]=e}),7!=u({},e)[r]||Object.keys(u({},t)).join("")!=n})?function assign(e,t){for(var r=a(e),u=arguments.length,l=1,c=i.f,p=o.f;u>l;)for(var f,d=s(arguments[l++]),h=c?n(d).concat(c(d)):n(d),m=h.length,v=0;m>v;)p.call(d,f=h[v++])&&(r[f]=d[f]);return r}:u},function(e,t,r){"use strict";var n=r(110),i=r(13),o=r(288),a=(r(289),r(133));r(8),r(581);function ReactComponent(e,t,r){this.props=e,this.context=t,this.refs=a,this.updater=r||o}function ReactPureComponent(e,t,r){this.props=e,this.context=t,this.refs=a,this.updater=r||o}function ComponentDummy(){}ReactComponent.prototype.isReactComponent={},ReactComponent.prototype.setState=function(e,t){"object"!=typeof e&&"function"!=typeof e&&null!=e&&n("85"),this.updater.enqueueSetState(this,e),t&&this.updater.enqueueCallback(this,t,"setState")},ReactComponent.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this),e&&this.updater.enqueueCallback(this,e,"forceUpdate")},ComponentDummy.prototype=ReactComponent.prototype,ReactPureComponent.prototype=new ComponentDummy,ReactPureComponent.prototype.constructor=ReactPureComponent,i(ReactPureComponent.prototype,ReactComponent.prototype),ReactPureComponent.prototype.isPureReactComponent=!0,e.exports={Component:ReactComponent,PureComponent:ReactPureComponent}},function(e,t,r){"use strict";r(9);var n={isMounted:function(e){return!1},enqueueCallback:function(e,t){},enqueueForceUpdate:function(e){},enqueueReplaceState:function(e,t){},enqueueSetState:function(e,t){}};e.exports=n},function(e,t,r){"use strict";var n=!1;e.exports=n},function(e,t,r){"use strict";var n="function"==typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103;e.exports=n},function(e,t,r){"use strict";var n=r(589);e.exports=function(e){return n(e,!1)}},function(e,t,r){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(294),i=r(606),o=r(607),a=r(608),s=r(298);r(297);r.d(t,"createStore",function(){return n.b}),r.d(t,"combineReducers",function(){return i.a}),r.d(t,"bindActionCreators",function(){return o.a}),r.d(t,"applyMiddleware",function(){return a.a}),r.d(t,"compose",function(){return s.a})},function(e,t,r){"use strict";r.d(t,"a",function(){return o}),t.b=function createStore(e,t,r){var a;"function"==typeof t&&void 0===r&&(r=t,t=void 0);if(void 0!==r){if("function"!=typeof r)throw new Error("Expected the enhancer to be a function.");return r(createStore)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var s=e;var u=t;var l=[];var c=l;var p=!1;function ensureCanMutateNextListeners(){c===l&&(c=l.slice())}function getState(){return u}function subscribe(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");var t=!0;return ensureCanMutateNextListeners(),c.push(e),function unsubscribe(){if(t){t=!1,ensureCanMutateNextListeners();var r=c.indexOf(e);c.splice(r,1)}}}function dispatch(e){if(!n.a(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(p)throw new Error("Reducers may not dispatch actions.");try{p=!0,u=s(u,e)}finally{p=!1}for(var t=l=c,r=0;ri?0:i+t),(r=r>i?i:r)<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var o=Array(i);++nf))return!1;var h=c.get(e);if(h&&c.get(t))return h==t;var m=-1,v=!0,g=r&s?new n:void 0;for(c.set(e,t),c.set(t,e);++m0?("string"==typeof t||a.objectMode||Object.getPrototypeOf(t)===l.prototype||(t=function _uint8ArrayToBuffer(e){return l.from(e)}(t)),n?a.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):addChunk(e,a,t,!0):a.ended?e.emit("error",new Error("stream.push() after EOF")):(a.reading=!1,a.decoder&&!r?(t=a.decoder.write(t),a.objectMode||0!==t.length?addChunk(e,a,t,!1):maybeReadMore(e,a)):addChunk(e,a,t,!1))):n||(a.reading=!1));return function needMoreData(e){return!e.ended&&(e.needReadable||e.lengtht.highWaterMark&&(t.highWaterMark=function computeNewHighWaterMark(e){return e>=y?e=y:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function emitReadable(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(d("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?i.nextTick(emitReadable_,e):emitReadable_(e))}function emitReadable_(e){d("emit readable"),e.emit("readable"),flow(e)}function maybeReadMore(e,t){t.readingMore||(t.readingMore=!0,i.nextTick(maybeReadMore_,e,t))}function maybeReadMore_(e,t){for(var r=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length=t.length?(r=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):r=function fromListPartial(e,t,r){var n;eo.length?o.length:e;if(a===o.length?i+=o:i+=o.slice(0,e),0===(e-=a)){a===o.length?(++n,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=o.slice(a));break}++n}return t.length-=n,i}(e,t):function copyFromBuffer(e,t){var r=l.allocUnsafe(e),n=t.head,i=1;n.data.copy(r),e-=n.data.length;for(;n=n.next;){var o=n.data,a=e>o.length?o.length:e;if(o.copy(r,r.length-e,0,a),0===(e-=a)){a===o.length?(++i,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=o.slice(a));break}++i}return t.length-=i,r}(e,t);return n}(e,t.buffer,t.decoder),r);var r}function endReadable(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,i.nextTick(endReadableNT,t,e))}function endReadableNT(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function indexOf(e,t){for(var r=0,n=e.length;r=t.highWaterMark||t.ended))return d("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?endReadable(this):emitReadable(this),null;if(0===(e=howMuchToRead(e,t))&&t.ended)return 0===t.length&&endReadable(this),null;var n,i=t.needReadable;return d("need readable",i),(0===t.length||t.length-e0?fromList(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),r!==e&&t.ended&&endReadable(this)),null!==n&&this.emit("data",n),n},Readable.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},Readable.prototype.pipe=function(e,t){var r=this,o=this._readableState;switch(o.pipesCount){case 0:o.pipes=e;break;case 1:o.pipes=[o.pipes,e];break;default:o.pipes.push(e)}o.pipesCount+=1,d("pipe count=%d opts=%j",o.pipesCount,t);var u=(!t||!1!==t.end)&&e!==n.stdout&&e!==n.stderr?onend:unpipe;function onunpipe(t,n){d("onunpipe"),t===r&&n&&!1===n.hasUnpiped&&(n.hasUnpiped=!0,function cleanup(){d("cleanup"),e.removeListener("close",onclose),e.removeListener("finish",onfinish),e.removeListener("drain",l),e.removeListener("error",onerror),e.removeListener("unpipe",onunpipe),r.removeListener("end",onend),r.removeListener("end",unpipe),r.removeListener("data",ondata),c=!0,!o.awaitDrain||e._writableState&&!e._writableState.needDrain||l()}())}function onend(){d("onend"),e.end()}o.endEmitted?i.nextTick(u):r.once("end",u),e.on("unpipe",onunpipe);var l=function pipeOnDrain(e){return function(){var t=e._readableState;d("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&s(e,"data")&&(t.flowing=!0,flow(e))}}(r);e.on("drain",l);var c=!1;var p=!1;function ondata(t){d("ondata"),p=!1,!1!==e.write(t)||p||((1===o.pipesCount&&o.pipes===e||o.pipesCount>1&&-1!==indexOf(o.pipes,e))&&!c&&(d("false write response, pause",r._readableState.awaitDrain),r._readableState.awaitDrain++,p=!0),r.pause())}function onerror(t){d("onerror",t),unpipe(),e.removeListener("error",onerror),0===s(e,"error")&&e.emit("error",t)}function onclose(){e.removeListener("finish",onfinish),unpipe()}function onfinish(){d("onfinish"),e.removeListener("close",onclose),unpipe()}function unpipe(){d("unpipe"),r.unpipe(e)}return r.on("data",ondata),function prependListener(e,t,r){if("function"==typeof e.prependListener)return e.prependListener(t,r);e._events&&e._events[t]?a(e._events[t])?e._events[t].unshift(r):e._events[t]=[r,e._events[t]]:e.on(t,r)}(e,"error",onerror),e.once("close",onclose),e.once("finish",onfinish),e.emit("pipe",r),o.flowing||(d("pipe resume"),r.resume()),e},Readable.prototype.unpipe=function(e){var t=this._readableState,r={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,r),this);if(!e){var n=t.pipes,i=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var o=0;o=0&&(e._idleTimeoutId=setTimeout(function onTimeout(){e._onTimeout&&e._onTimeout()},t))},r(707),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(t,r(18))},function(e,t,r){"use strict";var n=r(148).Buffer,i=n.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function StringDecoder(e){var t;switch(this.encoding=function normalizeEncoding(e){var t=function _normalizeEncoding(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(n.isEncoding===i||!i(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=utf16Text,this.end=utf16End,t=4;break;case"utf8":this.fillLast=utf8FillLast,t=4;break;case"base64":this.text=base64Text,this.end=base64End,t=3;break;default:return this.write=simpleWrite,void(this.end=simpleEnd)}this.lastNeed=0,this.lastTotal=0,this.lastChar=n.allocUnsafe(t)}function utf8CheckByte(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function utf8FillLast(e){var t=this.lastTotal-this.lastNeed,r=function utf8CheckExtraBytes(e,t,r){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==r?r:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function utf16Text(e,t){if((e.length-t)%2==0){var r=e.toString("utf16le",t);if(r){var n=r.charCodeAt(r.length-1);if(n>=55296&&n<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],r.slice(0,-1)}return r}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function utf16End(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var r=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,r)}return t}function base64Text(e,t){var r=(e.length-t)%3;return 0===r?e.toString("base64",t):(this.lastNeed=3-r,this.lastTotal=3,1===r?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-r))}function base64End(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function simpleWrite(e){return e.toString(this.encoding)}function simpleEnd(e){return e&&e.length?this.write(e):""}t.StringDecoder=StringDecoder,StringDecoder.prototype.write=function(e){if(0===e.length)return"";var t,r;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";r=this.lastNeed,this.lastNeed=0}else r=0;return r=0)return i>0&&(e.lastNeed=i-1),i;if(--n=0)return i>0&&(e.lastNeed=i-2),i;if(--n=0)return i>0&&(2===i?i=0:e.lastNeed=i-3),i;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=r;var n=e.length-(r-this.lastNeed);return e.copy(this.lastChar,0,n),e.toString("utf8",t,n)},StringDecoder.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,r){"use strict";e.exports=Transform;var n=r(67),i=r(112);function Transform(e){if(!(this instanceof Transform))return new Transform(e);n.call(this,e),this._transformState={afterTransform:function afterTransform(e,t){var r=this._transformState;r.transforming=!1;var n=r.writecb;if(!n)return this.emit("error",new Error("write callback called multiple times"));r.writechunk=null,r.writecb=null,null!=t&&this.push(t),n(e);var i=this._readableState;i.reading=!1,(i.needReadable||i.length=0?r&&i?i-1:i:1:!1!==e&&n(e)}},function(e,t,r){"use strict";e.exports=r(723)()?Object.assign:r(724)},function(e,t,r){"use strict";var n,i,o,a,s,u=r(69),l=function(e,t){return t};try{Object.defineProperty(l,"length",{configurable:!0,writable:!1,enumerable:!1,value:1})}catch(e){}1===l.length?(n={configurable:!0,writable:!1,enumerable:!1},i=Object.defineProperty,e.exports=function(e,t){return t=u(t),e.length===t?e:(n.value=t,i(e,"length",n))}):(a=r(339),s=[],o=function(e){var t,r=0;if(s[e])return s[e];for(t=[];e--;)t.push("a"+(++r).toString(36));return new Function("fn","return function ("+t.join(", ")+") { return fn.apply(this, arguments); };")},e.exports=function(e,t){var r;if(t=u(t),e.length===t)return e;r=o(t)(e);try{a(r,e)}catch(e){}return r})},function(e,t,r){"use strict";var n=r(85),i=Object.defineProperty,o=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,s=Object.getOwnPropertySymbols;e.exports=function(e,t){var r,u=Object(n(t));if(e=Object(n(e)),a(u).forEach(function(n){try{i(e,n,o(t,n))}catch(e){r=e}}),"function"==typeof s&&s(u).forEach(function(n){try{i(e,n,o(t,n))}catch(e){r=e}}),void 0!==r)throw r;return e}},function(e,t,r){"use strict";var n=r(58),i=r(149),o=Function.prototype.call;e.exports=function(e,t){var r={},a=arguments[2];return n(t),i(e,function(e,n,i,s){r[n]=o.call(t,a,e,n,i,s)}),r}},function(e,t){e.exports=function isPromise(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return{statePlugins:{err:{reducers:(0,n.default)(e),actions:i,selectors:o}}}};var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(343)),i=_interopRequireWildcard(r(134)),o=_interopRequireWildcard(r(348));function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(24)),i=_interopRequireDefault(r(25));t.default=function(e){var t;return t={},(0,n.default)(t,o.NEW_THROWN_ERR,function(t,r){var n=r.payload,o=(0,i.default)(u,n,{type:"thrown"});return t.update("errors",function(e){return(e||(0,a.List)()).push((0,a.fromJS)(o))}).update("errors",function(t){return(0,s.default)(t,e.getSystem())})}),(0,n.default)(t,o.NEW_THROWN_ERR_BATCH,function(t,r){var n=r.payload;return n=n.map(function(e){return(0,a.fromJS)((0,i.default)(u,e,{type:"thrown"}))}),t.update("errors",function(e){return(e||(0,a.List)()).concat((0,a.fromJS)(n))}).update("errors",function(t){return(0,s.default)(t,e.getSystem())})}),(0,n.default)(t,o.NEW_SPEC_ERR,function(t,r){var n=r.payload,i=(0,a.fromJS)(n);return i=i.set("type","spec"),t.update("errors",function(e){return(e||(0,a.List)()).push((0,a.fromJS)(i)).sortBy(function(e){return e.get("line")})}).update("errors",function(t){return(0,s.default)(t,e.getSystem())})}),(0,n.default)(t,o.NEW_SPEC_ERR_BATCH,function(t,r){var n=r.payload;return n=n.map(function(e){return(0,a.fromJS)((0,i.default)(u,e,{type:"spec"}))}),t.update("errors",function(e){return(e||(0,a.List)()).concat((0,a.fromJS)(n))}).update("errors",function(t){return(0,s.default)(t,e.getSystem())})}),(0,n.default)(t,o.NEW_AUTH_ERR,function(t,r){var n=r.payload,o=(0,a.fromJS)((0,i.default)({},n));return o=o.set("type","auth"),t.update("errors",function(e){return(e||(0,a.List)()).push((0,a.fromJS)(o))}).update("errors",function(t){return(0,s.default)(t,e.getSystem())})}),(0,n.default)(t,o.CLEAR,function(e,t){var r=t.payload;if(!r||!e.get("errors"))return e;var n=e.get("errors").filter(function(e){return e.keySeq().every(function(t){var n=e.get(t),i=r[t];return!i||n!==i})});return e.merge({errors:n})}),(0,n.default)(t,o.CLEAR_BY,function(e,t){var r=t.payload;if(!r||"function"!=typeof r)return e;var n=e.get("errors").filter(function(e){return r(e)});return e.merge({errors:n})}),t};var o=r(134),a=r(7),s=_interopRequireDefault(r(344));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var u={line:0,level:"error",message:"Unknown error"}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function transformErrors(e,t){var r={jsSpec:t.specSelectors.specJson().toJS()};return(0,n.default)(i,function(e,t){try{var n=t.transform(e,r);return n.filter(function(e){return!!e})}catch(t){return console.error("Transformer error:",t),e}},e).filter(function(e){return!!e}).map(function(e){return!e.get("line")&&e.get("path"),e})};var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(771));function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}var i=[_interopRequireWildcard(r(345)),_interopRequireWildcard(r(346)),_interopRequireWildcard(r(347))]},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.transform=function transform(e){return e.map(function(e){var t=e.get("message").indexOf("is not of a type(s)");if(t>-1){var r=e.get("message").slice(t+"is not of a type(s)".length).split(",");return e.set("message",e.get("message").slice(0,t)+function makeNewMessage(e){return e.reduce(function(e,t,r,n){return r===n.length-1&&n.length>1?e+"or "+t:n[r+1]&&n.length>2?e+t+", ":n[r+1]?e+t+" ":e+t},"should be a")}(r))}return e})}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.transform=function transform(e,t){t.jsSpec;return e};(function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}})(r(145)),r(7)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.transform=function transform(e){return e.map(function(e){return e.set("message",function removeSubstring(e,t){return e.replace(new RegExp(t,"g"),"")}(e.get("message"),"instance."))})}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.lastError=t.allErrors=void 0;var n=r(7),i=r(59),o=t.allErrors=(0,i.createSelector)(function state(e){return e},function(e){return e.get("errors",(0,n.List)())});t.lastError=(0,i.createSelector)(o,function(e){return e.last()})},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){return{statePlugins:{layout:{reducers:n.default,actions:i,selectors:o}}}};var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(350)),i=_interopRequireWildcard(r(212)),o=_interopRequireWildcard(r(351));function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n,i=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(24)),o=r(7),a=r(212);t.default=(n={},(0,i.default)(n,a.UPDATE_LAYOUT,function(e,t){return e.set("layout",t.payload)}),(0,i.default)(n,a.UPDATE_FILTER,function(e,t){return e.set("filter",t.payload)}),(0,i.default)(n,a.SHOW,function(e,t){var r=t.payload.shown,n=(0,o.fromJS)(t.payload.thing);return e.update("shown",(0,o.fromJS)({}),function(e){return e.set(n,r)})}),(0,i.default)(n,a.UPDATE_MODE,function(e,t){var r=t.payload.thing,n=t.payload.mode;return e.setIn(["modes"].concat(r),(n||"")+"")}),n)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.showSummary=t.whatMode=t.isShown=t.currentFilter=t.current=void 0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(86)),i=r(59),o=r(10),a=r(7);t.current=function current(e){return e.get("layout")},t.currentFilter=function currentFilter(e){return e.get("filter")};var s=t.isShown=function isShown(e,t,r){return t=(0,o.normalizeArray)(t),e.get("shown",(0,a.fromJS)({})).get((0,a.fromJS)(t),r)};t.whatMode=function whatMode(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";return t=(0,o.normalizeArray)(t),e.getIn(["modes"].concat((0,n.default)(t)),r)},t.showSummary=(0,i.createSelector)(function state(e){return e},function(e){return!s(e,"editor")})},function(e,t,r){var n=r(36);e.exports=function(e,t,r,i){try{return i?t(n(r)[0],r[1]):t(r)}catch(t){var o=e.return;throw void 0!==o&&n(o.call(e)),t}}},function(e,t,r){var n=r(72),i=r(20)("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(n.Array===e||o[i]===e)}},function(e,t,r){var n=r(20)("iterator"),i=!1;try{var o=[7][n]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var r=!1;try{var o=[7],a=o[n]();a.next=function(){return{done:r=!0}},o[n]=function(){return a},e(o)}catch(e){}return r}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){return{statePlugins:{spec:{wrapActions:a,reducers:n.default,actions:i,selectors:o}}}};var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(356)),i=_interopRequireWildcard(r(214)),o=_interopRequireWildcard(r(213)),a=_interopRequireWildcard(r(369));function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n,i=_interopRequireDefault(r(24)),o=_interopRequireDefault(r(25)),a=_interopRequireDefault(r(86)),s=r(7),u=r(10),l=_interopRequireDefault(r(32)),c=r(213),p=r(214);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=(n={},(0,i.default)(n,p.UPDATE_SPEC,function(e,t){return"string"==typeof t.payload?e.set("spec",t.payload):e}),(0,i.default)(n,p.UPDATE_URL,function(e,t){return e.set("url",t.payload+"")}),(0,i.default)(n,p.UPDATE_JSON,function(e,t){return e.set("json",(0,u.fromJSOrdered)(t.payload))}),(0,i.default)(n,p.UPDATE_RESOLVED,function(e,t){return e.setIn(["resolved"],(0,u.fromJSOrdered)(t.payload))}),(0,i.default)(n,p.UPDATE_RESOLVED_SUBTREE,function(e,t){var r=t.payload,n=r.value,i=r.path;return e.setIn(["resolvedSubtrees"].concat((0,a.default)(i)),(0,u.fromJSOrdered)(n))}),(0,i.default)(n,p.UPDATE_PARAM,function(e,t){var r=t.payload,n=r.path,i=r.paramName,o=r.paramIn,s=r.param,u=r.value,l=r.isXml,c=void 0;c=s&&s.hashCode&&!o&&!i?s.get("name")+"."+s.get("in")+".hash-"+s.hashCode():i+"."+o;var p=l?"value_xml":"value";return e.setIn(["meta","paths"].concat((0,a.default)(n),["parameters",c,p]),u)}),(0,i.default)(n,p.VALIDATE_PARAMS,function(e,t){var r=t.payload,n=r.pathMethod,i=r.isOAS3,o=e.getIn(["meta","paths"].concat((0,a.default)(n)),(0,s.fromJS)({})),l=/xml/i.test(o.get("consumes_value")),p=c.operationWithMeta.apply(void 0,[e].concat((0,a.default)(n)));return e.updateIn(["meta","paths"].concat((0,a.default)(n),["parameters"]),(0,s.fromJS)({}),function(e){return p.get("parameters",(0,s.List)()).reduce(function(e,t){var r=(0,u.validateParam)(t,l,i);return e.setIn([t.get("name")+"."+t.get("in"),"errors"],(0,s.fromJS)(r))},e)})}),(0,i.default)(n,p.CLEAR_VALIDATE_PARAMS,function(e,t){var r=t.payload.pathMethod;return e.updateIn(["meta","paths"].concat((0,a.default)(r),["parameters"]),(0,s.fromJS)([]),function(e){return e.map(function(e){return e.set("errors",(0,s.fromJS)([]))})})}),(0,i.default)(n,p.SET_RESPONSE,function(e,t){var r=t.payload,n=r.res,i=r.path,a=r.method,s=void 0;(s=n.error?(0,o.default)({error:!0,name:n.err.name,message:n.err.message,statusCode:n.err.statusCode},n.err.response):n).headers=s.headers||{};var c=e.setIn(["responses",i,a],(0,u.fromJSOrdered)(s));return l.default.Blob&&n.data instanceof l.default.Blob&&(c=c.setIn(["responses",i,a,"text"],n.data)),c}),(0,i.default)(n,p.SET_REQUEST,function(e,t){var r=t.payload,n=r.req,i=r.path,o=r.method;return e.setIn(["requests",i,o],(0,u.fromJSOrdered)(n))}),(0,i.default)(n,p.SET_MUTATED_REQUEST,function(e,t){var r=t.payload,n=r.req,i=r.path,o=r.method;return e.setIn(["mutatedRequests",i,o],(0,u.fromJSOrdered)(n))}),(0,i.default)(n,p.UPDATE_OPERATION_META_VALUE,function(e,t){var r=t.payload,n=r.path,i=r.value,o=r.key,u=["paths"].concat((0,a.default)(n)),l=["meta","paths"].concat((0,a.default)(n));return e.getIn(["json"].concat((0,a.default)(u)))||e.getIn(["resolved"].concat((0,a.default)(u)))||e.getIn(["resolvedSubtrees"].concat((0,a.default)(u)))?e.setIn([].concat((0,a.default)(l),[o]),(0,s.fromJS)(i)):e}),(0,i.default)(n,p.CLEAR_RESPONSE,function(e,t){var r=t.payload,n=r.path,i=r.method;return e.deleteIn(["responses",n,i])}),(0,i.default)(n,p.CLEAR_REQUEST,function(e,t){var r=t.payload,n=r.path,i=r.method;return e.deleteIn(["requests",n,i])}),(0,i.default)(n,p.SET_SCHEME,function(e,t){var r=t.payload,n=r.scheme,i=r.path,o=r.method;return i&&o?e.setIn(["scheme",i,o],n):i||o?void 0:e.setIn(["scheme","_defaultScheme"],n)}),n)},function(e,t,r){var n=r(36),i=r(100),o=r(20)("species");e.exports=function(e,t){var r,a=n(e).constructor;return void 0===a||void 0==(r=n(a)[o])?t:i(r)}},function(e,t,r){var n,i,o,a=r(53),s=r(779),u=r(263),l=r(167),c=r(23),p=c.process,f=c.setImmediate,d=c.clearImmediate,h=c.MessageChannel,m=c.Dispatch,v=0,g={},y=function(){var e=+this;if(g.hasOwnProperty(e)){var t=g[e];delete g[e],t()}},_=function(e){y.call(e.data)};f&&d||(f=function setImmediate(e){for(var t=[],r=1;arguments.length>r;)t.push(arguments[r++]);return g[++v]=function(){s("function"==typeof e?e:Function(e),t)},n(v),v},d=function clearImmediate(e){delete g[e]},"process"==r(99)(p)?n=function(e){p.nextTick(a(y,e,1))}:m&&m.now?n=function(e){m.now(a(y,e,1))}:h?(o=(i=new h).port2,i.port1.onmessage=_,n=a(o.postMessage,o,1)):c.addEventListener&&"function"==typeof postMessage&&!c.importScripts?(n=function(e){c.postMessage(e+"","*")},c.addEventListener("message",_,!1)):n="onreadystatechange"in l("script")?function(e){u.appendChild(l("script")).onreadystatechange=function(){u.removeChild(this),y.call(e)}}:function(e){setTimeout(a(y,e,1),0)}),e.exports={set:f,clear:d}},function(e,t){e.exports=function(e){try{return{e:!1,v:e()}}catch(e){return{e:!0,v:e}}}},function(e,t,r){var n=r(36),i=r(29),o=r(216);e.exports=function(e,t){if(n(e),i(t)&&t.constructor===e)return t;var r=o.f(e);return(0,r.resolve)(t),r.promise}},function(e,t,r){e.exports=r(784)},function(e,t,r){"use strict";t.__esModule=!0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(151));t.default=function(e){return function(){var t=e.apply(this,arguments);return new n.default(function(e,r){return function step(i,o){try{var a=t[i](o),s=a.value}catch(e){return void r(e)}if(!a.done)return n.default.resolve(s).then(function(e){step("next",e)},function(e){step("throw",e)});e(s)}("next")})}}},function(e,t,r){"use strict";var n=r(89);e.exports=new n({include:[r(364)]})},function(e,t,r){"use strict";var n=r(89);e.exports=new n({include:[r(219)],implicit:[r(792),r(793),r(794),r(795)]})},function(e,t,r){var n=r(64),i=r(21),o=r(50),a="[object String]";e.exports=function isString(e){return"string"==typeof e||!i(e)&&o(e)&&n(e)==a}},function(e,t,r){var n=r(154),i=r(82),o=r(142),a=r(38),s=r(83);e.exports=function baseSet(e,t,r,u){if(!a(e))return e;for(var l=-1,c=(t=i(t,e)).length,p=c-1,f=e;null!=f&&++l.":"function"==typeof t?" Instead of passing a class like Foo, pass React.createElement(Foo) or .":null!=t&&void 0!==t.props?" This may be caused by unintentionally loading two independent copies of React.":"");var o,s=a.createElement(A,{child:t});if(e){var u=f.get(e);o=u._processChildContext(u._context)}else o=g;var l=getTopLevelWrapperInContainer(r);if(l){var c=l._currentElement.props.child;if(b(c,t)){var p=l._renderedComponent.getPublicInstance(),d=i&&function(){i.call(p)};return R._updateRootComponent(l,s,o,r,d),p}R.unmountComponentAtNode(r)}var h=getReactRootElementInContainer(r),v=h&&!!internalGetID(h),y=hasNonRootReactChild(r),_=v&&!l&&!y,S=R._renderNewRootComponent(s,r,_,o)._renderedComponent.getPublicInstance();return i&&i.call(S),S},render:function(e,t,r){return R._renderSubtreeIntoContainer(null,e,t,r)},unmountComponentAtNode:function(e){isValidContainer(e)||n("40");var t=getTopLevelWrapperInContainer(e);if(!t){hasNonRootReactChild(e),1===e.nodeType&&e.hasAttribute(k);return!1}return delete w[t._instance.rootID],v.batchedUpdates(unmountComponentFromNode,t,e,!1),!0},_mountImageIntoNode:function(e,t,r,o,a){if(isValidContainer(t)||n("41"),o){var s=getReactRootElementInContainer(t);if(d.canReuseMarkup(e,s))return void u.precacheNode(r,s);var l=s.getAttribute(d.CHECKSUM_ATTR_NAME);s.removeAttribute(d.CHECKSUM_ATTR_NAME);var c=s.outerHTML;s.setAttribute(d.CHECKSUM_ATTR_NAME,l);var p=e,f=function firstDifferenceIndex(e,t){for(var r=Math.min(e.length,t.length),n=0;n1?n-1:0),a=1;a=i&&(t=console)[e].apply(t,o)}return log.warn=log.bind(null,"warn"),log.error=log.bind(null,"error"),log.info=log.bind(null,"info"),log.debug=log.bind(null,"debug"),{rootInjects:{log:log}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){return{fn:{AST:n},components:{JumpToPath:i.default}}};var n=function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}(r(411)),i=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(417))},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getLineNumberForPathAsync=t.positionRangeForPathAsync=t.pathForPositionAsync=void 0;var n=_interopRequireDefault(r(151)),i=_interopRequireDefault(r(44));t.getLineNumberForPath=getLineNumberForPath,t.positionRangeForPath=positionRangeForPath,t.pathForPosition=pathForPosition;var o=_interopRequireDefault(r(937)),a=_interopRequireDefault(r(21)),s=_interopRequireDefault(r(193));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var u=(0,r(10).memoize)(o.default.compose),l="tag:yaml.org,2002:map",c="tag:yaml.org,2002:seq";function getLineNumberForPath(e,t){if("string"!=typeof e)throw new TypeError("yaml should be a string");if(!(0,a.default)(t))throw new TypeError("path should be an array of strings");var r=0;return function find(e,t,n){if(!e)return n&&n.start_mark?n.start_mark.line:0;if(t.length&&e.tag===l)for(r=0;r=t.column:t.line===e.start_mark.line?t.column>=e.start_mark.column:t.line===e.end_mark.line?t.column<=e.end_mark.column:e.start_mark.linet.line}}(r)}t.pathForPositionAsync=promisifySyncFn(pathForPosition),t.positionRangeForPathAsync=promisifySyncFn(positionRangeForPath),t.getLineNumberForPathAsync=promisifySyncFn(getLineNumberForPath);function promisifySyncFn(e){return function(){for(var t=arguments.length,r=Array(t),i=0;i=0)throw new t.ConstructorError(null,null,"found unconstructable recursive node",e.start_mark);if(this.constructing_nodes.push(e.unique_id),r=null,s=null,e.tag in this.yaml_constructors)r=this.yaml_constructors[e.tag];else{for(a in this.yaml_multi_constructors)if(e.tag.indexOf(0===a)){s=e.tag.slice(a.length),r=this.yaml_multi_constructors[a];break}null==r&&(null in this.yaml_multi_constructors?(s=e.tag,r=this.yaml_multi_constructors.null):null in this.yaml_constructors?r=this.yaml_constructors.null:e instanceof i.ScalarNode?r=this.construct_scalar:e instanceof i.SequenceNode?r=this.construct_sequence:e instanceof i.MappingNode&&(r=this.construct_mapping))}return n=r.call(this,null!=s?s:e,e),this.constructed_objects[e.unique_id]=n,this.constructing_nodes.pop(),n},BaseConstructor.prototype.construct_scalar=function(e){if(!(e instanceof i.ScalarNode))throw new t.ConstructorError(null,null,"expected a scalar node but found "+e.id,e.start_mark);return e.value},BaseConstructor.prototype.construct_sequence=function(e){var r,n,o,a,s;if(!(e instanceof i.SequenceNode))throw new t.ConstructorError(null,null,"expected a sequence node but found "+e.id,e.start_mark);for(s=[],n=0,o=(a=e.value).length;n=0&&(c=c.slice(1)),"0"===c)return 0;if(0===c.indexOf("0b"))return l*parseInt(c.slice(2),2);if(0===c.indexOf("0x"))return l*parseInt(c.slice(2),16);if(0===c.indexOf("0o"))return l*parseInt(c.slice(2),8);if("0"===c[0])return l*parseInt(c,8);if(u.call(c,":")>=0){for((n=function(){var e,t,r,n;for(n=[],e=0,t=(r=c.split(/:/g)).length;e=0&&(c=c.slice(1)),".inf"===c)return Infinity*l;if(".nan"===c)return NaN;if(u.call(c,":")>=0){for((n=function(){var e,t,r,n;for(n=[],e=0,t=(r=c.split(/:/g)).length;e=0||"\r"===t&&"\n"!==this.string[this.index]?(this.line++,this.column=0):this.column++,r.push(e--);return r},Reader.prototype.get_mark=function(){return new e(this.line,this.column,this.string,this.index)},Reader.prototype.check_printable=function(){var e,n,i;if(n=r.exec(this.string))throw e=n[0],i=this.string.length-this.index+n.index,new t.ReaderError(i,e,"special characters are not allowed")},Reader}()}).call(this)},function(e,t,r){(function(){var e,n,i,o,a={}.hasOwnProperty,s=[].slice,u=[].indexOf||function(e){for(var t=0,r=this.length;t"===e&&0===this.flow_level)return this.fetch_folded();if("'"===e)return this.fetch_single();if('"'===e)return this.fetch_double();if(this.check_plain())return this.fetch_plain();throw new t.ScannerError("while scanning for the next token",null,"found character "+e+" that cannot start any token",this.get_mark())},Scanner.prototype.next_possible_simple_key=function(){var e,t,r,n;for(t in r=null,n=this.possible_simple_keys)a.call(n,t)&&(e=n[t],(null===r||e.token_numbere;)t=this.get_mark(),this.indent=this.indents.pop(),r.push(this.tokens.push(new i.BlockEndToken(t,t)));return r}},Scanner.prototype.add_indent=function(e){return e>this.indent&&(this.indents.push(this.indent),this.indent=e,!0)},Scanner.prototype.fetch_stream_start=function(){var e;return e=this.get_mark(),this.tokens.push(new i.StreamStartToken(e,e,this.encoding))},Scanner.prototype.fetch_stream_end=function(){var e;return this.unwind_indent(-1),this.remove_possible_simple_key(),this.allow_possible_simple_key=!1,this.possible_simple_keys={},e=this.get_mark(),this.tokens.push(new i.StreamEndToken(e,e)),this.done=!0},Scanner.prototype.fetch_directive=function(){return this.unwind_indent(-1),this.remove_possible_simple_key(),this.allow_simple_key=!1,this.tokens.push(this.scan_directive())},Scanner.prototype.fetch_document_start=function(){return this.fetch_document_indicator(i.DocumentStartToken)},Scanner.prototype.fetch_document_end=function(){return this.fetch_document_indicator(i.DocumentEndToken)},Scanner.prototype.fetch_document_indicator=function(e){var t;return this.unwind_indent(-1),this.remove_possible_simple_key(),this.allow_simple_key=!1,t=this.get_mark(),this.forward(3),this.tokens.push(new e(t,this.get_mark()))},Scanner.prototype.fetch_flow_sequence_start=function(){return this.fetch_flow_collection_start(i.FlowSequenceStartToken)},Scanner.prototype.fetch_flow_mapping_start=function(){return this.fetch_flow_collection_start(i.FlowMappingStartToken)},Scanner.prototype.fetch_flow_collection_start=function(e){var t;return this.save_possible_simple_key(),this.flow_level++,this.allow_simple_key=!0,t=this.get_mark(),this.forward(),this.tokens.push(new e(t,this.get_mark()))},Scanner.prototype.fetch_flow_sequence_end=function(){return this.fetch_flow_collection_end(i.FlowSequenceEndToken)},Scanner.prototype.fetch_flow_mapping_end=function(){return this.fetch_flow_collection_end(i.FlowMappingEndToken)},Scanner.prototype.fetch_flow_collection_end=function(e){var t;return this.remove_possible_simple_key(),this.flow_level--,this.allow_simple_key=!1,t=this.get_mark(),this.forward(),this.tokens.push(new e(t,this.get_mark()))},Scanner.prototype.fetch_flow_entry=function(){var e;return this.allow_simple_key=!0,this.remove_possible_simple_key(),e=this.get_mark(),this.forward(),this.tokens.push(new i.FlowEntryToken(e,this.get_mark()))},Scanner.prototype.fetch_block_entry=function(){var e,r;if(0===this.flow_level){if(!this.allow_simple_key)throw new t.ScannerError(null,null,"sequence entries are not allowed here",this.get_mark());this.add_indent(this.column)&&(e=this.get_mark(),this.tokens.push(new i.BlockSequenceStartToken(e,e)))}return this.allow_simple_key=!0,this.remove_possible_simple_key(),r=this.get_mark(),this.forward(),this.tokens.push(new i.BlockEntryToken(r,this.get_mark()))},Scanner.prototype.fetch_key=function(){var e,r;if(0===this.flow_level){if(!this.allow_simple_key)throw new t.ScannerError(null,null,"mapping keys are not allowed here",this.get_mark());this.add_indent(this.column)&&(e=this.get_mark(),this.tokens.push(new i.BlockMappingStartToken(e,e)))}return this.allow_simple_key=!this.flow_level,this.remove_possible_simple_key(),r=this.get_mark(),this.forward(),this.tokens.push(new i.KeyToken(r,this.get_mark()))},Scanner.prototype.fetch_value=function(){var e,r,n;if(e=this.possible_simple_keys[this.flow_level])delete this.possible_simple_keys[this.flow_level],this.tokens.splice(e.token_number-this.tokens_taken,0,new i.KeyToken(e.mark,e.mark)),0===this.flow_level&&this.add_indent(e.column)&&this.tokens.splice(e.token_number-this.tokens_taken,0,new i.BlockMappingStartToken(e.mark,e.mark)),this.allow_simple_key=!1;else{if(0===this.flow_level){if(!this.allow_simple_key)throw new t.ScannerError(null,null,"mapping values are not allowed here",this.get_mark());this.add_indent(this.column)&&(r=this.get_mark(),this.tokens.push(new i.BlockMappingStartToken(r,r)))}this.allow_simple_key=!this.flow_level,this.remove_possible_simple_key()}return n=this.get_mark(),this.forward(),this.tokens.push(new i.ValueToken(n,this.get_mark()))},Scanner.prototype.fetch_alias=function(){return this.save_possible_simple_key(),this.allow_simple_key=!1,this.tokens.push(this.scan_anchor(i.AliasToken))},Scanner.prototype.fetch_anchor=function(){return this.save_possible_simple_key(),this.allow_simple_key=!1,this.tokens.push(this.scan_anchor(i.AnchorToken))},Scanner.prototype.fetch_tag=function(){return this.save_possible_simple_key(),this.allow_simple_key=!1,this.tokens.push(this.scan_tag())},Scanner.prototype.fetch_literal=function(){return this.fetch_block_scalar("|")},Scanner.prototype.fetch_folded=function(){return this.fetch_block_scalar(">")},Scanner.prototype.fetch_block_scalar=function(e){return this.allow_simple_key=!0,this.remove_possible_simple_key(),this.tokens.push(this.scan_block_scalar(e))},Scanner.prototype.fetch_single=function(){return this.fetch_flow_scalar("'")},Scanner.prototype.fetch_double=function(){return this.fetch_flow_scalar('"')},Scanner.prototype.fetch_flow_scalar=function(e){return this.save_possible_simple_key(),this.allow_simple_key=!1,this.tokens.push(this.scan_flow_scalar(e))},Scanner.prototype.fetch_plain=function(){return this.save_possible_simple_key(),this.allow_simple_key=!1,this.tokens.push(this.scan_plain())},Scanner.prototype.check_directive=function(){return 0===this.column},Scanner.prototype.check_document_start=function(){var t;return 0===this.column&&"---"===this.prefix(3)&&(t=this.peek(3),u.call(e+r+"\0",t)>=0)},Scanner.prototype.check_document_end=function(){var t;return 0===this.column&&"..."===this.prefix(3)&&(t=this.peek(3),u.call(e+r+"\0",t)>=0)},Scanner.prototype.check_block_entry=function(){var t;return t=this.peek(1),u.call(e+r+"\0",t)>=0},Scanner.prototype.check_key=function(){var t;return 0!==this.flow_level||(t=this.peek(1),u.call(e+r+"\0",t)>=0)},Scanner.prototype.check_value=function(){var t;return 0!==this.flow_level||(t=this.peek(1),u.call(e+r+"\0",t)>=0)},Scanner.prototype.check_plain=function(){var t,n;return t=this.peek(),u.call(e+r+"\0-?:,[]{}#&*!|>'\"%@`",t)<0||(n=this.peek(1),u.call(e+r+"\0",n)<0&&("-"===t||0===this.flow_level&&u.call("?:",t)>=0))},Scanner.prototype.scan_to_next_token=function(){var t,r,n;for(0===this.index&&"\ufeff"===this.peek()&&this.forward(),t=!1,n=[];!t;){for(;" "===this.peek();)this.forward();if("#"===this.peek())for(;r=this.peek(),u.call(e+"\0",r)<0;)this.forward();this.scan_line_break()?0===this.flow_level?n.push(this.allow_simple_key=!0):n.push(void 0):n.push(t=!0)}return n},Scanner.prototype.scan_directive=function(){var t,r,n,o,a;if(o=this.get_mark(),this.forward(),a=null,"YAML"===(r=this.scan_directive_name(o)))a=this.scan_yaml_directive_value(o),t=this.get_mark();else if("TAG"===r)a=this.scan_tag_directive_value(o),t=this.get_mark();else for(t=this.get_mark();n=this.peek(),u.call(e+"\0",n)<0;)this.forward();return this.scan_directive_ignored_line(o),new i.DirectiveToken(r,a,o,t)},Scanner.prototype.scan_directive_name=function(r){var n,i,o;for(i=0,n=this.peek(i);"0"<=n&&n<="9"||"A"<=n&&n<="Z"||"a"<=n&&n<="z"||u.call("-_",n)>=0;)i++,n=this.peek(i);if(0===i)throw new t.ScannerError("while scanning a directive",r,"expected alphanumeric or numeric character but found "+n,this.get_mark());if(o=this.prefix(i),this.forward(i),n=this.peek(),u.call(e+"\0 ",n)<0)throw new t.ScannerError("while scanning a directive",r,"expected alphanumeric or numeric character but found "+n,this.get_mark());return o},Scanner.prototype.scan_yaml_directive_value=function(r){for(var n,i,o;" "===this.peek();)this.forward();if(n=this.scan_yaml_directive_number(r),"."!==this.peek())throw new t.ScannerError("while scanning a directive",r,"expected a digit or '.' but found "+this.peek(),this.get_mark());if(this.forward(),i=this.scan_yaml_directive_number(r),o=this.peek(),u.call(e+"\0 ",o)<0)throw new t.ScannerError("while scanning a directive",r,"expected a digit or ' ' but found "+this.peek(),this.get_mark());return[n,i]},Scanner.prototype.scan_yaml_directive_number=function(e){var r,n,i,o;if(!("0"<=(r=this.peek())&&r<="9"))throw new t.ScannerError("while scanning a directive",e,"expected a digit but found "+r,this.get_mark());for(n=0;"0"<=(i=this.peek(n))&&i<="9";)n++;return o=parseInt(this.prefix(n)),this.forward(n),o},Scanner.prototype.scan_tag_directive_value=function(e){for(var t;" "===this.peek();)this.forward();for(t=this.scan_tag_directive_handle(e);" "===this.peek();)this.forward();return[t,this.scan_tag_directive_prefix(e)]},Scanner.prototype.scan_tag_directive_handle=function(e){var r,n;if(n=this.scan_tag_handle("directive",e)," "!==(r=this.peek()))throw new t.ScannerError("while scanning a directive",e,"expected ' ' but found "+r,this.get_mark());return n},Scanner.prototype.scan_tag_directive_prefix=function(r){var n,i;if(i=this.scan_tag_uri("directive",r),n=this.peek(),u.call(e+"\0 ",n)<0)throw new t.ScannerError("while scanning a directive",r,"expected ' ' but found "+n,this.get_mark());return i},Scanner.prototype.scan_directive_ignored_line=function(r){for(var n,i;" "===this.peek();)this.forward();if("#"===this.peek())for(;i=this.peek(),u.call(e+"\0",i)<0;)this.forward();if(n=this.peek(),u.call(e+"\0",n)<0)throw new t.ScannerError("while scanning a directive",r,"expected a comment or a line break but found "+n,this.get_mark());return this.scan_line_break()},Scanner.prototype.scan_anchor=function(n){var i,o,a,s,l;for(s=this.get_mark(),a="*"===this.peek()?"alias":"anchor",this.forward(),o=0,i=this.peek(o);"0"<=i&&i<="9"||"A"<=i&&i<="Z"||"a"<=i&&i<="z"||u.call("-_",i)>=0;)o++,i=this.peek(o);if(0===o)throw new t.ScannerError("while scanning an "+a,s,"expected alphabetic or numeric character but found '"+i+"'",this.get_mark());if(l=this.prefix(o),this.forward(o),i=this.peek(),u.call(e+r+"\0?:,]}%@`",i)<0)throw new t.ScannerError("while scanning an "+a,s,"expected alphabetic or numeric character but found '"+i+"'",this.get_mark());return new n(l,s,this.get_mark())},Scanner.prototype.scan_tag=function(){var n,o,a,s,l,c;if(s=this.get_mark(),"<"===(n=this.peek(1))){if(o=null,this.forward(2),l=this.scan_tag_uri("tag",s),">"!==this.peek())throw new t.ScannerError("while parsing a tag",s,"expected '>' but found "+this.peek(),this.get_mark());this.forward()}else if(u.call(e+r+"\0",n)>=0)o=null,l="!",this.forward();else{for(a=1,c=!1;u.call(e+"\0 ",n)<0;){if("!"===n){c=!0;break}a++,n=this.peek(a)}c?o=this.scan_tag_handle("tag",s):(o="!",this.forward()),l=this.scan_tag_uri("tag",s)}if(n=this.peek(),u.call(e+"\0 ",n)<0)throw new t.ScannerError("while scanning a tag",s,"expected ' ' but found "+n,this.get_mark());return new i.TagToken([o,l],s,this.get_mark())},Scanner.prototype.scan_block_scalar=function(t){var r,n,a,s,l,c,p,f,d,h,m,v,g,y,_,b,S,k,x,E;for(l=">"===t,a=[],E=this.get_mark(),this.forward(),n=(g=this.scan_block_scalar_indicators(E))[0],c=g[1],this.scan_block_scalar_ignored_line(E),(v=this.indent+1)<1&&(v=1),null==c?(r=(y=this.scan_block_scalar_indentation())[0],m=y[1],s=y[2],p=Math.max(v,m)):(p=v+c-1,r=(_=this.scan_block_scalar_breaks(p))[0],s=_[1]),h="";this.column===p&&"\0"!==this.peek();){for(a=a.concat(r),b=this.peek(),f=u.call(" \t",b)<0,d=0;S=this.peek(d),u.call(e+"\0",S)<0;)d++;if(a.push(this.prefix(d)),this.forward(d),h=this.scan_line_break(),r=(k=this.scan_block_scalar_breaks(p))[0],s=k[1],this.column!==p||"\0"===this.peek())break;l&&"\n"===h&&f&&(x=this.peek(),u.call(" \t",x)<0)?o.is_empty(r)&&a.push(" "):a.push(h)}return!1!==n&&a.push(h),!0===n&&(a=a.concat(r)),new i.ScalarToken(a.join(""),!1,E,s,t)},Scanner.prototype.scan_block_scalar_indicators=function(r){var n,i,o;if(i=null,o=null,n=this.peek(),u.call("+-",n)>=0){if(i="+"===n,this.forward(),n=this.peek(),u.call("0123456789",n)>=0){if(0===(o=parseInt(n)))throw new t.ScannerError("while scanning a block scalar",r,"expected indentation indicator in the range 1-9 but found 0",this.get_mark());this.forward()}}else if(u.call("0123456789",n)>=0){if(0===(o=parseInt(n)))throw new t.ScannerError("while scanning a block scalar",r,"expected indentation indicator in the range 1-9 but found 0",this.get_mark());this.forward(),n=this.peek(),u.call("+-",n)>=0&&(i="+"===n,this.forward())}if(n=this.peek(),u.call(e+"\0 ",n)<0)throw new t.ScannerError("while scanning a block scalar",r,"expected chomping or indentation indicators, but found "+n,this.get_mark());return[i,o]},Scanner.prototype.scan_block_scalar_ignored_line=function(r){for(var n,i;" "===this.peek();)this.forward();if("#"===this.peek())for(;i=this.peek(),u.call(e+"\0",i)<0;)this.forward();if(n=this.peek(),u.call(e+"\0",n)<0)throw new t.ScannerError("while scanning a block scalar",r,"expected a comment or a line break but found "+n,this.get_mark());return this.scan_line_break()},Scanner.prototype.scan_block_scalar_indentation=function(){var t,r,n,i;for(t=[],n=0,r=this.get_mark();i=this.peek(),u.call(e+" ",i)>=0;)" "!==this.peek()?(t.push(this.scan_line_break()),r=this.get_mark()):(this.forward(),this.column>n&&(n=this.column));return[t,n,r]},Scanner.prototype.scan_block_scalar_breaks=function(t){var r,n,i;for(r=[],n=this.get_mark();this.column=0;)for(r.push(this.scan_line_break()),n=this.get_mark();this.column=0)a.push(o),this.forward();else{if(!n||"\\"!==o)return a;if(this.forward(),(o=this.peek())in c)a.push(c[o]),this.forward();else if(o in l){for(d=l[o],this.forward(),f=p=0,m=d;0<=m?pm;f=0<=m?++p:--p)if(v=this.peek(f),u.call("0123456789ABCDEFabcdef",v)<0)throw new t.ScannerError("while scanning a double-quoted scalar",i,"expected escape sequence of "+d+" hexadecimal numbers, but found "+this.peek(f),this.get_mark());s=parseInt(this.prefix(d),16),a.push(String.fromCharCode(s)),this.forward(d)}else{if(!(u.call(e,o)>=0))throw new t.ScannerError("while scanning a double-quoted scalar",i,"found unknown escape character "+o,this.get_mark());this.scan_line_break(),a=a.concat(this.scan_flow_scalar_breaks(n,i))}}else a.push("'"),this.forward(2)}},Scanner.prototype.scan_flow_scalar_spaces=function(n,i){var o,a,s,l,c,p,f;for(s=[],l=0;p=this.peek(l),u.call(r,p)>=0;)l++;if(f=this.prefix(l),this.forward(l),"\0"===(a=this.peek()))throw new t.ScannerError("while scanning a quoted scalar",i,"found unexpected end of stream",this.get_mark());return u.call(e,a)>=0?(c=this.scan_line_break(),o=this.scan_flow_scalar_breaks(n,i),"\n"!==c?s.push(c):0===o.length&&s.push(" "),s=s.concat(o)):s.push(f),s},Scanner.prototype.scan_flow_scalar_breaks=function(n,i){var o,a,s,l,c;for(o=[];;){if("---"===(a=this.prefix(3))||"..."===a&&(s=this.peek(3),u.call(e+r+"\0",s)>=0))throw new t.ScannerError("while scanning a quoted scalar",i,"found unexpected document separator",this.get_mark());for(;l=this.peek(),u.call(r,l)>=0;)this.forward();if(c=this.peek(),!(u.call(e,c)>=0))return o;o.push(this.scan_line_break())}},Scanner.prototype.scan_plain=function(){var n,o,a,s,l,c,p,f,d;for(o=[],d=a=this.get_mark(),s=this.indent+1,f=[];l=0,"#"!==this.peek();){for(;n=this.peek(l),!(u.call(e+r+"\0",n)>=0||0===this.flow_level&&":"===n&&(c=this.peek(l+1),u.call(e+r+"\0",c)>=0)||0!==this.flow_level&&u.call(",:?[]{}",n)>=0);)l++;if(0!==this.flow_level&&":"===n&&(p=this.peek(l+1),u.call(e+r+"\0,[]{}",p)<0))throw this.forward(l),new t.ScannerError("while scanning a plain scalar",d,"found unexpected ':'",this.get_mark(),"Please check http://pyyaml.org/wiki/YAMLColonInFlowContext");if(0===l)break;if(this.allow_simple_key=!1,(o=o.concat(f)).push(this.prefix(l)),this.forward(l),a=this.get_mark(),null==(f=this.scan_plain_spaces(s,d))||0===f.length||"#"===this.peek()||0===this.flow_level&&this.column=0;)s++;if(m=this.prefix(s),this.forward(s),o=this.peek(),u.call(e,o)>=0){if(l=this.scan_line_break(),this.allow_simple_key=!0,"---"===(c=this.prefix(3))||"..."===c&&(f=this.peek(3),u.call(e+r+"\0",f)>=0))return;for(i=[];h=this.peek(),u.call(e+" ",h)>=0;)if(" "===this.peek())this.forward();else if(i.push(this.scan_line_break()),"---"===(c=this.prefix(3))||"..."===c&&(d=this.peek(3),u.call(e+r+"\0",d)>=0))return;"\n"!==l?a.push(l):0===i.length&&a.push(" "),a=a.concat(i)}else m&&a.push(m);return a},Scanner.prototype.scan_tag_handle=function(e,r){var n,i,o;if("!"!==(n=this.peek()))throw new t.ScannerError("while scanning a "+e,r,"expected '!' but found "+n,this.get_mark());if(i=1," "!==(n=this.peek(i))){for(;"0"<=n&&n<="9"||"A"<=n&&n<="Z"||"a"<=n&&n<="z"||u.call("-_",n)>=0;)i++,n=this.peek(i);if("!"!==n)throw this.forward(i),new t.ScannerError("while scanning a "+e,r,"expected '!' but found "+n,this.get_mark());i++}return o=this.prefix(i),this.forward(i),o},Scanner.prototype.scan_tag_uri=function(e,r){var n,i,o;for(i=[],o=0,n=this.peek(o);"0"<=n&&n<="9"||"A"<=n&&n<="Z"||"a"<=n&&n<="z"||u.call("-;/?:@&=+$,_.!~*'()[]%",n)>=0;)"%"===n?(i.push(this.prefix(o)),this.forward(o),o=0,i.push(this.scan_uri_escapes(e,r))):o++,n=this.peek(o);if(0!==o&&(i.push(this.prefix(o)),this.forward(o),o=0),0===i.length)throw new t.ScannerError("while parsing a "+e,r,"expected URI but found "+n,this.get_mark());return i.join("")},Scanner.prototype.scan_uri_escapes=function(e,r){var n,i,o;for(n=[],this.get_mark();"%"===this.peek();){for(this.forward(),o=i=0;i<=2;o=++i)throw new t.ScannerError("while scanning a "+e,r,"expected URI escape sequence of 2 hexadecimal numbers but found "+this.peek(o),this.get_mark());n.push(String.fromCharCode(parseInt(this.prefix(2),16))),this.forward(2)}return n.join("")},Scanner.prototype.scan_line_break=function(){var e;return e=this.peek(),u.call("\r\n…",e)>=0?("\r\n"===this.prefix(2)?this.forward(2):this.forward(),"\n"):u.call("\u2028\u2029",e)>=0?(this.forward(),e):""},Scanner}()}).call(this)},function(e,t,r){(function(){var e,n,i,o={}.hasOwnProperty,a=[].slice;n=r(119),e=r(46).MarkedYAMLError,i=r(242),this.ParserError=function(t){function ParserError(){return ParserError.__super__.constructor.apply(this,arguments)}return function(e,t){for(var r in t)o.call(t,r)&&(e[r]=t[r]);function ctor(){this.constructor=e}ctor.prototype=t.prototype,e.prototype=new ctor,e.__super__=t.prototype}(ParserError,e),ParserError}(),this.Parser=function(){var e;function Parser(){this.current_event=null,this.yaml_version=null,this.tag_handles={},this.states=[],this.marks=[],this.state="parse_stream_start"}return e={"!":"!","!!":"tag:yaml.org,2002:"},Parser.prototype.dispose=function(){return this.states=[],this.state=null},Parser.prototype.check_event=function(){var e,t,r,n;if(t=1<=arguments.length?a.call(arguments,0):[],null===this.current_event&&null!=this.state&&(this.current_event=this[this.state]()),null!==this.current_event){if(0===t.length)return!0;for(r=0,n=t.length;r', but found "+this.peek_token().id,this.peek_token().start_mark);e=(u=this.get_token()).end_mark,r=new n.DocumentStartEvent(a,e,!0,l,s),this.states.push("parse_document_end"),this.state="parse_document_content"}return r},Parser.prototype.parse_document_end=function(){var e,t,r,o;return o=e=this.peek_token().start_mark,r=!1,this.check_token(i.DocumentEndToken)&&(e=this.get_token().end_mark,r=!0),t=new n.DocumentEndEvent(o,e,r),this.state="parse_document_start",t},Parser.prototype.parse_document_content=function(){var e;return this.check_token(i.DirectiveToken,i.DocumentStartToken,i.DocumentEndToken,i.StreamEndToken)?(e=this.process_empty_scalar(this.peek_token().start_mark),this.state=this.states.pop(),e):this.parse_block_node()},Parser.prototype.process_directives=function(){var r,n,a,s,u,l,c,p,f;for(this.yaml_version=null,this.tag_handles={};this.check_token(i.DirectiveToken);)if("YAML"===(p=this.get_token()).name){if(null!==this.yaml_version)throw new t.ParserError(null,null,"found duplicate YAML directive",p.start_mark);if(n=(s=p.value)[0],s[1],1!==n)throw new t.ParserError(null,null,"found incompatible YAML document (version 1.* is required)",p.start_mark);this.yaml_version=p.value}else if("TAG"===p.name){if(r=(u=p.value)[0],a=u[1],r in this.tag_handles)throw new t.ParserError(null,null,"duplicate tag handle "+r,p.start_mark);this.tag_handles[r]=a}for(r in c=null,l=this.tag_handles)o.call(l,r)&&(a=l[r],null==c&&(c={}),c[r]=a);for(r in f=[this.yaml_version,c],e)o.call(e,r)&&((a=e[r])in this.tag_handles||(this.tag_handles[r]=a));return f},Parser.prototype.parse_block_node=function(){return this.parse_node(!0)},Parser.prototype.parse_flow_node=function(){return this.parse_node()},Parser.prototype.parse_block_node_or_indentless_sequence=function(){return this.parse_node(!0,!0)},Parser.prototype.parse_node=function(e,r){var o,a,s,u,l,c,p,f,d,h,m;if(null==e&&(e=!1),null==r&&(r=!1),this.check_token(i.AliasToken))m=this.get_token(),s=new n.AliasEvent(m.value,m.start_mark,m.end_mark),this.state=this.states.pop();else{if(o=null,d=null,p=a=h=null,this.check_token(i.AnchorToken)?(p=(m=this.get_token()).start_mark,a=m.end_mark,o=m.value,this.check_token(i.TagToken)&&(h=(m=this.get_token()).start_mark,a=m.end_mark,d=m.value)):this.check_token(i.TagToken)&&(p=h=(m=this.get_token()).start_mark,a=m.end_mark,d=m.value,this.check_token(i.AnchorToken)&&(a=(m=this.get_token()).end_mark,o=m.value)),null!==d)if(u=d[0],f=d[1],null!==u){if(!(u in this.tag_handles))throw new t.ParserError("while parsing a node",p,"found undefined tag handle "+u,h);d=this.tag_handles[u]+f}else d=f;if(null===p&&(p=a=this.peek_token().start_mark),s=null,l=null===d||"!"===d,r&&this.check_token(i.BlockEntryToken))a=this.peek_token().end_mark,s=new n.SequenceStartEvent(o,d,l,p,a),this.state="parse_indentless_sequence_entry";else if(this.check_token(i.ScalarToken))a=(m=this.get_token()).end_mark,l=m.plain&&null===d||"!"===d?[!0,!1]:null===d?[!1,!0]:[!1,!1],s=new n.ScalarEvent(o,d,l,m.value,p,a,m.style),this.state=this.states.pop();else if(this.check_token(i.FlowSequenceStartToken))a=this.peek_token().end_mark,s=new n.SequenceStartEvent(o,d,l,p,a,!0),this.state="parse_flow_sequence_first_entry";else if(this.check_token(i.FlowMappingStartToken))a=this.peek_token().end_mark,s=new n.MappingStartEvent(o,d,l,p,a,!0),this.state="parse_flow_mapping_first_key";else if(e&&this.check_token(i.BlockSequenceStartToken))a=this.peek_token().end_mark,s=new n.SequenceStartEvent(o,d,l,p,a,!1),this.state="parse_block_sequence_first_entry";else if(e&&this.check_token(i.BlockMappingStartToken))a=this.peek_token().end_mark,s=new n.MappingStartEvent(o,d,l,p,a,!1),this.state="parse_block_mapping_first_key";else{if(null===o&&null===d)throw c=e?"block":"flow",m=this.peek_token(),new t.ParserError("while parsing a "+c+" node",p,"expected the node content, but found "+m.id,m.start_mark);s=new n.ScalarEvent(o,d,[l,!1],"",p,a),this.state=this.states.pop()}}return s},Parser.prototype.parse_block_sequence_first_entry=function(){var e;return e=this.get_token(),this.marks.push(e.start_mark),this.parse_block_sequence_entry()},Parser.prototype.parse_block_sequence_entry=function(){var e,r;if(this.check_token(i.BlockEntryToken))return r=this.get_token(),this.check_token(i.BlockEntryToken,i.BlockEndToken)?(this.state="parse_block_sequence_entry",this.process_empty_scalar(r.end_mark)):(this.states.push("parse_block_sequence_entry"),this.parse_block_node());if(!this.check_token(i.BlockEndToken))throw r=this.peek_token(),new t.ParserError("while parsing a block collection",this.marks.slice(-1)[0],"expected , but found "+r.id,r.start_mark);return r=this.get_token(),e=new n.SequenceEndEvent(r.start_mark,r.end_mark),this.state=this.states.pop(),this.marks.pop(),e},Parser.prototype.parse_indentless_sequence_entry=function(){var e,t;return this.check_token(i.BlockEntryToken)?(t=this.get_token(),this.check_token(i.BlockEntryToken,i.KeyToken,i.ValueToken,i.BlockEndToken)?(this.state="parse_indentless_sequence_entry",this.process_empty_scalar(t.end_mark)):(this.states.push("parse_indentless_sequence_entry"),this.parse_block_node())):(t=this.peek_token(),e=new n.SequenceEndEvent(t.start_mark,t.start_mark),this.state=this.states.pop(),e)},Parser.prototype.parse_block_mapping_first_key=function(){var e;return e=this.get_token(),this.marks.push(e.start_mark),this.parse_block_mapping_key()},Parser.prototype.parse_block_mapping_key=function(){var e,r;if(this.check_token(i.KeyToken))return r=this.get_token(),this.check_token(i.KeyToken,i.ValueToken,i.BlockEndToken)?(this.state="parse_block_mapping_value",this.process_empty_scalar(r.end_mark)):(this.states.push("parse_block_mapping_value"),this.parse_block_node_or_indentless_sequence());if(!this.check_token(i.BlockEndToken))throw r=this.peek_token(),new t.ParserError("while parsing a block mapping",this.marks.slice(-1)[0],"expected , but found "+r.id,r.start_mark);return r=this.get_token(),e=new n.MappingEndEvent(r.start_mark,r.end_mark),this.state=this.states.pop(),this.marks.pop(),e},Parser.prototype.parse_block_mapping_value=function(){var e;return this.check_token(i.ValueToken)?(e=this.get_token(),this.check_token(i.KeyToken,i.ValueToken,i.BlockEndToken)?(this.state="parse_block_mapping_key",this.process_empty_scalar(e.end_mark)):(this.states.push("parse_block_mapping_key"),this.parse_block_node_or_indentless_sequence())):(this.state="parse_block_mapping_key",e=this.peek_token(),this.process_empty_scalar(e.start_mark))},Parser.prototype.parse_flow_sequence_first_entry=function(){var e;return e=this.get_token(),this.marks.push(e.start_mark),this.parse_flow_sequence_entry(!0)},Parser.prototype.parse_flow_sequence_entry=function(e){var r,o;if(null==e&&(e=!1),!this.check_token(i.FlowSequenceEndToken)){if(!e){if(!this.check_token(i.FlowEntryToken))throw o=this.peek_token(),new t.ParserError("while parsing a flow sequence",this.marks.slice(-1)[0],"expected ',' or ']', but got "+o.id,o.start_mark);this.get_token()}if(this.check_token(i.KeyToken))return o=this.peek_token(),r=new n.MappingStartEvent(null,null,!0,o.start_mark,o.end_mark,!0),this.state="parse_flow_sequence_entry_mapping_key",r;if(!this.check_token(i.FlowSequenceEndToken))return this.states.push("parse_flow_sequence_entry"),this.parse_flow_node()}return o=this.get_token(),r=new n.SequenceEndEvent(o.start_mark,o.end_mark),this.state=this.states.pop(),this.marks.pop(),r},Parser.prototype.parse_flow_sequence_entry_mapping_key=function(){var e;return e=this.get_token(),this.check_token(i.ValueToken,i.FlowEntryToken,i.FlowSequenceEndToken)?(this.state="parse_flow_sequence_entry_mapping_value",this.process_empty_scalar(e.end_mark)):(this.states.push("parse_flow_sequence_entry_mapping_value"),this.parse_flow_node())},Parser.prototype.parse_flow_sequence_entry_mapping_value=function(){var e;return this.check_token(i.ValueToken)?(e=this.get_token(),this.check_token(i.FlowEntryToken,i.FlowSequenceEndToken)?(this.state="parse_flow_sequence_entry_mapping_end",this.process_empty_scalar(e.end_mark)):(this.states.push("parse_flow_sequence_entry_mapping_end"),this.parse_flow_node())):(this.state="parse_flow_sequence_entry_mapping_end",e=this.peek_token(),this.process_empty_scalar(e.start_mark))},Parser.prototype.parse_flow_sequence_entry_mapping_end=function(){var e;return this.state="parse_flow_sequence_entry",e=this.peek_token(),new n.MappingEndEvent(e.start_mark,e.start_mark)},Parser.prototype.parse_flow_mapping_first_key=function(){var e;return e=this.get_token(),this.marks.push(e.start_mark),this.parse_flow_mapping_key(!0)},Parser.prototype.parse_flow_mapping_key=function(e){var r,o;if(null==e&&(e=!1),!this.check_token(i.FlowMappingEndToken)){if(!e){if(!this.check_token(i.FlowEntryToken))throw o=this.peek_token(),new t.ParserError("while parsing a flow mapping",this.marks.slice(-1)[0],"expected ',' or '}', but got "+o.id,o.start_mark);this.get_token()}if(this.check_token(i.KeyToken))return o=this.get_token(),this.check_token(i.ValueToken,i.FlowEntryToken,i.FlowMappingEndToken)?(this.state="parse_flow_mapping_value",this.process_empty_scalar(o.end_mark)):(this.states.push("parse_flow_mapping_value"),this.parse_flow_node());if(!this.check_token(i.FlowMappingEndToken))return this.states.push("parse_flow_mapping_empty_value"),this.parse_flow_node()}return o=this.get_token(),r=new n.MappingEndEvent(o.start_mark,o.end_mark),this.state=this.states.pop(),this.marks.pop(),r},Parser.prototype.parse_flow_mapping_value=function(){var e;return this.check_token(i.ValueToken)?(e=this.get_token(),this.check_token(i.FlowEntryToken,i.FlowMappingEndToken)?(this.state="parse_flow_mapping_key",this.process_empty_scalar(e.end_mark)):(this.states.push("parse_flow_mapping_key"),this.parse_flow_node())):(this.state="parse_flow_mapping_key",e=this.peek_token(),this.process_empty_scalar(e.start_mark))},Parser.prototype.parse_flow_mapping_empty_value=function(){return this.state="parse_flow_mapping_key",this.process_empty_scalar(this.peek_token().start_mark)},Parser.prototype.process_empty_scalar=function(e){return new n.ScalarEvent(null,null,[!0,!1],"",e,e)},Parser}()}).call(this)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(4)),i=_interopRequireDefault(r(2)),o=_interopRequireDefault(r(3)),a=_interopRequireDefault(r(5)),s=_interopRequireDefault(r(6));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var u=function(e){function JumpToPath(){return(0,i.default)(this,JumpToPath),(0,a.default)(this,(JumpToPath.__proto__||(0,n.default)(JumpToPath)).apply(this,arguments))}return(0,s.default)(JumpToPath,e),(0,o.default)(JumpToPath,[{key:"render",value:function render(){return null}}]),JumpToPath}(_interopRequireDefault(r(0)).default.Component);t.default=u},function(e,t,r){"use strict";var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(419));e.exports=function(e){var t=e.configs;return{fn:{fetch:n.default.makeHttp(t.preFetch,t.postFetch),buildRequest:n.default.buildRequest,execute:n.default.execute,resolve:n.default.resolve,resolveSubtree:n.default.resolveSubtree,serializeRes:n.default.serializeRes,opId:n.default.helpers.opId}}}},function(e,t,r){e.exports=function(e){function t(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var r={};return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=23)}([function(e,t){e.exports=r(43)},function(e,t){e.exports=r(44)},function(e,t){e.exports=r(25)},function(e,t){e.exports=r(26)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function o(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",n=(arguments.length>3&&void 0!==arguments[3]?arguments[3]:{}).v2OperationIdCompatibilityMode;return e&&"object"===(void 0===e?"undefined":(0,h.default)(e))?(e.operationId||"").replace(/\s/g,"").length?y(e.operationId):i(t,r,{v2OperationIdCompatibilityMode:n}):null}function i(e,t){if((arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).v2OperationIdCompatibilityMode){var r=(t.toLowerCase()+"_"+e).replace(/[\s!@#$%^&*()_+=[{\]};:<>|.\/?,\\'""-]/g,"_");return(r=r||e.substring(1)+"_"+t).replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return""+g(t)+y(e)}function s(e,t){return g(t)+"-"+e}function c(e,t){return f(e,t,!0)||null}function f(e,t,r){if(!e||"object"!==(void 0===e?"undefined":(0,h.default)(e))||!e.paths||"object"!==(0,h.default)(e.paths))return null;var n=e.paths;for(var i in n)for(var o in n[i])if("PARAMETERS"!==o.toUpperCase()){var a=n[i][o];if(a&&"object"===(void 0===a?"undefined":(0,h.default)(a))){var s={spec:e,pathName:i,method:o.toUpperCase(),operation:a},u=t(s);if(r&&u)return s}}}Object.defineProperty(t,"__esModule",{value:!0});var d=n(r(18)),h=n(r(1));t.isOAS3=function a(e){var t=e.openapi;return!!t&&(0,v.default)(t,"3")},t.isSwagger2=function u(e){var t=e.swagger;return!!t&&(0,v.default)(t,"2")},t.opId=o,t.idFromPathMethod=i,t.legacyIdFromPathMethod=s,t.getOperationRaw=function l(e,t){return e&&e.paths?c(e,function(e){var r=e.pathName,n=e.method,i=e.operation;if(!i||"object"!==(void 0===i?"undefined":(0,h.default)(i)))return!1;var a=i.operationId;return[o(i,r,n),s(r,n),a].some(function(e){return e&&e===t})}):null},t.findOperation=c,t.eachOperation=f,t.normalizeSwagger=function p(e){var t=e.spec,r=t.paths,n={};if(!r||t.$$normalized)return e;for(var i in r){var a=r[i];if((0,m.default)(a)){var s=a.parameters;for(var u in a)!function(e){var r=a[e];if(!(0,m.default)(r))return"continue";var u=o(r,i,e);if(u){n[u]?n[u].push(r):n[u]=[r];var l=n[u];if(l.length>1)l.forEach(function(e,t){e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId=""+u+(t+1)});else if(void 0!==r.operationId){var c=l[0];c.__originalOperationId=c.__originalOperationId||r.operationId,c.operationId=u}}if("parameters"!==e){var p=[],f={};for(var h in t)"produces"!==h&&"consumes"!==h&&"security"!==h||(f[h]=t[h],p.push(f));if(s&&(f.parameters=s,p.push(f)),p.length){var v=!0,g=!1,y=void 0;try{for(var _,b=(0,d.default)(p);!(v=(_=b.next()).done);v=!0){var S=_.value;for(var k in S)if(r[k]){if("parameters"===k){var x=!0,E=!1,C=void 0;try{for(var w,D=(0,d.default)(S[k]);!(x=(w=D.next()).done);x=!0)!function(){var e=w.value;r[k].some(function(t){return t.name&&t.name===e.name||t.$ref&&t.$ref===e.$ref||t.$$ref&&t.$$ref===e.$$ref||t===e})||r[k].push(e)}()}catch(e){E=!0,C=e}finally{try{!x&&D.return&&D.return()}finally{if(E)throw C}}}}else r[k]=S[k]}}catch(e){g=!0,y=e}finally{try{!v&&b.return&&b.return()}finally{if(g)throw y}}}}}(u)}}return t.$$normalized=!0,e};var m=n(r(48)),v=n(r(14)),g=function(e){return String.prototype.toLowerCase.call(e)},y=function(e){return e.replace(/[^\w]/gi,"_")}},function(e,t){e.exports=r(946)},function(t,r,p){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function u(e,t){var r=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).loadSpec,n=void 0!==r&&r,i={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:o(e.headers)},s=i.headers["content-type"],u=n||x(s);return(u?e.text:e.blob||e.buffer).call(e).then(function(e){if(i.text=e,i.data=e,u)try{var t=function a(e,t){return"application/json"===t?JSON.parse(e):b.default.safeLoad(e)}(e,s);i.body=t,i.obj=t}catch(e){i.parseError=e}return i})}function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={};return"function"==typeof e.forEach?(e.forEach(function(e,r){void 0!==t[r]?(t[r]=Array.isArray(t[r])?t[r]:[t[r]],t[r].push(e)):t[r]=e}),t):t}function i(e){return"undefined"!=typeof File?e instanceof File:null!==e&&"object"===(void 0===e?"undefined":(0,g.default)(e))&&"function"==typeof e.pipe}function s(e,t){var r=e.collectionFormat,n=e.allowEmptyValue,o="object"===(void 0===e?"undefined":(0,g.default)(e))?e.value:e;if(void 0===o&&n)return"";if(i(o)||"boolean"==typeof o)return o;var a=encodeURIComponent;return t&&(a=(0,S.default)(o)?function(e){return e}:function(e){return(0,m.default)(e)}),"object"!==(void 0===o?"undefined":(0,g.default)(o))||Array.isArray(o)?Array.isArray(o)?Array.isArray(o)&&!r?o.map(a).join(","):"multi"===r?o.map(a):o.map(a).join({csv:",",ssv:"%20",tsv:"%09",pipes:"|"}[r]):a(o):""}function l(e){var t=(0,h.default)(e).reduce(function(t,r){var n=e[r],i=!!n.skipEncoding,o=i?r:encodeURIComponent(r),a=function(e){return e&&"object"===(void 0===e?"undefined":(0,g.default)(e))}(n)&&!Array.isArray(n);return t[o]=s(a?n:{value:n},i),t},{});return _.default.stringify(t,{encode:!1,indices:!1})||""}function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.url,r=void 0===t?"":t,n=e.query,o=e.form;if(o){var a=(0,h.default)(o).some(function(e){return i(o[e].value)}),u=e.headers["content-type"]||e.headers["Content-Type"];if(a||/multipart\/form-data/i.test(u)){var c=p(30);e.body=new c,(0,h.default)(o).forEach(function(t){e.body.append(t,s(o[t],!0))})}else e.body=l(o);delete e.form}if(n){var f=r.split("?"),m=(0,d.default)(f,2),v=m[0],g=m[1],y="";if(g){var b=_.default.parse(g);(0,h.default)(n).forEach(function(e){return delete b[e]}),y=_.default.stringify(b,{encode:!0})}var S=function(){for(var e=arguments.length,t=Array(e),r=0;r1&&void 0!==arguments[1]?arguments[1]:{};return v.default.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if("object"===(void 0===t?"undefined":(0,g.default)(t))&&(t=(a=t).url),a.headers=a.headers||{},k.mergeInQueryOrForm(a),!a.requestInterceptor){e.next=10;break}return e.next=6,a.requestInterceptor(a);case 6:if(e.t0=e.sent,e.t0){e.next=9;break}e.t0=a;case 9:a=e.t0;case 10:return r=a.headers["content-type"]||a.headers["Content-Type"],/multipart\/form-data/i.test(r)&&(delete a.headers["content-type"],delete a.headers["Content-Type"]),n=void 0,e.prev=13,e.next=16,(a.userFetch||fetch)(a.url,a);case 16:return n=e.sent,e.next=19,k.serializeRes(n,t,a);case 19:if(n=e.sent,!a.responseInterceptor){e.next=27;break}return e.next=23,a.responseInterceptor(n);case 23:if(e.t1=e.sent,e.t1){e.next=26;break}e.t1=n;case 26:n=e.t1;case 27:e.next=37;break;case 29:if(e.prev=29,e.t2=e.catch(13),n){e.next=33;break}throw e.t2;case 33:throw(i=new Error(n.statusText)).statusCode=i.status=n.status,i.responseError=e.t2,i;case 37:if(n.ok){e.next=42;break}throw(o=new Error(n.statusText)).statusCode=o.status=n.status,o.response=n,o;case 42:return e.abrupt("return",n);case 43:case"end":return e.stop()}},e,this,[[13,29]])}));return function e(r){return t.apply(this,arguments)}}();var x=r.shouldDownloadAsText=function(){return/(json|xml|yaml|text)\b/.test(arguments.length>0&&void 0!==arguments[0]?arguments[0]:"")}},function(e,t){e.exports=r(42)},function(e,t){e.exports=r(361)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function u(e){return Array.isArray(e)?e.length<1?"":"/"+e.map(function(e){return(e+"").replace(/~/g,"~0").replace(/\//g,"~1")}).join("/"):e}function i(e,t,r){return{op:"replace",path:e,value:t,meta:r}}function h(e,t,r){return k(P(e.filter(j).map(function(e){return t(e.value,r,e.path)})||[]))}function v(e,t,r){return r=r||[],Array.isArray(e)?e.map(function(e,n){return v(e,t,r.concat(n))}):w(e)?(0,R.default)(e).map(function(n){return v(e[n],t,r.concat(n))}):t(e,r[r.length-1],r)}function m(e,t,r){var n=[];if((r=r||[]).length>0){var i=t(e,r[r.length-1],r);i&&(n=n.concat(i))}if(Array.isArray(e)){var o=e.map(function(e,n){return m(e,t,r.concat(n))});o&&(n=n.concat(o))}else if(w(e)){var a=(0,R.default)(e).map(function(n){return m(e[n],t,r.concat(n))});a&&(n=n.concat(a))}return P(n)}function x(e){return Array.isArray(e)?e:[e]}function P(e){var t;return(t=[]).concat.apply(t,(0,D.default)(e.map(function(e){return Array.isArray(e)?P(e):e})))}function k(e){return e.filter(function(e){return void 0!==e})}function w(e){return e&&"object"===(void 0===e?"undefined":(0,S.default)(e))}function q(e){return e&&"function"==typeof e}function M(e){if(I(e)){var t=e.op;return"add"===t||"remove"===t||"replace"===t}return!1}function A(e){return M(e)||I(e)&&"mutation"===e.type}function j(e){return A(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function I(e){return e&&"object"===(void 0===e?"undefined":(0,S.default)(e))}function E(e,t){try{return B.default.getValueByPointer(e,t)}catch(e){return console.error(e),{}}}Object.defineProperty(t,"__esModule",{value:!0});var S=n(r(1)),D=n(r(34)),R=n(r(0)),T=n(r(35)),F=n(r(2)),B=n(r(36)),N=n(r(37)),L=r(38),z=n(r(39));t.default={add:function o(e,t){return{op:"add",path:e,value:t}},replace:i,remove:function s(e,t){return{op:"remove",path:e}},merge:function l(e,t){return{type:"mutation",op:"merge",path:e,value:t}},mergeDeep:function c(e,t){return{type:"mutation",op:"mergeDeep",path:e,value:t}},context:function f(e,t){return{type:"context",path:e,value:t}},getIn:function g(e,t){return t.reduce(function(e,t){return void 0!==t&&e?e[t]:e},e)},applyPatch:function a(e,t,r){if(r=r||{},"merge"===(t=(0,F.default)({},t,{path:t.path&&u(t.path)})).op){var n=E(e,t.path);(0,F.default)(n,t.value),B.default.applyPatch(e,[i(t.path,n)])}else if("mergeDeep"===t.op){var o=E(e,t.path);for(var a in t.value){var s=t.value[a],l=Array.isArray(s);if(l){var c=o[a]||[];o[a]=c.concat(s)}else if(w(s)&&!l){var p=(0,F.default)({},o[a]);for(var f in s){if(Object.prototype.hasOwnProperty.call(p,f)){p=(0,N.default)((0,z.default)({},p),s);break}(0,F.default)(p,(0,T.default)({},f,s[f]))}o[a]=p}else o[a]=s}}else if("add"===t.op&&""===t.path&&w(t.value)){var d=(0,R.default)(t.value).reduce(function(e,r){return e.push({op:"add",path:"/"+u(r),value:t.value[r]}),e},[]);B.default.applyPatch(e,d)}else if("replace"===t.op&&""===t.path){var h=t.value;r.allowMetaPatches&&t.meta&&j(t)&&(Array.isArray(t.value)||w(t.value))&&(h=(0,F.default)({},h,t.meta)),e=h}else if(B.default.applyPatch(e,[t]),r.allowMetaPatches&&t.meta&&j(t)&&(Array.isArray(t.value)||w(t.value))){var m=E(e,t.path),v=(0,F.default)({},m,t.meta);B.default.applyPatch(e,[i(t.path,v)])}return e},parentPathMatch:function y(e,t){if(!Array.isArray(t))return!1;for(var r=0,n=t.length;r1&&void 0!==arguments[1]?arguments[1]:{},r=t.requestInterceptor,n=t.responseInterceptor,i=e.withCredentials?"include":"same-origin";return function(t){return e({url:t,loadSpec:!0,requestInterceptor:r,responseInterceptor:n,headers:{Accept:"application/json"},credentials:i}).then(function(e){return e.body})}}Object.defineProperty(i,"__esModule",{value:!0});var l=n(s(8)),c=n(s(11));i.makeFetchJSON=a,i.clearCache=function u(){f.plugins.refs.clearCache()},i.default=function o(e){function t(t){var r=this;k&&(f.plugins.refs.docCache[k]=t),f.plugins.refs.fetchJSON=a(S,{requestInterceptor:y,responseInterceptor:_});var n=[f.plugins.refs];return"function"==typeof g&&n.push(f.plugins.parameters),"function"==typeof v&&n.push(f.plugins.properties),"strict"!==o&&n.push(f.plugins.allOf),(0,d.default)({spec:t,context:{baseDoc:k},plugins:n,allowMetaPatches:u,pathDiscriminator:m,parameterMacro:g,modelPropertyMacro:v}).then(b?function(){var e=(0,c.default)(l.default.mark(function e(t){return l.default.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",t);case 1:case"end":return e.stop()}},e,r)}));return function(t){return e.apply(this,arguments)}}():h.normalizeSwagger)}var r=e.fetch,n=e.spec,i=e.url,o=e.mode,s=e.allowMetaPatches,u=void 0===s||s,m=e.pathDiscriminator,v=e.modelPropertyMacro,g=e.parameterMacro,y=e.requestInterceptor,_=e.responseInterceptor,b=e.skipNormalization,S=e.http,k=e.baseDoc;return k=k||i,S=r||S||p.default,n?t(n):a(S,{requestInterceptor:y,responseInterceptor:_})(k).then(t)};var p=n(s(6)),f=s(31),d=n(f),h=s(4)},function(e,t){e.exports=r(151)},function(e,t){e.exports=r(97)},function(e,t){e.exports=r(2)},function(e,t){e.exports=r(3)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function n(e,t){function r(){Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack;for(var e=arguments.length,r=Array(e),n=0;n-1||o.indexOf(r)>-1};var i=["properties"],o=["definitions","parameters","responses","securityDefinitions","components/schemas","components/responses","components/parameters","components/securitySchemes"]},function(e,t,r){e.exports=r(24)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if("string"==typeof e?r.url=e:r=e,!(this instanceof a))return new a(r);(0,o.default)(this,r);var n=this.resolve().then(function(){return t.disableInterfaces||(0,o.default)(t,a.makeApisTagOperation(t)),t});return n.client=this,n}Object.defineProperty(t,"__esModule",{value:!0});var i=n(r(3)),o=n((n(r(25)),r(5))),s=n(r(14)),u=n(r(10)),l=r(6),c=n(l),p=r(16),f=n(p),d=n(r(49)),h=r(50),m=r(52),v=r(4);a.http=c.default,a.makeHttp=l.makeHttp.bind(null,a.http),a.resolve=f.default,a.resolveSubtree=d.default,a.execute=m.execute,a.serializeRes=l.serializeRes,a.serializeHeaders=l.serializeHeaders,a.clearCache=p.clearCache,a.parameterBuilders=m.PARAMETER_BUILDERS,a.makeApisTagOperation=h.makeApisTagOperation,a.buildRequest=m.buildRequest,a.helpers={opId:v.opId},a.prototype={http:c.default,execute:function(e){return this.applyDefaults(),a.execute((0,i.default)({spec:this.spec,http:this.http,securities:{authorized:this.authorizations},contextUrl:"string"==typeof this.url?this.url:void 0},e))},resolve:function(){var e=this;return a.resolve({spec:this.spec,url:this.url,allowMetaPatches:this.allowMetaPatches,requestInterceptor:this.requestInterceptor||null,responseInterceptor:this.responseInterceptor||null}).then(function(t){return e.originalSpec=e.spec,e.spec=t.spec,e.errors=t.errors,e})}},a.prototype.applyDefaults=function(){var e=this.spec,t=this.url;if(t&&(0,s.default)(t,"http")){var r=u.default.parse(t);e.host||(e.host=r.host),e.schemes||(e.schemes=[r.protocol.replace(":","")]),e.basePath||(e.basePath="/")}},t.default=a,e.exports=t.default},function(e,t){e.exports=r(958)},function(e,t){e.exports=r(19)},function(e,t){e.exports=r(959)},function(e,t){e.exports=r(960)},function(e,t){e.exports=r(365)},function(e,t){e.exports=r(963)},function(t,r,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(r,"__esModule",{value:!0}),r.plugins=r.SpecMap=void 0;var o=n(i(7)),s=n(i(1)),u=n(i(17)),l=n(i(8)),c=n(i(0)),p=n(i(18)),f=n(i(32)),d=n(i(2)),h=n(i(19)),m=n(i(20));r.default=function a(e){return new x(e).dispatch()};var v=n(i(33)),g=n(i(9)),y=n(i(40)),_=n(i(44)),b=n(i(45)),S=n(i(46)),k=n(i(47)),x=function(){function e(t){(0,h.default)(this,e),(0,d.default)(this,{spec:"",debugLevel:"info",plugins:[],pluginHistory:{},errors:[],mutations:[],promisedPatches:[],state:{},patches:[],context:{},contextTree:new k.default,showDebug:!1,allPatches:[],pluginProp:"specMap",libMethods:(0,d.default)((0,f.default)(this),g.default),allowMetaPatches:!1},t),this.get=this._get.bind(this),this.getContext=this._getContext.bind(this),this.hasRun=this._hasRun.bind(this),this.wrappedPlugins=this.plugins.map(this.wrapPlugin.bind(this)).filter(g.default.isFunction),this.patches.push(g.default.add([],this.spec)),this.patches.push(g.default.context([],this.context)),this.updatePatches(this.patches)}return(0,m.default)(e,[{key:"debug",value:function(e){if(this.debugLevel===e){for(var t,r=arguments.length,n=Array(r>1?r-1:0),i=1;i1?r-1:0),i=1;i0})}},{key:"nextPromisedPatch",value:function(){if(this.promisedPatches.length>0)return u.default.race(this.promisedPatches.map(function(e){return e.value}))}},{key:"getPluginHistory",value:function(e){var t=this.getPluginName(e);return this.pluginHistory[t]||[]}},{key:"getPluginRunCount",value:function(e){return this.getPluginHistory(e).length}},{key:"getPluginHistoryTip",value:function(e){var t=this.getPluginHistory(e);return t&&t[t.length-1]||{}}},{key:"getPluginMutationIndex",value:function(e){var t=this.getPluginHistoryTip(e).mutationIndex;return"number"!=typeof t?-1:t}},{key:"getPluginName",value:function(e){return e.pluginName}},{key:"updatePluginHistory",value:function(e,t){var r=this.getPluginName(e);(this.pluginHistory[r]=this.pluginHistory[r]||[]).push(t)}},{key:"updatePatches",value:function(e,t){var r=this;g.default.normalizeArray(e).forEach(function(e){if(e instanceof Error)r.errors.push(e);else try{if(!g.default.isObject(e))return void r.debug("updatePatches","Got a non-object patch",e);if(r.showDebug&&r.allPatches.push(e),g.default.isPromise(e.value))return r.promisedPatches.push(e),void r.promisedPatchThen(e);if(g.default.isContextPatch(e))return void r.setContext(e.path,e.value);if(g.default.isMutation(e))return void r.updateMutations(e)}catch(e){console.error(e),r.errors.push(e)}})}},{key:"updateMutations",value:function(e){"object"===(0,s.default)(e.value)&&!Array.isArray(e.value)&&this.allowMetaPatches&&(e.value=(0,d.default)({},e.value));var t=g.default.applyPatch(this.state,e,{allowMetaPatches:this.allowMetaPatches});t&&(this.mutations.push(e),this.state=t)}},{key:"removePromisedPatch",value:function(e){var t=this.promisedPatches.indexOf(e);t<0?this.debug("Tried to remove a promisedPatch that isn't there!"):this.promisedPatches.splice(t,1)}},{key:"promisedPatchThen",value:function(e){var t=this;return e.value=e.value.then(function(r){var n=(0,d.default)({},e,{value:r});t.removePromisedPatch(e),t.updatePatches(n)}).catch(function(r){t.removePromisedPatch(e),t.updatePatches(r)})}},{key:"getMutations",value:function(e,t){return e=e||0,"number"!=typeof t&&(t=this.mutations.length),this.mutations.slice(e,t)}},{key:"getCurrentMutations",value:function(){return this.getMutationsForPlugin(this.getCurrentPlugin())}},{key:"getMutationsForPlugin",value:function(e){var t=this.getPluginMutationIndex(e);return this.getMutations(t+1)}},{key:"getCurrentPlugin",value:function(){return this.currentPlugin}},{key:"getPatchesOfType",value:function(e,t){return e.filter(t)}},{key:"getLib",value:function(){return this.libMethods}},{key:"_get",value:function(e){return g.default.getIn(this.state,e)}},{key:"_getContext",value:function(e){return this.contextTree.get(e)}},{key:"setContext",value:function(e,t){return this.contextTree.set(e,t)}},{key:"_hasRun",value:function(e){return this.getPluginRunCount(this.getCurrentPlugin())>(e||0)}},{key:"_clone",value:function(e){return JSON.parse((0,o.default)(e))}},{key:"dispatch",value:function(){function e(e){e&&(e=g.default.fullyNormalizeArray(e),r.updatePatches(e,n))}var t=this,r=this,n=this.nextPlugin();if(!n){var i=this.nextPromisedPatch();if(i)return i.then(function(){return t.dispatch()}).catch(function(){return t.dispatch()});var o={spec:this.state,errors:this.errors};return this.showDebug&&(o.patches=this.allPatches),u.default.resolve(o)}if(r.pluginCount=r.pluginCount||{},r.pluginCount[n]=(r.pluginCount[n]||0)+1,r.pluginCount[n]>100)return u.default.resolve({spec:r.state,errors:r.errors.concat(new Error("We've reached a hard limit of 100 plugin runs"))});if(n!==this.currentPlugin&&this.promisedPatches.length){var a=this.promisedPatches.map(function(e){return e.value});return u.default.all(a.map(function(e){return e.then(Function,Function)})).then(function(){return t.dispatch()})}return function(){r.currentPlugin=n;var t=r.getCurrentMutations(),i=r.mutations.length-1;try{if(n.isGeneratorFunction){var o=!0,a=!1,s=void 0;try{for(var u,l=(0,p.default)(n(t,r.getLib()));!(o=(u=l.next()).done);o=!0)e(u.value)}catch(e){a=!0,s=e}finally{try{!o&&l.return&&l.return()}finally{if(a)throw s}}}else e(n(t,r.getLib()))}catch(t){console.error(t),e([(0,d.default)((0,f.default)(t),{plugin:n})])}finally{r.updatePluginHistory(n,{mutationIndex:i})}return r.dispatch()}()}}]),e}(),E={refs:y.default,allOf:_.default,parameters:b.default,properties:S.default};r.SpecMap=x,r.plugins=E},function(e,t){e.exports=r(372)},function(e,t){e.exports=r(193)},function(e,t){e.exports=r(86)},function(e,t){e.exports=r(24)},function(e,t){e.exports=r(964)},function(e,t){e.exports=r(189)},function(e,t){e.exports=r(967)},function(e,t){e.exports=r(968)},function(e,t,_){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e,t){if(!O.test(e)){if(!t)throw new P("Tried to resolve a relative URL, without having a basePath. path: '"+e+"' basePath: '"+t+"'");return A.default.resolve(t,e)}return e}function u(e,t){return new P("Could not resolve reference because of: "+e.message,t,e)}function o(e){return(e+"").split("#")}function i(e,t){var r=I[e];if(r&&!R.default.isPromise(r))try{var n=f(t,r);return(0,E.default)(k.default.resolve(n),{__value:n})}catch(e){return k.default.reject(e)}return l(e).then(function(e){return f(t,e)})}function l(e){var t=I[e];return t?R.default.isPromise(t)?t:k.default.resolve(t):(I[e]=B.fetchJSON(e).then(function(t){return I[e]=t,t}),I[e])}function f(e,t){var r=p(e);if(r.length<1)return t;var n=R.default.getIn(t,r);if(void 0===n)throw new P("Could not resolve pointer: "+e+" does not exist in document",{pointer:e});return n}function p(e){if("string"!=typeof e)throw new TypeError("Expected a string, got a "+(void 0===e?"undefined":(0,b.default)(e)));return"/"===e[0]&&(e=e.substr(1)),""===e?[]:e.split("/").map(d)}function d(e){return"string"!=typeof e?e:D.default.unescape(e.replace(/~1/g,"/").replace(/~0/g,"~"))}function h(e){return D.default.escape(e.replace(/~/g,"~0").replace(/\//g,"~1"))}function m(e,t){if(N(t))return!0;var r=e.charAt(t.length),n=t.slice(-1);return 0===e.indexOf(t)&&(!r||"/"===r||"#"===r)&&"#"!==n}function y(e,t,r,n){var i=q.get(n);i||(i={},q.set(n,i));var o=function v(e){return 0===e.length?"":"/"+e.map(h).join("/")}(r),a=(t||"")+"#"+e;if(t==n.contextTree.get([]).baseDoc&&m(o,e))return!0;var s="";if(r.some(function(e){return s=s+"/"+h(e),i[s]&&i[s].some(function(e){return m(e,a)||m(a,e)})}))return!0;i[o]=(i[o]||[]).concat(a)}Object.defineProperty(t,"__esModule",{value:!0});var b=n(_(1)),S=n(_(0)),k=n(_(17)),x=n(_(41)),E=n(_(2)),C=_(42),w=n(_(15)),D=n(_(43)),A=n(_(10)),R=n(_(9)),M=n(_(21)),T=_(22),O=new RegExp("^([a-z]+://|//)","i"),P=(0,M.default)("JSONRefError",function(e,t,r){this.originalError=r,(0,E.default)(this,t||{})}),I={},q=new x.default,F={key:"$ref",plugin:function(e,t,r,n){var s=r.slice(0,-1);if(!(0,T.isFreelyNamed)(s)){var l=n.getContext(r).baseDoc;if("string"!=typeof e)return new P("$ref: must be a string (JSON-Ref)",{$ref:e,baseDoc:l,fullPath:r});var c=o(e),f=c[0],d=c[1]||"",h=void 0;try{h=l||f?a(f,l):null}catch(t){return u(t,{pointer:d,$ref:e,basePath:h,fullPath:r})}var m=void 0,v=void 0;if(!y(d,h,s,n)){if(null==h?(v=p(d),void 0===(m=n.get(v))&&(m=new P("Could not resolve reference: "+e,{pointer:d,$ref:e,baseDoc:l,fullPath:r}))):m=null!=(m=i(h,d)).__value?m.__value:m.catch(function(t){throw u(t,{pointer:d,$ref:e,baseDoc:l,fullPath:r})}),m instanceof Error)return[R.default.remove(r),m];var _=R.default.replace(s,m,{$$ref:e});return h&&h!==l?[_,R.default.context(s,{baseDoc:h})]:function g(e,t){var n=[e];return t.path.reduce(function(e,t){return n.push(e[t]),e[t]},e),function r(e){return R.default.isObject(e)&&(n.indexOf(e)>=0||(0,S.default)(e).some(function(t){return r(e[t])}))}(t.value)}(n.state,_)?void 0:_}}}},B=(0,E.default)(F,{docCache:I,absoluteify:a,clearCache:function s(e){void 0!==e?delete I[e]:(0,S.default)(I).forEach(function(e){delete I[e]})},JSONRefError:P,wrapError:u,getDoc:l,split:o,extractFromDoc:i,fetchJSON:function c(e){return(0,C.fetch)(e,{headers:{Accept:"application/json, application/yaml"},loadSpec:!0}).then(function(e){return e.text()}).then(function(e){return w.default.safeLoad(e)})},extract:f,jsonPointerToArray:p,unescapeJsonPointerToken:d});t.default=B;var N=function(e){return!e||"/"===e||"#"===e};e.exports=t.default},function(e,t){e.exports=r(969)},function(e,t){e.exports=r(980)},function(e,t){e.exports=r(981)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(e){return e&&e.__esModule?e:{default:e}}(r(2)),i=r(22);t.default={key:"allOf",plugin:function(e,t,r,o,a){if(!a.meta||!a.meta.$$ref){var s=r.slice(0,-1);if(!(0,i.isFreelyNamed)(s)){if(!Array.isArray(e)){var u=new TypeError("allOf must be an array");return u.fullPath=r,u}var l=!1,c=a.value;s.forEach(function(e){c&&(c=c[e])}),delete(c=(0,n.default)({},c)).allOf;var p=[o.replace(s,{})].concat(e.map(function(e,t){if(!o.isObject(e)){if(l)return null;l=!0;var n=new TypeError("Elements in allOf must be objects");return n.fullPath=r,n}return o.mergeDeep(s,e)}));return p.push(o.mergeDeep(s,c)),c.$$ref||p.push(o.remove([].concat(s,"$$ref"))),p}}}},e.exports=t.default},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(r(2)),o=n(r(9));t.default={key:"parameters",plugin:function(e,t,r,n,a){if(Array.isArray(e)&&e.length){var s=(0,i.default)([],e),u=r.slice(0,-1),l=(0,i.default)({},o.default.getIn(n.spec,u));return e.forEach(function(e,t){try{s[t].default=n.parameterMacro(l,e)}catch(e){var i=new Error(e);return i.fullPath=r,i}}),o.default.replace(r,s)}return o.default.replace(r,e)}},e.exports=t.default},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(r(2)),o=n(r(9));t.default={key:"properties",plugin:function(e,t,r,n){var a=(0,i.default)({},e);for(var s in e)try{a[s].default=n.modelPropertyMacro(a[s])}catch(e){var u=new Error(e);return u.fullPath=r,u}return o.default.replace(r,a)}},e.exports=t.default},function(t,r,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e,t){return u({children:{}},e,t)}function u(e,t,r){return e.value=t||{},e.protoValue=r?(0,s.default)({},r.protoValue,e.value):e.value,(0,o.default)(e.children).forEach(function(t){var r=e.children[t];e.children[t]=u(r,r.value,e)}),e}Object.defineProperty(r,"__esModule",{value:!0});var o=n(i(0)),s=n(i(3)),l=n(i(19)),c=n(i(20)),p=function(){function e(t){(0,l.default)(this,e),this.root=a(t||{})}return(0,c.default)(e,[{key:"set",value:function(e,t){var r=this.getParent(e,!0);if(r){var n=e[e.length-1],i=r.children;i[n]?u(i[n],t,r):i[n]=a(t,r)}else u(this.root,t,null)}},{key:"get",value:function(e){if((e=e||[]).length<1)return this.root.value;for(var t=this.root,r=void 0,n=void 0,i=0;i2&&void 0!==arguments[2]?arguments[2]:{};return o.default.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return n=y.returnEntireTree,i=y.baseDoc,s=y.requestInterceptor,p=y.responseInterceptor,f=y.parameterMacro,d=y.modelPropertyMacro,h={pathDiscriminator:r,baseDoc:i,requestInterceptor:s,responseInterceptor:p,parameterMacro:f,modelPropertyMacro:d},m=(0,c.normalizeSwagger)({spec:t}),v=m.spec,e.next=5,(0,l.default)((0,a.default)({},h,{spec:v,allowMetaPatches:!0,skipNormalization:!0}));case 5:return g=e.sent,!n&&Array.isArray(r)&&r.length&&(g.spec=(0,u.default)(g.spec,r)||null),e.abrupt("return",g);case 8:case"end":return e.stop()}},e,this)}));return function e(r,n){return t.apply(this,arguments)}}(),t.exports=r.default},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return function(t){var r=t.pathName,n=t.method,i=t.operationId;return function(t){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.execute((0,s.default)({spec:e.spec},(0,l.default)(e,"requestInterceptor","responseInterceptor","userFetch"),{pathName:r,method:n,parameters:t,operationId:i},o))}}}function i(e){var t=e.spec,r=e.cb,n=void 0===r?p:r,i=e.defaultTag,o=void 0===i?"default":i,a=e.v2OperationIdCompatibilityMode,s={},u={};return(0,c.eachOperation)(t,function(e){var r=e.pathName,i=e.method,l=e.operation;(l.tags?f(l.tags):[o]).forEach(function(e){if("string"==typeof e){var o=u[e]=u[e]||{},p=(0,c.opId)(l,r,i,{v2OperationIdCompatibilityMode:a}),f=n({spec:t,pathName:r,method:i,operation:l,operationId:p});if(s[p])s[p]++,o[""+p+s[p]]=f;else if(void 0!==o[p]){var d=s[p]||1;s[p]=d+1,o[""+p+s[p]]=f;var h=o[p];delete o[p],o[""+p+d]=h}else o[p]=f}})}),u}Object.defineProperty(t,"__esModule",{value:!0}),t.self=void 0;var s=n(r(3));t.makeExecute=a,t.makeApisTagOperationsOperationExecute=function u(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=d.makeExecute(e),r=d.mapTagOperations({v2OperationIdCompatibilityMode:e.v2OperationIdCompatibilityMode,spec:e.spec,cb:t}),n={};for(var i in r)for(var o in n[i]={operations:{}},r[i])n[i].operations[o]={execute:r[i][o]};return{apis:n}},t.makeApisTagOperation=function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=d.makeExecute(e);return{apis:d.mapTagOperations({v2OperationIdCompatibilityMode:e.v2OperationIdCompatibilityMode,spec:e.spec,cb:t})}},t.mapTagOperations=i;var l=n(r(51)),c=r(4),p=function(){return null},f=function(e){return Array.isArray(e)?e:[e]},d=t.self={mapTagOperations:i,makeExecute:a}},function(e,t){e.exports=r(982)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function u(e){var t=e.spec,r=e.operationId,n=(e.securities,e.requestContentType,e.responseContentType),i=e.scheme,a=e.requestInterceptor,s=e.responseInterceptor,u=e.contextUrl,l=e.userFetch,c=(e.requestBody,e.server),p=e.serverVariables,d=e.http,m=e.parameters,v=e.parameterBuilders,g=(0,A.isOAS3)(t);v||(v=g?C.default:E.default);var y={url:"",credentials:d&&d.withCredentials?"include":"same-origin",headers:{},cookies:{}};a&&(y.requestInterceptor=a),s&&(y.responseInterceptor=s),l&&(y.userFetch=l);var _=(0,A.getOperationRaw)(t,r);if(!_)throw new M("Operation "+r+" not found");var k=_.operation,x=void 0===k?{}:k,P=_.method,I=_.pathName;if(y.url+=o({spec:t,scheme:i,contextUrl:u,server:c,serverVariables:p,pathName:I,method:P}),!r)return delete y.cookies,y;y.url+=I,y.method=(""+P).toUpperCase(),m=m||{};var q=t.paths[I]||{};n&&(y.headers.accept=n);var F=O([].concat(R(x.parameters)).concat(R(q.parameters)));F.forEach(function(e){var r=v[e.in],n=void 0;if("body"===e.in&&e.schema&&e.schema.properties&&(n=m),void 0===(n=e&&e.name&&m[e.name])?n=e&&e.name&&m[e.in+"."+e.name]:T(e.name,F).length>1&&console.warn("Parameter '"+e.name+"' is ambiguous because the defined spec has more than one parameter with the name: '"+e.name+"' and the passed-in parameter values did not define an 'in' value."),void 0!==e.default&&void 0===n&&(n=e.default),void 0===n&&e.required&&!e.allowEmptyValue)throw new Error("Required parameter "+e.name+" is not provided");if(g&&e.schema&&"object"===e.schema.type&&"string"==typeof n)try{n=JSON.parse(n)}catch(e){throw new Error("Could not parse object parameter value string as JSON")}r&&r({req:y,parameter:e,value:n,operation:x,spec:t})});var B=(0,f.default)({},e,{operation:x});if((y=g?(0,w.default)(B,y):(0,D.default)(B,y)).cookies&&(0,h.default)(y.cookies).length){var N=(0,h.default)(y.cookies).reduce(function(e,t){var r=y.cookies[t];return e+(e?"&":"")+b.default.serialize(t,r)},"");y.headers.Cookie=N}return y.cookies&&delete y.cookies,(0,S.mergeInQueryOrForm)(y),y}function o(e){return(0,A.isOAS3)(e.spec)?function i(e){var t=e.spec,r=e.pathName,n=e.method,i=e.server,o=e.contextUrl,a=e.serverVariables,u=void 0===a?{}:a,c=(0,v.default)(t,["paths",r,(n||"").toLowerCase(),"servers"])||(0,v.default)(t,["paths",r,"servers"])||(0,v.default)(t,["servers"]),p="",f=null;if(i&&c&&c.length){var d=c.map(function(e){return e.url});d.indexOf(i)>-1&&(p=i,f=c[d.indexOf(i)])}!p&&c&&c.length&&(p=c[0].url,f=c[0]),p.indexOf("{")>-1&&function l(e){for(var t=[],r=/{([^}]+)}/g,n=void 0;n=r.exec(e);)t.push(n[1]);return t}(p).forEach(function(e){if(f.variables&&f.variables[e]){var t=f.variables[e],r=u[e]||t.default,n=new RegExp("{"+e+"}","g");p=p.replace(n,r)}});return function s(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=_.default.parse(e),n=_.default.parse(t),i=I(r.protocol)||I(n.protocol)||"",o=r.host||n.host,a=r.pathname||"",s=void 0;return"/"===(s=i&&o?i+"://"+(o+a):a)[s.length-1]?s.slice(0,-1):s}(p,o)}(e):function c(e){var t=e.spec,r=e.scheme,n=e.contextUrl,i=void 0===n?"":n,o=_.default.parse(i),a=Array.isArray(t.schemes)?t.schemes[0]:null,s=r||a||I(o.protocol)||"http",u=t.host||o.host||"",l=t.basePath||"",c=void 0;return"/"===(c=s&&u?s+"://"+(u+l):l)[c.length-1]?c.slice(0,-1):c}(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.self=void 0;var p=n(r(7)),f=n(r(3)),d=n(r(53)),h=n(r(0)),m=n(r(2));t.execute=function a(e){var t=e.http,r=e.fetch,n=e.spec,i=e.operationId,o=e.pathName,a=e.method,s=e.parameters,u=e.securities,l=(0,d.default)(e,["http","fetch","spec","operationId","pathName","method","parameters","securities"]),c=t||r||k.default;o&&a&&!i&&(i=(0,A.legacyIdFromPathMethod)(o,a));var h=P.buildRequest((0,f.default)({spec:n,operationId:i,parameters:s,securities:u,http:c},l));return h.body&&((0,g.default)(h.body)||(0,y.default)(h.body))&&(h.body=(0,p.default)(h.body)),c(h)},t.buildRequest=u,t.baseUrl=o;var v=n((n(r(5)),r(12))),g=n(r(54)),y=n(r(55)),_=n((n(r(13)),r(10))),b=n(r(56)),S=r(6),k=n(S),x=n(r(21)),E=n(r(57)),C=n(r(58)),w=n(r(63)),D=n(r(65)),A=r(4),R=function(e){return Array.isArray(e)?e:[]},M=(0,x.default)("OperationNotFoundError",function(e,t,r){this.originalError=r,(0,m.default)(this,t||{})}),T=function(e,t){return t.filter(function(t){return t.name===e})},O=function(e){var t={};e.forEach(function(e){t[e.in]||(t[e.in]={}),t[e.in][e.name]=e});var r=[];return(0,h.default)(t).forEach(function(e){(0,h.default)(t[e]).forEach(function(n){r.push(t[e][n])})}),r},P=t.self={buildRequest:u},I=function(e){return e?e.replace(/\W/g,""):null}},function(e,t){e.exports=r(87)},function(e,t){e.exports=r(238)},function(e,t){e.exports=r(21)},function(e,t){e.exports=r(985)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={body:function n(e){var t=e.req,r=e.value;t.body=r},header:function u(e){var t=e.req,r=e.parameter,n=e.value;t.headers=t.headers||{},void 0!==n&&(t.headers[r.name]=n)},query:function i(e){var t=e.req,r=e.value,n=e.parameter;if(t.query=t.query||{},!1===r&&"boolean"===n.type&&(r="false"),0===r&&["number","integer"].indexOf(n.type)>-1&&(r="0"),r)t.query[n.name]={collectionFormat:n.collectionFormat,value:r};else if(n.allowEmptyValue&&void 0!==r){var i=n.name;t.query[i]=t.query[i]||{},t.query[i].allowEmptyValue=!0}},path:function o(e){var t=e.req,r=e.value,n=e.parameter;t.url=t.url.replace("{"+n.name+"}",encodeURIComponent(r))},formData:function a(e){var t=e.req,r=e.value,n=e.parameter;(r||n.allowEmptyValue)&&(t.form=t.form||{},t.form[n.name]={value:r,allowEmptyValue:n.allowEmptyValue,collectionFormat:n.collectionFormat})}},e.exports=t.default},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var s=n(r(0)),l=n(r(1)),c=n(r(59));t.default={path:function a(e){var t=e.req,r=e.value,n=e.parameter,i=n.name,o=n.style,a=n.explode,s=(0,c.default)({key:n.name,value:r,style:o||"simple",explode:a||!1,escape:!1});t.url=t.url.replace("{"+i+"}",s)},query:function u(e){var t=e.req,r=e.value,n=e.parameter;if(t.query=t.query||{},!1===r&&(r="false"),0===r&&(r="0"),r){var i=void 0===r?"undefined":(0,l.default)(r);"deepObject"===n.style?(0,s.default)(r).forEach(function(e){var i=r[e];t.query[n.name+"["+e+"]"]={value:(0,c.default)({key:e,value:i,style:"deepObject",escape:n.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}}):"object"!==i||Array.isArray(r)||"form"!==n.style&&n.style||!n.explode&&void 0!==n.explode?t.query[n.name]={value:(0,c.default)({key:n.name,value:r,style:n.style||"form",explode:void 0===n.explode||n.explode,escape:n.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}:(0,s.default)(r).forEach(function(e){var i=r[e];t.query[e]={value:(0,c.default)({key:e,value:i,style:n.style||"form",escape:n.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}})}else if(n.allowEmptyValue&&void 0!==r){var o=n.name;t.query[o]=t.query[o]||{},t.query[o].allowEmptyValue=!0}},header:function o(e){var t=e.req,r=e.parameter,n=e.value;t.headers=t.headers||{},p.indexOf(r.name.toLowerCase())>-1||void 0!==n&&(t.headers[r.name]=(0,c.default)({key:r.name,value:n,style:r.style||"simple",explode:void 0!==r.explode&&r.explode,escape:!1}))},cookie:function i(e){var t=e.req,r=e.parameter,n=e.value;t.headers=t.headers||{};var i=void 0===n?"undefined":(0,l.default)(n);if("undefined"!==i){var o="object"===i&&!Array.isArray(n)&&r.explode?"":r.name+"=";t.headers.Cookie=o+(0,c.default)({key:r.name,value:n,escape:!1,style:r.style||"form",explode:void 0!==r.explode&&r.explode})}}};var p=["accept","authorization","content-type"];e.exports=t.default},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).escape,r=arguments[2];return"number"==typeof e&&(e=e.toString()),"string"==typeof e&&e.length&&t?r?JSON.parse(e):(0,p.stringToCharArray)(e).map(function(e){return d(e)?e:f(e)&&"unsafe"===t?e:((0,c.default)(e)||[]).map(function(e){return e.toString(16).toUpperCase()}).map(function(e){return"%"+e}).join("")}).join(""):e}Object.defineProperty(t,"__esModule",{value:!0});var s=n(r(0)),l=n(r(1));t.encodeDisallowedCharacters=a,t.default=function(e){var t=e.value;return Array.isArray(t)?function u(e){var t=e.key,r=e.value,n=e.style,i=e.explode,o=e.escape,s=function(e){return a(e,{escape:o})};if("simple"===n)return r.map(function(e){return s(e)}).join(",");if("label"===n)return"."+r.map(function(e){return s(e)}).join(".");if("matrix"===n)return r.map(function(e){return s(e)}).reduce(function(e,r){return!e||i?(e||"")+";"+t+"="+r:e+","+r},"");if("form"===n){var u=i?"&"+t+"=":",";return r.map(function(e){return s(e)}).join(u)}if("spaceDelimited"===n){var l=i?t+"=":"";return r.map(function(e){return s(e)}).join(" "+l)}if("pipeDelimited"===n){var c=i?t+"=":"";return r.map(function(e){return s(e)}).join("|"+c)}}(e):"object"===(void 0===t?"undefined":(0,l.default)(t))?function o(e){var t=e.key,r=e.value,n=e.style,i=e.explode,o=e.escape,u=function(e){return a(e,{escape:o})},l=(0,s.default)(r);return"simple"===n?l.reduce(function(e,t){var n=u(r[t]);return(e?e+",":"")+t+(i?"=":",")+n},""):"label"===n?l.reduce(function(e,t){var n=u(r[t]);return(e?e+".":".")+t+(i?"=":".")+n},""):"matrix"===n&&i?l.reduce(function(e,t){var n=u(r[t]);return(e?e+";":";")+t+"="+n},""):"matrix"===n?l.reduce(function(e,n){var i=u(r[n]);return(e?e+",":";"+t+"=")+n+","+i},""):"form"===n?l.reduce(function(e,t){var n=u(r[t]);return(e?e+(i?"&":","):"")+t+(i?"=":",")+n},""):void 0}(e):function i(e){var t=e.key,r=e.value,n=e.style,i=e.escape,o=function(e){return a(e,{escape:i})};return"simple"===n?o(r):"label"===n?"."+o(r):"matrix"===n?";"+t+"="+o(r):"form"===n?o(r):"deepObject"===n?o(r):void 0}(e)};var c=n((n(r(60)),r(61))),p=r(62),f=function(e){return":/?#[]@!$&'()*+,;=".indexOf(e)>-1},d=function(e){return/^[a-z0-9\-._~]+$/i.test(e)}},function(e,t){e.exports=r(986)},function(e,t){e.exports=r(987)},function(e,t){e.exports=r(988)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=e.request,r=e.securities,n=void 0===r?{}:r,i=e.operation,o=void 0===i?{}:i,a=e.spec,p=(0,u.default)({},t),f=n.authorized,d=void 0===f?{}:f,h=o.security||a.security||[],m=d&&!!(0,s.default)(d).length,v=(0,l.default)(a,["components","securitySchemes"])||{};return p.headers=p.headers||{},p.query=p.query||{},(0,s.default)(n).length&&m&&h&&(!Array.isArray(o.security)||o.security.length)?(h.forEach(function(e,t){for(var r in e){var n=d[r],i=v[r];if(n){var o=n.value||n,a=i.type;if(n)if("apiKey"===a)"query"===i.in&&(p.query[i.name]=o),"header"===i.in&&(p.headers[i.name]=o),"cookie"===i.in&&(p.cookies[i.name]=o);else if("http"===a){if("basic"===i.scheme){var s=o.username,u=o.password,l=(0,c.default)(s+":"+u);p.headers.Authorization="Basic "+l}"bearer"===i.scheme&&(p.headers.Authorization="Bearer "+o)}else if("oauth2"===a){var f=n.token||{},h=f.access_token,m=f.token_type;m&&"bearer"!==m.toLowerCase()||(m="Bearer"),p.headers.Authorization=m+" "+h}}}}),p):t}Object.defineProperty(t,"__esModule",{value:!0});var i=n(r(7)),o=n(r(1)),s=n(r(0));t.default=function(e,t){var r=e.operation,n=e.requestBody,u=e.securities,l=e.spec,c=e.attachContentTypeForEmptyPayload,f=e.requestContentType;t=a({request:t,securities:u,operation:r,spec:l});var d=r.requestBody||{},h=(0,s.default)(d.content||{}),m=f&&h.indexOf(f)>-1;if(n||c){if(f&&m)t.headers["Content-Type"]=f;else if(!f){var v=h[0];v&&(t.headers["Content-Type"]=v,f=v)}}else f&&m&&(t.headers["Content-Type"]=f);return n&&(f?h.indexOf(f)>-1&&("application/x-www-form-urlencoded"===f||0===f.indexOf("multipart/")?"object"===(void 0===n?"undefined":(0,o.default)(n))?(t.form={},(0,s.default)(n).forEach(function(e){var r,a=n[e],s=void 0;"undefined"!=typeof File&&(s=a instanceof File),"undefined"!=typeof Blob&&(s=s||a instanceof Blob),void 0!==p.Buffer&&(s=s||p.Buffer.isBuffer(a)),r="object"!==(void 0===a?"undefined":(0,o.default)(a))||s?a:Array.isArray(a)?a.toString():(0,i.default)(a),t.form[e]={value:r}})):t.form=n:t.body=n):t.body=n),t},t.applySecurities=a;var u=n(r(5)),l=n(r(12)),c=n(r(13)),p=r(64)},function(e,t){e.exports=r(48)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=e.request,r=e.securities,n=void 0===r?{}:r,a=e.operation,u=void 0===a?{}:a,l=e.spec,c=(0,s.default)({},t),p=n.authorized,f=void 0===p?{}:p,d=n.specSecurity,h=void 0===d?[]:d,m=u.security||h,v=f&&!!(0,i.default)(f).length,g=l.securityDefinitions;return c.headers=c.headers||{},c.query=c.query||{},(0,i.default)(n).length&&v&&m&&(!Array.isArray(u.security)||u.security.length)?(m.forEach(function(e,t){for(var r in e){var n=f[r];if(n){var i=n.token,a=n.value||n,s=g[r],u=s.type,l=i&&i.access_token,p=i&&i.token_type;if(n)if("apiKey"===u){var d="query"===s.in?"query":"headers";c[d]=c[d]||{},c[d][s.name]=a}else"basic"===u?a.header?c.headers.authorization=a.header:(a.base64=(0,o.default)(a.username+":"+a.password),c.headers.authorization="Basic "+a.base64):"oauth2"===u&&l&&(p=p&&"bearer"!==p.toLowerCase()?p:"Bearer",c.headers.authorization=p+" "+l)}}}),c):t}Object.defineProperty(t,"__esModule",{value:!0});var i=n(r(0));t.default=function(e,t){var r=e.spec,n=e.operation,i=e.securities,o=e.requestContentType,s=e.attachContentTypeForEmptyPayload;if((t=a({request:t,securities:i,operation:n,spec:r})).body||t.form||s)o?t.headers["Content-Type"]=o:Array.isArray(n.consumes)?t.headers["Content-Type"]=n.consumes[0]:Array.isArray(r.consumes)?t.headers["Content-Type"]=r.consumes[0]:n.parameters&&n.parameters.filter(function(e){return"file"===e.type}).length?t.headers["Content-Type"]="multipart/form-data":n.parameters&&n.parameters.filter(function(e){return"formData"===e.in}).length&&(t.headers["Content-Type"]="application/x-www-form-urlencoded");else if(o){var u=n.parameters&&n.parameters.filter(function(e){return"body"===e.in}).length>0,l=n.parameters&&n.parameters.filter(function(e){return"formData"===e.in}).length>0;(u||l)&&(t.headers["Content-Type"]=o)}return t},t.applySecurities=a;var o=n(r(13)),s=n(r(5));n(r(6))}])},function(e,t,r){"use strict";var n=Object.prototype.hasOwnProperty,i=function(){for(var e=[],t=0;t<256;++t)e.push("%"+((t<16?"0":"")+t.toString(16)).toUpperCase());return e}();t.arrayToObject=function arrayToObject(e,t){for(var r=t&&t.plainObjects?Object.create(null):{},n=0;n=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122?r+=t.charAt(n):o<128?r+=i[o]:o<2048?r+=i[192|o>>6]+i[128|63&o]:o<55296||o>=57344?r+=i[224|o>>12]+i[128|o>>6&63]+i[128|63&o]:(n+=1,o=65536+((1023&o)<<10|1023&t.charCodeAt(n)),r+=i[240|o>>18]+i[128|o>>12&63]+i[128|o>>6&63]+i[128|63&o])}return r},t.compact=function compact(e){for(var t=[{obj:{o:e},prop:"o"}],r=[],n=0;n=0;s--)if(l[s]!=c[s])return!1;for(s=l.length-1;s>=0;s--)if(u=l[s],!a(e[u],t[u],r))return!1;return typeof e==typeof t}(e,t,r))};function isUndefinedOrNull(e){return null===e||void 0===e}function isBuffer(e){return!(!e||"object"!=typeof e||"number"!=typeof e.length)&&("function"==typeof e.copy&&"function"==typeof e.slice&&!(e.length>0&&"number"!=typeof e[0]))}},function(e,t,r){var n={strict:!0},i=r(422),o=function(e,t){return i(e,t,n)},a=r(243);t.JsonPatchError=a.PatchError,t.deepClone=a._deepClone;var s={add:function(e,t,r){return e[t]=this.value,{newDocument:r}},remove:function(e,t,r){var n=e[t];return delete e[t],{newDocument:r,removed:n}},replace:function(e,t,r){var n=e[t];return e[t]=this.value,{newDocument:r,removed:n}},move:function(e,t,r){var n=getValueByPointer(r,this.path);n&&(n=a._deepClone(n));var i=applyOperation(r,{op:"remove",path:this.from}).removed;return applyOperation(r,{op:"add",path:this.path,value:i}),{newDocument:r,removed:n}},copy:function(e,t,r){var n=getValueByPointer(r,this.from);return applyOperation(r,{op:"add",path:this.path,value:a._deepClone(n)}),{newDocument:r}},test:function(e,t,r){return{newDocument:r,test:o(e[t],this.value)}},_get:function(e,t,r){return this.value=e[t],{newDocument:r}}},u={add:function(e,t,r){return a.isInteger(t)?e.splice(t,0,this.value):e[t]=this.value,{newDocument:r,index:t}},remove:function(e,t,r){return{newDocument:r,removed:e.splice(t,1)[0]}},replace:function(e,t,r){var n=e[t];return e[t]=this.value,{newDocument:r,removed:n}},move:s.move,copy:s.copy,test:s.test,_get:s._get};function getValueByPointer(e,t){if(""==t)return e;var r={op:"_get",path:t};return applyOperation(e,r),r.value}function applyOperation(e,r,n,i){if(void 0===n&&(n=!1),void 0===i&&(i=!0),n&&("function"==typeof n?n(r,0,e,r.path):validator(r,0)),""===r.path){var l={newDocument:e};if("add"===r.op)return l.newDocument=r.value,l;if("replace"===r.op)return l.newDocument=r.value,l.removed=e,l;if("move"===r.op||"copy"===r.op)return l.newDocument=getValueByPointer(e,r.from),"move"===r.op&&(l.removed=e),l;if("test"===r.op){if(l.test=o(e,r.value),!1===l.test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",0,r,e);return l.newDocument=e,l}if("remove"===r.op)return l.removed=e,l.newDocument=null,l;if("_get"===r.op)return r.value=e,l;if(n)throw new t.JsonPatchError("Operation `op` property is not one of operations defined in RFC-6902","OPERATION_OP_INVALID",0,r,e);return l}i||(e=a._deepClone(e));var c=(r.path||"").split("/"),p=e,f=1,d=c.length,h=void 0,m=void 0,v=void 0;for(v="function"==typeof n?n:validator;;){if(m=c[f],n&&void 0===h&&(void 0===p[m]?h=c.slice(0,f).join("/"):f==d-1&&(h=r.path),void 0!==h&&v(r,0,e,h)),f++,Array.isArray(p)){if("-"===m)m=p.length;else{if(n&&!a.isInteger(m))throw new t.JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",0,r.path,r);a.isInteger(m)&&(m=~~m)}if(f>=d){if(n&&"add"===r.op&&m>p.length)throw new t.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",0,r.path,r);if(!1===(l=u[r.op].call(r,p,m,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",0,r,e);return l}}else if(m&&-1!=m.indexOf("~")&&(m=a.unescapePathComponent(m)),f>=d){if(!1===(l=s[r.op].call(r,p,m,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",0,r,e);return l}p=p[m]}}function applyPatch(e,r,n,i){if(void 0===i&&(i=!0),n&&!Array.isArray(r))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");i||(e=a._deepClone(e));for(var o=new Array(r.length),s=0,u=r.length;s0)throw new t.JsonPatchError('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",r,e,n);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new t.JsonPatchError("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",r,e,n);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",r,e,n);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&a.hasUndefined(e.value))throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",r,e,n);if(n)if("add"==e.op){var o=e.path.split("/").length,u=i.split("/").length;if(o!==u+1&&o!==u)throw new t.JsonPatchError("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",r,e,n)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==i)throw new t.JsonPatchError("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",r,e,n)}else if("move"===e.op||"copy"===e.op){var l=validate([{op:"_get",path:e.from,value:void 0}],n);if(l&&"OPERATION_PATH_UNRESOLVABLE"===l.name)throw new t.JsonPatchError("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",r,e,n)}}function validate(e,r,n){try{if(!Array.isArray(e))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(r)applyPatch(a._deepClone(r),a._deepClone(e),n||!0);else{n=n||validator;for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:(0,a.List)();return function(e){return(e.authSelectors.definitionsToAuthorize()||(0,a.List)()).filter(function(e){return t.some(function(t){return t.get(e.keySeq().first())})})}},t.authorized=(0,o.createSelector)(s,function(e){return e.get("authorized")||(0,a.Map)()}),t.isAuthorized=function isAuthorized(e,t){return function(e){var r=e.authSelectors.authorized();return a.List.isList(t)?!!t.toJS().filter(function(e){return-1===(0,n.default)(e).map(function(e){return!!r.get(e)}).indexOf(!1)}).length:null}},t.getConfigs=(0,o.createSelector)(s,function(e){return e.get("configs")})},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.execute=void 0;var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(26));t.execute=function execute(e,t){var r=t.authSelectors,i=t.specSelectors;return function(t){var o=t.path,a=t.method,s=t.operation,u=t.extras,l={authorized:r.authorized()&&r.authorized().toJS(),definitions:i.securityDefinitions()&&i.securityDefinitions().toJS(),specSecurity:i.security()&&i.security().toJS()};return e((0,n.default)({path:o,method:a,operation:s,securities:l},u))}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){return{fn:{shallowEqualKeys:n.shallowEqualKeys}}};var n=r(10)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function SplitPaneModePlugin(){return{components:{SplitPaneMode:n.default}}};var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(431))},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(4)),i=_interopRequireDefault(r(2)),o=_interopRequireDefault(r(3)),a=_interopRequireDefault(r(5)),s=_interopRequireDefault(r(6)),u=_interopRequireDefault(r(0)),l=_interopRequireDefault(r(1)),c=_interopRequireDefault(r(989));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var p=["split-pane-mode"],f="left",d="right",h="both",m=function(e){function SplitPaneMode(){var e,t,r,o;(0,i.default)(this,SplitPaneMode);for(var s=arguments.length,u=Array(s),l=0;l=400)return a.updateLoadingStatus("failed"),n.newThrownErr((0,i.default)(new Error((t.message||t.statusText)+" "+e),{source:"fetch"})),void(!t.status&&t instanceof Error&&function checkPossibleFailReasons(){try{var t=void 0;if("URL"in s.default?t=new URL(e):(t=document.createElement("a")).href=e,"https:"!==t.protocol&&"https:"===s.default.location.protocol){var r=(0,i.default)(new Error("Possible mixed-content issue? The page was loaded over https:// but a "+t.protocol+"// URL was specified. Check that you are not attempting to load mixed content."),{source:"fetch"});return void n.newThrownErr(r)}if(t.origin!==s.default.location.origin){var o=(0,i.default)(new Error("Possible cross-origin (CORS) issue? The URL origin ("+t.origin+") does not match the page ("+s.default.location.origin+"). Check the server returns the correct 'Access-Control-Allow-*' headers."),{source:"fetch"});n.newThrownErr(o)}}catch(e){return}}());a.updateLoadingStatus("success"),a.updateSpec(t.text),o.url()!==e&&a.updateUrl(e)}e=e||o.url(),a.updateLoadingStatus("loading"),n.clear({source:"fetch"}),l({url:e,loadSpec:!0,requestInterceptor:c.requestInterceptor||function(e){return e},responseInterceptor:c.responseInterceptor||function(e){return e},credentials:"same-origin",headers:{Accept:"application/json,*/*"}}).then(next,next)}},updateLoadingStatus:function updateLoadingStatus(e){var t=[null,"loading","failed","success","failedConfig"];return-1===t.indexOf(e)&&console.error("Error: "+e+" is not one of "+(0,n.default)(t)),{type:"spec_update_loading_status",payload:e}}},u={loadingStatus:(0,o.createSelector)(function(e){return e||(0,a.Map)()},function(e){return e.get("loadingStatus")||null})};return{statePlugins:{spec:{actions:r,reducers:{spec_update_loading_status:function spec_update_loading_status(e,t){return"string"==typeof t.payload?e.set("loadingStatus",t.payload):e}},selectors:u}}}};var o=r(59),a=r(7),s=_interopRequireDefault(r(32));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function configsPlugin(){return{statePlugins:{spec:{actions:a,selectors:l},configs:{reducers:u.default,actions:o,selectors:s}}}};var n=_interopRequireDefault(r(1025)),i=r(249),o=_interopRequireWildcard(r(250)),a=_interopRequireWildcard(r(438)),s=_interopRequireWildcard(r(439)),u=_interopRequireDefault(r(440));function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var l={getLocalConfig:function getLocalConfig(){return(0,i.parseYamlConfig)(n.default)}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getConfigByUrl=t.downloadConfig=void 0;var n=r(249);t.downloadConfig=function downloadConfig(e){return function(t){return(0,t.fn.fetch)(e)}},t.getConfigByUrl=function getConfigByUrl(e,t){return function(r){var i=r.specActions;if(e)return i.downloadConfig(e).then(next,next);function next(r){r instanceof Error||r.status>=400?(i.updateLoadingStatus("failedConfig"),i.updateLoadingStatus("failedConfig"),i.updateUrl(""),console.error(r.statusText+" "+e.url),t(null)):t((0,n.parseYamlConfig)(r.text))}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.get=function get(e,t){return e.getIn(Array.isArray(t)?t:[t])}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n,i=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(24)),o=r(7),a=r(250);t.default=(n={},(0,i.default)(n,a.UPDATE_CONFIGS,function(e,t){return e.merge((0,o.fromJS)(t.payload))}),(0,i.default)(n,a.TOGGLE_CONFIGS,function(e,t){var r=t.payload,n=e.get(r);return e.set(r,!n)}),n)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){return[n.default,{statePlugins:{configs:{wrapActions:{loaded:function loaded(e,t){return function(){e.apply(void 0,arguments);var r=window.location.hash;t.layoutActions.parseDeepLinkHash(r)}}}}},wrapComponents:{operation:i.default,OperationTag:o.default}}]};var n=_interopRequireDefault(r(442)),i=_interopRequireDefault(r(444)),o=_interopRequireDefault(r(445));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.clearScrollTo=t.scrollToElement=t.readyToScroll=t.parseDeepLinkHash=t.scrollTo=t.show=void 0;var n,i=_interopRequireDefault(r(24)),o=_interopRequireDefault(r(19)),a=r(443),s=_interopRequireDefault(r(1026)),u=r(7),l=_interopRequireDefault(u);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var c=t.show=function show(e,t){var r=t.getConfigs,n=t.layoutSelectors;return function(){for(var t=arguments.length,i=Array(t),s=0;s-1?t:e.content.clientHeight},this.getWrapperStyle=function(t){if(e.state.currentState===u&&e.state.to){var r=e.props.fixedHeight;return r>-1?{overflow:"hidden",height:r}:{height:"auto"}}return"WAITING"!==e.state.currentState||e.state.to?{overflow:"hidden",height:Math.max(0,t)}:{overflow:"hidden",height:0}},this.getMotionProps=function(){var t=e.props.springConfig;return e.state.currentState===u?{defaultStyle:{height:e.state.to},style:{height:e.state.to}}:{defaultStyle:{height:e.state.from},style:{height:(0,s.spring)(e.state.to,n({precision:1},t))}}},this.renderContent=function(t){var r=t.height,i=e.props,a=(i.isOpened,i.springConfig,i.forceInitialAnimation,i.hasNestedCollapse,i.fixedHeight,i.theme),s=i.style,u=i.onRender,l=(i.onRest,i.onMeasure,i.children),c=function _objectWithoutProperties(e,t){var r={};for(var n in e)t.indexOf(n)>=0||Object.prototype.hasOwnProperty.call(e,n)&&(r[n]=e[n]);return r}(i,["isOpened","springConfig","forceInitialAnimation","hasNestedCollapse","fixedHeight","theme","style","onRender","onRest","onMeasure","children"]),p=e.state;return u({current:r,from:p.from,to:p.to}),o.default.createElement("div",n({ref:e.onWrapperRef,className:a.collapse,style:n({},e.getWrapperStyle(Math.max(0,r)),s)},c),o.default.createElement("div",{ref:e.onContentRef,className:a.content},l))}}},function(e,t,r){"use strict";t.__esModule=!0,t.default={noWobble:{stiffness:170,damping:26},gentle:{stiffness:120,damping:14},wobbly:{stiffness:180,damping:12},stiff:{stiffness:210,damping:20}},e.exports=t.default},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Collapse=t.Link=t.Select=t.Input=t.TextArea=t.Button=t.Row=t.Col=t.Container=void 0;var n=_interopRequireDefault(r(26)),i=_interopRequireDefault(r(87)),o=_interopRequireDefault(r(4)),a=_interopRequireDefault(r(2)),s=_interopRequireDefault(r(3)),u=_interopRequireDefault(r(5)),l=_interopRequireDefault(r(6)),c=_interopRequireDefault(r(0)),p=_interopRequireDefault(r(1)),f=r(450);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}function xclass(){for(var e=arguments.length,t=Array(e),r=0;r",Gt:"≫",gt:">",gtcc:"⪧",gtcir:"⩺",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",HARDcy:"Ъ",hardcy:"ъ",hArr:"⇔",harr:"↔",harrcir:"⥈",harrw:"↭",Hat:"^",hbar:"ℏ",Hcirc:"Ĥ",hcirc:"ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",Hfr:"ℌ",hfr:"𝔥",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",Hopf:"ℍ",hopf:"𝕙",horbar:"―",HorizontalLine:"─",Hscr:"ℋ",hscr:"𝒽",hslash:"ℏ",Hstrok:"Ħ",hstrok:"ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",Iacute:"Í",iacute:"í",ic:"⁣",Icirc:"Î",icirc:"î",Icy:"И",icy:"и",Idot:"İ",IEcy:"Е",iecy:"е",iexcl:"¡",iff:"⇔",Ifr:"ℑ",ifr:"𝔦",Igrave:"Ì",igrave:"ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",IJlig:"IJ",ijlig:"ij",Im:"ℑ",Imacr:"Ī",imacr:"ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",imof:"⊷",imped:"Ƶ",Implies:"⇒",in:"∈",incare:"℅",infin:"∞",infintie:"⧝",inodot:"ı",Int:"∬",int:"∫",intcal:"⊺",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"⁣",InvisibleTimes:"⁢",IOcy:"Ё",iocy:"ё",Iogon:"Į",iogon:"į",Iopf:"𝕀",iopf:"𝕚",Iota:"Ι",iota:"ι",iprod:"⨼",iquest:"¿",Iscr:"ℐ",iscr:"𝒾",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"⁢",Itilde:"Ĩ",itilde:"ĩ",Iukcy:"І",iukcy:"і",Iuml:"Ï",iuml:"ï",Jcirc:"Ĵ",jcirc:"ĵ",Jcy:"Й",jcy:"й",Jfr:"𝔍",jfr:"𝔧",jmath:"ȷ",Jopf:"𝕁",jopf:"𝕛",Jscr:"𝒥",jscr:"𝒿",Jsercy:"Ј",jsercy:"ј",Jukcy:"Є",jukcy:"є",Kappa:"Κ",kappa:"κ",kappav:"ϰ",Kcedil:"Ķ",kcedil:"ķ",Kcy:"К",kcy:"к",Kfr:"𝔎",kfr:"𝔨",kgreen:"ĸ",KHcy:"Х",khcy:"х",KJcy:"Ќ",kjcy:"ќ",Kopf:"𝕂",kopf:"𝕜",Kscr:"𝒦",kscr:"𝓀",lAarr:"⇚",Lacute:"Ĺ",lacute:"ĺ",laemptyv:"⦴",lagran:"ℒ",Lambda:"Λ",lambda:"λ",Lang:"⟪",lang:"⟨",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",Larr:"↞",lArr:"⇐",larr:"←",larrb:"⇤",larrbfs:"⤟",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",lat:"⪫",lAtail:"⤛",latail:"⤙",late:"⪭",lates:"⪭︀",lBarr:"⤎",lbarr:"⤌",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",Lcaron:"Ľ",lcaron:"ľ",Lcedil:"Ļ",lcedil:"ļ",lceil:"⌈",lcub:"{",Lcy:"Л",lcy:"л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",lE:"≦",le:"≤",LeftAngleBracket:"⟨",LeftArrow:"←",Leftarrow:"⇐",leftarrow:"←",LeftArrowBar:"⇤",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVector:"⇃",LeftDownVectorBar:"⥙",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",LeftRightArrow:"↔",Leftrightarrow:"⇔",leftrightarrow:"↔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTee:"⊣",LeftTeeArrow:"↤",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangle:"⊲",LeftTriangleBar:"⧏",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVector:"↿",LeftUpVectorBar:"⥘",LeftVector:"↼",LeftVectorBar:"⥒",lEg:"⪋",leg:"⋚",leq:"≤",leqq:"≦",leqslant:"⩽",les:"⩽",lescc:"⪨",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",Lfr:"𝔏",lfr:"𝔩",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",LJcy:"Љ",ljcy:"љ",Ll:"⋘",ll:"≪",llarr:"⇇",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",Lmidot:"Ŀ",lmidot:"ŀ",lmoust:"⎰",lmoustache:"⎰",lnap:"⪉",lnapprox:"⪉",lnE:"≨",lne:"⪇",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",LongLeftArrow:"⟵",Longleftarrow:"⟸",longleftarrow:"⟵",LongLeftRightArrow:"⟷",Longleftrightarrow:"⟺",longleftrightarrow:"⟷",longmapsto:"⟼",LongRightArrow:"⟶",Longrightarrow:"⟹",longrightarrow:"⟶",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",Lopf:"𝕃",lopf:"𝕝",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"‎",lrtri:"⊿",lsaquo:"‹",Lscr:"ℒ",lscr:"𝓁",Lsh:"↰",lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",Lstrok:"Ł",lstrok:"ł",LT:"<",Lt:"≪",lt:"<",ltcc:"⪦",ltcir:"⩹",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",Map:"⤅",map:"↦",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",Mcy:"М",mcy:"м",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",Mfr:"𝔐",mfr:"𝔪",mho:"℧",micro:"µ",mid:"∣",midast:"*",midcir:"⫰",middot:"·",minus:"−",minusb:"⊟",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",Mopf:"𝕄",mopf:"𝕞",mp:"∓",Mscr:"ℳ",mscr:"𝓂",mstpos:"∾",Mu:"Μ",mu:"μ",multimap:"⊸",mumap:"⊸",nabla:"∇",Nacute:"Ń",nacute:"ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natur:"♮",natural:"♮",naturals:"ℕ",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",Ncaron:"Ň",ncaron:"ň",Ncedil:"Ņ",ncedil:"ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",Ncy:"Н",ncy:"н",ndash:"–",ne:"≠",nearhk:"⤤",neArr:"⇗",nearr:"↗",nearrow:"↗",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\n",nexist:"∄",nexists:"∄",Nfr:"𝔑",nfr:"𝔫",ngE:"≧̸",nge:"≱",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",nGt:"≫⃒",ngt:"≯",ngtr:"≯",nGtv:"≫̸",nhArr:"⇎",nharr:"↮",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",NJcy:"Њ",njcy:"њ",nlArr:"⇍",nlarr:"↚",nldr:"‥",nlE:"≦̸",nle:"≰",nLeftarrow:"⇍",nleftarrow:"↚",nLeftrightarrow:"⇎",nleftrightarrow:"↮",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nLt:"≪⃒",nlt:"≮",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"⁠",NonBreakingSpace:" ",Nopf:"ℕ",nopf:"𝕟",Not:"⫬",not:"¬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangle:"⋪",NotLeftTriangleBar:"⧏̸",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangle:"⋫",NotRightTriangleBar:"⧐̸",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",npar:"∦",nparallel:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",npre:"⪯̸",nprec:"⊀",npreceq:"⪯̸",nrArr:"⇏",nrarr:"↛",nrarrc:"⤳̸",nrarrw:"↝̸",nRightarrow:"⇏",nrightarrow:"↛",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",Nscr:"𝒩",nscr:"𝓃",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsubE:"⫅̸",nsube:"⊈",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupE:"⫆̸",nsupe:"⊉",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",Ntilde:"Ñ",ntilde:"ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",Nu:"Ν",nu:"ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nVDash:"⊯",nVdash:"⊮",nvDash:"⊭",nvdash:"⊬",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwArr:"⇖",nwarr:"↖",nwarrow:"↖",nwnear:"⤧",Oacute:"Ó",oacute:"ó",oast:"⊛",ocir:"⊚",Ocirc:"Ô",ocirc:"ô",Ocy:"О",ocy:"о",odash:"⊝",Odblac:"Ő",odblac:"ő",odiv:"⨸",odot:"⊙",odsold:"⦼",OElig:"Œ",oelig:"œ",ofcir:"⦿",Ofr:"𝔒",ofr:"𝔬",ogon:"˛",Ograve:"Ò",ograve:"ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",Omacr:"Ō",omacr:"ō",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",omid:"⦶",ominus:"⊖",Oopf:"𝕆",oopf:"𝕠",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",Or:"⩔",or:"∨",orarr:"↻",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",Oscr:"𝒪",oscr:"ℴ",Oslash:"Ø",oslash:"ø",osol:"⊘",Otilde:"Õ",otilde:"õ",Otimes:"⨷",otimes:"⊗",otimesas:"⨶",Ouml:"Ö",ouml:"ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",par:"∥",para:"¶",parallel:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",Pcy:"П",pcy:"п",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",Pfr:"𝔓",pfr:"𝔭",Phi:"Φ",phi:"φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",Pi:"Π",pi:"π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plus:"+",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",Popf:"ℙ",popf:"𝕡",pound:"£",Pr:"⪻",pr:"≺",prap:"⪷",prcue:"≼",prE:"⪳",pre:"⪯",prec:"≺",precapprox:"⪷",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",precsim:"≾",Prime:"″",prime:"′",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportion:"∷",Proportional:"∝",propto:"∝",prsim:"≾",prurel:"⊰",Pscr:"𝒫",pscr:"𝓅",Psi:"Ψ",psi:"ψ",puncsp:" ",Qfr:"𝔔",qfr:"𝔮",qint:"⨌",Qopf:"ℚ",qopf:"𝕢",qprime:"⁗",Qscr:"𝒬",qscr:"𝓆",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",QUOT:'"',quot:'"',rAarr:"⇛",race:"∽̱",Racute:"Ŕ",racute:"ŕ",radic:"√",raemptyv:"⦳",Rang:"⟫",rang:"⟩",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",Rarr:"↠",rArr:"⇒",rarr:"→",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",Rarrtl:"⤖",rarrtl:"↣",rarrw:"↝",rAtail:"⤜",ratail:"⤚",ratio:"∶",rationals:"ℚ",RBarr:"⤐",rBarr:"⤏",rbarr:"⤍",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",Rcaron:"Ř",rcaron:"ř",Rcedil:"Ŗ",rcedil:"ŗ",rceil:"⌉",rcub:"}",Rcy:"Р",rcy:"р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",Re:"ℜ",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",rect:"▭",REG:"®",reg:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",Rfr:"ℜ",rfr:"𝔯",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",Rho:"Ρ",rho:"ρ",rhov:"ϱ",RightAngleBracket:"⟩",RightArrow:"→",Rightarrow:"⇒",rightarrow:"→",RightArrowBar:"⇥",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVector:"⇂",RightDownVectorBar:"⥕",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTee:"⊢",RightTeeArrow:"↦",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangle:"⊳",RightTriangleBar:"⧐",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVector:"↾",RightUpVectorBar:"⥔",RightVector:"⇀",RightVectorBar:"⥓",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"‏",rmoust:"⎱",rmoustache:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",Ropf:"ℝ",ropf:"𝕣",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",Rscr:"ℛ",rscr:"𝓇",Rsh:"↱",rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",Sacute:"Ś",sacute:"ś",sbquo:"‚",Sc:"⪼",sc:"≻",scap:"⪸",Scaron:"Š",scaron:"š",sccue:"≽",scE:"⪴",sce:"⪰",Scedil:"Ş",scedil:"ş",Scirc:"Ŝ",scirc:"ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",Scy:"С",scy:"с",sdot:"⋅",sdotb:"⊡",sdote:"⩦",searhk:"⤥",seArr:"⇘",searr:"↘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",Sfr:"𝔖",sfr:"𝔰",sfrown:"⌢",sharp:"♯",SHCHcy:"Щ",shchcy:"щ",SHcy:"Ш",shcy:"ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"­",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",SOFTcy:"Ь",softcy:"ь",sol:"/",solb:"⧄",solbar:"⌿",Sopf:"𝕊",sopf:"𝕤",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",squ:"□",Square:"□",square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squf:"▪",srarr:"→",Sscr:"𝒮",sscr:"𝓈",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",Star:"⋆",star:"☆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",Sub:"⋐",sub:"⊂",subdot:"⪽",subE:"⫅",sube:"⊆",subedot:"⫃",submult:"⫁",subnE:"⫋",subne:"⊊",subplus:"⪿",subrarr:"⥹",Subset:"⋐",subset:"⊂",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succ:"≻",succapprox:"⪸",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",Sum:"∑",sum:"∑",sung:"♪",Sup:"⋑",sup:"⊃",sup1:"¹",sup2:"²",sup3:"³",supdot:"⪾",supdsub:"⫘",supE:"⫆",supe:"⊇",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supnE:"⫌",supne:"⊋",supplus:"⫀",Supset:"⋑",supset:"⊃",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swArr:"⇙",swarr:"↙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\t",target:"⌖",Tau:"Τ",tau:"τ",tbrk:"⎴",Tcaron:"Ť",tcaron:"ť",Tcedil:"Ţ",tcedil:"ţ",Tcy:"Т",tcy:"т",tdot:"⃛",telrec:"⌕",Tfr:"𝔗",tfr:"𝔱",there4:"∴",Therefore:"∴",therefore:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",thinsp:" ",ThinSpace:" ",thkap:"≈",thksim:"∼",THORN:"Þ",thorn:"þ",Tilde:"∼",tilde:"˜",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",times:"×",timesb:"⊠",timesbar:"⨱",timesd:"⨰",tint:"∭",toea:"⤨",top:"⊤",topbot:"⌶",topcir:"⫱",Topf:"𝕋",topf:"𝕥",topfork:"⫚",tosa:"⤩",tprime:"‴",TRADE:"™",trade:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",Tscr:"𝒯",tscr:"𝓉",TScy:"Ц",tscy:"ц",TSHcy:"Ћ",tshcy:"ћ",Tstrok:"Ŧ",tstrok:"ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",Uacute:"Ú",uacute:"ú",Uarr:"↟",uArr:"⇑",uarr:"↑",Uarrocir:"⥉",Ubrcy:"Ў",ubrcy:"ў",Ubreve:"Ŭ",ubreve:"ŭ",Ucirc:"Û",ucirc:"û",Ucy:"У",ucy:"у",udarr:"⇅",Udblac:"Ű",udblac:"ű",udhar:"⥮",ufisht:"⥾",Ufr:"𝔘",ufr:"𝔲",Ugrave:"Ù",ugrave:"ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",Umacr:"Ū",umacr:"ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",Uogon:"Ų",uogon:"ų",Uopf:"𝕌",uopf:"𝕦",UpArrow:"↑",Uparrow:"⇑",uparrow:"↑",UpArrowBar:"⤒",UpArrowDownArrow:"⇅",UpDownArrow:"↕",Updownarrow:"⇕",updownarrow:"↕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",Upsi:"ϒ",upsi:"υ",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",UpTee:"⊥",UpTeeArrow:"↥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",Uring:"Ů",uring:"ů",urtri:"◹",Uscr:"𝒰",uscr:"𝓊",utdot:"⋰",Utilde:"Ũ",utilde:"ũ",utri:"▵",utrif:"▴",uuarr:"⇈",Uuml:"Ü",uuml:"ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",vArr:"⇕",varr:"↕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",Vbar:"⫫",vBar:"⫨",vBarv:"⫩",Vcy:"В",vcy:"в",VDash:"⊫",Vdash:"⊩",vDash:"⊨",vdash:"⊢",Vdashl:"⫦",Vee:"⋁",vee:"∨",veebar:"⊻",veeeq:"≚",vellip:"⋮",Verbar:"‖",verbar:"|",Vert:"‖",vert:"|",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",Vfr:"𝔙",vfr:"𝔳",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",Vopf:"𝕍",vopf:"𝕧",vprop:"∝",vrtri:"⊳",Vscr:"𝒱",vscr:"𝓋",vsubnE:"⫋︀",vsubne:"⊊︀",vsupnE:"⫌︀",vsupne:"⊋︀",Vvdash:"⊪",vzigzag:"⦚",Wcirc:"Ŵ",wcirc:"ŵ",wedbar:"⩟",Wedge:"⋀",wedge:"∧",wedgeq:"≙",weierp:"℘",Wfr:"𝔚",wfr:"𝔴",Wopf:"𝕎",wopf:"𝕨",wp:"℘",wr:"≀",wreath:"≀",Wscr:"𝒲",wscr:"𝓌",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",Xfr:"𝔛",xfr:"𝔵",xhArr:"⟺",xharr:"⟷",Xi:"Ξ",xi:"ξ",xlArr:"⟸",xlarr:"⟵",xmap:"⟼",xnis:"⋻",xodot:"⨀",Xopf:"𝕏",xopf:"𝕩",xoplus:"⨁",xotime:"⨂",xrArr:"⟹",xrarr:"⟶",Xscr:"𝒳",xscr:"𝓍",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",Yacute:"Ý",yacute:"ý",YAcy:"Я",yacy:"я",Ycirc:"Ŷ",ycirc:"ŷ",Ycy:"Ы",ycy:"ы",yen:"¥",Yfr:"𝔜",yfr:"𝔶",YIcy:"Ї",yicy:"ї",Yopf:"𝕐",yopf:"𝕪",Yscr:"𝒴",yscr:"𝓎",YUcy:"Ю",yucy:"ю",Yuml:"Ÿ",yuml:"ÿ",Zacute:"Ź",zacute:"ź",Zcaron:"Ž",zcaron:"ž",Zcy:"З",zcy:"з",Zdot:"Ż",zdot:"ż",zeetrf:"ℨ",ZeroWidthSpace:"​",Zeta:"Ζ",zeta:"ζ",Zfr:"ℨ",zfr:"𝔷",ZHcy:"Ж",zhcy:"ж",zigrarr:"⇝",Zopf:"ℤ",zopf:"𝕫",Zscr:"𝒵",zscr:"𝓏",zwj:"‍",zwnj:"‌"}},function(e,t,r){"use strict";var n=r(458),i=r(28).unescapeMd;e.exports=function parseLinkDestination(e,t){var r,o,a,s=t,u=e.posMax;if(60===e.src.charCodeAt(t)){for(t++;t8&&r<14);)if(92===r&&t+11)break;if(41===r&&--o<0)break;t++}return s!==t&&(a=i(e.src.slice(s,t)),!!e.parser.validateLink(a)&&(e.linkContent=a,e.pos=t,!0))}},function(e,t,r){"use strict";var n=r(28).replaceEntities;e.exports=function normalizeLink(e){var t=n(e);try{t=decodeURI(t)}catch(e){}return encodeURI(t)}},function(e,t,r){"use strict";var n=r(28).unescapeMd;e.exports=function parseLinkTitle(e,t){var r,i=t,o=e.posMax,a=e.src.charCodeAt(t);if(34!==a&&39!==a&&40!==a)return!1;for(t++,40===a&&(a=41);t1?i-1:0),a=1;a1?r-1:0),i=1;i0?Array(e+1).join(" ")+t:t}).join("\n")}(0,(0,n.default)(a,null,2))||"{}",c.default.createElement("br",null)))}}]),OperationLink}(l.Component);d.propTypes={getComponent:p.default.func.isRequired,link:f.default.orderedMap.isRequired,name:p.default.String},t.default=d},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(4)),i=_interopRequireDefault(r(2)),o=_interopRequireDefault(r(3)),a=_interopRequireDefault(r(5)),s=_interopRequireDefault(r(6)),u=_interopRequireDefault(r(0)),l=r(7),c=_interopRequireDefault(r(1)),p=_interopRequireDefault(r(12));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var f=function(e){function Servers(){var e,t,r,o;(0,i.default)(this,Servers);for(var s=arguments.length,u=Array(s),l=0;l=55296&&a<=57343){if(a>=55296&&a<=56319&&i+1=56320&&s<=57343){l+=encodeURIComponent(e[i]+e[i+1]),i++;continue}l+="%EF%BF%BD"}else l+=encodeURIComponent(e[i]);return l}encode.defaultChars=";/?:@&=+$,-_.!~*'()#",encode.componentChars="-_.!~*'()",e.exports=encode},function(e,t,r){"use strict";var n={};function decode(e,t){var r;return"string"!=typeof t&&(t=decode.defaultChars),r=function getDecodeCache(e){var t,r,i=n[e];if(i)return i;for(i=n[e]=[],t=0;t<128;t++)r=String.fromCharCode(t),i.push(r);for(t=0;t=55296&&u<=57343?"���":String.fromCharCode(u),t+=6):240==(248&i)&&t+91114111?l+="����":(u-=65536,l+=String.fromCharCode(55296+(u>>10),56320+(1023&u))),t+=9):l+="�";return l})}decode.defaultChars=";/?:@&=+$,#",decode.componentChars="",e.exports=decode},function(e,t){e.exports={amp:"&",apos:"'",gt:">",lt:"<",quot:'"'}},function(e,t){e.exports={Aacute:"Á",aacute:"á",Abreve:"Ă",abreve:"ă",ac:"∾",acd:"∿",acE:"∾̳",Acirc:"Â",acirc:"â",acute:"´",Acy:"А",acy:"а",AElig:"Æ",aelig:"æ",af:"⁡",Afr:"𝔄",afr:"𝔞",Agrave:"À",agrave:"à",alefsym:"ℵ",aleph:"ℵ",Alpha:"Α",alpha:"α",Amacr:"Ā",amacr:"ā",amalg:"⨿",amp:"&",AMP:"&",andand:"⩕",And:"⩓",and:"∧",andd:"⩜",andslope:"⩘",andv:"⩚",ang:"∠",ange:"⦤",angle:"∠",angmsdaa:"⦨",angmsdab:"⦩",angmsdac:"⦪",angmsdad:"⦫",angmsdae:"⦬",angmsdaf:"⦭",angmsdag:"⦮",angmsdah:"⦯",angmsd:"∡",angrt:"∟",angrtvb:"⊾",angrtvbd:"⦝",angsph:"∢",angst:"Å",angzarr:"⍼",Aogon:"Ą",aogon:"ą",Aopf:"𝔸",aopf:"𝕒",apacir:"⩯",ap:"≈",apE:"⩰",ape:"≊",apid:"≋",apos:"'",ApplyFunction:"⁡",approx:"≈",approxeq:"≊",Aring:"Å",aring:"å",Ascr:"𝒜",ascr:"𝒶",Assign:"≔",ast:"*",asymp:"≈",asympeq:"≍",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",awconint:"∳",awint:"⨑",backcong:"≌",backepsilon:"϶",backprime:"‵",backsim:"∽",backsimeq:"⋍",Backslash:"∖",Barv:"⫧",barvee:"⊽",barwed:"⌅",Barwed:"⌆",barwedge:"⌅",bbrk:"⎵",bbrktbrk:"⎶",bcong:"≌",Bcy:"Б",bcy:"б",bdquo:"„",becaus:"∵",because:"∵",Because:"∵",bemptyv:"⦰",bepsi:"϶",bernou:"ℬ",Bernoullis:"ℬ",Beta:"Β",beta:"β",beth:"ℶ",between:"≬",Bfr:"𝔅",bfr:"𝔟",bigcap:"⋂",bigcirc:"◯",bigcup:"⋃",bigodot:"⨀",bigoplus:"⨁",bigotimes:"⨂",bigsqcup:"⨆",bigstar:"★",bigtriangledown:"▽",bigtriangleup:"△",biguplus:"⨄",bigvee:"⋁",bigwedge:"⋀",bkarow:"⤍",blacklozenge:"⧫",blacksquare:"▪",blacktriangle:"▴",blacktriangledown:"▾",blacktriangleleft:"◂",blacktriangleright:"▸",blank:"␣",blk12:"▒",blk14:"░",blk34:"▓",block:"█",bne:"=⃥",bnequiv:"≡⃥",bNot:"⫭",bnot:"⌐",Bopf:"𝔹",bopf:"𝕓",bot:"⊥",bottom:"⊥",bowtie:"⋈",boxbox:"⧉",boxdl:"┐",boxdL:"╕",boxDl:"╖",boxDL:"╗",boxdr:"┌",boxdR:"╒",boxDr:"╓",boxDR:"╔",boxh:"─",boxH:"═",boxhd:"┬",boxHd:"╤",boxhD:"╥",boxHD:"╦",boxhu:"┴",boxHu:"╧",boxhU:"╨",boxHU:"╩",boxminus:"⊟",boxplus:"⊞",boxtimes:"⊠",boxul:"┘",boxuL:"╛",boxUl:"╜",boxUL:"╝",boxur:"└",boxuR:"╘",boxUr:"╙",boxUR:"╚",boxv:"│",boxV:"║",boxvh:"┼",boxvH:"╪",boxVh:"╫",boxVH:"╬",boxvl:"┤",boxvL:"╡",boxVl:"╢",boxVL:"╣",boxvr:"├",boxvR:"╞",boxVr:"╟",boxVR:"╠",bprime:"‵",breve:"˘",Breve:"˘",brvbar:"¦",bscr:"𝒷",Bscr:"ℬ",bsemi:"⁏",bsim:"∽",bsime:"⋍",bsolb:"⧅",bsol:"\\",bsolhsub:"⟈",bull:"•",bullet:"•",bump:"≎",bumpE:"⪮",bumpe:"≏",Bumpeq:"≎",bumpeq:"≏",Cacute:"Ć",cacute:"ć",capand:"⩄",capbrcup:"⩉",capcap:"⩋",cap:"∩",Cap:"⋒",capcup:"⩇",capdot:"⩀",CapitalDifferentialD:"ⅅ",caps:"∩︀",caret:"⁁",caron:"ˇ",Cayleys:"ℭ",ccaps:"⩍",Ccaron:"Č",ccaron:"č",Ccedil:"Ç",ccedil:"ç",Ccirc:"Ĉ",ccirc:"ĉ",Cconint:"∰",ccups:"⩌",ccupssm:"⩐",Cdot:"Ċ",cdot:"ċ",cedil:"¸",Cedilla:"¸",cemptyv:"⦲",cent:"¢",centerdot:"·",CenterDot:"·",cfr:"𝔠",Cfr:"ℭ",CHcy:"Ч",chcy:"ч",check:"✓",checkmark:"✓",Chi:"Χ",chi:"χ",circ:"ˆ",circeq:"≗",circlearrowleft:"↺",circlearrowright:"↻",circledast:"⊛",circledcirc:"⊚",circleddash:"⊝",CircleDot:"⊙",circledR:"®",circledS:"Ⓢ",CircleMinus:"⊖",CirclePlus:"⊕",CircleTimes:"⊗",cir:"○",cirE:"⧃",cire:"≗",cirfnint:"⨐",cirmid:"⫯",cirscir:"⧂",ClockwiseContourIntegral:"∲",CloseCurlyDoubleQuote:"”",CloseCurlyQuote:"’",clubs:"♣",clubsuit:"♣",colon:":",Colon:"∷",Colone:"⩴",colone:"≔",coloneq:"≔",comma:",",commat:"@",comp:"∁",compfn:"∘",complement:"∁",complexes:"ℂ",cong:"≅",congdot:"⩭",Congruent:"≡",conint:"∮",Conint:"∯",ContourIntegral:"∮",copf:"𝕔",Copf:"ℂ",coprod:"∐",Coproduct:"∐",copy:"©",COPY:"©",copysr:"℗",CounterClockwiseContourIntegral:"∳",crarr:"↵",cross:"✗",Cross:"⨯",Cscr:"𝒞",cscr:"𝒸",csub:"⫏",csube:"⫑",csup:"⫐",csupe:"⫒",ctdot:"⋯",cudarrl:"⤸",cudarrr:"⤵",cuepr:"⋞",cuesc:"⋟",cularr:"↶",cularrp:"⤽",cupbrcap:"⩈",cupcap:"⩆",CupCap:"≍",cup:"∪",Cup:"⋓",cupcup:"⩊",cupdot:"⊍",cupor:"⩅",cups:"∪︀",curarr:"↷",curarrm:"⤼",curlyeqprec:"⋞",curlyeqsucc:"⋟",curlyvee:"⋎",curlywedge:"⋏",curren:"¤",curvearrowleft:"↶",curvearrowright:"↷",cuvee:"⋎",cuwed:"⋏",cwconint:"∲",cwint:"∱",cylcty:"⌭",dagger:"†",Dagger:"‡",daleth:"ℸ",darr:"↓",Darr:"↡",dArr:"⇓",dash:"‐",Dashv:"⫤",dashv:"⊣",dbkarow:"⤏",dblac:"˝",Dcaron:"Ď",dcaron:"ď",Dcy:"Д",dcy:"д",ddagger:"‡",ddarr:"⇊",DD:"ⅅ",dd:"ⅆ",DDotrahd:"⤑",ddotseq:"⩷",deg:"°",Del:"∇",Delta:"Δ",delta:"δ",demptyv:"⦱",dfisht:"⥿",Dfr:"𝔇",dfr:"𝔡",dHar:"⥥",dharl:"⇃",dharr:"⇂",DiacriticalAcute:"´",DiacriticalDot:"˙",DiacriticalDoubleAcute:"˝",DiacriticalGrave:"`",DiacriticalTilde:"˜",diam:"⋄",diamond:"⋄",Diamond:"⋄",diamondsuit:"♦",diams:"♦",die:"¨",DifferentialD:"ⅆ",digamma:"ϝ",disin:"⋲",div:"÷",divide:"÷",divideontimes:"⋇",divonx:"⋇",DJcy:"Ђ",djcy:"ђ",dlcorn:"⌞",dlcrop:"⌍",dollar:"$",Dopf:"𝔻",dopf:"𝕕",Dot:"¨",dot:"˙",DotDot:"⃜",doteq:"≐",doteqdot:"≑",DotEqual:"≐",dotminus:"∸",dotplus:"∔",dotsquare:"⊡",doublebarwedge:"⌆",DoubleContourIntegral:"∯",DoubleDot:"¨",DoubleDownArrow:"⇓",DoubleLeftArrow:"⇐",DoubleLeftRightArrow:"⇔",DoubleLeftTee:"⫤",DoubleLongLeftArrow:"⟸",DoubleLongLeftRightArrow:"⟺",DoubleLongRightArrow:"⟹",DoubleRightArrow:"⇒",DoubleRightTee:"⊨",DoubleUpArrow:"⇑",DoubleUpDownArrow:"⇕",DoubleVerticalBar:"∥",DownArrowBar:"⤓",downarrow:"↓",DownArrow:"↓",Downarrow:"⇓",DownArrowUpArrow:"⇵",DownBreve:"̑",downdownarrows:"⇊",downharpoonleft:"⇃",downharpoonright:"⇂",DownLeftRightVector:"⥐",DownLeftTeeVector:"⥞",DownLeftVectorBar:"⥖",DownLeftVector:"↽",DownRightTeeVector:"⥟",DownRightVectorBar:"⥗",DownRightVector:"⇁",DownTeeArrow:"↧",DownTee:"⊤",drbkarow:"⤐",drcorn:"⌟",drcrop:"⌌",Dscr:"𝒟",dscr:"𝒹",DScy:"Ѕ",dscy:"ѕ",dsol:"⧶",Dstrok:"Đ",dstrok:"đ",dtdot:"⋱",dtri:"▿",dtrif:"▾",duarr:"⇵",duhar:"⥯",dwangle:"⦦",DZcy:"Џ",dzcy:"џ",dzigrarr:"⟿",Eacute:"É",eacute:"é",easter:"⩮",Ecaron:"Ě",ecaron:"ě",Ecirc:"Ê",ecirc:"ê",ecir:"≖",ecolon:"≕",Ecy:"Э",ecy:"э",eDDot:"⩷",Edot:"Ė",edot:"ė",eDot:"≑",ee:"ⅇ",efDot:"≒",Efr:"𝔈",efr:"𝔢",eg:"⪚",Egrave:"È",egrave:"è",egs:"⪖",egsdot:"⪘",el:"⪙",Element:"∈",elinters:"⏧",ell:"ℓ",els:"⪕",elsdot:"⪗",Emacr:"Ē",emacr:"ē",empty:"∅",emptyset:"∅",EmptySmallSquare:"◻",emptyv:"∅",EmptyVerySmallSquare:"▫",emsp13:" ",emsp14:" ",emsp:" ",ENG:"Ŋ",eng:"ŋ",ensp:" ",Eogon:"Ę",eogon:"ę",Eopf:"𝔼",eopf:"𝕖",epar:"⋕",eparsl:"⧣",eplus:"⩱",epsi:"ε",Epsilon:"Ε",epsilon:"ε",epsiv:"ϵ",eqcirc:"≖",eqcolon:"≕",eqsim:"≂",eqslantgtr:"⪖",eqslantless:"⪕",Equal:"⩵",equals:"=",EqualTilde:"≂",equest:"≟",Equilibrium:"⇌",equiv:"≡",equivDD:"⩸",eqvparsl:"⧥",erarr:"⥱",erDot:"≓",escr:"ℯ",Escr:"ℰ",esdot:"≐",Esim:"⩳",esim:"≂",Eta:"Η",eta:"η",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",euro:"€",excl:"!",exist:"∃",Exists:"∃",expectation:"ℰ",exponentiale:"ⅇ",ExponentialE:"ⅇ",fallingdotseq:"≒",Fcy:"Ф",fcy:"ф",female:"♀",ffilig:"ffi",fflig:"ff",ffllig:"ffl",Ffr:"𝔉",ffr:"𝔣",filig:"fi",FilledSmallSquare:"◼",FilledVerySmallSquare:"▪",fjlig:"fj",flat:"♭",fllig:"fl",fltns:"▱",fnof:"ƒ",Fopf:"𝔽",fopf:"𝕗",forall:"∀",ForAll:"∀",fork:"⋔",forkv:"⫙",Fouriertrf:"ℱ",fpartint:"⨍",frac12:"½",frac13:"⅓",frac14:"¼",frac15:"⅕",frac16:"⅙",frac18:"⅛",frac23:"⅔",frac25:"⅖",frac34:"¾",frac35:"⅗",frac38:"⅜",frac45:"⅘",frac56:"⅚",frac58:"⅝",frac78:"⅞",frasl:"⁄",frown:"⌢",fscr:"𝒻",Fscr:"ℱ",gacute:"ǵ",Gamma:"Γ",gamma:"γ",Gammad:"Ϝ",gammad:"ϝ",gap:"⪆",Gbreve:"Ğ",gbreve:"ğ",Gcedil:"Ģ",Gcirc:"Ĝ",gcirc:"ĝ",Gcy:"Г",gcy:"г",Gdot:"Ġ",gdot:"ġ",ge:"≥",gE:"≧",gEl:"⪌",gel:"⋛",geq:"≥",geqq:"≧",geqslant:"⩾",gescc:"⪩",ges:"⩾",gesdot:"⪀",gesdoto:"⪂",gesdotol:"⪄",gesl:"⋛︀",gesles:"⪔",Gfr:"𝔊",gfr:"𝔤",gg:"≫",Gg:"⋙",ggg:"⋙",gimel:"ℷ",GJcy:"Ѓ",gjcy:"ѓ",gla:"⪥",gl:"≷",glE:"⪒",glj:"⪤",gnap:"⪊",gnapprox:"⪊",gne:"⪈",gnE:"≩",gneq:"⪈",gneqq:"≩",gnsim:"⋧",Gopf:"𝔾",gopf:"𝕘",grave:"`",GreaterEqual:"≥",GreaterEqualLess:"⋛",GreaterFullEqual:"≧",GreaterGreater:"⪢",GreaterLess:"≷",GreaterSlantEqual:"⩾",GreaterTilde:"≳",Gscr:"𝒢",gscr:"ℊ",gsim:"≳",gsime:"⪎",gsiml:"⪐",gtcc:"⪧",gtcir:"⩺",gt:">",GT:">",Gt:"≫",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",HARDcy:"Ъ",hardcy:"ъ",harrcir:"⥈",harr:"↔",hArr:"⇔",harrw:"↭",Hat:"^",hbar:"ℏ",Hcirc:"Ĥ",hcirc:"ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",hfr:"𝔥",Hfr:"ℌ",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",hopf:"𝕙",Hopf:"ℍ",horbar:"―",HorizontalLine:"─",hscr:"𝒽",Hscr:"ℋ",hslash:"ℏ",Hstrok:"Ħ",hstrok:"ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",Iacute:"Í",iacute:"í",ic:"⁣",Icirc:"Î",icirc:"î",Icy:"И",icy:"и",Idot:"İ",IEcy:"Е",iecy:"е",iexcl:"¡",iff:"⇔",ifr:"𝔦",Ifr:"ℑ",Igrave:"Ì",igrave:"ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",IJlig:"IJ",ijlig:"ij",Imacr:"Ī",imacr:"ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",Im:"ℑ",imof:"⊷",imped:"Ƶ",Implies:"⇒",incare:"℅",in:"∈",infin:"∞",infintie:"⧝",inodot:"ı",intcal:"⊺",int:"∫",Int:"∬",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"⁣",InvisibleTimes:"⁢",IOcy:"Ё",iocy:"ё",Iogon:"Į",iogon:"į",Iopf:"𝕀",iopf:"𝕚",Iota:"Ι",iota:"ι",iprod:"⨼",iquest:"¿",iscr:"𝒾",Iscr:"ℐ",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"⁢",Itilde:"Ĩ",itilde:"ĩ",Iukcy:"І",iukcy:"і",Iuml:"Ï",iuml:"ï",Jcirc:"Ĵ",jcirc:"ĵ",Jcy:"Й",jcy:"й",Jfr:"𝔍",jfr:"𝔧",jmath:"ȷ",Jopf:"𝕁",jopf:"𝕛",Jscr:"𝒥",jscr:"𝒿",Jsercy:"Ј",jsercy:"ј",Jukcy:"Є",jukcy:"є",Kappa:"Κ",kappa:"κ",kappav:"ϰ",Kcedil:"Ķ",kcedil:"ķ",Kcy:"К",kcy:"к",Kfr:"𝔎",kfr:"𝔨",kgreen:"ĸ",KHcy:"Х",khcy:"х",KJcy:"Ќ",kjcy:"ќ",Kopf:"𝕂",kopf:"𝕜",Kscr:"𝒦",kscr:"𝓀",lAarr:"⇚",Lacute:"Ĺ",lacute:"ĺ",laemptyv:"⦴",lagran:"ℒ",Lambda:"Λ",lambda:"λ",lang:"⟨",Lang:"⟪",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",larrb:"⇤",larrbfs:"⤟",larr:"←",Larr:"↞",lArr:"⇐",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",latail:"⤙",lAtail:"⤛",lat:"⪫",late:"⪭",lates:"⪭︀",lbarr:"⤌",lBarr:"⤎",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",Lcaron:"Ľ",lcaron:"ľ",Lcedil:"Ļ",lcedil:"ļ",lceil:"⌈",lcub:"{",Lcy:"Л",lcy:"л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",le:"≤",lE:"≦",LeftAngleBracket:"⟨",LeftArrowBar:"⇤",leftarrow:"←",LeftArrow:"←",Leftarrow:"⇐",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVectorBar:"⥙",LeftDownVector:"⇃",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",leftrightarrow:"↔",LeftRightArrow:"↔",Leftrightarrow:"⇔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTeeArrow:"↤",LeftTee:"⊣",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangleBar:"⧏",LeftTriangle:"⊲",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVectorBar:"⥘",LeftUpVector:"↿",LeftVectorBar:"⥒",LeftVector:"↼",lEg:"⪋",leg:"⋚",leq:"≤",leqq:"≦",leqslant:"⩽",lescc:"⪨",les:"⩽",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",Lfr:"𝔏",lfr:"𝔩",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",LJcy:"Љ",ljcy:"љ",llarr:"⇇",ll:"≪",Ll:"⋘",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",Lmidot:"Ŀ",lmidot:"ŀ",lmoustache:"⎰",lmoust:"⎰",lnap:"⪉",lnapprox:"⪉",lne:"⪇",lnE:"≨",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",longleftarrow:"⟵",LongLeftArrow:"⟵",Longleftarrow:"⟸",longleftrightarrow:"⟷",LongLeftRightArrow:"⟷",Longleftrightarrow:"⟺",longmapsto:"⟼",longrightarrow:"⟶",LongRightArrow:"⟶",Longrightarrow:"⟹",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",Lopf:"𝕃",lopf:"𝕝",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"‎",lrtri:"⊿",lsaquo:"‹",lscr:"𝓁",Lscr:"ℒ",lsh:"↰",Lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",Lstrok:"Ł",lstrok:"ł",ltcc:"⪦",ltcir:"⩹",lt:"<",LT:"<",Lt:"≪",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",Map:"⤅",map:"↦",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",Mcy:"М",mcy:"м",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",Mfr:"𝔐",mfr:"𝔪",mho:"℧",micro:"µ",midast:"*",midcir:"⫰",mid:"∣",middot:"·",minusb:"⊟",minus:"−",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",Mopf:"𝕄",mopf:"𝕞",mp:"∓",mscr:"𝓂",Mscr:"ℳ",mstpos:"∾",Mu:"Μ",mu:"μ",multimap:"⊸",mumap:"⊸",nabla:"∇",Nacute:"Ń",nacute:"ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natural:"♮",naturals:"ℕ",natur:"♮",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",Ncaron:"Ň",ncaron:"ň",Ncedil:"Ņ",ncedil:"ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",Ncy:"Н",ncy:"н",ndash:"–",nearhk:"⤤",nearr:"↗",neArr:"⇗",nearrow:"↗",ne:"≠",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\n",nexist:"∄",nexists:"∄",Nfr:"𝔑",nfr:"𝔫",ngE:"≧̸",nge:"≱",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",nGt:"≫⃒",ngt:"≯",ngtr:"≯",nGtv:"≫̸",nharr:"↮",nhArr:"⇎",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",NJcy:"Њ",njcy:"њ",nlarr:"↚",nlArr:"⇍",nldr:"‥",nlE:"≦̸",nle:"≰",nleftarrow:"↚",nLeftarrow:"⇍",nleftrightarrow:"↮",nLeftrightarrow:"⇎",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nLt:"≪⃒",nlt:"≮",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"⁠",NonBreakingSpace:" ",nopf:"𝕟",Nopf:"ℕ",Not:"⫬",not:"¬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangleBar:"⧏̸",NotLeftTriangle:"⋪",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangleBar:"⧐̸",NotRightTriangle:"⋫",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",nparallel:"∦",npar:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",nprec:"⊀",npreceq:"⪯̸",npre:"⪯̸",nrarrc:"⤳̸",nrarr:"↛",nrArr:"⇏",nrarrw:"↝̸",nrightarrow:"↛",nRightarrow:"⇏",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",Nscr:"𝒩",nscr:"𝓃",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsubE:"⫅̸",nsube:"⊈",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupE:"⫆̸",nsupe:"⊉",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",Ntilde:"Ñ",ntilde:"ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",Nu:"Ν",nu:"ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nvdash:"⊬",nvDash:"⊭",nVdash:"⊮",nVDash:"⊯",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwarr:"↖",nwArr:"⇖",nwarrow:"↖",nwnear:"⤧",Oacute:"Ó",oacute:"ó",oast:"⊛",Ocirc:"Ô",ocirc:"ô",ocir:"⊚",Ocy:"О",ocy:"о",odash:"⊝",Odblac:"Ő",odblac:"ő",odiv:"⨸",odot:"⊙",odsold:"⦼",OElig:"Œ",oelig:"œ",ofcir:"⦿",Ofr:"𝔒",ofr:"𝔬",ogon:"˛",Ograve:"Ò",ograve:"ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",Omacr:"Ō",omacr:"ō",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",omid:"⦶",ominus:"⊖",Oopf:"𝕆",oopf:"𝕠",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",orarr:"↻",Or:"⩔",or:"∨",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",Oscr:"𝒪",oscr:"ℴ",Oslash:"Ø",oslash:"ø",osol:"⊘",Otilde:"Õ",otilde:"õ",otimesas:"⨶",Otimes:"⨷",otimes:"⊗",Ouml:"Ö",ouml:"ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",para:"¶",parallel:"∥",par:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",Pcy:"П",pcy:"п",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",Pfr:"𝔓",pfr:"𝔭",Phi:"Φ",phi:"φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",Pi:"Π",pi:"π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plus:"+",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",popf:"𝕡",Popf:"ℙ",pound:"£",prap:"⪷",Pr:"⪻",pr:"≺",prcue:"≼",precapprox:"⪷",prec:"≺",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",pre:"⪯",prE:"⪳",precsim:"≾",prime:"′",Prime:"″",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportional:"∝",Proportion:"∷",propto:"∝",prsim:"≾",prurel:"⊰",Pscr:"𝒫",pscr:"𝓅",Psi:"Ψ",psi:"ψ",puncsp:" ",Qfr:"𝔔",qfr:"𝔮",qint:"⨌",qopf:"𝕢",Qopf:"ℚ",qprime:"⁗",Qscr:"𝒬",qscr:"𝓆",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",quot:'"',QUOT:'"',rAarr:"⇛",race:"∽̱",Racute:"Ŕ",racute:"ŕ",radic:"√",raemptyv:"⦳",rang:"⟩",Rang:"⟫",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarr:"→",Rarr:"↠",rArr:"⇒",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",Rarrtl:"⤖",rarrtl:"↣",rarrw:"↝",ratail:"⤚",rAtail:"⤜",ratio:"∶",rationals:"ℚ",rbarr:"⤍",rBarr:"⤏",RBarr:"⤐",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",Rcaron:"Ř",rcaron:"ř",Rcedil:"Ŗ",rcedil:"ŗ",rceil:"⌉",rcub:"}",Rcy:"Р",rcy:"р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",Re:"ℜ",rect:"▭",reg:"®",REG:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",rfr:"𝔯",Rfr:"ℜ",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",Rho:"Ρ",rho:"ρ",rhov:"ϱ",RightAngleBracket:"⟩",RightArrowBar:"⇥",rightarrow:"→",RightArrow:"→",Rightarrow:"⇒",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVectorBar:"⥕",RightDownVector:"⇂",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTeeArrow:"↦",RightTee:"⊢",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangleBar:"⧐",RightTriangle:"⊳",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVectorBar:"⥔",RightUpVector:"↾",RightVectorBar:"⥓",RightVector:"⇀",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"‏",rmoustache:"⎱",rmoust:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",ropf:"𝕣",Ropf:"ℝ",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",rscr:"𝓇",Rscr:"ℛ",rsh:"↱",Rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",Sacute:"Ś",sacute:"ś",sbquo:"‚",scap:"⪸",Scaron:"Š",scaron:"š",Sc:"⪼",sc:"≻",sccue:"≽",sce:"⪰",scE:"⪴",Scedil:"Ş",scedil:"ş",Scirc:"Ŝ",scirc:"ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",Scy:"С",scy:"с",sdotb:"⊡",sdot:"⋅",sdote:"⩦",searhk:"⤥",searr:"↘",seArr:"⇘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",Sfr:"𝔖",sfr:"𝔰",sfrown:"⌢",sharp:"♯",SHCHcy:"Щ",shchcy:"щ",SHcy:"Ш",shcy:"ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"­",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",SOFTcy:"Ь",softcy:"ь",solbar:"⌿",solb:"⧄",sol:"/",Sopf:"𝕊",sopf:"𝕤",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",square:"□",Square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squ:"□",squf:"▪",srarr:"→",Sscr:"𝒮",sscr:"𝓈",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",Star:"⋆",star:"☆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",sub:"⊂",Sub:"⋐",subdot:"⪽",subE:"⫅",sube:"⊆",subedot:"⫃",submult:"⫁",subnE:"⫋",subne:"⊊",subplus:"⪿",subrarr:"⥹",subset:"⊂",Subset:"⋐",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succapprox:"⪸",succ:"≻",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",sum:"∑",Sum:"∑",sung:"♪",sup1:"¹",sup2:"²",sup3:"³",sup:"⊃",Sup:"⋑",supdot:"⪾",supdsub:"⫘",supE:"⫆",supe:"⊇",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supnE:"⫌",supne:"⊋",supplus:"⫀",supset:"⊃",Supset:"⋑",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swarr:"↙",swArr:"⇙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\t",target:"⌖",Tau:"Τ",tau:"τ",tbrk:"⎴",Tcaron:"Ť",tcaron:"ť",Tcedil:"Ţ",tcedil:"ţ",Tcy:"Т",tcy:"т",tdot:"⃛",telrec:"⌕",Tfr:"𝔗",tfr:"𝔱",there4:"∴",therefore:"∴",Therefore:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",ThinSpace:" ",thinsp:" ",thkap:"≈",thksim:"∼",THORN:"Þ",thorn:"þ",tilde:"˜",Tilde:"∼",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",timesbar:"⨱",timesb:"⊠",times:"×",timesd:"⨰",tint:"∭",toea:"⤨",topbot:"⌶",topcir:"⫱",top:"⊤",Topf:"𝕋",topf:"𝕥",topfork:"⫚",tosa:"⤩",tprime:"‴",trade:"™",TRADE:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",Tscr:"𝒯",tscr:"𝓉",TScy:"Ц",tscy:"ц",TSHcy:"Ћ",tshcy:"ћ",Tstrok:"Ŧ",tstrok:"ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",Uacute:"Ú",uacute:"ú",uarr:"↑",Uarr:"↟",uArr:"⇑",Uarrocir:"⥉",Ubrcy:"Ў",ubrcy:"ў",Ubreve:"Ŭ",ubreve:"ŭ",Ucirc:"Û",ucirc:"û",Ucy:"У",ucy:"у",udarr:"⇅",Udblac:"Ű",udblac:"ű",udhar:"⥮",ufisht:"⥾",Ufr:"𝔘",ufr:"𝔲",Ugrave:"Ù",ugrave:"ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",Umacr:"Ū",umacr:"ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",Uogon:"Ų",uogon:"ų",Uopf:"𝕌",uopf:"𝕦",UpArrowBar:"⤒",uparrow:"↑",UpArrow:"↑",Uparrow:"⇑",UpArrowDownArrow:"⇅",updownarrow:"↕",UpDownArrow:"↕",Updownarrow:"⇕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",upsi:"υ",Upsi:"ϒ",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",UpTeeArrow:"↥",UpTee:"⊥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",Uring:"Ů",uring:"ů",urtri:"◹",Uscr:"𝒰",uscr:"𝓊",utdot:"⋰",Utilde:"Ũ",utilde:"ũ",utri:"▵",utrif:"▴",uuarr:"⇈",Uuml:"Ü",uuml:"ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",varr:"↕",vArr:"⇕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",vBar:"⫨",Vbar:"⫫",vBarv:"⫩",Vcy:"В",vcy:"в",vdash:"⊢",vDash:"⊨",Vdash:"⊩",VDash:"⊫",Vdashl:"⫦",veebar:"⊻",vee:"∨",Vee:"⋁",veeeq:"≚",vellip:"⋮",verbar:"|",Verbar:"‖",vert:"|",Vert:"‖",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",Vfr:"𝔙",vfr:"𝔳",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",Vopf:"𝕍",vopf:"𝕧",vprop:"∝",vrtri:"⊳",Vscr:"𝒱",vscr:"𝓋",vsubnE:"⫋︀",vsubne:"⊊︀",vsupnE:"⫌︀",vsupne:"⊋︀",Vvdash:"⊪",vzigzag:"⦚",Wcirc:"Ŵ",wcirc:"ŵ",wedbar:"⩟",wedge:"∧",Wedge:"⋀",wedgeq:"≙",weierp:"℘",Wfr:"𝔚",wfr:"𝔴",Wopf:"𝕎",wopf:"𝕨",wp:"℘",wr:"≀",wreath:"≀",Wscr:"𝒲",wscr:"𝓌",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",Xfr:"𝔛",xfr:"𝔵",xharr:"⟷",xhArr:"⟺",Xi:"Ξ",xi:"ξ",xlarr:"⟵",xlArr:"⟸",xmap:"⟼",xnis:"⋻",xodot:"⨀",Xopf:"𝕏",xopf:"𝕩",xoplus:"⨁",xotime:"⨂",xrarr:"⟶",xrArr:"⟹",Xscr:"𝒳",xscr:"𝓍",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",Yacute:"Ý",yacute:"ý",YAcy:"Я",yacy:"я",Ycirc:"Ŷ",ycirc:"ŷ",Ycy:"Ы",ycy:"ы",yen:"¥",Yfr:"𝔜",yfr:"𝔶",YIcy:"Ї",yicy:"ї",Yopf:"𝕐",yopf:"𝕪",Yscr:"𝒴",yscr:"𝓎",YUcy:"Ю",yucy:"ю",yuml:"ÿ",Yuml:"Ÿ",Zacute:"Ź",zacute:"ź",Zcaron:"Ž",zcaron:"ž",Zcy:"З",zcy:"з",Zdot:"Ż",zdot:"ż",zeetrf:"ℨ",ZeroWidthSpace:"​",Zeta:"Ζ",zeta:"ζ",zfr:"𝔷",Zfr:"ℨ",ZHcy:"Ж",zhcy:"ж",zigrarr:"⇝",zopf:"𝕫",Zopf:"ℤ",Zscr:"𝒵",zscr:"𝓏",zwj:"‍",zwnj:"‌"}},function(e,t){ +/*! http://mths.be/repeat v0.2.0 by @mathias */ +String.prototype.repeat||function(){"use strict";var e=function(){try{var e={},t=Object.defineProperty,r=t(e,e,e)&&t}catch(e){}return r}(),t=function(e){if(null==this)throw TypeError();var t=String(this),r=e?Number(e):0;if(r!=r&&(r=0),r<0||r==1/0)throw RangeError();for(var n="";r;)r%2==1&&(n+=t),r>1&&(t+=t),r>>=1;return n};e?e(String.prototype,"repeat",{value:t,configurable:!0,writable:!0}):String.prototype.repeat=t}()},function(e,t,r){"use strict";function Renderer(){}Renderer.prototype.render=function render(e){var t,r,n=e.walker();for(this.buffer="",this.lastOut="\n";t=n.next();)this[r=t.node.type]&&this[r](t.node,t.entering);return this.buffer},Renderer.prototype.out=function out(e){this.lit(e)},Renderer.prototype.lit=function lit(e){this.buffer+=e,this.lastOut=e},Renderer.prototype.cr=function cr(){"\n"!==this.lastOut&&this.lit("\n")},Renderer.prototype.esc=function esc(e){return e},e.exports=Renderer},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(87)),i=_interopRequireDefault(r(0)),o=r(35);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=(0,o.OAS3ComponentWrapFactory)(function(e){var t=e.Ori,r=(0,n.default)(e,["Ori"]),o=r.schema,a=r.getComponent,s=r.errSelectors,u=r.authorized,l=r.onAuthChange,c=r.name,p=a("HttpAuth");return"http"===o.get("type")?i.default.createElement(p,{key:c,schema:o,name:c,errSelectors:s,authorized:u,getComponent:a,onChange:l}):i.default.createElement(t,r)})},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(86)),i=_interopRequireDefault(r(4)),o=_interopRequireDefault(r(2)),a=_interopRequireDefault(r(3)),s=_interopRequireDefault(r(5)),u=_interopRequireDefault(r(6)),l=r(0),c=_interopRequireDefault(l),p=_interopRequireDefault(r(1)),f=r(7),d=_interopRequireDefault(f),h=_interopRequireDefault(r(12)),m=r(35);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var v=function(e){function Parameters(e){(0,o.default)(this,Parameters);var t=(0,s.default)(this,(Parameters.__proto__||(0,i.default)(Parameters)).call(this,e));return t.onChange=function(e,r,n){var i=t.props;(0,i.specActions.changeParamByIdentity)(i.onChangeKey,e,r,n)},t.onChangeConsumesWrapper=function(e){var r=t.props;(0,r.specActions.changeConsumesValue)(r.onChangeKey,e)},t.toggleTab=function(e){return"parameters"===e?t.setState({parametersVisible:!0,callbackVisible:!1}):"callbacks"===e?t.setState({callbackVisible:!0,parametersVisible:!1}):void 0},t.state={callbackVisible:!1,parametersVisible:!0},t}return(0,u.default)(Parameters,e),(0,a.default)(Parameters,[{key:"render",value:function render(){var e=this,t=this.props,r=t.onTryoutClick,i=t.onCancelClick,o=t.parameters,a=t.allowTryItOut,s=t.tryItOutEnabled,u=t.fn,l=t.getComponent,p=t.getConfigs,h=t.specSelectors,m=t.oas3Actions,v=t.oas3Selectors,g=t.pathMethod,y=t.specPath,_=t.operation,b=l("parameterRow"),S=l("TryItOutButton"),k=l("contentType"),x=l("Callbacks",!0),E=l("RequestBody",!0),C=s&&a,w=h.isOAS3,D=_.get("requestBody"),A=y.slice(0,-1).push("requestBody");return c.default.createElement("div",{className:"opblock-section"},c.default.createElement("div",{className:"opblock-section-header"},c.default.createElement("div",{className:"tab-header"},c.default.createElement("div",{onClick:function onClick(){return e.toggleTab("parameters")},className:"tab-item "+(this.state.parametersVisible&&"active")},c.default.createElement("h4",{className:"opblock-title"},c.default.createElement("span",null,"Parameters"))),_.get("callbacks")?c.default.createElement("div",{onClick:function onClick(){return e.toggleTab("callbacks")},className:"tab-item "+(this.state.callbackVisible&&"active")},c.default.createElement("h4",{className:"opblock-title"},c.default.createElement("span",null,"Callbacks"))):null),a?c.default.createElement(S,{enabled:s,onCancelClick:i,onTryoutClick:r}):null),this.state.parametersVisible?c.default.createElement("div",{className:"parameters-container"},o.count()?c.default.createElement("div",{className:"table-container"},c.default.createElement("table",{className:"parameters"},c.default.createElement("thead",null,c.default.createElement("tr",null,c.default.createElement("th",{className:"col col_header parameters-col_name"},"Name"),c.default.createElement("th",{className:"col col_header parameters-col_description"},"Description"))),c.default.createElement("tbody",null,function eachMap(e,t){return e.valueSeq().filter(d.default.Map.isMap).map(t)}(o,function(t,r){return c.default.createElement(b,{fn:u,getComponent:l,specPath:y.push(r),getConfigs:p,rawParam:t,param:h.parameterWithMetaByIdentity(g,t),key:t.get("name"),onChange:e.onChange,onChangeConsumes:e.onChangeConsumesWrapper,specSelectors:h,pathMethod:g,isExecute:C})}).toArray()))):c.default.createElement("div",{className:"opblock-description-wrapper"},c.default.createElement("p",null,"No parameters"))):"",this.state.callbackVisible?c.default.createElement("div",{className:"callbacks-container opblock-description-wrapper"},c.default.createElement(x,{callbacks:(0,f.Map)(_.get("callbacks")),specPath:y.slice(0,-1).push("callbacks")})):"",w()&&D&&this.state.parametersVisible&&c.default.createElement("div",{className:"opblock-section"},c.default.createElement("div",{className:"opblock-section-header"},c.default.createElement("h4",{className:"opblock-title parameter__name "+(D.get("required")&&"required")},"Request body"),c.default.createElement("label",null,c.default.createElement(k,{value:v.requestContentType.apply(v,(0,n.default)(g)),contentTypes:D.get("content").keySeq(),onChange:function onChange(e){m.setRequestContentType({value:e,pathMethod:g})},className:"body-param-content-type"}))),c.default.createElement("div",{className:"opblock-description-wrapper"},c.default.createElement(E,{specPath:A,requestBody:D,requestBodyValue:v.requestBodyValue.apply(v,(0,n.default)(g))||(0,f.Map)(),isExecute:C,onChange:function onChange(e,t){if(t){var r=v.requestBodyValue.apply(v,(0,n.default)(g)),i=f.Map.isMap(r)?r:(0,f.Map)();return m.setRequestBodyValue({pathMethod:g,value:i.setIn(t,e)})}m.setRequestBodyValue({value:e,pathMethod:g})},contentType:v.requestContentType.apply(v,(0,n.default)(g))}))))}}]),Parameters}(l.Component);v.propTypes={parameters:h.default.list.isRequired,specActions:p.default.object.isRequired,operation:p.default.object.isRequired,getComponent:p.default.func.isRequired,getConfigs:p.default.func.isRequired,specSelectors:p.default.object.isRequired,oas3Actions:p.default.object.isRequired,oas3Selectors:p.default.object.isRequired,fn:p.default.object.isRequired,tryItOutEnabled:p.default.bool,allowTryItOut:p.default.bool,specPath:h.default.list.isRequired,onTryoutClick:p.default.func,onCancelClick:p.default.func,onChangeKey:p.default.array,pathMethod:p.default.array.isRequired},v.defaultProps={onTryoutClick:Function.prototype,onCancelClick:Function.prototype,tryItOutEnabled:!1,allowTryItOut:!0,onChangeKey:[]},t.default=(0,m.OAS3ComponentWrapFactory)(v)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}(r(0)),i=r(35);t.default=(0,i.OAS3ComponentWrapFactory)(function(e){var t=e.Ori;return n.default.createElement("span",null,n.default.createElement(t,e),n.default.createElement("small",{style:{backgroundColor:"#89bf04"}},n.default.createElement("pre",{className:"version"},"OAS3")))})},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(35);t.default=(0,n.OAS3ComponentWrapFactory)(function(){return null})},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(26)),i=_interopRequireDefault(r(4)),o=_interopRequireDefault(r(2)),a=_interopRequireDefault(r(3)),s=_interopRequireDefault(r(5)),u=_interopRequireDefault(r(6)),l=r(0),c=_interopRequireDefault(l),p=_interopRequireDefault(r(1)),f=r(35),d=r(454);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var h=function(e){function ModelComponent(){return(0,o.default)(this,ModelComponent),(0,s.default)(this,(ModelComponent.__proto__||(0,i.default)(ModelComponent)).apply(this,arguments))}return(0,u.default)(ModelComponent,e),(0,a.default)(ModelComponent,[{key:"render",value:function render(){var e=this.props,t=e.getConfigs,r=["model-box"],i=null;return!0===e.schema.get("deprecated")&&(r.push("deprecated"),i=c.default.createElement("span",{className:"model-deprecated-warning"},"Deprecated:")),c.default.createElement("div",{className:r.join(" ")},i,c.default.createElement(d.Model,(0,n.default)({},this.props,{getConfigs:t,depth:1,expandDepth:this.props.expandDepth||0})))}}]),ModelComponent}(l.Component);h.propTypes={schema:p.default.object.isRequired,name:p.default.string,getComponent:p.default.func.isRequired,getConfigs:p.default.func.isRequired,specSelectors:p.default.object.isRequired,expandDepth:p.default.number},t.default=(0,f.OAS3ComponentWrapFactory)(h)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=_interopRequireDefault(r(87)),i=_interopRequireDefault(r(0)),o=r(35);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=(0,o.OAS3ComponentWrapFactory)(function(e){var t=e.Ori,r=(0,n.default)(e,["Ori"]),o=r.schema,a=r.getComponent,s=r.errors,u=r.onChange,l=o.type,c=o.format,p=a("Input");return"string"!==l||"binary"!==c&&"base64"!==c?i.default.createElement(t,r):i.default.createElement(p,{type:"file",className:s.length?"invalid":"",title:s.length?s:"",onChange:function onChange(e){u(e.target.files[0])},disabled:t.isDisabled})})},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.serverEffectiveValue=t.serverVariables=t.serverVariableValue=t.responseContentType=t.requestContentType=t.requestBodyValue=t.selectedServer=void 0;var n=r(7),i=r(35);function onlyOAS3(e){return function(){for(var t=arguments.length,r=Array(t),n=0;n=e.length?(this._t=void 0,i(1)):i(0,"keys"==t?r:"values"==t?e[r]:[r,e[r]])},"values"),o.Arguments=o.Array,n("keys"),n("values"),n("entries")},function(e,t){e.exports=function(){}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,r){"use strict";var n=r(170),i=r(101),o=r(103),a={};r(54)(a,r(20)("iterator"),function(){return this}),e.exports=function(e,t,r){e.prototype=n(a,{next:i(1,r)}),o(e,t+" Iterator")}},function(e,t,r){var n=r(41),i=r(36),o=r(102);e.exports=r(47)?Object.defineProperties:function defineProperties(e,t){i(e);for(var r,a=o(t),s=a.length,u=0;s>u;)n.f(e,r=a[u++],t[r]);return e}},function(e,t,r){var n=r(73),i=r(122),o=r(500);e.exports=function(e){return function(t,r,a){var s,u=n(t),l=i(u.length),c=o(a,l);if(e&&r!=r){for(;l>c;)if((s=u[c++])!=s)return!0}else for(;l>c;c++)if((e||c in u)&&u[c]===r)return e||c||0;return!e&&-1}}},function(e,t,r){var n=r(171),i=Math.max,o=Math.min;e.exports=function(e,t){return(e=n(e))<0?i(e+t,0):o(e,t)}},function(e,t,r){var n=r(171),i=r(166);e.exports=function(e){return function(t,r){var o,a,s=String(i(t)),u=n(r),l=s.length;return u<0||u>=l?e?"":void 0:(o=s.charCodeAt(u))<55296||o>56319||u+1===l||(a=s.charCodeAt(u+1))<56320||a>57343?e?s.charAt(u):o:e?s.slice(u,u+2):a-56320+(o-55296<<10)+65536}}},function(e,t,r){var n=r(36),i=r(175);e.exports=r(15).getIterator=function(e){var t=i(e);if("function"!=typeof t)throw TypeError(e+" is not iterable!");return n(t.call(e))}},function(e,t,r){r(504),r(267),r(515),r(519),r(530),r(531),e.exports=r(63).Promise},function(e,t,r){"use strict";var n=r(177),i={};i[r(17)("toStringTag")]="z",i+""!="[object z]"&&r(75)(Object.prototype,"toString",function toString(){return"[object "+n(this)+"]"},!0)},function(e,t,r){e.exports=!r(106)&&!r(107)(function(){return 7!=Object.defineProperty(r(179)("div"),"a",{get:function(){return 7}}).a})},function(e,t,r){var n=r(76);e.exports=function(e,t){if(!n(e))return e;var r,i;if(t&&"function"==typeof(r=e.toString)&&!n(i=r.call(e)))return i;if("function"==typeof(r=e.valueOf)&&!n(i=r.call(e)))return i;if(!t&&"function"==typeof(r=e.toString)&&!n(i=r.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},function(e,t,r){"use strict";var n=r(508),i=r(266),o=r(181),a={};r(61)(a,r(17)("iterator"),function(){return this}),e.exports=function(e,t,r){e.prototype=n(a,{next:i(1,r)}),o(e,t+" Iterator")}},function(e,t,r){var n=r(62),i=r(509),o=r(273),a=r(180)("IE_PROTO"),s=function(){},u=function(){var e,t=r(179)("iframe"),n=o.length;for(t.style.display="none",r(274).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("