diff --git a/.gitignore b/.gitignore
index 9eb2e047..a0409be7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
.vs/
.vscode/
.idea/
+[Pp]roperties/
# Build results
[Dd]ebug/
diff --git a/.gitmodules b/.gitmodules
index 3d0f5fc4..ca8763b0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
-[submodule "TS3Client/Declarations"]
- path = TS3Client/Declarations
+[submodule "TSLib/Declarations"]
+ path = TSLib/Declarations
url = https://github.com/ReSpeak/tsdeclarations
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 77337080..09d17b02 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -1,6 +1,8 @@
-
-
+
+
+
+
diff --git a/GitVersion.yml b/GitVersion.yml
new file mode 100644
index 00000000..063f877f
--- /dev/null
+++ b/GitVersion.yml
@@ -0,0 +1,23 @@
+assembly-versioning-scheme: MajorMinorPatch
+mode: Mainline
+branches:
+ master:
+ tag: ''
+ increment: Patch
+ prevent-increment-of-merged-branch-version: true
+ track-merge-target: false
+ regex: ^master$
+ tracks-release-branches: false
+ is-release-branch: false
+ is-mainline: true
+ develop:
+ tag: alpha
+ increment: Minor
+ prevent-increment-of-merged-branch-version: false
+ track-merge-target: true
+ regex: ^develop$
+ tracks-release-branches: true
+ is-release-branch: false
+ignore:
+ sha: []
+merge-message-formats: {}
diff --git a/README.md b/README.md
index 530cc523..1144a0bf 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ Translations need to be manually approved and will then be automatically built a
This project is licensed under [OSL-3.0](https://opensource.org/licenses/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.
+- 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 TSLib as a library.
- If you create plugins you do not have to make them public like in GPL. (Although we would be happy 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
diff --git a/TS3ABotUnitTests/BotCommandTests.cs b/TS3ABotUnitTests/BotCommandTests.cs
index 4bc37874..14376c5f 100644
--- a/TS3ABotUnitTests/BotCommandTests.cs
+++ b/TS3ABotUnitTests/BotCommandTests.cs
@@ -1,48 +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 .
+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.Ast;
+using TS3AudioBot.CommandSystem.CommandResults;
+using TS3AudioBot.CommandSystem.Commands;
+using TS3AudioBot.Dependency;
+using TSLib;
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.Dependency;
- using TS3AudioBot.Algorithm;
- using TS3AudioBot.Audio;
- using TS3AudioBot.CommandSystem;
- using TS3AudioBot.CommandSystem.Ast;
- using TS3AudioBot.CommandSystem.CommandResults;
- using TS3AudioBot.CommandSystem.Commands;
-
[TestFixture]
public class BotCommandTests
{
- private readonly CommandManager cmdMgr;
-
- public BotCommandTests()
- {
- cmdMgr = new CommandManager(null);
- cmdMgr.RegisterCollection(MainCommands.Bag);
- }
-
[Test]
public void BotCommandTest()
{
var execInfo = Utils.GetExecInfo("ic3");
string CallCommand(string command)
{
- return cmdMgr.CommandSystem.ExecuteCommand(execInfo, command);
+ return CommandManager.ExecuteCommand(execInfo, command);
}
var output = CallCommand("!help");
@@ -93,6 +76,23 @@ string CallCommand(string command)
Assert.Throws(() => CallCommand("!if a == b text (!)"));
}
+ [Test]
+ public void TailStringTest()
+ {
+ var execInfo = Utils.GetExecInfo("ic3");
+ var group = execInfo.GetModule().RootGroup;
+ group.AddCommand("cmd", new FunctionCommand(s => s));
+ string CallCommand(string command)
+ {
+ return CommandManager.ExecuteCommand(execInfo, command);
+ }
+
+ Assert.AreEqual("a", CallCommand("!cmd a"));
+ Assert.AreEqual("a b", CallCommand("!cmd a b"));
+ Assert.AreEqual("a", CallCommand("!cmd a \" b"));
+ Assert.AreEqual("a b 1", CallCommand("!cmd a b 1"));
+ }
+
[Test]
public void XCommandSystemFilterTest()
{
@@ -145,32 +145,30 @@ public void XCommandSystemFilterTest()
[Test]
public void XCommandSystemTest()
{
- var execInfo = Utils.GetExecInfo("ic3");
- var commandSystem = new XCommandSystem();
- var group = commandSystem.RootCommand;
+ var execInfo = Utils.GetExecInfo("ic3", false);
+ var group = execInfo.GetModule().RootGroup;
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(execInfo,
- new ICommand[] { new StringCommand("one") })).Content);
- Assert.AreEqual("ONE", commandSystem.ExecuteCommand(execInfo, "!one"));
- Assert.AreEqual("TWO", commandSystem.ExecuteCommand(execInfo, "!t"));
- Assert.AreEqual("TEST", commandSystem.ExecuteCommand(execInfo, "!e TEST"));
- Assert.AreEqual("ONE", commandSystem.ExecuteCommand(execInfo, "!o"));
+ Assert.AreEqual("ONE", CommandManager.ExecuteCommand(execInfo, new ICommand[] { new ResultCommand(new PrimitiveResult("one")) }));
+ Assert.AreEqual("ONE", CommandManager.ExecuteCommand(execInfo, "!one"));
+ Assert.AreEqual("TWO", CommandManager.ExecuteCommand(execInfo, "!t"));
+ Assert.AreEqual("TEST", CommandManager.ExecuteCommand(execInfo, "!e TEST"));
+ Assert.AreEqual("ONE", CommandManager.ExecuteCommand(execInfo, "!o"));
// Optional parameters
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!e"));
- Assert.AreEqual("NULL", commandSystem.ExecuteCommand(execInfo, "!op"));
- Assert.AreEqual("NOT NULL", commandSystem.ExecuteCommand(execInfo, "!op 1"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!e"));
+ Assert.AreEqual("NULL", CommandManager.ExecuteCommand(execInfo, "!op"));
+ Assert.AreEqual("NOT NULL", CommandManager.ExecuteCommand(execInfo, "!op 1"));
// Command chaining
- Assert.AreEqual("TEST", commandSystem.ExecuteCommand(execInfo, "!e (!e TEST)"));
- Assert.AreEqual("TWO", commandSystem.ExecuteCommand(execInfo, "!e (!t)"));
- Assert.AreEqual("NOT NULL", commandSystem.ExecuteCommand(execInfo, "!op (!e TEST)"));
- Assert.AreEqual("ONE", commandSystem.ExecuteCommand(execInfo, "!(!e on)"));
+ Assert.AreEqual("TEST", CommandManager.ExecuteCommand(execInfo, "!e (!e TEST)"));
+ Assert.AreEqual("TWO", CommandManager.ExecuteCommand(execInfo, "!e (!t)"));
+ Assert.AreEqual("NOT NULL", CommandManager.ExecuteCommand(execInfo, "!op (!e TEST)"));
+ Assert.AreEqual("ONE", CommandManager.ExecuteCommand(execInfo, "!(!e on)"));
// Command overloading
var intCom = new Func(_ => "INT");
@@ -180,17 +178,16 @@ public void XCommandSystemTest()
new FunctionCommand(strCom.Method, strCom.Target)
}));
- Assert.AreEqual("INT", commandSystem.ExecuteCommand(execInfo, "!overlord 1"));
- Assert.AreEqual("STRING", commandSystem.ExecuteCommand(execInfo, "!overlord a"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!overlord"));
+ Assert.AreEqual("INT", CommandManager.ExecuteCommand(execInfo, "!overlord 1"));
+ Assert.AreEqual("STRING", CommandManager.ExecuteCommand(execInfo, "!overlord a"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!overlord"));
}
[Test]
public void XCommandSystemTest2()
{
var execInfo = Utils.GetExecInfo("exact");
- var commandSystem = new XCommandSystem();
- var group = commandSystem.RootCommand;
+ var group = execInfo.GetModule().RootGroup;
var o1 = new OverloadedFunctionCommand();
o1.AddCommand(new FunctionCommand(new Action((_) => { })));
@@ -204,25 +201,25 @@ public void XCommandSystemTest2()
o2.AddCommand("b", new FunctionCommand(new Action(() => { })));
group.AddCommand("three", o2);
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!one"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!one \"\""));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!one (!print \"\")"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!one string"));
- Assert.DoesNotThrow(() => commandSystem.ExecuteCommand(execInfo, "!one 42"));
- Assert.DoesNotThrow(() => commandSystem.ExecuteCommand(execInfo, "!one 4200000000000"));
-
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!two"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!two \"\""));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!two (!print \"\")"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!two 42"));
- Assert.DoesNotThrow(() => commandSystem.ExecuteCommand(execInfo, "!two None"));
-
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!three"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!three \"\""));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!three (!print \"\")"));
- Assert.Throws(() => commandSystem.ExecuteCommand(execInfo, "!three c"));
- Assert.DoesNotThrow(() => commandSystem.ExecuteCommand(execInfo, "!three a"));
- Assert.DoesNotThrow(() => commandSystem.ExecuteCommand(execInfo, "!three b"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!one"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!one \"\""));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!one (!print \"\")"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!one string"));
+ Assert.DoesNotThrow(() => CommandManager.ExecuteCommand(execInfo, "!one 42"));
+ Assert.DoesNotThrow(() => CommandManager.ExecuteCommand(execInfo, "!one 4200000000000"));
+
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!two"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!two \"\""));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!two (!print \"\")"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!two 42"));
+ Assert.DoesNotThrow(() => CommandManager.ExecuteCommand(execInfo, "!two None"));
+
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!three"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!three \"\""));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!three (!print \"\")"));
+ Assert.Throws(() => CommandManager.ExecuteCommand(execInfo, "!three c"));
+ Assert.DoesNotThrow(() => CommandManager.ExecuteCommand(execInfo, "!three a"));
+ Assert.DoesNotThrow(() => CommandManager.ExecuteCommand(execInfo, "!three b"));
}
[Test]
@@ -231,10 +228,16 @@ public void EnsureAllCommandsHaveEnglishDocumentationEntry()
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en");
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en");
+ var execInfo = Utils.GetExecInfo("exact");
+ var cmdMgr = execInfo.GetModule();
+ var errors = new List();
foreach (var cmd in cmdMgr.AllCommands)
{
- Assert.IsFalse(string.IsNullOrEmpty(cmd.Description), $"Command {cmd.FullQualifiedName} has no documentation");
+ if (string.IsNullOrEmpty(cmd.Description))
+ errors.Add($"Command {cmd.FullQualifiedName} has no documentation");
}
+ if (errors.Count > 0)
+ Assert.Fail(string.Join("\n", errors));
}
[Test]
@@ -269,19 +272,15 @@ public static void TestStringParsing(string inp, string outp)
internal static class Utils
{
- private static readonly CommandManager cmdMgr;
-
- static Utils()
+ public static ExecutionInformation GetExecInfo(string matcher, bool addMainCommands = true)
{
- cmdMgr = new CommandManager(null);
- cmdMgr.RegisterCollection(MainCommands.Bag);
- }
+ var cmdMgr = new CommandManager(null);
+ if (addMainCommands)
+ cmdMgr.RegisterCollection(MainCommands.Bag);
- public static ExecutionInformation GetExecInfo(string matcher)
- {
var execInfo = new ExecutionInformation();
execInfo.AddModule(new CallerInfo(false) { SkipRightsChecks = true, CommandComplexityMax = int.MaxValue });
- execInfo.AddModule(new InvokerData("InvokerUid"));
+ execInfo.AddModule(new InvokerData((Uid)"InvokerUid"));
execInfo.AddModule(Filter.GetFilterByName(matcher));
execInfo.AddModule(cmdMgr);
return execInfo;
diff --git a/TS3ABotUnitTests/M3uParserTests.cs b/TS3ABotUnitTests/M3uParserTests.cs
index f20ae08a..c4d132c0 100644
--- a/TS3ABotUnitTests/M3uParserTests.cs
+++ b/TS3ABotUnitTests/M3uParserTests.cs
@@ -1,12 +1,12 @@
+using NUnit.Framework;
+using System.IO;
+using System.Text;
+using TS3AudioBot.ResourceFactories.AudioTags;
+
namespace TS3ABotUnitTests
{
- using NUnit.Framework;
- using System.IO;
- using System.Text;
- using TS3AudioBot.ResourceFactories.AudioTags;
-
[TestFixture]
- class M3uParserTests
+ internal class M3uParserTests
{
[Test]
public void SimpleListTest()
diff --git a/TS3ABotUnitTests/RingQueueTest.cs b/TS3ABotUnitTests/RingQueueTest.cs
index 9162e4d4..36398cc2 100644
--- a/TS3ABotUnitTests/RingQueueTest.cs
+++ b/TS3ABotUnitTests/RingQueueTest.cs
@@ -1,11 +1,11 @@
+using NUnit.Framework;
+using System;
+using TSLib.Full;
+
namespace TS3ABotUnitTests
{
- using NUnit.Framework;
- using System;
- using TS3Client.Full;
-
[TestFixture]
- class RingQueueTest
+ internal class RingQueueTest
{
[Test]
diff --git a/TS3ABotUnitTests/TS3ABotUnitTests.csproj b/TS3ABotUnitTests/TS3ABotUnitTests.csproj
index 42d4c87f..b2a6d5cd 100644
--- a/TS3ABotUnitTests/TS3ABotUnitTests.csproj
+++ b/TS3ABotUnitTests/TS3ABotUnitTests.csproj
@@ -2,7 +2,7 @@
Library
- netcoreapp2.2;net472
+ netcoreapp2.2;netcoreapp3.1;net472
7.3
AnyCPU
@@ -11,13 +11,16 @@
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
+
diff --git a/TS3ABotUnitTests/TS3MessageParserTests.cs b/TS3ABotUnitTests/TS3MessageParserTests.cs
index ff567684..2a992bd7 100644
--- a/TS3ABotUnitTests/TS3MessageParserTests.cs
+++ b/TS3ABotUnitTests/TS3MessageParserTests.cs
@@ -1,12 +1,12 @@
+using NUnit.Framework;
+using System.Collections;
+using System.Reflection;
+using System.Text;
+using TSLib;
+using TSLib.Messages;
+
namespace TS3ABotUnitTests
{
- using NUnit.Framework;
- using System.Collections;
- using System.Reflection;
- using System.Text;
- using TS3Client;
- using TS3Client.Messages;
-
[TestFixture]
public class TS3MessageParserTests
{
@@ -20,7 +20,7 @@ public void Deserializer1Test()
var notifv = notif.Value;
Assert.AreEqual(notifv.Length, 1);
var notifs = notifv[0];
- AssertEx.PropertyValuesAreEquals(notifs, new ChannelChanged() { ChannelId = 6 });
+ AssertEx.PropertyValuesAreEquals(notifs, new ChannelChanged() { ChannelId = (ChannelId)6 });
}
[Test]
@@ -31,7 +31,7 @@ public void Deserializer2Test()
var notifv = notif.Value;
Assert.AreEqual(notifv.Length, 1);
var notifs = notifv[0];
- AssertEx.PropertyValuesAreEquals(notifs, new ClientChatComposing() { ClientId = 42, ClientUid = "asdfe/rvt==" });
+ AssertEx.PropertyValuesAreEquals(notifs, new ClientChatComposing() { ClientId = (ClientId)42, ClientUid = (Uid)"asdfe/rvt==" });
}
[Test]
@@ -41,8 +41,8 @@ public void Deserializer3Test()
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 });
+ AssertEx.PropertyValuesAreEquals(notifv[0], new ChannelChanged() { ChannelId = (ChannelId)5 });
+ AssertEx.PropertyValuesAreEquals(notifv[1], new ChannelChanged() { ChannelId = (ChannelId)4 });
}
[Test]
@@ -52,8 +52,8 @@ public void Deserializer4Test()
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==" });
+ AssertEx.PropertyValuesAreEquals(notifv[0], new ClientChatComposing() { ClientId = (ClientId)42, ClientUid = (Uid)"asdfe/rvt==" });
+ AssertEx.PropertyValuesAreEquals(notifv[1], new ClientChatComposing() { ClientId = (ClientId)1337, ClientUid = (Uid)"asdfe/rvt==" });
}
[Test]
@@ -68,10 +68,10 @@ public void Deserializer5Test()
Assert.True(notif.Ok);
var notifv = notif.Value;
Assert.AreEqual(notifv.Length, 4);
- AssertEx.PropertyValuesAreEquals(notifv[0], new ClientList() { ClientId = 1, ChannelId = 1, DatabaseId = 2, Name = "TestBob1", ClientType = ClientType.Full, Uid = "u/dFMOFFipxS9fJ8HKv0KH6WVzA=" });
- AssertEx.PropertyValuesAreEquals(notifv[1], new ClientList() { ClientId = 2, ChannelId = 4, DatabaseId = 2, Name = "TestBob", ClientType = ClientType.Full, Uid = "u/dFMOFFipxS9fJ8HKv0KH6WVzA=" });
- AssertEx.PropertyValuesAreEquals(notifv[2], new ClientList() { ClientId = 3, ChannelId = 4, DatabaseId = 6, Name = "Splamy", ClientType = ClientType.Full, Uid = "uA0U7t4PBxdJ5TLnarsOHQh4/tY=" });
- AssertEx.PropertyValuesAreEquals(notifv[3], new ClientList() { ClientId = 4, ChannelId = 4, DatabaseId = 7, Name = "AudioBud", ClientType = ClientType.Full, Uid = "b+P0CqXms5I0C+A66HZ4Sbu/PNw=" });
+ AssertEx.PropertyValuesAreEquals(notifv[0], new ClientList() { ClientId = (ClientId)1, ChannelId = (ChannelId)1, DatabaseId = (ClientDbId)2, Name = "TestBob1", ClientType = ClientType.Full, Uid = (Uid)"u/dFMOFFipxS9fJ8HKv0KH6WVzA=" });
+ AssertEx.PropertyValuesAreEquals(notifv[1], new ClientList() { ClientId = (ClientId)2, ChannelId = (ChannelId)4, DatabaseId = (ClientDbId)2, Name = "TestBob", ClientType = ClientType.Full, Uid = (Uid)"u/dFMOFFipxS9fJ8HKv0KH6WVzA=" });
+ AssertEx.PropertyValuesAreEquals(notifv[2], new ClientList() { ClientId = (ClientId)3, ChannelId = (ChannelId)4, DatabaseId = (ClientDbId)6, Name = "Splamy", ClientType = ClientType.Full, Uid = (Uid)"uA0U7t4PBxdJ5TLnarsOHQh4/tY=" });
+ AssertEx.PropertyValuesAreEquals(notifv[3], new ClientList() { ClientId = (ClientId)4, ChannelId = (ChannelId)4, DatabaseId = (ClientDbId)7, Name = "AudioBud", ClientType = ClientType.Full, Uid = (Uid)"b+P0CqXms5I0C+A66HZ4Sbu/PNw=" });
}
[Test]
diff --git a/TS3ABotUnitTests/UnitTests.cs b/TS3ABotUnitTests/UnitTests.cs
index 4fa1fb7f..ede3526b 100644
--- a/TS3ABotUnitTests/UnitTests.cs
+++ b/TS3ABotUnitTests/UnitTests.cs
@@ -1,31 +1,24 @@
-// 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 .
+using NUnit.Framework;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using TS3AudioBot;
+using TS3AudioBot.Config;
+using TS3AudioBot.Helper;
+using TS3AudioBot.History;
+using TS3AudioBot.Playlists.Shuffle;
+using TS3AudioBot.ResourceFactories;
+using TS3AudioBot.ResourceFactories.Youtube;
+using TSLib;
+using TSLib.Full;
+using TSLib.Messages;
namespace TS3ABotUnitTests
{
- using NUnit.Framework;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Text.RegularExpressions;
- using System.Threading;
- using TS3AudioBot;
- using TS3AudioBot.Config;
- using TS3AudioBot.Helper;
- using TS3AudioBot.History;
- using TS3AudioBot.Playlists.Shuffle;
- using TS3AudioBot.ResourceFactories;
- using TS3Client.Full;
- using TS3Client.Messages;
-
[TestFixture]
public class UnitTests
{
@@ -39,8 +32,8 @@ public void HistoryFileIntergrityTest()
string testFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "history.test");
if (File.Exists(testFile)) File.Delete(testFile);
- var inv1 = new ClientList { ClientId = 10, Uid = "Uid1", Name = "Invoker1" };
- var inv2 = new ClientList { ClientId = 20, Uid = "Uid2", Name = "Invoker2" };
+ var inv1 = new ClientList { ClientId = (ClientId)10, Uid = (Uid)"Uid1", Name = "Invoker1" };
+ var inv2 = new ClientList { ClientId = (ClientId)20, Uid = (Uid)"Uid2", Name = "Invoker2" };
var ar1 = new AudioResource("asdf", "sc_ar1", "soundcloud");
var ar2 = new AudioResource("./File.mp3", "me_ar2", "media");
@@ -48,7 +41,7 @@ public void HistoryFileIntergrityTest()
var data1 = new HistorySaveData(ar1, inv1.Uid);
var data2 = new HistorySaveData(ar2, inv2.Uid);
- var data3 = new HistorySaveData(ar3, "Uid3");
+ var data3 = new HistorySaveData(ar3, (Uid)"Uid3");
var confHistory = ConfigTable.CreateRoot();
confHistory.FillDeletedIds.Value = false;
@@ -100,7 +93,7 @@ void CreateDbStore()
var ale1 = hf.FindEntryByResource(ar1);
hf.RenameEntry(ale1, "sc_ar1X");
- hf.LogAudioResourceDelayed(new HistorySaveData(ale1.AudioResource, "Uid4"));
+ hf.LogAudioResourceDelayed(new HistorySaveData(ale1.AudioResource, (Uid)"Uid4"));
db.Dispose();
@@ -114,14 +107,14 @@ void CreateDbStore()
var ale2 = hf.FindEntryByResource(ar2);
hf.RenameEntry(ale2, "me_ar2_loong1");
- hf.LogAudioResourceDelayed(new HistorySaveData(ale2.AudioResource, "Uid4"));
+ hf.LogAudioResourceDelayed(new HistorySaveData(ale2.AudioResource, (Uid)"Uid4"));
ale1 = hf.FindEntryByResource(ar1);
hf.RenameEntry(ale1, "sc_ar1X_loong1");
- hf.LogAudioResourceDelayed(new HistorySaveData(ale1.AudioResource, "Uid4"));
+ hf.LogAudioResourceDelayed(new HistorySaveData(ale1.AudioResource, (Uid)"Uid4"));
hf.RenameEntry(ale2, "me_ar2_exxxxxtra_loong1");
- hf.LogAudioResourceDelayed(new HistorySaveData(ale2.AudioResource, "Uid4"));
+ hf.LogAudioResourceDelayed(new HistorySaveData(ale2.AudioResource, (Uid)"Uid4"));
db.Dispose();
@@ -183,22 +176,28 @@ public void UtilSeedTest()
[Test]
public void NormalOrderTest()
{
- TestShuffleAlgorithm(new NormalOrder());
+ TestShuffleAlgorithmBiDir(new NormalOrder());
}
[Test]
public void ListedShuffleTest()
{
- TestShuffleAlgorithm(new ListedShuffle());
+ TestShuffleAlgorithmBiDir(new ListedShuffle());
}
[Test]
public void LinearFeedbackShiftRegisterTest()
{
- TestShuffleAlgorithm(new LinearFeedbackShiftRegister());
+ TestShuffleAlgorithmBiDir(new LinearFeedbackShiftRegister());
+ }
+
+ private static void TestShuffleAlgorithmBiDir(IShuffleAlgorithm algo)
+ {
+ TestShuffleAlgorithm(algo, true);
+ TestShuffleAlgorithm(algo, false);
}
- private static void TestShuffleAlgorithm(IShuffleAlgorithm algo)
+ private static void TestShuffleAlgorithm(IShuffleAlgorithm algo, bool forward)
{
for (int i = 1; i < 1000; i++)
{
@@ -209,7 +208,8 @@ private static void TestShuffleAlgorithm(IShuffleAlgorithm algo)
for (int j = 0; j < i; j++)
{
- algo.Next();
+ if (forward) algo.Next();
+ else algo.Prev();
int shufNum = algo.Index;
if (checkNumbers.Get(shufNum))
Assert.Fail("Duplicate number");
@@ -223,25 +223,26 @@ private static void TestShuffleAlgorithm(IShuffleAlgorithm algo)
[Test]
public void Factory_YoutubeFactoryTest()
{
- using (IResourceFactory rfac = new YoutubeFactory())
+ var ctx = new ResolveContext(null, null);
+ using (IResourceResolver rfac = new YoutubeResolver(new ConfFactories()))
{
// matching links
- Assert.AreEqual(rfac.MatchResource(@"https://www.youtube.com/watch?v=robqdGEhQWo"), MatchCertainty.Always);
- Assert.AreEqual(rfac.MatchResource(@"https://youtu.be/robqdGEhQWo"), MatchCertainty.Always);
- Assert.AreEqual(rfac.MatchResource(@"https://discarded-ideas.org/sites/discarded-ideas.org/files/music/darkforestkeep_symphonic.mp3"), MatchCertainty.Never);
- Assert.AreNotEqual(rfac.MatchResource(@"http://splamy.de/youtube.com/youtu.be/fake.mp3"), MatchCertainty.Always);
+ Assert.AreEqual(rfac.MatchResource(ctx, @"https://www.youtube.com/watch?v=robqdGEhQWo"), MatchCertainty.Always);
+ Assert.AreEqual(rfac.MatchResource(ctx, @"https://youtu.be/robqdGEhQWo"), MatchCertainty.Always);
+ Assert.AreEqual(rfac.MatchResource(ctx, @"https://discarded-ideas.org/sites/discarded-ideas.org/files/music/darkforestkeep_symphonic.mp3"), MatchCertainty.Never);
+ Assert.AreNotEqual(rfac.MatchResource(ctx, @"http://splamy.de/youtube.com/youtu.be/fake.mp3"), MatchCertainty.Always);
// restoring links
- Assert.AreEqual("https://youtu.be/robqdGEhQWo", rfac.RestoreLink(new AudioResource { ResourceId = "robqdGEhQWo" }));
+ Assert.AreEqual("https://youtu.be/robqdGEhQWo", rfac.RestoreLink(ctx, new AudioResource { ResourceId = "robqdGEhQWo" }));
}
}
- /* ======================= TS3Client Tests ========================*/
+ /* ======================= TSLib Tests ========================*/
[Test]
public void VersionSelfCheck()
{
- Ts3Crypt.VersionSelfCheck();
+ TsCrypt.VersionSelfCheck();
}
}
diff --git a/TS3AudioBot.ruleset b/TS3AudioBot.ruleset
index dbfef148..ce3d5cd3 100644
--- a/TS3AudioBot.ruleset
+++ b/TS3AudioBot.ruleset
@@ -1,5 +1,5 @@
-
-
+
+
@@ -83,7 +83,7 @@
-
+
@@ -93,4 +93,4 @@
-
\ No newline at end of file
+
diff --git a/TS3AudioBot.sln b/TS3AudioBot.sln
index 53cc1fad..4b9159f3 100644
--- a/TS3AudioBot.sln
+++ b/TS3AudioBot.sln
@@ -6,12 +6,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3AudioBot", "TS3AudioBot\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3ABotUnitTests", "TS3ABotUnitTests\TS3ABotUnitTests.csproj", "{20B6F767-5396-41D9-83D8-98B5730C6E2E}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3Client", "TS3Client\TS3Client.csproj", "{0EB99E9D-87E5-4534-A100-55D231C2B6A6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TSLib", "TSLib\TSLib.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
Directory.Build.targets = Directory.Build.targets
+ GitVersion.yml = GitVersion.yml
README.md = README.md
TS3AudioBot.ruleset = TS3AudioBot.ruleset
EndProjectSection
diff --git a/TS3AudioBot/Algorithm/IFilterAlgorithm.cs b/TS3AudioBot/Algorithm/IFilterAlgorithm.cs
index 2ee75994..b813988e 100644
--- a/TS3AudioBot/Algorithm/IFilterAlgorithm.cs
+++ b/TS3AudioBot/Algorithm/IFilterAlgorithm.cs
@@ -7,11 +7,11 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Collections.Generic;
+using System.Linq;
+
namespace TS3AudioBot.Algorithm
{
- using System.Collections.Generic;
- using System.Linq;
-
public interface IFilter
{
IEnumerable> Filter(IEnumerable> list, string filter);
diff --git a/TS3AudioBot/Algorithm/LruCache.cs b/TS3AudioBot/Algorithm/LruCache.cs
index 03738f05..69e57906 100644
--- a/TS3AudioBot/Algorithm/LruCache.cs
+++ b/TS3AudioBot/Algorithm/LruCache.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Collections.Generic;
+
namespace TS3AudioBot.Algorithm
{
- using System.Collections.Generic;
-
public class LruCache
{
private readonly int maxCapacity;
diff --git a/TS3AudioBot/Algorithm/TimedCache.cs b/TS3AudioBot/Algorithm/TimedCache.cs
index a833577a..6be37bcd 100644
--- a/TS3AudioBot/Algorithm/TimedCache.cs
+++ b/TS3AudioBot/Algorithm/TimedCache.cs
@@ -7,13 +7,13 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using TSLib.Helper;
+
namespace TS3AudioBot.Algorithm
{
- using System;
- using System.Collections.Concurrent;
- using System.Linq;
- using TS3AudioBot.Helper;
-
public class TimedCache
{
public TimeSpan Timeout { get; }
@@ -30,7 +30,7 @@ public TimedCache(TimeSpan timeout)
public bool TryGetValue(TK key, out TV value)
{
if (!cachedData.TryGetValue(key, out var data)
- || Util.GetNow() - Timeout > data.Timestamp)
+ || Tools.Now - Timeout > data.Timestamp)
{
CleanCache();
value = default;
@@ -42,7 +42,7 @@ public bool TryGetValue(TK key, out TV value)
public void Set(TK key, TV value)
{
- cachedData[key] = new TimedData { Data = value, Timestamp = Util.GetNow() };
+ cachedData[key] = new TimedData { Data = value, Timestamp = Tools.Now };
}
public void Clear()
@@ -52,7 +52,7 @@ public void Clear()
private void CleanCache()
{
- var now = Util.GetNow() - Timeout;
+ var now = Tools.Now - Timeout;
foreach (var item in cachedData.Where(kvp => now > kvp.Value.Timestamp).ToList())
{
cachedData.TryRemove(item.Key, out _);
diff --git a/TS3AudioBot/Audio/AudioValues.cs b/TS3AudioBot/Audio/AudioValues.cs
index 2a11f4d3..53601bf3 100644
--- a/TS3AudioBot/Audio/AudioValues.cs
+++ b/TS3AudioBot/Audio/AudioValues.cs
@@ -7,11 +7,11 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using TSLib.Helper;
+
namespace TS3AudioBot.Audio
{
- using Helper;
- using System;
-
public static class AudioValues
{
public const float MinVolume = 0;
@@ -33,7 +33,7 @@ public static float HumanVolumeToFactor(float value)
value = (value - MinVolume) / (MaxVolume - MinVolume);
// Scale the value logarithmically
- return Util.Clamp((float)(fact_a * Math.Exp(fact_b * value)) - fact_a, 0, 1);
+ return Tools.Clamp((float)(fact_a * Math.Exp(fact_b * value)) - fact_a, 0, 1);
}
public static float FactorToHumanVolume(float value)
@@ -42,7 +42,7 @@ public static float FactorToHumanVolume(float value)
if (value > 1) return MaxVolume;
// Undo logarithmical scale
- value = Util.Clamp((float)(Math.Log((value + fact_a) / fact_a) / fact_b), 0, 1);
+ value = Tools.Clamp((float)(Math.Log((value + fact_a) / fact_a) / fact_b), 0, 1);
// Map input values from [0, 1] to [MinVolume, MaxVolume]
return (value * (MaxVolume - MinVolume)) + MinVolume;
diff --git a/TS3AudioBot/Audio/CustomTargetPipe.cs b/TS3AudioBot/Audio/CustomTargetPipe.cs
index ac2d3e57..1f5c2104 100644
--- a/TS3AudioBot/Audio/CustomTargetPipe.cs
+++ b/TS3AudioBot/Audio/CustomTargetPipe.cs
@@ -7,16 +7,15 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TSLib;
+using TSLib.Audio;
+using TSLib.Full;
+
namespace TS3AudioBot.Audio
{
- using Helper;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using TS3Client;
- using TS3Client.Audio;
- using TS3Client.Full;
-
internal class CustomTargetPipe : IVoiceTarget, IAudioPassiveConsumer
{
public TargetSendMode SendMode { get; set; } = TargetSendMode.None;
@@ -24,11 +23,11 @@ internal class CustomTargetPipe : IVoiceTarget, IAudioPassiveConsumer
public GroupWhisperType GroupWhisperType { get; private set; }
public GroupWhisperTarget GroupWhisperTarget { get; private set; }
- public IReadOnlyCollection WhisperClients
+ public IReadOnlyCollection WhisperClients
{
get { lock (subscriptionLockObj) { return clientSubscriptionsSetup.ToArray(); } }
}
- public IReadOnlyCollection WhisperChannel
+ public IReadOnlyCollection WhisperChannel
{
get { lock (subscriptionLockObj) { return channelSubscriptionsSetup.Keys.ToArray(); } }
}
@@ -50,20 +49,18 @@ public bool Active
}
}
- private readonly Dictionary channelSubscriptionsSetup;
- private readonly HashSet clientSubscriptionsSetup;
- private ulong[] channelSubscriptionsCache;
- private ushort[] clientSubscriptionsCache;
+ private readonly Dictionary channelSubscriptionsSetup = new Dictionary();
+ private readonly HashSet clientSubscriptionsSetup = new HashSet();
+ private ChannelId[] channelSubscriptionsCache;
+ private ClientId[] clientSubscriptionsCache;
private bool subscriptionSetupChanged;
private readonly object subscriptionLockObj = new object();
- private readonly Ts3FullClient client;
+ private readonly TsFullClient client;
- public CustomTargetPipe(Ts3FullClient client)
+ public CustomTargetPipe(TsFullClient client)
{
this.client = client;
- Util.Init(out channelSubscriptionsSetup);
- Util.Init(out clientSubscriptionsSetup);
subscriptionSetupChanged = true;
}
@@ -99,7 +96,7 @@ public void SetGroupWhisper(GroupWhisperType type, GroupWhisperTarget target, ul
GroupWhisperTargetId = targetId;
}
- public void WhisperChannelSubscribe(bool temp, params ulong[] channels)
+ public void WhisperChannelSubscribe(bool temp, params ChannelId[] channels)
{
lock (subscriptionLockObj)
{
@@ -118,7 +115,7 @@ public void WhisperChannelSubscribe(bool temp, params ulong[] channels)
}
}
- public void WhisperChannelUnsubscribe(bool temp, params ulong[] channels)
+ public void WhisperChannelUnsubscribe(bool temp, params ChannelId[] channels)
{
lock (subscriptionLockObj)
{
@@ -140,7 +137,7 @@ public void WhisperChannelUnsubscribe(bool temp, params ulong[] channels)
}
}
- public void WhisperClientSubscribe(params ushort[] userId)
+ public void WhisperClientSubscribe(params ClientId[] userId)
{
lock (subscriptionLockObj)
{
@@ -149,7 +146,7 @@ public void WhisperClientSubscribe(params ushort[] userId)
}
}
- public void WhisperClientUnsubscribe(params ushort[] userId)
+ public void WhisperClientUnsubscribe(params ClientId[] userId)
{
lock (subscriptionLockObj)
{
@@ -162,7 +159,7 @@ public void ClearTemporary()
{
lock (subscriptionLockObj)
{
- ulong[] removeList = channelSubscriptionsSetup
+ var removeList = channelSubscriptionsSetup
.Where(kvp => kvp.Value)
.Select(kvp => kvp.Key)
.ToArray();
diff --git a/TS3AudioBot/Audio/FfmpegProducer.cs b/TS3AudioBot/Audio/FfmpegProducer.cs
index be18ead5..f9343560 100644
--- a/TS3AudioBot/Audio/FfmpegProducer.cs
+++ b/TS3AudioBot/Audio/FfmpegProducer.cs
@@ -7,22 +7,22 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using TS3AudioBot.Config;
+using TS3AudioBot.Helper;
+using TSLib.Audio;
+using TSLib.Helper;
+
namespace TS3AudioBot.Audio
{
- using Config;
- using Helper;
- using System;
- using System.ComponentModel;
- using System.Diagnostics;
- using System.Globalization;
- using System.IO;
- using System.Text;
- using System.Text.RegularExpressions;
- using System.Threading;
- using TS3Client.Audio;
- using TS3Client.Helper;
-
- public class FfmpegProducer : IAudioPassiveProducer, ISampleInfo, IDisposable
+ public class FfmpegProducer : IPlayerSource, ISampleInfo, IDisposable
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
private readonly Id id;
@@ -31,7 +31,7 @@ public class FfmpegProducer : IAudioPassiveProducer, ISampleInfo, IDisposable
private const string PreLinkConf = "-hide_banner -nostats -threads 1 -i \"";
private const string PostLinkConf = "\" -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1";
private const string LinkConfIcy = "-hide_banner -nostats -threads 1 -i pipe:0 -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1";
- private readonly TimeSpan retryOnDropBeforeEnd = TimeSpan.FromSeconds(10);
+ private static readonly TimeSpan retryOnDropBeforeEnd = TimeSpan.FromSeconds(10);
private readonly ConfToolsFfmpeg config;
@@ -50,7 +50,7 @@ public FfmpegProducer(ConfToolsFfmpeg config, Id id)
this.id = id;
}
- public E AudioStart(string url) => StartFfmpegProcess(url, TimeSpan.Zero);
+ public E AudioStart(string url, TimeSpan? startOff = null) => StartFfmpegProcess(url, startOff ?? TimeSpan.Zero);
public E AudioStartIcy(string url) => StartFfmpegProcessIcy(url);
@@ -184,15 +184,16 @@ private R SetPosition(TimeSpan value)
return StartFfmpegProcess(lastLink, value);
}
- private R StartFfmpegProcess(string url, TimeSpan offset)
+ private R StartFfmpegProcess(string url, TimeSpan? offsetOpt)
{
StopFfmpegProcess();
Log.Trace("Start request {0}", url);
string arguments;
+ var offset = offsetOpt ?? TimeSpan.Zero;
if (offset > TimeSpan.Zero)
{
- var seek = $"-ss {offset.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}";
+ var seek = string.Format(CultureInfo.InvariantCulture, @"-ss {0:hh\:mm\:ss\.fff}", offset);
arguments = string.Concat(seek, " ", PreLinkConf, url, PostLinkConf, " ", seek);
}
else
@@ -393,7 +394,7 @@ public void FfmpegProcess_ErrorDataReceived(object sender, DataReceivedEventArgs
public void ReadStreamLoop(Id id)
{
- Util.SetLogId(id.ToString());
+ Tools.SetLogId(id.ToString());
const int IcyMaxMeta = 255 * 16;
const int ReadBufferSize = 4096;
diff --git a/TS3AudioBot/Audio/IPlayerConnection.cs b/TS3AudioBot/Audio/IPlayerSource.cs
similarity index 58%
rename from TS3AudioBot/Audio/IPlayerConnection.cs
rename to TS3AudioBot/Audio/IPlayerSource.cs
index 01d23b72..2af20f1d 100644
--- a/TS3AudioBot/Audio/IPlayerConnection.cs
+++ b/TS3AudioBot/Audio/IPlayerSource.cs
@@ -1,4 +1,4 @@
-// TS3AudioBot - An advanced Musicbot for Teamspeak 3
+// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -7,24 +7,17 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using TSLib.Audio;
+
namespace TS3AudioBot.Audio
{
- using System;
- using TS3AudioBot.ResourceFactories;
-
- /// Slim interface to control the audio player.
- public interface IPlayerConnection : IDisposable
+ public interface IPlayerSource : IAudioPassiveProducer
{
event EventHandler OnSongEnd;
event EventHandler OnSongUpdated;
- float Volume { get; set; }
- TimeSpan Position { get; set; }
- bool Paused { get; set; }
TimeSpan Length { get; }
- bool Playing { get; }
-
- E AudioStart(PlayResource url);
- E AudioStop();
+ TimeSpan Position { get; set; }
}
}
diff --git a/TS3AudioBot/Audio/IVoiceTarget.cs b/TS3AudioBot/Audio/IVoiceTarget.cs
index 5e9747a2..ea9a4cf7 100644
--- a/TS3AudioBot/Audio/IVoiceTarget.cs
+++ b/TS3AudioBot/Audio/IVoiceTarget.cs
@@ -7,12 +7,12 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Collections.Generic;
+using TSLib;
+using TSLib.Audio;
+
namespace TS3AudioBot.Audio
{
- using System.Collections.Generic;
- using TS3Client;
- using TS3Client.Audio;
-
/// Used to specify playing mode and active targets to send to.
public interface IVoiceTarget
{
@@ -22,21 +22,21 @@ public interface IVoiceTarget
GroupWhisperTarget GroupWhisperTarget { get; }
void SetGroupWhisper(GroupWhisperType type, GroupWhisperTarget target, ulong targetId);
- IReadOnlyCollection WhisperClients { get; }
- IReadOnlyCollection WhisperChannel { get; }
+ IReadOnlyCollection WhisperClients { get; }
+ IReadOnlyCollection WhisperChannel { get; }
/// Adds a channel to the audio streaming list.
/// The id of the channel.
/// When set to true this channel will be cleared with
/// the next call (unless overwritten with false).
- void WhisperChannelSubscribe(bool temp, params ulong[] channel);
+ void WhisperChannelSubscribe(bool temp, params ChannelId[] channel);
/// Removes a channel from the audio streaming list.
/// The id of the channel.
/// When set to true this channel will be cleared with
/// the next call (unless overwritten with false).
- void WhisperChannelUnsubscribe(bool temp, params ulong[] channel);
+ void WhisperChannelUnsubscribe(bool temp, params ChannelId[] channel);
void ClearTemporary();
- void WhisperClientSubscribe(params ushort[] userId);
- void WhisperClientUnsubscribe(params ushort[] userId);
+ void WhisperClientSubscribe(params ClientId[] userId);
+ void WhisperClientUnsubscribe(params ClientId[] userId);
}
}
diff --git a/TS3AudioBot/Audio/MetaData.cs b/TS3AudioBot/Audio/MetaData.cs
index 40727f6b..bfc12043 100644
--- a/TS3AudioBot/Audio/MetaData.cs
+++ b/TS3AudioBot/Audio/MetaData.cs
@@ -7,19 +7,21 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using TSLib;
+
namespace TS3AudioBot.Audio
{
public sealed class MetaData
{
/// Defaults to: invoker.Uid - Can be set if the owner of a song differs from the invoker.
- public string ResourceOwnerUid { get; set; }
- /// Default: false - Indicates whether the song has been requested from a playlist.
- public PlaySource From { get; set; } = PlaySource.PlayRequest;
+ public Uid ResourceOwnerUid { get; set; }
+ ///
+ public TimeSpan? StartOffset { get; set; }
- public MetaData Clone() => new MetaData
+ public MetaData(TimeSpan? startOffset = null)
{
- ResourceOwnerUid = ResourceOwnerUid,
- From = From,
- };
+ StartOffset = startOffset;
+ }
}
}
diff --git a/TS3AudioBot/Audio/PlayInfoEventArgs.cs b/TS3AudioBot/Audio/PlayInfoEventArgs.cs
index 3ee931ea..a3d1c560 100644
--- a/TS3AudioBot/Audio/PlayInfoEventArgs.cs
+++ b/TS3AudioBot/Audio/PlayInfoEventArgs.cs
@@ -7,24 +7,23 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using TS3AudioBot.ResourceFactories;
+
namespace TS3AudioBot.Audio
{
- using System;
- using TS3AudioBot.ResourceFactories;
-
public sealed class PlayInfoEventArgs : EventArgs
{
public InvokerData Invoker { get; }
public PlayResource PlayResource { get; }
public AudioResource ResourceData => PlayResource.BaseData;
- public MetaData MetaData { get; }
+ public MetaData MetaData => PlayResource.Meta;
public string SourceLink { get; }
- public PlayInfoEventArgs(InvokerData invoker, PlayResource playResource, MetaData meta, string sourceLink)
+ public PlayInfoEventArgs(InvokerData invoker, PlayResource playResource, string sourceLink)
{
Invoker = invoker;
PlayResource = playResource;
- MetaData = meta;
SourceLink = sourceLink;
}
}
diff --git a/TS3AudioBot/Audio/PlayManager.cs b/TS3AudioBot/Audio/PlayManager.cs
index 0e8ce027..6b57a7ae 100644
--- a/TS3AudioBot/Audio/PlayManager.cs
+++ b/TS3AudioBot/Audio/PlayManager.cs
@@ -7,26 +7,29 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TS3AudioBot.Config;
+using TS3AudioBot.Environment;
+using TS3AudioBot.Helper;
+using TS3AudioBot.Localization;
+using TS3AudioBot.Playlists;
+using TS3AudioBot.ResourceFactories;
+using TSLib.Helper;
+
namespace TS3AudioBot.Audio
{
- using Config;
- using Localization;
- using Playlists;
- using ResourceFactories;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using TS3AudioBot.Helper;
-
- /// Provides a convenient inferface for enqueing, playing and registering song events.
+ /// Provides a convenient inferface for enqueing, playing and registering song events.
public class PlayManager
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
private readonly ConfBot confBot;
- private readonly IPlayerConnection playerConnection;
+ private readonly Player playerConnection;
private readonly PlaylistManager playlistManager;
- private readonly ResourceFactory resourceFactory;
+ private readonly ResolveContext resourceResolver;
+ private readonly Stats stats;
public PlayInfoEventArgs CurrentPlayData { get; private set; }
public bool IsPlaying => CurrentPlayData != null;
@@ -34,48 +37,54 @@ public class PlayManager
public event EventHandler OnResourceUpdated;
public event EventHandler BeforeResourceStarted;
public event EventHandler AfterResourceStarted;
- public event EventHandler BeforeResourceStopped;
- public event EventHandler AfterResourceStopped;
+ public event EventHandler ResourceStopped;
+ public event EventHandler PlaybackStopped;
- public PlayManager(ConfBot config, IPlayerConnection playerConnection, PlaylistManager playlistManager, ResourceFactory resourceFactory)
+ public PlayManager(ConfBot config, Player playerConnection, PlaylistManager playlistManager, ResolveContext resourceResolver, Stats stats)
{
confBot = config;
this.playerConnection = playerConnection;
this.playlistManager = playlistManager;
- this.resourceFactory = resourceFactory;
+ this.resourceResolver = resourceResolver;
+ this.stats = stats;
}
- public E Enqueue(InvokerData invoker, AudioResource ar) => Enqueue(invoker, new PlaylistItem(ar));
- public E Enqueue(InvokerData invoker, string message, string audioType = null)
+ public E Enqueue(InvokerData invoker, AudioResource ar, MetaData meta = null) => Enqueue(invoker, new PlaylistItem(ar, meta));
+ public E Enqueue(InvokerData invoker, string message, string audioType = null, MetaData meta = null)
{
- var result = resourceFactory.Load(message, audioType);
+ var result = resourceResolver.Load(message, audioType);
if (!result)
+ {
+ stats.TrackSongLoad(audioType, false, true);
return result.Error;
- return Enqueue(invoker, new PlaylistItem(result.Value.BaseData));
+ }
+ return Enqueue(invoker, new PlaylistItem(result.Value.BaseData, meta));
}
public E Enqueue(InvokerData invoker, IEnumerable items)
{
+ var startOff = playlistManager.CurrentList.Items.Count;
playlistManager.Queue(items.Select(x => UpdateItem(invoker, x)));
- return PostEnqueue(invoker);
+ return PostEnqueue(invoker, startOff);
}
public E Enqueue(InvokerData invoker, PlaylistItem item)
{
+ var startOff = playlistManager.CurrentList.Items.Count;
playlistManager.Queue(UpdateItem(invoker, item));
- return PostEnqueue(invoker);
+ return PostEnqueue(invoker, startOff);
}
private static PlaylistItem UpdateItem(InvokerData invoker, PlaylistItem item)
{
+ item.Meta = item.Meta ?? new MetaData();
item.Meta.ResourceOwnerUid = invoker.ClientUid;
- item.Meta.From = PlaySource.FromQueue;
return item;
}
- private E PostEnqueue(InvokerData invoker)
+ private E PostEnqueue(InvokerData invoker, int startIndex)
{
if (IsPlaying)
return R.Ok;
- playlistManager.Index = 0;
+ playlistManager.Index = startIndex;
return StartCurrent(invoker);
}
@@ -89,9 +98,12 @@ public E Play(InvokerData invoker, AudioResource ar, MetaData meta = n
if (ar is null)
throw new ArgumentNullException(nameof(ar));
- var result = resourceFactory.Load(ar);
+ var result = resourceResolver.Load(ar);
if (!result)
+ {
+ stats.TrackSongLoad(ar.AudioType, false, true);
return result.Error;
+ }
return Play(invoker, result.Value, meta);
}
@@ -103,10 +115,13 @@ public E Play(InvokerData invoker, AudioResource ar, MetaData meta = n
/// Ok if successful, or an error message otherwise.
public E Play(InvokerData invoker, string link, string audioType = null, MetaData meta = null)
{
- var result = resourceFactory.Load(link, audioType);
+ var result = resourceResolver.Load(link, audioType);
if (!result)
+ {
+ stats.TrackSongLoad(audioType, false, true);
return result.Error;
- return Play(invoker, result.Value, meta ?? new MetaData());
+ }
+ return Play(invoker, result.Value, meta);
}
public E Play(InvokerData invoker, IEnumerable items, int index = 0)
@@ -122,15 +137,16 @@ public E Play(InvokerData invoker, PlaylistItem item)
if (item is null)
throw new ArgumentNullException(nameof(item));
- if (item.Resource is null)
+ if (item.AudioResource is null)
throw new Exception("Invalid playlist item");
-
playlistManager.Clear();
playlistManager.Queue(item);
playlistManager.Index = 0;
return StartResource(invoker, item);
}
+ public E Play(InvokerData invoker) => StartCurrent(invoker);
+
/// Plays the passed
/// The invoker of this resource. Used for responses and association.
/// The associated resource type string to a factory.
@@ -142,25 +158,24 @@ public E Play(InvokerData invoker, PlayResource play, MetaData meta =
playlistManager.Clear();
playlistManager.Queue(new PlaylistItem(play.BaseData, meta));
playlistManager.Index = 0;
+ stats.TrackSongLoad(play.BaseData.AudioType, true, true);
return StartResource(invoker, play, meta);
}
private E StartResource(InvokerData invoker, PlaylistItem item)
{
- var result = resourceFactory.Load(item.Resource);
+ var result = resourceResolver.Load(item.AudioResource);
+ stats.TrackSongLoad(item.AudioResource.AudioType, result.Ok, false);
if (!result)
return result.Error;
-
return StartResource(invoker, result.Value, item.Meta);
}
- private E StartResource(InvokerData invoker, PlayResource play, MetaData meta)
+ private E StartResource(InvokerData invoker, PlayResource play, MetaData meta = null)
{
- if (meta.From != PlaySource.FromPlaylist)
- meta.ResourceOwnerUid = invoker.ClientUid;
-
- var sourceLink = resourceFactory.RestoreLink(play.BaseData).OkOr(null);
- var playInfo = new PlayInfoEventArgs(invoker, play, meta, sourceLink);
+ play.Meta = meta ?? play.Meta ?? new MetaData();
+ var sourceLink = resourceResolver.RestoreLink(play.BaseData).OkOr(null);
+ var playInfo = new PlayInfoEventArgs(invoker, play, sourceLink);
BeforeResourceStarted?.Invoke(this, playInfo);
if (string.IsNullOrWhiteSpace(play.PlayUri))
@@ -170,14 +185,14 @@ private E StartResource(InvokerData invoker, PlayResource play, MetaDa
}
Log.Debug("AudioResource start: {0}", play);
- var result = playerConnection.AudioStart(play);
+ var result = playerConnection.Play(play);
if (!result)
{
Log.Error("Error return from player: {0}", result.Error);
return new LocalStr(strings.error_playmgr_internal_error);
}
- playerConnection.Volume = Util.Clamp(playerConnection.Volume, confBot.Audio.Volume.Min, confBot.Audio.Volume.Max);
+ playerConnection.Volume = Tools.Clamp(playerConnection.Volume, confBot.Audio.Volume.Min, confBot.Audio.Volume.Max);
CurrentPlayData = playInfo; // TODO meta as readonly
AfterResourceStarted?.Invoke(this, playInfo);
@@ -192,7 +207,7 @@ private E StartCurrent(InvokerData invoker, bool manually = true)
var result = StartResource(invoker, pli);
if (result.Ok)
return result;
- Log.Warn("Skipping: {0} because {1}", pli.DisplayString, result.Error.Str);
+ Log.Warn("Skipping: {0} because {1}", pli, result.Error.Str);
return Next(invoker, manually);
}
@@ -205,7 +220,7 @@ public E Next(InvokerData invoker, bool manually = true)
var result = StartResource(invoker, pli);
if (result.Ok)
return result;
- Log.Warn("Skipping: {0} because {1}", pli.DisplayString, result.Error.Str);
+ Log.Warn("Skipping: {0} because {1}", pli, result.Error.Str);
}
if (pli is null)
return new LocalStr(strings.info_playmgr_no_next_song);
@@ -215,25 +230,14 @@ public E Next(InvokerData invoker, bool manually = true)
public E Previous(InvokerData invoker, bool manually = true)
{
- bool skipPrev = CurrentPlayData?.MetaData.From != PlaySource.FromPlaylist;
PlaylistItem pli = null;
for (int i = 0; i < 10; i++)
{
- if (skipPrev)
- {
- pli = playlistManager.Current;
- skipPrev = false;
- }
- else
- {
- pli = playlistManager.Previous(manually);
- }
- if (pli is null) break;
-
+ if ((pli = playlistManager.Previous(manually)) is null) break;
var result = StartResource(invoker, pli);
if (result.Ok)
return result;
- Log.Warn("Skipping: {0} because {1}", pli.DisplayString, result.Error.Str);
+ Log.Warn("Skipping: {0} because {1}", pli, result.Error.Str);
}
if (pli is null)
return new LocalStr(strings.info_playmgr_no_previous_song);
@@ -247,7 +251,7 @@ public E Previous(InvokerData invoker, bool manually = true)
private void StopInternal(bool songEndedByCallback)
{
- BeforeResourceStopped?.Invoke(this, new SongEndEventArgs(songEndedByCallback));
+ ResourceStopped?.Invoke(this, new SongEndEventArgs(songEndedByCallback));
if (songEndedByCallback)
{
@@ -258,11 +262,11 @@ private void StopInternal(bool songEndedByCallback)
}
else
{
- playerConnection.AudioStop();
+ playerConnection.Stop();
}
CurrentPlayData = null;
- AfterResourceStopped?.Invoke(this, EventArgs.Empty);
+ PlaybackStopped?.Invoke(this, EventArgs.Empty);
}
public void Update(SongInfoChanged newInfo)
@@ -275,5 +279,21 @@ public void Update(SongInfoChanged newInfo)
// further properties...
OnResourceUpdated?.Invoke(this, data);
}
+
+ public static MetaData ParseAttributes(string[] attrs)
+ {
+ if (attrs is null || attrs.Length == 0)
+ return null;
+
+ var meta = new MetaData();
+ foreach (var attr in attrs)
+ {
+ if (attr.StartsWith("@"))
+ {
+ meta.StartOffset = TextUtil.ParseTime(attr.Substring(1));
+ }
+ }
+ return meta;
+ }
}
}
diff --git a/TS3AudioBot/Audio/Player.cs b/TS3AudioBot/Audio/Player.cs
new file mode 100644
index 00000000..64b53048
--- /dev/null
+++ b/TS3AudioBot/Audio/Player.cs
@@ -0,0 +1,167 @@
+// 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 .
+
+using System;
+using TS3AudioBot.Config;
+using TS3AudioBot.Helper;
+using TS3AudioBot.ResourceFactories;
+using TSLib;
+using TSLib.Audio;
+using TSLib.Helper;
+
+namespace TS3AudioBot.Audio
+{
+ public class Player : IDisposable
+ {
+ private const Codec SendCodec = Codec.OpusMusic;
+
+ public IPlayerSource CurrentPlayerSource { get; private set; }
+ public StallCheckPipe StallCheckPipe { get; }
+ public VolumePipe VolumePipe { get; }
+ public FfmpegProducer FfmpegProducer { get; }
+ public PreciseTimedPipe TimePipe { get; }
+ public PassiveMergePipe MergePipe { get; }
+ public EncoderPipe EncoderPipe { get; }
+ public IAudioPassiveConsumer PlayerSink { get; private set; }
+
+ public Player(ConfBot config, Id id)
+ {
+ FfmpegProducer = new FfmpegProducer(config.GetParent().Tools.Ffmpeg, id);
+ 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, id);
+ MergePipe = new PassiveMergePipe();
+
+ config.Audio.Bitrate.Changed += (s, e) => EncoderPipe.Bitrate = ScaleBitrate(e.NewValue);
+
+ MergePipe.Into(TimePipe).Chain().Chain(StallCheckPipe).Chain(VolumePipe).Chain(EncoderPipe);
+ }
+
+ public void SetTarget(IAudioPassiveConsumer target)
+ {
+ PlayerSink = target;
+ EncoderPipe.Chain(target);
+ }
+
+ private static int ScaleBitrate(int value) => Tools.Clamp(value, 1, 255) * 1000;
+
+ public event EventHandler OnSongEnd;
+ public event EventHandler OnSongUpdated;
+
+ private void TriggerSongEnd(object o, EventArgs e) => OnSongEnd?.Invoke(this, EventArgs.Empty);
+ private void TriggerSongUpdated(object o, SongInfoChanged e) => OnSongUpdated?.Invoke(this, e);
+
+ public E Play(PlayResource res)
+ {
+ E result;
+ if (res is MediaPlayResource mres && mres.IsIcyStream)
+ result = FfmpegProducer.AudioStartIcy(res.PlayUri);
+ else
+ result = FfmpegProducer.AudioStart(res.PlayUri, res.Meta?.StartOffset);
+ if (result)
+ Play(FfmpegProducer);
+ return result;
+ }
+
+ public void Play(IPlayerSource source)
+ {
+ var oldSource = CurrentPlayerSource;
+ if (oldSource != source)
+ {
+ // Clean up old
+ CleanSource(oldSource);
+ // Set events
+ source.OnSongEnd += TriggerSongEnd;
+ source.OnSongUpdated += TriggerSongUpdated;
+ // Update pipes
+ MergePipe.Add(source);
+ CurrentPlayerSource = source;
+ }
+ // Start Ticker
+ TimePipe.AudioTimer.Reset();
+ TimePipe.Paused = false;
+ }
+
+ private void CleanSource(IPlayerSource source)
+ {
+ if (source is null)
+ return;
+ source.OnSongEnd -= TriggerSongEnd;
+ source.OnSongUpdated -= TriggerSongUpdated;
+ MergePipe.Remove(source);
+ source.Dispose();
+ }
+
+ public void Stop()
+ {
+ CurrentPlayerSource?.Dispose();
+ if (MergePipe.Count <= 1)
+ TimePipe.Paused = true;
+ }
+
+ public void StopAll()
+ {
+ Stop();
+ TimePipe.Paused = true;
+ MergePipe.Dispose();
+ }
+
+ public TimeSpan Length => CurrentPlayerSource?.Length ?? TimeSpan.Zero;
+
+ public TimeSpan Position
+ {
+ get => CurrentPlayerSource?.Position ?? TimeSpan.Zero;
+ set
+ {
+ if (CurrentPlayerSource != null)
+ CurrentPlayerSource.Position = value;
+ }
+ }
+
+ public float Volume
+ {
+ get => AudioValues.FactorToHumanVolume(VolumePipe.Volume);
+ set => VolumePipe.Volume = AudioValues.HumanVolumeToFactor(value);
+ }
+
+ public bool Paused
+ {
+ get => TimePipe.Paused;
+ set => TimePipe.Paused = value;
+ }
+
+ // Extras
+
+ public void SetStall() => StallCheckPipe.SetStall();
+
+ [Obsolete(AttributeStrings.UnderDevelopment)]
+ public void MixInStreamOnce(IPlayerSource producer)
+ {
+ producer.OnSongEnd += (s, e) =>
+ {
+ MergePipe.Remove(producer);
+ producer.Dispose();
+ };
+ MergePipe.Add(producer);
+ TimePipe.Paused = false;
+ }
+
+ public void Dispose()
+ {
+ StopAll();
+ CleanSource(CurrentPlayerSource);
+ TimePipe.Dispose();
+ FfmpegProducer.Dispose();
+ EncoderPipe.Dispose();
+ }
+ }
+}
diff --git a/TS3AudioBot/Audio/SongEndEventArgs.cs b/TS3AudioBot/Audio/SongEndEventArgs.cs
index 6b3d8b6f..00c69bb4 100644
--- a/TS3AudioBot/Audio/SongEndEventArgs.cs
+++ b/TS3AudioBot/Audio/SongEndEventArgs.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+
namespace TS3AudioBot.Audio
{
- using System;
-
public class SongEndEventArgs : EventArgs
{
public bool SongEndedByCallback { get; }
diff --git a/TS3AudioBot/Audio/SongInfoChanged.cs b/TS3AudioBot/Audio/SongInfoChanged.cs
index 5d710bed..9b5e02c5 100644
--- a/TS3AudioBot/Audio/SongInfoChanged.cs
+++ b/TS3AudioBot/Audio/SongInfoChanged.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+
namespace TS3AudioBot.Audio
{
- using System;
-
public class SongInfoChanged : EventArgs
{
public string Title { get; set; }
diff --git a/TS3AudioBot/Audio/StallCheckPipe.cs b/TS3AudioBot/Audio/StallCheckPipe.cs
index 52d56ef3..dfa34eb5 100644
--- a/TS3AudioBot/Audio/StallCheckPipe.cs
+++ b/TS3AudioBot/Audio/StallCheckPipe.cs
@@ -7,11 +7,11 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using TSLib.Audio;
+
namespace TS3AudioBot.Audio
{
- using System;
- using TS3Client.Audio;
-
public class StallCheckPipe : IAudioPipe
{
private const uint StallCountInterval = 10;
diff --git a/TS3AudioBot/Audio/StreamAudioPlayerSource.cs b/TS3AudioBot/Audio/StreamAudioPlayerSource.cs
new file mode 100644
index 00000000..369af6d7
--- /dev/null
+++ b/TS3AudioBot/Audio/StreamAudioPlayerSource.cs
@@ -0,0 +1,56 @@
+// 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 .
+
+using System;
+using TSLib.Audio;
+
+namespace TS3AudioBot.Audio
+{
+ public class StreamAudioPlayerSource : IPlayerSource, IAudioActiveConsumer
+ {
+ private bool hasFired = false;
+
+ public IAudioPassiveProducer InStream { get; set; }
+ public TimeSpan Length => TimeSpan.Zero;
+ public TimeSpan Position { get => TimeSpan.Zero; set { } }
+
+ public event EventHandler OnSongEnd;
+ event EventHandler IPlayerSource.OnSongUpdated { add { } remove { } }
+
+ public StreamAudioPlayerSource() { }
+
+ public StreamAudioPlayerSource(IAudioPassiveProducer stream) : this()
+ {
+ InStream = stream;
+ }
+
+ public int Read(byte[] buffer, int offset, int length, out Meta meta)
+ {
+ var stream = InStream;
+ if (stream is null)
+ {
+ meta = default;
+ return 0;
+ }
+
+ var read = stream.Read(buffer, offset, length, out meta);
+ if (read == 0 && !hasFired)
+ {
+ hasFired = true;
+ OnSongEnd?.Invoke(this, EventArgs.Empty);
+ return 0;
+ }
+ return read;
+ }
+
+ public void Reset() => hasFired = false;
+
+ public void Dispose() => InStream?.Dispose();
+ }
+}
diff --git a/TS3AudioBot/Bot.cs b/TS3AudioBot/Bot.cs
index e0c22eb7..7e0082e6 100644
--- a/TS3AudioBot/Bot.cs
+++ b/TS3AudioBot/Bot.cs
@@ -7,31 +7,32 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using TS3AudioBot.Algorithm;
+using TS3AudioBot.Audio;
+using TS3AudioBot.CommandSystem;
+using TS3AudioBot.CommandSystem.Text;
+using TS3AudioBot.Config;
+using TS3AudioBot.Dependency;
+using TS3AudioBot.Environment;
+using TS3AudioBot.Helper;
+using TS3AudioBot.History;
+using TS3AudioBot.Localization;
+using TS3AudioBot.Playlists;
+using TS3AudioBot.Plugins;
+using TS3AudioBot.ResourceFactories;
+using TS3AudioBot.Sessions;
+using TSLib;
+using TSLib.Full;
+using TSLib.Helper;
+using TSLib.Messages;
+
namespace TS3AudioBot
{
- using Audio;
- using Algorithm;
- using CommandSystem;
- using CommandSystem.CommandResults;
- using CommandSystem.Text;
- using Config;
- using Dependency;
- using Helper;
- using History;
- using Localization;
- using Playlists;
- using Plugins;
- using Sessions;
- using System;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using TS3AudioBot.ResourceFactories;
- using TS3Client;
- using TS3Client.Messages;
- using TS3Client.Helper;
- using TS3Client.Full;
-
/// Core class managing all bots and utility modules.
public sealed class Bot : IDisposable
{
@@ -39,6 +40,7 @@ public sealed class Bot : IDisposable
private readonly ConfBot config;
private TickWorker idleTickWorker;
+ private TickWorker aloneTickWorker;
internal object SyncRoot { get; } = new object();
internal bool IsDisposed { get; private set; }
@@ -49,22 +51,20 @@ public sealed class Bot : IDisposable
public string Name => config.Name;
public bool QuizMode { get; set; }
- private readonly ResourceFactory resourceFactory;
- private readonly CommandManager commandManager;
- private Ts3Client clientConnection;
- private Ts3FullClient tsFullClient;
+ private ResolveContext resourceResolver;
+ private Ts3Client ts3client;
+ private TsFullClient ts3FullClient;
private SessionManager sessionManager;
private PlayManager playManager;
private IVoiceTarget targetManager;
- private IPlayerConnection playerConnection;
+ private Player player;
+ private Stats stats;
- public Bot(Id id, ConfBot config, BotInjector injector, ResourceFactory resourceFactory, CommandManager commandManager)
+ public Bot(Id id, ConfBot config, BotInjector injector)
{
this.Id = id;
this.config = config;
this.Injector = injector;
- this.resourceFactory = resourceFactory;
- this.commandManager = commandManager;
}
public E InitializeBot()
@@ -78,7 +78,7 @@ public E InitializeBot()
if (!langResult.Ok)
Log.Error("Failed to load language file ({0})", langResult.Error);
};
- config.Events.IdleTime.Changed += (s, e) => EnableIdleTickWorker();
+ config.Events.IdleDelay.Changed += (s, e) => EnableIdleTickWorker();
config.Events.OnIdle.Changed += (s, e) => EnableIdleTickWorker();
var builder = new DependencyBuilder(Injector);
@@ -89,11 +89,17 @@ public E InitializeBot()
builder.RequestModule();
builder.RequestModule();
builder.AddModule(Id);
- builder.AddModule(new Ts3FullClient());
- builder.RequestModule();
+ builder.AddModule(new TsFullClient());
+ builder.RequestModule();
builder.RequestModule();
- builder.RequestModule();
+ builder.RequestModule();
+ builder.RequestModule();
+ builder.RequestModule();
builder.RequestModule();
+ builder.RequestModule();
+ if (!builder.TryCreate(out var commandManager))
+ return "Failed to create commandManager";
+ builder.AddModule(commandManager);
if (config.History.Enabled)
{
builder.AddModule(config.History);
@@ -107,46 +113,61 @@ public E InitializeBot()
return "Could not load all bot modules";
}
- tsFullClient = Injector.GetModule();
- clientConnection = Injector.GetModule();
- playerConnection = clientConnection;
- Injector.AddModule(clientConnection.TargetPipe);
- Injector.AddModule(tsFullClient.Book);
+ resourceResolver = Injector.GetModule();
+ ts3FullClient = Injector.GetModule();
+ ts3client = Injector.GetModule();
+ player = Injector.GetModule();
+ player.SetTarget(Injector.GetModule());
+ Injector.AddModule(ts3FullClient.Book);
playManager = Injector.GetModule();
targetManager = Injector.GetModule();
sessionManager = Injector.GetModule();
+ stats = Injector.GetModule();
- playerConnection.OnSongEnd += playManager.SongStoppedEvent;
- playerConnection.OnSongUpdated += (s, e) => playManager.Update(e);
+ player.OnSongEnd += playManager.SongStoppedEvent;
+ player.OnSongUpdated += (s, e) => playManager.Update(e);
// Update idle status events
playManager.BeforeResourceStarted += (s, e) => DisableIdleTickWorker();
- playManager.AfterResourceStopped += (s, e) => EnableIdleTickWorker();
+ playManager.PlaybackStopped += (s, e) => EnableIdleTickWorker();
// Used for the voice_mode script
playManager.BeforeResourceStarted += BeforeResourceStarted;
// Update the own status text to the current song title
playManager.AfterResourceStarted += LoggedUpdateBotStatus;
- playManager.AfterResourceStopped += LoggedUpdateBotStatus;
+ playManager.PlaybackStopped += LoggedUpdateBotStatus;
playManager.OnResourceUpdated += LoggedUpdateBotStatus;
// Log our resource in the history
if (Injector.TryGet(out var historyManager))
- playManager.AfterResourceStarted += (s, e) => historyManager.LogAudioResource(new HistorySaveData(e.PlayResource.BaseData, e.Invoker.ClientUid));
+ playManager.AfterResourceStarted += (s, e) => historyManager.LogAudioResource(new HistorySaveData(e.PlayResource.BaseData, e.MetaData.ResourceOwnerUid));
// Update our thumbnail
playManager.AfterResourceStarted += GenerateStatusImage;
- playManager.AfterResourceStopped += GenerateStatusImage;
+ playManager.PlaybackStopped += GenerateStatusImage;
+ // Stats
+ playManager.AfterResourceStarted += (s, e) => stats.TrackSongStart(Id, e.ResourceData.AudioType);
+ playManager.ResourceStopped += (s, e) => stats.TrackSongStop(Id);
// Register callback for all messages happening
- clientConnection.OnMessageReceived += OnMessageReceived;
+ ts3client.OnMessageReceived += OnMessageReceived;
// Register callback to remove open private sessions, when user disconnects
- tsFullClient.OnEachClientLeftView += OnClientLeftView;
- clientConnection.OnBotConnected += OnBotConnected;
- clientConnection.OnBotDisconnect += OnBotDisconnect;
-
+ ts3FullClient.OnEachClientLeftView += OnClientLeftView;
+ ts3client.OnBotConnected += OnBotConnected;
+ ts3client.OnBotDisconnect += OnBotDisconnect;
+ // Alone mode
+ ts3client.OnAloneChanged += OnAloneChanged;
+ // Whisper stall
+ ts3client.OnWhisperNoTarget += (s, e) => player.SetStall();
+
+ commandManager.RegisterCollection(MainCommands.Bag);
+ // TODO remove after plugin rework
+ var pluginManager = Injector.GetModule();
+ foreach (var plugin in pluginManager.Plugins)
+ if (plugin.Type == PluginType.CorePlugin || plugin.Type == PluginType.Commands)
+ commandManager.RegisterCollection(plugin.CorePlugin.Bag);
// Restore all alias from the config
foreach (var alias in config.Commands.Alias.GetAllItems())
commandManager.RegisterAlias(alias.Key, alias.Value).UnwrapToLog(Log);
- // Connect the query after everyting is set up
- return clientConnection.Connect();
+ // Connect after everyting is set up
+ return ts3client.Connect();
}
private void OnBotConnected(object sender, EventArgs e)
@@ -157,7 +178,7 @@ private void OnBotConnected(object sender, EventArgs e)
var badges = config.Connect.Badges.Value;
if (!string.IsNullOrEmpty(badges))
- clientConnection?.ChangeBadges(badges);
+ ts3client?.ChangeBadges(badges);
var onStart = config.Events.OnConnect.Value;
if (!string.IsNullOrEmpty(onStart))
@@ -200,26 +221,28 @@ private void OnMessageReceived(object sender, TextMessage textMessage)
Log.Info("User {0} requested: {1}", textMessage.InvokerName, textMessage.Message);
- clientConnection.InvalidateClientBuffer();
+ ts3client.InvalidateClientBuffer();
- ulong? channelId = null, databaseId = null, channelGroup = null;
- ulong[] serverGroups = null;
+ ChannelId? channelId = null;
+ ClientDbId? databaseId = null;
+ ChannelGroupId? channelGroup = null;
+ ServerGroupId[] serverGroups = null;
- if (tsFullClient.Book.Clients.TryGetValue(textMessage.InvokerId, out var bookClient))
+ if (ts3FullClient.Book.Clients.TryGetValue(textMessage.InvokerId, out var bookClient))
{
channelId = bookClient.Channel;
databaseId = bookClient.DatabaseId;
serverGroups = bookClient.ServerGroups.ToArray();
channelGroup = bookClient.ChannelGroup;
}
- else if (!clientConnection.GetClientInfoById(textMessage.InvokerId).GetOk(out var infoClient).GetError(out var infoClientError))
+ else if (!ts3client.GetClientInfoById(textMessage.InvokerId).GetOk(out var infoClient).GetError(out var infoClientError))
{
channelId = infoClient.ChannelId;
databaseId = infoClient.DatabaseId;
serverGroups = infoClient.ServerGroups;
channelGroup = infoClient.ChannelGroup;
}
- else if (!clientConnection.GetCachedClientById(textMessage.InvokerId).GetOk(out var cachedClient).GetError(out var cachedClientError))
+ else if (!ts3client.GetCachedClientById(textMessage.InvokerId).GetOk(out var cachedClient).GetError(out var cachedClientError))
{
channelId = cachedClient.ChannelId;
databaseId = cachedClient.DatabaseId;
@@ -271,6 +294,50 @@ private void OnClientLeftView(object sender, ClientLeftView eventArgs)
sessionManager.RemoveSession(eventArgs.ClientId);
}
+ private void OnAloneChanged(object sender, AloneChanged e)
+ {
+ string script;
+ TimeSpan delay;
+ if (e.Alone)
+ {
+ script = config.Events.OnAlone.Value;
+ delay = config.Events.AloneDelay.Value;
+ }
+ else
+ {
+ script = config.Events.OnParty.Value;
+ delay = config.Events.PartyDelay.Value;
+ }
+ if (string.IsNullOrEmpty(script))
+ return;
+
+ void RunEvent()
+ {
+ var info = CreateExecInfo();
+ CallScript(info, script, false, true);
+ };
+
+ SetAloneTickWorker(null);
+ if (delay <= TimeSpan.Zero)
+ {
+ RunEvent();
+ }
+ else
+ {
+ var worker = TickPool.RegisterTickOnce(RunEvent, delay);
+ SetAloneTickWorker(worker);
+ }
+ }
+
+ private void SetAloneTickWorker(TickWorker worker)
+ {
+ var oldWoker = Interlocked.Exchange(ref aloneTickWorker, worker);
+ if (oldWoker != null)
+ {
+ TickPool.UnregisterTicker(oldWoker);
+ }
+ }
+
private void LoggedUpdateBotStatus(object sender, EventArgs e)
{
if (IsDisposed)
@@ -299,7 +366,7 @@ public E UpdateBotStatus(string overrideStr = null)
setString = strings.info_botstatus_sleeping;
}
- return clientConnection.ChangeDescription(setString ?? "");
+ return ts3client.ChangeDescription(setString ?? "");
}
private void GenerateStatusImage(object sender, EventArgs e)
@@ -307,36 +374,63 @@ private void GenerateStatusImage(object sender, EventArgs e)
if (!config.GenerateStatusAvatar || IsDisposed)
return;
- if (e is PlayInfoEventArgs startEvent)
+ Stream GetRandomFile(string prefix)
{
- Task.Run(() =>
+ try
{
- var thumresult = resourceFactory.GetThumbnail(startEvent.PlayResource);
- if (!thumresult.Ok)
- {
- clientConnection.DeleteAvatar();
- return;
- }
+ if (string.IsNullOrEmpty(config.LocalConfigDir))
+ return null;
+ var avatarPath = new DirectoryInfo(Path.Combine(config.LocalConfigDir, BotPaths.Avatars));
+ if (!avatarPath.Exists)
+ return null;
+ var avatars = avatarPath.EnumerateFiles(prefix).ToArray();
+ if (avatars.Length == 0)
+ return null;
+ var pickedAvatar = Tools.PickRandom(avatars);
+ return pickedAvatar.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
+ }
+ catch (Exception ex)
+ {
+ Log.Warn(ex, "Failed to load local avatar");
+ return null;
+ }
+ }
- using (var image = ImageUtil.ResizeImage(thumresult.Value))
+ void Upload(Stream setStream)
+ {
+ if (setStream != null)
+ {
+ using (setStream)
{
- if (image is null)
- return;
- var result = clientConnection.UploadAvatar(image);
+ var result = ts3client.UploadAvatar(setStream);
if (!result.Ok)
Log.Warn("Could not save avatar: {0}", result.Error);
}
- });
+ }
}
- else
+
+ Task.Run(() =>
{
- using (var sleepPic = Util.GetEmbeddedFile("TS3AudioBot.Media.SleepingKitty.png"))
+ Stream setStream = null;
+ if (e is PlayInfoEventArgs startEvent)
{
- var result = clientConnection.UploadAvatar(sleepPic);
- if (!result.Ok)
- Log.Warn("Could not save avatar: {0}", result.Error);
+ setStream = ImageUtil.ResizeImageSave(resourceResolver.GetThumbnail(startEvent.PlayResource).OkOr(null), out _).OkOr(null);
+ setStream = setStream ?? GetRandomFile("play*");
+ Upload(setStream);
}
- }
+ else
+ {
+ setStream = GetRandomFile("sleep*");
+ setStream = setStream ?? Util.GetEmbeddedFile("TS3AudioBot.Media.SleepingKitty.png");
+ Upload(setStream);
+ }
+
+ if (setStream is null)
+ {
+ ts3client.DeleteAvatar();
+ return;
+ }
+ });
}
private void BeforeResourceStarted(object sender, PlayInfoEventArgs e)
@@ -365,6 +459,7 @@ private void BeforeResourceStarted(object sender, PlayInfoEventArgs e)
private void CallScript(ExecutionInformation info, string command, bool answer, bool skipRights)
{
Log.Debug("Calling script (skipRights:{0}, answer:{1}): {2}", skipRights, answer, command);
+ stats.TrackCommandCall(answer);
info.AddModule(new CallerInfo(false)
{
@@ -375,28 +470,15 @@ private void CallScript(ExecutionInformation info, string command, bool answer,
TryCatchCommand(info, answer, () =>
{
- // parse (and execute) the command
- var res = commandManager.CommandSystem.Execute(info, command);
+ // parse and execute the command
+ var s = CommandManager.ExecuteCommand(info, command);
if (!answer)
return;
// Write result to user
- switch (res.ResultType)
- {
- case CommandResultType.String:
- var sRes = (StringCommandResult)res;
- if (!string.IsNullOrEmpty(sRes.Content))
- info.Write(sRes.Content).UnwrapToLog(Log);
- break;
-
- case CommandResultType.Empty:
- break;
-
- default:
- Log.Warn("Got result which is not a string/empty. Result: {0}", res.ToString());
- break;
- }
+ if (!string.IsNullOrEmpty(s))
+ info.Write(s).UnwrapToLog(Log);
});
}
@@ -425,9 +507,12 @@ private void OnIdle()
private void EnableIdleTickWorker()
{
- var idleTime = config.Events.IdleTime.Value;
+ var idleTime = config.Events.IdleDelay.Value;
if (idleTime <= TimeSpan.Zero || string.IsNullOrEmpty(config.Events.OnIdle.Value))
+ {
+ DisableIdleTickWorker();
return;
+ }
var newWorker = TickPool.RegisterTick(OnIdle, idleTime, false);
SetIdleTickWorker(newWorker);
newWorker.Active = true;
@@ -486,8 +571,8 @@ public BotLock GetBotLock()
{
Id = Id,
Name = config.Name,
- Server = tsFullClient.ConnectionData.Address,
- Status = tsFullClient.Connected ? BotStatus.Connected : BotStatus.Connecting,
+ Server = ts3FullClient.ConnectionData.Address,
+ Status = ts3FullClient.Connected ? BotStatus.Connected : BotStatus.Connecting,
};
public void Dispose()
@@ -505,7 +590,7 @@ public void Dispose()
Injector.GetModule()?.StopPlugins(this);
Injector.GetModule()?.Stop();
- Injector.GetModule()?.Dispose();
+ Injector.GetModule()?.Dispose();
Injector.GetModule()?.Dispose();
config.ClearEvents();
}
diff --git a/TS3AudioBot/BotManager.cs b/TS3AudioBot/BotManager.cs
index 5907cbc5..e95ac7f4 100644
--- a/TS3AudioBot/BotManager.cs
+++ b/TS3AudioBot/BotManager.cs
@@ -7,21 +7,22 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using TS3AudioBot.Config;
+using TS3AudioBot.Dependency;
+using TS3AudioBot.Helper;
+using TSLib.Helper;
+
namespace TS3AudioBot
{
- using Config;
- using Dependency;
- using Helper;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading;
-
public class BotManager : IDisposable
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
- private List activeBots;
+ private List activeBots = new List();
private readonly object lockObj = new object();
private readonly ConfRoot confRoot;
@@ -29,7 +30,6 @@ public class BotManager : IDisposable
public BotManager(ConfRoot confRoot, CoreInjector coreInjector)
{
- Util.Init(out activeBots);
this.confRoot = confRoot;
this.coreInjector = coreInjector;
}
@@ -56,7 +56,7 @@ public void RunBots(bool interactive)
string address = Interactive.LoopAction("Please enter the ip, domain or nickname (with port; default: 9987) where to connect to:", addr =>
{
- if (TS3Client.TsDnsResolver.TryResolve(addr, out _))
+ if (TSLib.TsDnsResolver.TryResolve(addr, out _))
return true;
Console.WriteLine("The address seems invalid or could not be resolved, continue anyway? [y/N]");
return Interactive.UserAgree(defaultTo: false);
@@ -129,7 +129,7 @@ public R RunBot(ConfBot config)
var botInjector = new BotInjector(coreInjector);
botInjector.AddModule(botInjector);
- botInjector.AddModule(new TS3Client.Helper.Id(id.Value));
+ botInjector.AddModule(new Id(id.Value));
botInjector.AddModule(config);
if (!botInjector.TryCreate(out bot))
return "Failed to create new Bot";
@@ -220,6 +220,19 @@ public BotLock GetBotLock(string name)
return bot.GetBotLock();
}
+ internal void IterateAll(Action body)
+ {
+ lock (lockObj)
+ {
+ if (activeBots is null)
+ return;
+ foreach (var bot in activeBots)
+ {
+ body(bot);
+ }
+ }
+ }
+
public void StopBot(Bot bot)
{
RemoveBot(bot);
@@ -250,6 +263,16 @@ public BotInfo[] GetBotInfolist()
}
}
+ public uint GetRunningBotCount()
+ {
+ lock (lockObj)
+ {
+ if (activeBots is null)
+ return 0;
+ return (uint)activeBots.Count(x => x != null);
+ }
+ }
+
public void Dispose()
{
List disposeBots;
diff --git a/TS3AudioBot/ClientCall.cs b/TS3AudioBot/ClientCall.cs
index f3ffdf1c..384f9c87 100644
--- a/TS3AudioBot/ClientCall.cs
+++ b/TS3AudioBot/ClientCall.cs
@@ -7,6 +7,8 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using TSLib;
+
namespace TS3AudioBot
{
public class ClientCall : InvokerData
@@ -14,17 +16,18 @@ public class ClientCall : InvokerData
/// The original unmodified string which was received by the client.
public string TextMessage { get; }
- public ulong? DatabaseId { get; }
- public ulong? ChannelId { get; }
- public ushort? ClientId { get; }
+ public ClientDbId? DatabaseId { get; }
+ public ChannelId? ChannelId { get; }
+ public ClientId? ClientId { get; }
public string NickName { get; }
- public ulong[] ServerGroups { get; }
- public ulong? ChannelGroup { get; }
- public TS3Client.TextMessageTargetMode? Visibiliy { get; internal set; }
+ public ServerGroupId[] ServerGroups { get; }
+ public ChannelGroupId? ChannelGroup { get; }
+ public TextMessageTargetMode? Visibiliy { get; internal set; }
- public ClientCall(string clientUid, string textMessage, ulong? databaseId = null, ulong? channelId = null,
- ushort? clientId = null, string nickName = null, TS3Client.TextMessageTargetMode? visibiliy = null,
- ulong[] serverGroups = null, ulong? channelGroup = null) : base(clientUid)
+ public ClientCall(Uid clientUid, string textMessage, ClientDbId? databaseId = null,
+ ChannelId? channelId = null, ClientId? clientId = null, string nickName = null,
+ TextMessageTargetMode? visibiliy = null, ServerGroupId[] serverGroups = null,
+ ChannelGroupId? channelGroup = null) : base(clientUid)
{
TextMessage = textMessage;
DatabaseId = databaseId;
diff --git a/TS3AudioBot/CommandSystem/Ast/AstCommand.cs b/TS3AudioBot/CommandSystem/Ast/AstCommand.cs
index 56798565..d1ec2ecd 100644
--- a/TS3AudioBot/CommandSystem/Ast/AstCommand.cs
+++ b/TS3AudioBot/CommandSystem/Ast/AstCommand.cs
@@ -7,11 +7,11 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Collections.Generic;
+using System.Text;
+
namespace TS3AudioBot.CommandSystem.Ast
{
- using System.Collections.Generic;
- using System.Text;
-
internal class AstCommand : AstNode
{
public override AstType Type => AstType.Command;
diff --git a/TS3AudioBot/CommandSystem/Ast/AstError.cs b/TS3AudioBot/CommandSystem/Ast/AstError.cs
index cf14e8c7..082e1ccb 100644
--- a/TS3AudioBot/CommandSystem/Ast/AstError.cs
+++ b/TS3AudioBot/CommandSystem/Ast/AstError.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Text;
+
namespace TS3AudioBot.CommandSystem.Ast
{
- using System.Text;
-
internal class AstError : AstNode
{
public override AstType Type => AstType.Error;
diff --git a/TS3AudioBot/CommandSystem/Ast/AstNode.cs b/TS3AudioBot/CommandSystem/Ast/AstNode.cs
index 533f6adb..6d1e5a60 100644
--- a/TS3AudioBot/CommandSystem/Ast/AstNode.cs
+++ b/TS3AudioBot/CommandSystem/Ast/AstNode.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Text;
+
namespace TS3AudioBot.CommandSystem.Ast
{
- using System.Text;
-
public abstract class AstNode
{
public abstract AstType Type { get; }
diff --git a/TS3AudioBot/CommandSystem/Ast/AstValue.cs b/TS3AudioBot/CommandSystem/Ast/AstValue.cs
index 0d6af962..9b270be2 100644
--- a/TS3AudioBot/CommandSystem/Ast/AstValue.cs
+++ b/TS3AudioBot/CommandSystem/Ast/AstValue.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System.Text;
+
namespace TS3AudioBot.CommandSystem.Ast
{
- using System.Text;
-
internal class AstValue : AstNode
{
private string value;
diff --git a/TS3AudioBot/CommandSystem/Ast/StringType.cs b/TS3AudioBot/CommandSystem/Ast/StringType.cs
index 8338886d..c84b217b 100644
--- a/TS3AudioBot/CommandSystem/Ast/StringType.cs
+++ b/TS3AudioBot/CommandSystem/Ast/StringType.cs
@@ -9,7 +9,7 @@
namespace TS3AudioBot.CommandSystem.Ast
{
- enum StringType
+ internal enum StringType
{
FreeString,
QuotedString,
diff --git a/TS3AudioBot/CommandSystem/BotCommand.cs b/TS3AudioBot/CommandSystem/BotCommand.cs
index 2d6e93e8..7c36b3f6 100644
--- a/TS3AudioBot/CommandSystem/BotCommand.cs
+++ b/TS3AudioBot/CommandSystem/BotCommand.cs
@@ -7,28 +7,30 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using TS3AudioBot.CommandSystem.Commands;
+using TS3AudioBot.Localization;
+
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}")]
+ [JsonObject(MemberSerialization.OptIn)]
public class BotCommand : FunctionCommand
{
private readonly string helpLookupName;
private string cachedFullQualifiedName;
- private object cachedAsJsonObj;
+ [JsonProperty(PropertyName = "Name")]
public string InvokeName { get; }
private readonly string[] requiredRights;
public string RequiredRight => requiredRights[0];
+ [JsonProperty(PropertyName = "Description")]
public string Description => LocalizationManager.GetString(helpLookupName);
public UsageAttribute[] UsageList { get; }
public string FullQualifiedName
@@ -40,9 +42,9 @@ public string FullQualifiedName
var strb = new StringBuilder();
strb.Append(InvokeName);
strb.Append(" (");
- strb.Append(string.Join(", ", CommandParameter.Where(p => !p.kind.IsNormal()).Select(p => p.type.FullName).OrderBy(p => p)));
+ strb.Append(string.Join(", ", CommandParameter.Where(p => !p.Kind.IsNormal()).Select(p => p.Type.FullName).OrderBy(p => p)));
strb.Append("|");
- strb.Append(string.Join(", ", CommandParameter.Where(p => p.kind.IsNormal()).Select(p => p.type.FullName)));
+ strb.Append(string.Join(", ", CommandParameter.Where(p => p.Kind.IsNormal()).Select(p => p.Type.FullName)));
strb.Append(")");
cachedFullQualifiedName = strb.ToString();
}
@@ -50,6 +52,13 @@ public string FullQualifiedName
}
}
+ [JsonProperty(PropertyName = "Return")]
+ public string Return { get; set; }
+ [JsonProperty(PropertyName = "Parameter")]
+ public (string name, string type, bool optional)[] Parameter { get; }
+ [JsonProperty(PropertyName = "Modules")]
+ public (string type, bool optional)[] Modules { get; }
+
public string DebuggerDisplay
{
get
@@ -63,20 +72,41 @@ public string DebuggerDisplay
}
}
- public object AsJsonObj => cachedAsJsonObj ?? (cachedAsJsonObj = new CommandSerializeObj(this));
-
public BotCommand(CommandBuildInfo buildInfo) : base(buildInfo.Method, buildInfo.Parent)
{
InvokeName = buildInfo.CommandData.CommandNameSpace;
helpLookupName = buildInfo.CommandData.OverrideHelpName ?? ("cmd_" + InvokeName.Replace(" ", "_") + "_help");
requiredRights = new[] { "cmd." + string.Join(".", InvokeName.Split(' ')) };
UsageList = buildInfo.UsageList?.ToArray() ?? Array.Empty();
+ // Serialization
+ Return = UnwrapReturnType(CommandReturn).Name;
+ Parameter = (
+ from x in CommandParameter
+ where x.Kind.IsNormal()
+ select (x.Name, UnwrapParamType(x.Type).Name, x.Optional)).ToArray();
+ Modules = (
+ from x in CommandParameter
+ where x.Kind == ParamKind.Dependency
+ select (x.Type.Name, x.Optional)).ToArray();
}
public override string ToString()
{
var strb = new StringBuilder();
- strb.Append("\n!").Append(InvokeName).Append(": ").Append(Description ?? strings.error_no_help ?? "");
+ strb.Append("\n!")
+ .Append(InvokeName);
+
+ foreach (var (name, _, optional) in Parameter)
+ {
+ strb.Append(' ');
+ if (optional)
+ strb.Append("[<").Append(name).Append(">]");
+ else
+ strb.Append('<').Append(name).Append('>');
+ }
+
+ strb.Append(": ")
+ .Append(Description ?? strings.error_no_help ?? "");
if (UsageList.Length > 0)
{
@@ -88,7 +118,7 @@ public override string ToString()
return strb.ToString();
}
- public override ICommandResult Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
+ public override object Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
{
// Check call complexity
info.UseComplexityTokens(1);
@@ -99,32 +129,6 @@ public override ICommandResult Execute(ExecutionInformation info, IReadOnlyList<
return base.Execute(info, arguments, returnTypes);
}
-
- private class CommandSerializeObj
- {
- private readonly BotCommand botCmd;
- public string Name => botCmd.InvokeName;
- public string Description => botCmd.Description;
- public string[] Parameter { get; }
- public string[] Modules { get; }
- public string Return { get; }
-
- public CommandSerializeObj(BotCommand botCmd)
- {
- this.botCmd = botCmd;
- Parameter = (
- from x in botCmd.CommandParameter
- where x.kind.IsNormal()
- select UnwrapParamType(x.type).Name + (x.optional ? "?" : "")).ToArray();
- Modules = (
- from x in botCmd.CommandParameter
- where x.kind == ParamKind.Dependency
- select x.type.Name + (x.optional ? "?" : "")).ToArray();
- Return = UnwrapReturnType(botCmd.CommandReturn).Name;
- }
-
- public override string ToString() => botCmd.ToString();
- }
}
public class CommandBuildInfo
diff --git a/TS3AudioBot/CommandSystem/CommandAttribute.cs b/TS3AudioBot/CommandSystem/CommandAttribute.cs
index a2120e79..df718eb4 100644
--- a/TS3AudioBot/CommandSystem/CommandAttribute.cs
+++ b/TS3AudioBot/CommandSystem/CommandAttribute.cs
@@ -7,10 +7,10 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+
namespace TS3AudioBot.CommandSystem
{
- using System;
-
///
/// Marks a method as callable from the CommandSystem.
/// The containing class must be registered in the CommandSystem to use this method.
diff --git a/TS3AudioBot/CommandSystem/CommandException.cs b/TS3AudioBot/CommandSystem/CommandException.cs
index 8361703c..ae7ae2b5 100644
--- a/TS3AudioBot/CommandSystem/CommandException.cs
+++ b/TS3AudioBot/CommandSystem/CommandException.cs
@@ -7,11 +7,11 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Runtime.Serialization;
+
namespace TS3AudioBot.CommandSystem
{
- using System;
- using System.Runtime.Serialization;
-
[Serializable]
public class CommandException : Exception
{
diff --git a/TS3AudioBot/CommandSystem/CommandManager.cs b/TS3AudioBot/CommandSystem/CommandManager.cs
index b6a598cb..285b18b4 100644
--- a/TS3AudioBot/CommandSystem/CommandManager.cs
+++ b/TS3AudioBot/CommandSystem/CommandManager.cs
@@ -7,18 +7,24 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using TS3AudioBot.CommandSystem.Ast;
+using TS3AudioBot.CommandSystem.CommandResults;
+using TS3AudioBot.CommandSystem.Commands;
+using TS3AudioBot.CommandSystem.Text;
+using TS3AudioBot.Dependency;
+using TS3AudioBot.Helper;
+using TS3AudioBot.Localization;
+using TS3AudioBot.Rights;
+using TSLib.Helper;
+using static TS3AudioBot.CommandSystem.CommandSystemTypes;
+
namespace TS3AudioBot.CommandSystem
{
- using Commands;
- using Helper;
- using Rights;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Reflection;
- using System.Text.RegularExpressions;
- using TS3AudioBot.Localization;
-
/// Mangement for the bot command system.
public class CommandManager
{
@@ -26,30 +32,28 @@ public class CommandManager
private static readonly Regex CommandNamespaceValidator =
new Regex(@"^[a-z\d]+( [a-z\d]+)*$", Util.DefaultRegexConfig & ~RegexOptions.IgnoreCase);
- private readonly Dictionary aliasPaths;
- private readonly HashSet commandPaths;
- private readonly HashSet baggedCommands;
+ private readonly Dictionary aliasPaths = new Dictionary();
+ private readonly HashSet commandPaths = new HashSet();
+ private readonly HashSet baggedCommands = new HashSet();
private readonly RightsManager rightsManager;
+ public RootGroup RootGroup { get; } = new RootGroup();
+
public CommandManager(RightsManager rightsManager)
{
- CommandSystem = new XCommandSystem();
- Util.Init(out aliasPaths);
- Util.Init(out commandPaths);
- Util.Init(out baggedCommands);
this.rightsManager = rightsManager;
}
- public XCommandSystem CommandSystem { get; }
-
public IEnumerable AllCommands => baggedCommands.SelectMany(x => x.BagCommands);
public IEnumerable AllRights => AllCommands.Select(x => x.RequiredRight).Concat(baggedCommands.SelectMany(x => x.AdditionalRights));
+ #region Management
+
public void RegisterCollection(ICommandBag bag)
{
if (baggedCommands.Contains(bag))
- throw new InvalidOperationException("This bag is already loaded.");
+ return;
CheckDistinct(bag.BagCommands);
baggedCommands.Add(bag);
@@ -84,7 +88,7 @@ public E RegisterAlias(string path, string command)
if (aliasPaths.ContainsKey(path))
return new LocalStr("Already exists"); // TODO
- var dac = new AliasCommand(CommandSystem, command);
+ var dac = new AliasCommand(command);
var res = LoadICommand(dac, path);
if (!res)
return new LocalStr(res.Error); // TODO
@@ -181,40 +185,39 @@ private E LoadICommand(ICommand com, string path)
private R BuildAndGet(IEnumerable comPath)
{
- CommandGroup group = CommandSystem.RootCommand;
+ CommandGroup group = RootGroup;
// this for loop iterates through the separate names of
// the command to be added.
foreach (var comPathPart in comPath)
{
- ICommand currentCommand = group.GetCommand(comPathPart);
-
+ switch (group.GetCommand(comPathPart))
+ {
// if a group to hold the next level command doesn't exist
// it will be created here
- if (currentCommand is null)
- {
+ case null:
var nextGroup = new CommandGroup();
group.AddCommand(comPathPart, nextGroup);
group = nextGroup;
- }
+ break;
+
// if the group already exists we can take it.
- else if (currentCommand is CommandGroup cgCommand)
- {
+ case CommandGroup cgCommand:
group = cgCommand;
- }
+ break;
+
// if the element is anything else, we have to replace it
// with a group and put the old element back into it.
- else if (currentCommand is FunctionCommand)
- {
+ case FunctionCommand fnCommand:
var subGroup = new CommandGroup();
group.RemoveCommand(comPathPart);
group.AddCommand(comPathPart, subGroup);
- var insertResult = InsertInto(group, currentCommand, comPathPart);
+ var insertResult = InsertInto(group, fnCommand, comPathPart);
if (!insertResult.Ok)
return insertResult.Error;
group = subGroup;
- }
- else
- {
+ break;
+
+ default:
return "An overloaded command cannot be replaced by a CommandGroup";
}
}
@@ -298,7 +301,7 @@ private void UnloadICommand(ICommand com, string path)
var node = new CommandUnloadNode
{
ParentNode = null,
- Self = CommandSystem.RootCommand,
+ Self = RootGroup,
};
// build up the list to our desired node
@@ -313,26 +316,28 @@ private void UnloadICommand(ICommand com, string path)
Self = nextGroup,
};
}
+
var subGroup = node.Self.GetCommand(comPath.Last());
+
+ switch (subGroup)
+ {
// nothing to remove
- if (subGroup is null)
+ case null:
return;
// if the subnode is a plain FunctionCommand then we found our command to delete
- else if (subGroup is FunctionCommand || subGroup is AliasCommand)
- {
+ case FunctionCommand _:
+ case AliasCommand _:
node.Self.RemoveCommand(com);
- }
+ break;
// here we can delete our command from the overloader
- else if (subGroup is OverloadedFunctionCommand subOverloadGroup)
- {
+ case OverloadedFunctionCommand subOverloadGroup:
if (com is FunctionCommand funcCom)
subOverloadGroup.RemoveCommand(funcCom);
else
return;
- }
+ break;
// now to the special case when a command gets inserted with an empty string
- else if (subGroup is CommandGroup insertGroup)
- {
+ case CommandGroup insertGroup:
// since we check precisely that only one command and only a simple FunctionCommand
// can be added with an empty string, wen can delete it safely this way
insertGroup.RemoveCommand(string.Empty);
@@ -342,6 +347,7 @@ private void UnloadICommand(ICommand com, string path)
ParentNode = node,
Self = insertGroup,
};
+ break;
}
// and finally clean all empty nodes up
@@ -358,5 +364,120 @@ private class CommandUnloadNode
public CommandUnloadNode ParentNode { get; set; }
public CommandGroup Self { get; set; }
}
+
+ #endregion
+
+ #region Execution
+
+ internal static ICommand AstToCommandResult(AstNode node)
+ {
+ switch (node.Type)
+ {
+ case AstType.Error:
+ throw new CommandException("Found an unconvertable ASTNode of type Error", CommandExceptionReason.InternalError);
+ case AstType.Command:
+ var cmd = (AstCommand)node;
+ var arguments = new ICommand[cmd.Parameter.Count];
+ int tailCandidates = 0;
+ for (int i = cmd.Parameter.Count - 1; i >= 1; i--)
+ {
+ var para = cmd.Parameter[i];
+ if (!(para is AstValue astVal) || astVal.StringType != StringType.FreeString)
+ break;
+
+ arguments[i] = new TailStringAutoConvertCommand(new TailString(astVal.Value, astVal.TailString));
+ tailCandidates++;
+ }
+ for (int i = 0; i < cmd.Parameter.Count - tailCandidates; i++)
+ arguments[i] = AstToCommandResult(cmd.Parameter[i]);
+ return new RootCommand(arguments);
+ case AstType.Value:
+ var astNode = (AstValue)node;
+ // Quoted strings are always strings, the rest gets automatically converted
+ if (astNode.StringType == StringType.FreeString)
+ return new AutoConvertResultCommand(astNode.Value);
+ else
+ return new ResultCommand(new PrimitiveResult(astNode.Value));
+ default:
+ throw Tools.UnhandledDefault(node.Type);
+ }
+ }
+
+ public static object Execute(ExecutionInformation info, string command, IReadOnlyList returnTypes)
+ {
+ var ast = CommandParser.ParseCommandRequest(command);
+ var cmd = AstToCommandResult(ast);
+ return cmd.Execute(info, Array.Empty(), returnTypes);
+ }
+
+ public static object Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
+ => info.GetModule().RootGroup.Execute(info, arguments, returnTypes);
+
+ public static string ExecuteCommand(ExecutionInformation info, string command)
+ => CastResult(Execute(info, command, ReturnStringOrNothing));
+
+ public static string ExecuteCommand(ExecutionInformation info, IReadOnlyList arguments)
+ => CastResult(Execute(info, arguments, ReturnStringOrNothing));
+
+ private static string CastResult(object result)
+ {
+ if (result is IPrimitiveResult s)
+ return s.Get();
+ if (result == null)
+ return null;
+ throw new CommandException("Expected a string or nothing as result", CommandExceptionReason.NoReturnMatch);
+ }
+
+ public static object GetEmpty(IReadOnlyList resultTypes)
+ {
+ foreach (var item in resultTypes)
+ {
+ if (item == null)
+ return null;
+ else if (item == typeof(string))
+ return string.Empty;
+ }
+ throw new CommandException("No empty return type available", CommandExceptionReason.NoReturnMatch);
+ }
+
+ public static string GetTree(ICommand com)
+ {
+ var strb = new TextModBuilder();
+ GetTree(com, strb, 0);
+ return strb.ToString();
+ }
+
+ private static void GetTree(ICommand com, TextModBuilder strb, int indent)
+ {
+ switch (com)
+ {
+ case CommandGroup group:
+ strb.AppendFormat("\n".Mod().Color(Color.Red));
+ foreach (var subCom in group.Commands)
+ {
+ strb.Append(new string(' ', (indent + 1) * 2)).Append(subCom.Key);
+ GetTree(subCom.Value, strb, indent + 1);
+ }
+ break;
+
+ case FunctionCommand _:
+ strb.AppendFormat("\n".Mod().Color(Color.Green));
+ break;
+
+ case OverloadedFunctionCommand ofunc:
+ strb.AppendFormat($"\n".Mod().Color(Color.Blue));
+ break;
+
+ case AliasCommand _:
+ strb.AppendFormat($"\n".Mod().Color(Color.Yellow));
+ break;
+
+ default:
+ strb.AppendFormat("\n");
+ break;
+ }
+ }
+
+ #endregion
}
}
diff --git a/TS3AudioBot/CommandSystem/CommandParser.cs b/TS3AudioBot/CommandSystem/CommandParser.cs
index f753cdc5..7ee63d67 100644
--- a/TS3AudioBot/CommandSystem/CommandParser.cs
+++ b/TS3AudioBot/CommandSystem/CommandParser.cs
@@ -7,13 +7,13 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Text;
+using TS3AudioBot.CommandSystem.Ast;
+
namespace TS3AudioBot.CommandSystem
{
- using Ast;
- using System;
- using System.Collections.Generic;
- using System.Text;
-
internal static class CommandParser
{
public const char DefaultCommandChar = '!';
diff --git a/TS3AudioBot/CommandSystem/CommandResults/CommandCommandResult.cs b/TS3AudioBot/CommandSystem/CommandResults/CommandCommandResult.cs
deleted file mode 100644
index bf94cc92..00000000
--- a/TS3AudioBot/CommandSystem/CommandResults/CommandCommandResult.cs
+++ /dev/null
@@ -1,27 +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.CommandSystem.CommandResults
-{
- using Commands;
-
- public class CommandCommandResult : ICommandResult
- {
- public CommandResultType ResultType => CommandResultType.Command;
-
- public virtual ICommand Command { get; }
-
- public CommandCommandResult(ICommand commandArg)
- {
- Command = commandArg;
- }
-
- public override string ToString() => "CommandCommandResult can't be converted into a string";
- }
-}
diff --git a/TS3AudioBot/CommandSystem/CommandResults/ICommandResult.cs b/TS3AudioBot/CommandSystem/CommandResults/IAudioResourceResult.cs
similarity index 79%
rename from TS3AudioBot/CommandSystem/CommandResults/ICommandResult.cs
rename to TS3AudioBot/CommandSystem/CommandResults/IAudioResourceResult.cs
index db91d319..2909fe3f 100644
--- a/TS3AudioBot/CommandSystem/CommandResults/ICommandResult.cs
+++ b/TS3AudioBot/CommandSystem/CommandResults/IAudioResourceResult.cs
@@ -7,10 +7,12 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using TS3AudioBot.ResourceFactories;
+
namespace TS3AudioBot.CommandSystem.CommandResults
{
- public interface ICommandResult
+ public interface IAudioResourceResult
{
- CommandResultType ResultType { get; }
+ AudioResource AudioResource { get; }
}
}
diff --git a/TS3AudioBot/CommandSystem/CommandResults/IPrimitiveResult.cs b/TS3AudioBot/CommandSystem/CommandResults/IPrimitiveResult.cs
new file mode 100644
index 00000000..6fc2de2b
--- /dev/null
+++ b/TS3AudioBot/CommandSystem/CommandResults/IPrimitiveResult.cs
@@ -0,0 +1,26 @@
+// 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.CommandSystem.CommandResults
+{
+ ///
+ /// A result which can safely used as a primitive like a string.
+ ///
+ /// The complete list of primitive types is .
+ ///
+ public interface IPrimitiveResult : IPrimitiveResult
+ {
+ new T Get();
+ }
+
+ public interface IPrimitiveResult
+ {
+ object Get();
+ }
+}
diff --git a/TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs b/TS3AudioBot/CommandSystem/CommandResults/PrimitiveResult.cs
similarity index 60%
rename from TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs
rename to TS3AudioBot/CommandSystem/CommandResults/PrimitiveResult.cs
index e4560f51..ddbcc8ff 100644
--- a/TS3AudioBot/CommandSystem/CommandResults/EmptyCommandResult.cs
+++ b/TS3AudioBot/CommandSystem/CommandResults/PrimitiveResult.cs
@@ -9,13 +9,17 @@
namespace TS3AudioBot.CommandSystem.CommandResults
{
- public sealed class EmptyCommandResult : ICommandResult
+ public class PrimitiveResult : IPrimitiveResult
{
- public static EmptyCommandResult Instance { get; } = new EmptyCommandResult();
+ public T Content { get; }
- private EmptyCommandResult() { }
+ public PrimitiveResult(T contentArg)
+ {
+ Content = contentArg;
+ }
- public CommandResultType ResultType => CommandResultType.Empty;
- public override string ToString() => string.Empty;
+ public virtual T Get() => Content;
+
+ object IPrimitiveResult.Get() => Content;
}
}
diff --git a/TS3AudioBot/CommandSystem/CommandResults/ResultHelper.cs b/TS3AudioBot/CommandSystem/CommandResults/ResultHelper.cs
new file mode 100644
index 00000000..0acc7741
--- /dev/null
+++ b/TS3AudioBot/CommandSystem/CommandResults/ResultHelper.cs
@@ -0,0 +1,61 @@
+// 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 .
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using static TS3AudioBot.CommandSystem.CommandSystemTypes;
+
+namespace TS3AudioBot.CommandSystem.CommandResults
+{
+ public static class ResultHelper
+ {
+ public static bool IsValidResult(object result, IReadOnlyList returnTypes)
+ {
+ if (result == null)
+ return returnTypes.Contains(null);
+
+ return IsValidResultType(result.GetType(), returnTypes);
+ }
+
+ public static bool IsValidResultType(Type resultType, IReadOnlyList returnTypes)
+ {
+ return returnTypes.Any(t =>
+ {
+ if (t == null)
+ return false;
+ if (BasicTypes.Contains(t))
+ {
+ var genType = typeof(IPrimitiveResult<>).MakeGenericType(t);
+ return genType.IsAssignableFrom(resultType);
+ }
+ else
+ return t.IsAssignableFrom(resultType);
+ });
+ }
+
+ ///
+ /// Automaticall wrapes primitive results in .
+ /// Otherwise returns the result.
+ ///
+ /// The valid result.
+ /// The type for the result.
+ /// The result value.
+ public static object ToResult(Type resultType, object result)
+ {
+ if (BasicTypes.Contains(resultType))
+ {
+ var genType = typeof(PrimitiveResult<>).MakeGenericType(resultType);
+ return Activator.CreateInstance(genType, new object[] { result });
+ }
+ else
+ return result;
+ }
+ }
+}
diff --git a/TS3AudioBot/CommandSystem/CommandResults/StringCommandResult.cs b/TS3AudioBot/CommandSystem/CommandResults/StringCommandResult.cs
deleted file mode 100644
index f64b9827..00000000
--- a/TS3AudioBot/CommandSystem/CommandResults/StringCommandResult.cs
+++ /dev/null
@@ -1,26 +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.CommandSystem.CommandResults
-{
- public class StringCommandResult : ICommandResult
- {
- public static readonly StringCommandResult Empty = new StringCommandResult(string.Empty);
-
- public CommandResultType ResultType => CommandResultType.String;
- public virtual string Content { get; }
-
- public StringCommandResult(string contentArg)
- {
- Content = contentArg;
- }
-
- public override string ToString() => Content;
- }
-}
diff --git a/TS3AudioBot/CommandSystem/CommandResults/TailStringCommandResult.cs b/TS3AudioBot/CommandSystem/CommandResults/TailString.cs
similarity index 68%
rename from TS3AudioBot/CommandSystem/CommandResults/TailStringCommandResult.cs
rename to TS3AudioBot/CommandSystem/CommandResults/TailString.cs
index 4ba8ef14..0ea9a919 100644
--- a/TS3AudioBot/CommandSystem/CommandResults/TailStringCommandResult.cs
+++ b/TS3AudioBot/CommandSystem/CommandResults/TailString.cs
@@ -9,13 +9,13 @@
namespace TS3AudioBot.CommandSystem.CommandResults
{
- public class TailStringCommandResult : StringCommandResult
+ public class TailString : PrimitiveResult
{
- public string TailString { get; }
+ public string Tail { get; }
- public TailStringCommandResult(string contentArg, string tailArg) : base(contentArg)
+ public TailString(string contentArg, string tailArg) : base(contentArg)
{
- TailString = tailArg;
+ Tail = tailArg;
}
}
}
diff --git a/TS3AudioBot/CommandSystem/CommandSystemExtensions.cs b/TS3AudioBot/CommandSystem/CommandSystemExtensions.cs
index 99c527f4..a768350f 100644
--- a/TS3AudioBot/CommandSystem/CommandSystemExtensions.cs
+++ b/TS3AudioBot/CommandSystem/CommandSystemExtensions.cs
@@ -7,11 +7,12 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using TS3AudioBot.Algorithm;
+using TS3AudioBot.Dependency;
+
namespace TS3AudioBot.CommandSystem
{
- using Algorithm;
- using Dependency;
-
public static class CommandSystemExtensions
{
public static IFilter GetFilter(this IInjector injector)
@@ -20,5 +21,8 @@ public static IFilter GetFilter(this IInjector injector)
return filter;
return Filter.DefaultFilter;
}
+
+ public static Lazy GetFilterLazy(this IInjector injector)
+ => new Lazy(() => injector.GetFilter(), false);
}
}
diff --git a/TS3AudioBot/CommandSystem/CommandSystemTypes.cs b/TS3AudioBot/CommandSystem/CommandSystemTypes.cs
new file mode 100644
index 00000000..c89c4a41
--- /dev/null
+++ b/TS3AudioBot/CommandSystem/CommandSystemTypes.cs
@@ -0,0 +1,49 @@
+// 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 .
+
+using System;
+using System.Collections.Generic;
+using TS3AudioBot.CommandSystem.CommandResults;
+using TS3AudioBot.CommandSystem.Commands;
+using TS3AudioBot.Web.Api;
+
+namespace TS3AudioBot.CommandSystem
+{
+ public static class CommandSystemTypes
+ {
+ public static readonly Type[] ReturnJson = { typeof(JsonObject) };
+ public static readonly Type[] ReturnJsonOrDataOrNothing = { typeof(JsonObject), typeof(DataStream), null };
+ public static readonly Type[] ReturnString = { typeof(string) };
+ public static readonly Type[] ReturnStringOrNothing = { typeof(string), null };
+ public static readonly Type[] ReturnCommandOrString = { typeof(ICommand), typeof(string) };
+ public static readonly Type[] ReturnAnyPreferNothing = { null, typeof(string), typeof(JsonObject), typeof(ICommand) };
+
+ ///
+ /// The order of types, the first item has the highest priority,
+ /// items not in the list have higher priority as they are special types.
+ ///
+ public static readonly Type[] TypeOrder = {
+ typeof(bool),
+ typeof(sbyte), typeof(byte),
+ typeof(short), typeof(ushort),
+ typeof(int), typeof(uint),
+ typeof(long), typeof(ulong),
+ typeof(float), typeof(double),
+ typeof(TimeSpan), typeof(DateTime),
+ typeof(string) };
+ public static readonly HashSet BasicTypes = new HashSet(TypeOrder);
+
+ public static readonly HashSet AdvancedTypes = new HashSet(new Type[] {
+ typeof(IAudioResourceResult),
+ typeof(System.Collections.IEnumerable),
+ typeof(ResourceFactories.AudioResource),
+ typeof(History.AudioLogEntry),
+ typeof(Playlists.PlaylistItem),
+ });
+ }
+}
diff --git a/TS3AudioBot/CommandSystem/Commands/AliasCommand.cs b/TS3AudioBot/CommandSystem/Commands/AliasCommand.cs
index 0dbc36d1..17cb4732 100644
--- a/TS3AudioBot/CommandSystem/Commands/AliasCommand.cs
+++ b/TS3AudioBot/CommandSystem/Commands/AliasCommand.cs
@@ -7,27 +7,26 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TS3AudioBot.Dependency;
+
namespace TS3AudioBot.CommandSystem.Commands
{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using TS3AudioBot.CommandSystem.CommandResults;
- using TS3AudioBot.Dependency;
-
public class AliasCommand : ICommand
{
private readonly ICommand aliasCommand;
public string AliasString { get; }
- public AliasCommand(XCommandSystem root, string command)
+ public AliasCommand(string command)
{
var ast = CommandParser.ParseCommandRequest(command);
- aliasCommand = root.AstToCommandResult(ast);
+ aliasCommand = CommandManager.AstToCommandResult(ast);
AliasString = command;
}
- public ICommandResult Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
+ public object Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
{
info.UseComplexityTokens(1);
diff --git a/TS3AudioBot/CommandSystem/Commands/AppliedCommand.cs b/TS3AudioBot/CommandSystem/Commands/AppliedCommand.cs
index be2128f0..d4db70f7 100644
--- a/TS3AudioBot/CommandSystem/Commands/AppliedCommand.cs
+++ b/TS3AudioBot/CommandSystem/Commands/AppliedCommand.cs
@@ -7,11 +7,11 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+
namespace TS3AudioBot.CommandSystem.Commands
{
- using CommandResults;
- using System.Collections.Generic;
-
public class AppliedCommand : ICommand
{
private readonly ICommand internCommand;
@@ -23,7 +23,7 @@ public AppliedCommand(ICommand command, IReadOnlyList arguments)
internArguments = arguments;
}
- public virtual ICommandResult Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
+ public virtual object Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
{
var merged = new ICommand[internArguments.Count + arguments.Count];
internArguments.CopyTo(0, merged, 0);
diff --git a/TS3AudioBot/CommandSystem/Commands/AutoConvertResultCommand.cs b/TS3AudioBot/CommandSystem/Commands/AutoConvertResultCommand.cs
new file mode 100644
index 00000000..450eee22
--- /dev/null
+++ b/TS3AudioBot/CommandSystem/Commands/AutoConvertResultCommand.cs
@@ -0,0 +1,53 @@
+// 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 .
+
+using System;
+using System.Collections.Generic;
+using TS3AudioBot.CommandSystem.CommandResults;
+using TS3AudioBot.Localization;
+
+namespace TS3AudioBot.CommandSystem.Commands
+{
+ ///
+ /// A command that stores a result and returns it.
+ ///
+ public class AutoConvertResultCommand : ICommand
+ {
+ private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
+
+ public IConvertible Content { get; }
+
+ public AutoConvertResultCommand(IConvertible contentArg)
+ {
+ Content = contentArg;
+ }
+
+ public virtual object Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
+ {
+ var filterLazy = info.GetFilterLazy();
+ foreach (var type in returnTypes)
+ {
+ try
+ {
+ var result = FunctionCommand.ConvertParam(Content, type, filterLazy);
+ Log.Debug("Converting command result {0} to {1} returns {2}", Content, type, result);
+
+ return ResultHelper.ToResult(type, result);
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Converting command result {0} to {1} failed", Content, type);
+ }
+ }
+ throw new CommandException(strings.error_cmd_no_matching_overload, CommandExceptionReason.NoReturnMatch);
+ }
+
+ public override string ToString() => "";
+ }
+}
diff --git a/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs b/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs
index ccf946eb..3dc1d06f 100644
--- a/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs
+++ b/TS3AudioBot/CommandSystem/Commands/CommandGroup.cs
@@ -7,14 +7,14 @@
// You should have received a copy of the Open Software License along with this
// program. If not, see .
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TS3AudioBot.CommandSystem.CommandResults;
+using TS3AudioBot.Localization;
+
namespace TS3AudioBot.CommandSystem.Commands
{
- using CommandResults;
- using Localization;
- using System;
- using System.Collections.Generic;
- using System.Linq;
-
public class CommandGroup : ICommand
{
private readonly IDictionary commands = new Dictionary();
@@ -33,19 +33,19 @@ public bool RemoveCommand(ICommand command)
public bool IsEmpty => commands.Count == 0;
public IEnumerable> Commands => commands;
- public virtual ICommandResult Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
+ public virtual object Execute(ExecutionInformation info, IReadOnlyList arguments, IReadOnlyList returnTypes)
{
string result;
if (arguments.Count == 0)
{
- if (returnTypes.Contains(CommandResultType.Command))
- return new CommandCommandResult(this);
+ if (returnTypes.Contains(typeof(ICommand)))
+ return this;
result = string.Empty;
}
else
{
- var comResult = arguments[0].Execute(info, Array.Empty(), XCommandSystem.ReturnString);
- result = ((StringCommandResult)comResult).Content;
+ var comResult = arguments[0].Execute(info, Array.Empty