diff --git a/Experiments/ExternalActionManager/DemoBot/ActionManager/Actions/GuidAction.cs b/Experiments/ExternalActionManager/DemoBot/ActionManager/Actions/GuidAction.cs new file mode 100644 index 0000000..1c87d64 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/ActionManager/Actions/GuidAction.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; +using TelegramBotBase.Sessions; + +namespace DemoBot.ActionManager.Actions +{ + public class GuidAction : IExternalAction + { + public Type FormType { get; } + + public String Method { get; set; } + + public Action SetProperty { get; set; } + + Guid? _lastValue { get; set; } + + public GuidAction(Type formType, string method, Action setProperty) + { + FormType = formType; + Method = method; + SetProperty = setProperty; + } + + public bool DoesFit(string raw_action) + { + var cd = CallbackData.Deserialize(raw_action); + + if (cd == null) + return false; + + if (cd.Method != Method) + return false; + + Guid g; + + if (Guid.TryParse(cd.Value, out g)) + _lastValue = g; + + return true; + } + + + public async Task DoAction(UpdateResult ur, MessageResult mr, DeviceSession session) + { + await mr.ConfirmAction(); + + var new_form = FormType.GetConstructor(new Type[] { })?.Invoke(new object[] { }) as FormBase; + + if (_lastValue != null) + SetProperty(new_form, _lastValue.Value); + + await session.ActiveForm.NavigateTo(new_form); + } + + + public static CallbackData GetCallback(String method, Guid guid) + { + return new CallbackData(method, guid.ToString()); + } + } + + public class GuidAction : IExternalAction + where TForm : FormBase + { + public String Method { get; set; } + + public Action SetProperty { get; set; } + + Guid? _lastValue { get; set; } + + public GuidAction(string method, Action setProperty) + { + Method = method; + SetProperty = setProperty; + } + + public bool DoesFit(string raw_action) + { + var cd = CallbackData.Deserialize(raw_action); + + if (cd == null) + return false; + + if (cd.Method != Method) + return false; + + Guid g; + + if (Guid.TryParse(cd.Value, out g)) + _lastValue = g; + + return true; + } + + + public async Task DoAction(UpdateResult ur, MessageResult mr, DeviceSession session) + { + await mr.ConfirmAction(); + + var type = typeof(TForm); + + TForm new_form = type.GetConstructor(new Type[] { })?.Invoke(new object[] { }) as TForm; + + if (_lastValue != null) + SetProperty(new_form, _lastValue.Value); + + await session.ActiveForm.NavigateTo(new_form); + } + + + public static CallbackData GetCallback(String method, Guid guid) + { + return new CallbackData(method, guid.ToString()); + } + + } + + public static class GuidAction_Extensions + { + + public static void AddGuidAction(this ExternalActionManager manager, string method, Action action) + where TForm : FormBase + { + if (!typeof(FormBase).IsAssignableFrom(typeof(TForm))) + { + throw new ArgumentException($"{nameof(TForm)} argument must be a {nameof(FormBase)} type"); + } + + manager.Add(new GuidAction(method, action)); + } + + public static void AddGuidAction(this ExternalActionManager manager, Type formType, string method, Action action) + { + if (!typeof(FormBase).IsAssignableFrom(formType)) + { + throw new ArgumentException($"{nameof(formType)} argument must be a {nameof(FormBase)} type"); + } + + manager.Add(new GuidAction(formType, method, action)); + } + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/ActionManager/Actions/StartWithAction.cs b/Experiments/ExternalActionManager/DemoBot/ActionManager/Actions/StartWithAction.cs new file mode 100644 index 0000000..fe46773 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/ActionManager/Actions/StartWithAction.cs @@ -0,0 +1,128 @@ +using System; +using System.Diagnostics; +using TelegramBotBase.Base; +using TelegramBotBase.DependencyInjection; +using TelegramBotBase.Form; +using TelegramBotBase.Interfaces; +using TelegramBotBase.Sessions; + +namespace DemoBot.ActionManager.Actions +{ + public class StartWithAction : IExternalAction + { + public Type FormType { get; } + + public string Value { get; set; } + + public Action SetProperty { get; set; } + + String? _lastValue { get; set; } + + + public StartWithAction(Type formType, string value, Action setProperty) + { + FormType = formType; + Value = value; + SetProperty = setProperty; + } + + public bool DoesFit(string raw_action) + { + if (!raw_action.StartsWith(Value)) + return false; + + _lastValue = raw_action; + + return true; + } + + + public async Task DoAction(UpdateResult ur, MessageResult mr, DeviceSession session) + { + await mr.ConfirmAction(); + + var new_form = FormType.GetConstructor(new Type[] { })?.Invoke(new object[] { }) as FormBase; + + if (_lastValue != null) + { + SetProperty(new_form, _lastValue); + } + + await session.ActiveForm.NavigateTo(new_form); + } + } + + public class StartWithAction : IExternalAction + where TForm : FormBase + { + public string Value { get; set; } + + public Action SetProperty { get; set; } + + String? _lastValue { get; set; } + + public StartWithAction(string value, Action setProperty) + { + Value = value; + SetProperty = setProperty; + } + + + public bool DoesFit(string raw_action) + { + if (!raw_action.StartsWith(Value)) + return false; + + _lastValue = raw_action; + + return true; + } + + + public async Task DoAction(UpdateResult ur, MessageResult mr, DeviceSession session) + { + await mr.ConfirmAction(); + + var type = typeof(TForm); + + TForm new_form = type.GetConstructor(new Type[] { })?.Invoke(new object[] { }) as TForm; + + if (_lastValue != null) + { + SetProperty(new_form, _lastValue); + } + + await session.ActiveForm.NavigateTo(new_form); + } + + + + } + + public static class StartWithAction_Extensions + { + + public static void AddStartsWithAction(this ExternalActionManager manager, string value, Action setProperty) + where TForm : FormBase + { + if (!typeof(FormBase).IsAssignableFrom(typeof(TForm))) + { + throw new ArgumentException($"{nameof(TForm)} argument must be a {nameof(FormBase)} type"); + } + + manager.Add(new StartWithAction(value, setProperty)); + } + + public static void AddStartsWithAction(this ExternalActionManager manager, Type formType, string value, Action setProperty) + { + if (!typeof(FormBase).IsAssignableFrom(formType)) + { + throw new ArgumentException($"{nameof(formType)} argument must be a {nameof(FormBase)} type"); + } + + manager.Add(new StartWithAction(formType, value, setProperty)); + } + + } + +} diff --git a/Experiments/ExternalActionManager/DemoBot/ActionManager/ExternalActionManager.cs b/Experiments/ExternalActionManager/DemoBot/ActionManager/ExternalActionManager.cs new file mode 100644 index 0000000..bc09fee --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/ActionManager/ExternalActionManager.cs @@ -0,0 +1,42 @@ +using DemoBot.ActionManager.Actions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Args; +using TelegramBotBase.Base; +using TelegramBotBase.Form; +using TelegramBotBase.Interfaces; +using TelegramBotBase.Sessions; + +namespace DemoBot.ActionManager +{ + public partial class ExternalActionManager + { + + List actions = new List(); + + public void Add(IExternalAction action) + { + actions.Add(action); + } + + public async Task ManageCall(UpdateResult ur, MessageResult mr, DeviceSession session) + { + + foreach (var action in actions) + { + if (!action.DoesFit(mr.RawData)) + continue; + + await action.DoAction(ur, mr, session); + + return true; + } + + + return false; + } + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/ActionManager/IExternalAction.cs b/Experiments/ExternalActionManager/DemoBot/ActionManager/IExternalAction.cs new file mode 100644 index 0000000..ea0c954 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/ActionManager/IExternalAction.cs @@ -0,0 +1,15 @@ +using TelegramBotBase.Base; +using TelegramBotBase.Interfaces; +using TelegramBotBase.Sessions; + +namespace DemoBot.ActionManager +{ + + public interface IExternalAction + { + bool DoesFit(string raw_action); + + Task DoAction(UpdateResult ur, MessageResult mr, DeviceSession session); + } + +} diff --git a/Experiments/ExternalActionManager/DemoBot/CustomFormBaseMessageLoop.cs b/Experiments/ExternalActionManager/DemoBot/CustomFormBaseMessageLoop.cs new file mode 100644 index 0000000..f2f1543 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/CustomFormBaseMessageLoop.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DemoBot.ActionManager; +using Telegram.Bot.Types.Enums; +using TelegramBotBase; +using TelegramBotBase.Args; +using TelegramBotBase.Base; +using TelegramBotBase.Interfaces; +using TelegramBotBase.Sessions; + +namespace DemoBot +{ + public class CustomFormBaseMessageLoop : IMessageLoopFactory + { + private static readonly object EvUnhandledCall = new(); + + private readonly EventHandlerList _events = new(); + + public ExternalActionManager ExternalActionManager { get; set; } + + public async Task MessageLoop(BotBase bot, DeviceSession session, UpdateResult ur, MessageResult mr) + { + var update = ur.RawData; + + + if (update.Type != UpdateType.Message + && update.Type != UpdateType.EditedMessage + && update.Type != UpdateType.CallbackQuery) + { + return; + } + + //Is this a bot command ? + if (mr.IsFirstHandler && mr.IsBotCommand && bot.IsKnownBotCommand(mr.BotCommand)) + { + var sce = new BotCommandEventArgs(mr.BotCommand, mr.BotCommandParameters, mr.Message, session.DeviceId, + session); + await bot.OnBotCommand(sce); + + if (sce.Handled) + { + return; + } + } + + mr.Device = session; + ur.Device = session; + + var activeForm = session.ActiveForm; + + //Pre Loading Event + await activeForm.PreLoad(mr); + + //Send Load event to controls + await activeForm.LoadControls(mr); + + //Loading Event + await activeForm.Load(mr); + + + //Is Attachment ? (Photo, Audio, Video, Contact, Location, Document) (Ignore Callback Queries) + if (update.Type == UpdateType.Message) + { + if ((mr.MessageType == MessageType.Contact) + | (mr.MessageType == MessageType.Document) + | (mr.MessageType == MessageType.Location) + | (mr.MessageType == MessageType.Photo) + | (mr.MessageType == MessageType.Video) + | (mr.MessageType == MessageType.Audio)) + { + await activeForm.SentData(new DataResult(ur)); + } + } + + //Message edited ? + if (update.Type == UpdateType.EditedMessage) + { + await activeForm.Edited(mr); + } + + //Action Event + if (!session.FormSwitched && mr.IsAction) + { + //Send Action event to controls + await activeForm.ActionControls(mr); + + //Send Action event to form itself + await activeForm.Action(mr); + + if (!mr.Handled) + { + var handled = await ExternalActionManager?.ManageCall(ur, mr, session); + + if (handled) + { + mr.Handled = true; + if (!session.FormSwitched) + { + return; + } + } + else + { + var uhc = new UnhandledCallEventArgs(ur.Message.Text, mr.RawData, session.DeviceId, mr.MessageId, ur.Message, session); + + OnUnhandledCall(uhc); + + if (uhc.Handled) + { + mr.Handled = true; + if (!session.FormSwitched) + { + return; + } + } + } + + } + } + + if (!session.FormSwitched) + { + //Render Event + await activeForm.RenderControls(mr); + + await activeForm.Render(mr); + } + } + + /// + /// Will be called if no form handled this call + /// + public event EventHandler UnhandledCall + { + add => _events.AddHandler(EvUnhandledCall, value); + remove => _events.RemoveHandler(EvUnhandledCall, value); + } + + public void OnUnhandledCall(UnhandledCallEventArgs e) + { + (_events[EvUnhandledCall] as EventHandler)?.Invoke(this, e); + } + + + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/ExternalActionManager.csproj b/Experiments/ExternalActionManager/DemoBot/ExternalActionManager.csproj new file mode 100644 index 0000000..d09a345 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/ExternalActionManager.csproj @@ -0,0 +1,15 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + diff --git a/Experiments/ExternalActionManager/DemoBot/Forms/HiddenForm.cs b/Experiments/ExternalActionManager/DemoBot/Forms/HiddenForm.cs new file mode 100644 index 0000000..034b5d9 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/Forms/HiddenForm.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace DemoBot.Forms +{ + public class HiddenForm : AutoCleanForm + { + public String value { get; set; } + + public HiddenForm() { + + DeleteMode = TelegramBotBase.Enums.EDeleteMode.OnLeavingForm; + DeleteSide = TelegramBotBase.Enums.EDeleteSide.Both; + } + + public override async Task Action(MessageResult message) + { + if (message.RawData != "start") + { + return; + } + + await message.ConfirmAction("Lets go"); + + message.Handled = true; + + var st = new StartForm(); + + await NavigateTo(st); + } + + public override async Task Render(MessageResult message) + { + + var bf = new ButtonForm(); + + bf.AddButtonRow("Goto Start", "start"); + + value = value.Replace("_", "\\_"); + + await Device.Send($"Welcome to Hidden form\n\nThe given value is {value}", bf); + + } + + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/Forms/HiddenOpenForm.cs b/Experiments/ExternalActionManager/DemoBot/Forms/HiddenOpenForm.cs new file mode 100644 index 0000000..62b031f --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/Forms/HiddenOpenForm.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace DemoBot.Forms +{ + public class HiddenOpenForm : AutoCleanForm + { + + public Guid guid { get; set; } + + public HiddenOpenForm() + { + + DeleteMode = TelegramBotBase.Enums.EDeleteMode.OnLeavingForm; + DeleteSide = TelegramBotBase.Enums.EDeleteSide.Both; + } + + public override async Task Action(MessageResult message) + { + if (message.RawData != "start") + { + return; + } + + await message.ConfirmAction("Lets go"); + + message.Handled = true; + + var st = new StartForm(); + + await NavigateTo(st); + } + + public override async Task Render(MessageResult message) + { + + var bf = new ButtonForm(); + + bf.AddButtonRow("Goto Start", "start"); + + await Device.Send($"Welcome to Hidden open form\n\nYour guid is: {guid}", bf); + + + } + + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/Forms/HiddenTicketForm.cs b/Experiments/ExternalActionManager/DemoBot/Forms/HiddenTicketForm.cs new file mode 100644 index 0000000..577f49d --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/Forms/HiddenTicketForm.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace DemoBot.Forms +{ + public class HiddenTicketForm : AutoCleanForm + { + + public Guid ticketId { get; set; } + + public HiddenTicketForm() + { + + DeleteMode = TelegramBotBase.Enums.EDeleteMode.OnLeavingForm; + DeleteSide = TelegramBotBase.Enums.EDeleteSide.Both; + } + + public override async Task Action(MessageResult message) + { + if (message.RawData != "start") + { + return; + } + + + await message.ConfirmAction("Lets go"); + + message.Handled = true; + + var st = new StartForm(); + + await NavigateTo(st); + } + + public override async Task Render(MessageResult message) + { + + var bf = new ButtonForm(); + + bf.AddButtonRow("Goto Start", "start"); + + await Device.Send($"Welcome to Hidden ticket form\n\nYour ticket Id is: {ticketId}", bf); + + + } + + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/Forms/StartForm.cs b/Experiments/ExternalActionManager/DemoBot/Forms/StartForm.cs new file mode 100644 index 0000000..1b5810c --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/Forms/StartForm.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace DemoBot.Forms +{ + internal class StartForm : AutoCleanForm + { + public StartForm() + { + + DeleteMode = TelegramBotBase.Enums.EDeleteMode.OnLeavingForm; + DeleteSide = TelegramBotBase.Enums.EDeleteSide.Both; + + Opened += StartForm_Opened; + } + + private async Task StartForm_Opened(object sender, EventArgs e) + { + await Device.Send("Hey!", disableNotification: true); + } + + public override async Task Load(MessageResult message) + { + + + + + } + + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/Program.cs b/Experiments/ExternalActionManager/DemoBot/Program.cs new file mode 100644 index 0000000..84e6220 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/Program.cs @@ -0,0 +1,165 @@ +using DemoBot.ActionManager; +using DemoBot.ActionManager.Actions; +using DemoBot.Forms; +using Telegram.Bot; +using Telegram.Bot.Types.ReplyMarkups; +using TelegramBotBase.Builder; +using TelegramBotBase.Commands; +using TelegramBotBase.Form; + +namespace DemoBot +{ + internal class Program + { + public static String Token = Environment.GetEnvironmentVariable("API_KEY") ?? throw new Exception("API_KEY is not set"); + + static async Task Main(string[] args) + { + + //Using a custom FormBase message loop which is based on the original FormBaseMessageLoop within the framework. + //Would integrate this later into the BotBaseBuilder -> MessageLoop step. + var cfb = new CustomFormBaseMessageLoop(); + + var eam = new ExternalActionManager(); + + eam.AddStartsWithAction("n_", (a, b) => + { + a.value = b; + }); + + eam.AddStartsWithAction(typeof(HiddenForm), "t_", (a, b) => + { + var hf = a as HiddenForm; + if (hf == null) + return; + + hf.value = b; + }); + + eam.AddGuidAction("tickets", (a, b) => + { + a.ticketId = b; + }); + + eam.AddGuidAction("open", (a, b) => + { + a.guid = b; + }); + + cfb.ExternalActionManager = eam; + + var bb = BotBaseBuilder.Create() + .WithAPIKey(Token) + .CustomMessageLoop(cfb) + .WithStartForm() + .NoProxy() + .CustomCommands(a => + { + a.Start("Starts the bot"); + a.Add("test", "Sends a test notification"); + + + }) + .NoSerialization() + .UseGerman() + .UseSingleThread() + .Build(); + + bb.BotCommand += Bb_BotCommand; + + bb.UnhandledCall += Bb_UnhandledCall; + + await bb.Start(); + + await bb.UploadBotCommands(); + + Console.WriteLine("Bot started."); + + + Console.ReadLine(); + + + await bb.Stop(); + + + } + + private static void Bb_UnhandledCall(object? sender, TelegramBotBase.Args.UnhandledCallEventArgs e) + { + + Console.WriteLine($"Unhandled call: {e.RawData}"); + + } + + private static async Task Bb_BotCommand(object sender, TelegramBotBase.Args.BotCommandEventArgs e) + { + + var current_form = e.Device.ActiveForm; + + switch (e.Command) + { + case "/start": + + //Already on start form + if (current_form.GetType() == typeof(Forms.StartForm)) + { + return; + } + + var st = new Forms.StartForm(); + + await current_form.NavigateTo(st); + + break; + + case "/test": + + //Send test notification + + //Test values + + String max_value = "n_".PadRight(32, '5'); //Starts with + + String max_value2 = "t_".PadRight(32, '5'); //Starts with + + Guid test_value = Guid.NewGuid(); //Unhandled caller + + var callback_guid = GuidAction.GetCallback("open", Guid.NewGuid()); //HiddenOpenForm + + var callback_tickets = GuidAction.GetCallback("tickets", Guid.NewGuid()); //HiddenTicketForm + + + String message = $"Test notification from 'outside'\n\nTest values are:\n\nTest: {max_value}\nTest2: {max_value2}\nTest (Guid): {test_value.ToString()}\nTest (Callback Guid): {callback_guid.Value}\nTickets (Guid): {callback_tickets.Value}\n"; + + + var tb = new TelegramBotClient(Token); + + var bf = new ButtonForm(); + + bf.AddButtonRow(new ButtonBase("Ok", "n_ok"), new ButtonBase("Later", "n_later")); + + bf.AddButtonRow("Test", max_value); + + bf.AddButtonRow("Test2", max_value2); + + bf.AddButtonRow("Test (Guid)", test_value.ToString()); + + bf.AddButtonRow("Test (Callback Gui)", callback_guid); + + bf.AddButtonRow("Tickets", callback_tickets); + + bf.AddButtonRow("Close", "close"); + + await tb.SendTextMessageAsync(e.DeviceId, message, disableNotification: true, replyMarkup: (InlineKeyboardMarkup)bf); + + break; + + + } + + + + + } + } +} diff --git a/Experiments/ExternalActionManager/DemoBot/Readme.md b/Experiments/ExternalActionManager/DemoBot/Readme.md new file mode 100644 index 0000000..529a7b0 --- /dev/null +++ b/Experiments/ExternalActionManager/DemoBot/Readme.md @@ -0,0 +1,11 @@ +# ExternalActionManager + +Idea of this experiment is to find a good intuitive way for handling "Unhandled calls". +Right now they are "thrown" into an event handler and you have to take care over them on yourself. + + +Source of them would be in most cases Telegram Bot messages which has an Inlinekeyboard attached. + +And the button press should navigate to a different form, or invoke a different action. + +Begin: 18.01.2024 \ No newline at end of file diff --git a/README.md b/README.md index a4ec1b1..81c6170 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ +# Development Lab + +Attention: This is a lab for **experimental features**. Here we can try out different ideas and scenarios if they will work and what can be improved. +There is no guarantee that any of these ideas comes in the main project. + +Please try everything on your own. Nothing can work, so everything can as well. + +You will find all experiments within the ["Experiments"](Experiements) sub folder. + +For feedback please join our Telegram group. + +**Support group: [@tgbotbase](https://t.me/tgbotbase)** + + +--- + + # .NET Telegram Bot Framework - Context based addon [![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/vpre/TelegramBotBase.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase/) diff --git a/TelegramBotFramework.sln b/TelegramBotFramework.sln index 84dbf82..ee2bafb 100644 --- a/TelegramBotFramework.sln +++ b/TelegramBotFramework.sln @@ -36,6 +36,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection", "Exam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelegramBotBase.Extensions.Images.IronSoftware", "TelegramBotBase.Extensions.Images.IronSoftware\TelegramBotBase.Extensions.Images.IronSoftware.csproj", "{DC521A4C-7446-46F7-845B-AAF10EDCF8C6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Experiments", "Experiments", "{2E1FF127-4DC2-40A1-849C-ED1432204E8A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExternalActionManager", "Experiments\ExternalActionManager\DemoBot\ExternalActionManager.csproj", "{5184D3F8-8526-413D-8EB0-23636EA7A4EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -90,6 +94,10 @@ Global {DC521A4C-7446-46F7-845B-AAF10EDCF8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC521A4C-7446-46F7-845B-AAF10EDCF8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC521A4C-7446-46F7-845B-AAF10EDCF8C6}.Release|Any CPU.Build.0 = Release|Any CPU + {5184D3F8-8526-413D-8EB0-23636EA7A4EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5184D3F8-8526-413D-8EB0-23636EA7A4EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5184D3F8-8526-413D-8EB0-23636EA7A4EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5184D3F8-8526-413D-8EB0-23636EA7A4EF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,6 +113,7 @@ Global {067E8EBE-F90A-4AFF-A0FF-20578216486E} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} {689B16BC-200E-4C68-BB2E-8B209070849B} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} {DC521A4C-7446-46F7-845B-AAF10EDCF8C6} = {E3193182-6FDA-4FA3-AD26-A487291E7681} + {5184D3F8-8526-413D-8EB0-23636EA7A4EF} = {2E1FF127-4DC2-40A1-849C-ED1432204E8A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {59CB40E1-9FA7-4867-A56F-4F418286F057}