diff --git a/Assembly.cs b/Assembly.cs new file mode 100644 index 00000000..a8f3fba1 --- /dev/null +++ b/Assembly.cs @@ -0,0 +1,13 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by GitVersion. +// +// You can modify this code as we will not overwrite it when re-executing GitVersion +// +//------------------------------------------------------------------------------ + +using System.Reflection; + +[assembly: AssemblyFileVersion("4.6.9.0")] +[assembly: AssemblyVersion("4.6.9.0")] +[assembly: AssemblyInformationalVersion("4.6.9-Adaptive-Rest.11+Branch.Adaptive-Rest.Sha.33bef312cbe591ec20a605b542c227530fd6b9de")] \ No newline at end of file diff --git a/Bot.Builder.Community.Samples.sln b/Bot.Builder.Community.Samples.sln index 4a60181f..57ca6bf9 100644 --- a/Bot.Builder.Community.Samples.sln +++ b/Bot.Builder.Community.Samples.sln @@ -4,8 +4,6 @@ VisualStudioVersion = 16.0.29318.209 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Location Dialog Sample", "samples\Location Dialog Sample\Location Dialog Sample.csproj", "{10BEDE11-C9DE-4B67-9467-4E48272229C6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google Adapter Sample", "samples\Google Adapter Sample\Google Adapter Sample.csproj", "{3312C17E-01FA-4390-9B8F-6E7733E0B1E7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Form Flow Sample", "samples\Form Flow Sample\Form Flow Sample.csproj", "{B982923B-163B-49FD-960B-0DF5BB6E6562}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cortana Assistant Alexa Sample", "samples\Cortana Assistant Alexa Sample\Cortana Assistant Alexa Sample.csproj", "{2FD06287-25A1-47A6-BB85-2E508E543E66}" @@ -24,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RingCentral Adapter Sample" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Alexa Adapter Sample", "samples\Alexa Adapter Sample\Alexa Adapter Sample.csproj", "{53BCE929-C57D-4AF9-91C9-FCA5C7E2CE41}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google Adapter Sample", "samples\Google Adapter Sample\Google Adapter Sample.csproj", "{89A6903F-2911-4A27-B178-EA28D697D46B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Zoom Adapter Sample", "samples\Zoom Adapter Sample\Zoom Adapter Sample.csproj", "{D5F5BFB0-CDEC-4980-9EBB-864724B6B2B6}" EndProject Global @@ -32,14 +32,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7927A7EE-C327-4403-A5F4-B5E2C0BF4D47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7927A7EE-C327-4403-A5F4-B5E2C0BF4D47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7927A7EE-C327-4403-A5F4-B5E2C0BF4D47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7927A7EE-C327-4403-A5F4-B5E2C0BF4D47}.Release|Any CPU.Build.0 = Release|Any CPU {10BEDE11-C9DE-4B67-9467-4E48272229C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10BEDE11-C9DE-4B67-9467-4E48272229C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {10BEDE11-C9DE-4B67-9467-4E48272229C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {10BEDE11-C9DE-4B67-9467-4E48272229C6}.Release|Any CPU.Build.0 = Release|Any CPU - {3312C17E-01FA-4390-9B8F-6E7733E0B1E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3312C17E-01FA-4390-9B8F-6E7733E0B1E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3312C17E-01FA-4390-9B8F-6E7733E0B1E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3312C17E-01FA-4390-9B8F-6E7733E0B1E7}.Release|Any CPU.Build.0 = Release|Any CPU {B982923B-163B-49FD-960B-0DF5BB6E6562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B982923B-163B-49FD-960B-0DF5BB6E6562}.Debug|Any CPU.Build.0 = Debug|Any CPU {B982923B-163B-49FD-960B-0DF5BB6E6562}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -60,6 +60,14 @@ Global {55F863F1-9EC8-420C-9099-BD8378D72F18}.Debug|Any CPU.Build.0 = Debug|Any CPU {55F863F1-9EC8-420C-9099-BD8378D72F18}.Release|Any CPU.ActiveCfg = Release|Any CPU {55F863F1-9EC8-420C-9099-BD8378D72F18}.Release|Any CPU.Build.0 = Release|Any CPU + {37ED6FBD-12B5-40CF-837F-6F75ACC65484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37ED6FBD-12B5-40CF-837F-6F75ACC65484}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37ED6FBD-12B5-40CF-837F-6F75ACC65484}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37ED6FBD-12B5-40CF-837F-6F75ACC65484}.Release|Any CPU.Build.0 = Release|Any CPU + {89A6903F-2911-4A27-B178-EA28D697D46B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89A6903F-2911-4A27-B178-EA28D697D46B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89A6903F-2911-4A27-B178-EA28D697D46B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89A6903F-2911-4A27-B178-EA28D697D46B}.Release|Any CPU.Build.0 = Release|Any CPU {D1C3490D-477A-4D79-A41C-4D15CA9DC183}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1C3490D-477A-4D79-A41C-4D15CA9DC183}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1C3490D-477A-4D79-A41C-4D15CA9DC183}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Bot.Builder.Community.sln b/Bot.Builder.Community.sln index a1bdb2da..5efb76de 100644 --- a/Bot.Builder.Community.sln +++ b/Bot.Builder.Community.sln @@ -71,12 +71,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.Twitter.Tests", "tests\Bot.Builder.Community.Adapters.Tests\Bot.Builder.Community.Adapters.Twitter.Tests.csproj", "{873C9B8C-2677-4586-89D3-C80FB1263596}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.RingCentral", "libraries\Bot.Builder.Community.Adapters.RingCentral\Bot.Builder.Community.Adapters.RingCentral.csproj", "{F0C1BE0F-D1F1-4104-8617-4E87291C1416}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.RingCentral.Tests", "tests\Bot.Builder.Community.Adapters.RingCentral.Tests\Bot.Builder.Community.Adapters.RingCentral.Tests.csproj", "{441E05D3-43A9-4C4B-AB5B-06AF27742885}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Dialogs.Adaptive.Rest", "libraries\Bot.Builder.Community.Dialogs.Adaptive.Rest\Bot.Builder.Community.Dialogs.Adaptive.Rest.csproj", "{63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Dialogs.Adaptive.Rest.Tests", "tests\Bot.Builder.Community.Dialogs.Adaptive.Rest.Tests\Bot.Builder.Community.Dialogs.Adaptive.Rest.Tests.csproj", "{E14564ED-C929-4F43-BD1B-46D20DFAAF3A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.RingCentral", "libraries\Bot.Builder.Community.Adapters.RingCentral\Bot.Builder.Community.Adapters.RingCentral.csproj", "{042403D4-2D7B-482E-87DD-51B1CFDEC210}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.Zoom", "libraries\Bot.Builder.Community.Adapters.Zoom\Bot.Builder.Community.Adapters.Zoom.csproj", "{DA1A84A1-D921-412F-A855-93FC9A525D27}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.Infobip", "libraries\Bot.Builder.Community.Adapters.Infobip\Bot.Builder.Community.Adapters.Infobip.csproj", "{0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.Infobip.Tests", "tests\Bot.Builder.Community.Adapters.Infobip.Tests\Bot.Builder.Community.Adapters.Infobip.Tests.csproj", "{6356EF6A-406B-4B2E-A229-C1D042C2FCF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.Google.Core", "libraries\Bot.Builder.Community.Adapters.Google.Core\Bot.Builder.Community.Adapters.Google.Core.csproj", "{7856219A-3537-4F81-86A8-4CB020BD2B9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU @@ -275,14 +285,6 @@ Global {873C9B8C-2677-4586-89D3-C80FB1263596}.Documentation|Any CPU.Build.0 = Debug|Any CPU {873C9B8C-2677-4586-89D3-C80FB1263596}.Release|Any CPU.ActiveCfg = Release|Any CPU {873C9B8C-2677-4586-89D3-C80FB1263596}.Release|Any CPU.Build.0 = Release|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Documentation|Any CPU.Build.0 = Debug|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F0C1BE0F-D1F1-4104-8617-4E87291C1416}.Release|Any CPU.Build.0 = Release|Any CPU {441E05D3-43A9-4C4B-AB5B-06AF27742885}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU {441E05D3-43A9-4C4B-AB5B-06AF27742885}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU {441E05D3-43A9-4C4B-AB5B-06AF27742885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -291,6 +293,30 @@ Global {441E05D3-43A9-4C4B-AB5B-06AF27742885}.Documentation|Any CPU.Build.0 = Debug|Any CPU {441E05D3-43A9-4C4B-AB5B-06AF27742885}.Release|Any CPU.ActiveCfg = Release|Any CPU {441E05D3-43A9-4C4B-AB5B-06AF27742885}.Release|Any CPU.Build.0 = Release|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D}.Release|Any CPU.Build.0 = Release|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A}.Release|Any CPU.Build.0 = Release|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Debug|Any CPU.Build.0 = Debug|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Release|Any CPU.ActiveCfg = Release|Any CPU + {042403D4-2D7B-482E-87DD-51B1CFDEC210}.Release|Any CPU.Build.0 = Release|Any CPU {DA1A84A1-D921-412F-A855-93FC9A525D27}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU {DA1A84A1-D921-412F-A855-93FC9A525D27}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU {DA1A84A1-D921-412F-A855-93FC9A525D27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -299,6 +325,30 @@ Global {DA1A84A1-D921-412F-A855-93FC9A525D27}.Documentation|Any CPU.Build.0 = Debug|Any CPU {DA1A84A1-D921-412F-A855-93FC9A525D27}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA1A84A1-D921-412F-A855-93FC9A525D27}.Release|Any CPU.Build.0 = Release|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7856219A-3537-4F81-86A8-4CB020BD2B9A}.Release|Any CPU.Build.0 = Release|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C}.Release|Any CPU.Build.0 = Release|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -333,9 +383,14 @@ Global {0311DB07-D853-4649-9DA1-F1CA2C20B406} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} {ACEAA5FA-1DE3-4E3A-B435-A988A4276231} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} {873C9B8C-2677-4586-89D3-C80FB1263596} = {840D4038-9AB8-4750-9FFE-365386CE47E2} - {F0C1BE0F-D1F1-4104-8617-4E87291C1416} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} {441E05D3-43A9-4C4B-AB5B-06AF27742885} = {840D4038-9AB8-4750-9FFE-365386CE47E2} + {63AFED39-D4AC-4B1E-94B9-EF58EE687B3D} = {08759E28-8592-4EBA-9A07-19A5BED3FB0C} + {E14564ED-C929-4F43-BD1B-46D20DFAAF3A} = {840D4038-9AB8-4750-9FFE-365386CE47E2} + {042403D4-2D7B-482E-87DD-51B1CFDEC210} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} {DA1A84A1-D921-412F-A855-93FC9A525D27} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} + {7856219A-3537-4F81-86A8-4CB020BD2B9A} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} + {0F8ADFCE-3E91-4F85-BC0B-4B0A5C98797C} = {BF310E8A-8DA1-441F-90E9-DE0E66553048} + {6356EF6A-406B-4B2E-A229-C1D042C2FCF6} = {840D4038-9AB8-4750-9FFE-365386CE47E2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9FE3B75E-BA2B-45BC-BBF0-DDA8BA10C4F0} diff --git a/assistant-sdk-getting-started.jpg b/assistant-sdk-getting-started.jpg new file mode 100644 index 00000000..c6f93f3f Binary files /dev/null and b/assistant-sdk-getting-started.jpg differ diff --git a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs index d3f9b4ec..eed33213 100644 --- a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs +++ b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs @@ -1,412 +1,412 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security; -using System.Xml; -using System.Xml.Linq; -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; -using Bot.Builder.Community.Adapters.Alexa.Core.Attachments; -using Bot.Builder.Community.Adapters.Alexa.Core.Helpers; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Rest; -using AlexaResponse = Alexa.NET.Response; - -namespace Bot.Builder.Community.Adapters.Alexa.Core -{ - public class AlexaRequestMapper - { - private AlexaRequestMapperOptions _options; - private ILogger _logger; - - public AlexaRequestMapper(AlexaRequestMapperOptions options = null, ILogger logger = null) - { - _options = options ?? new AlexaRequestMapperOptions(); - _logger = logger ?? NullLogger.Instance; - } - - /// - /// Returns an Activity object created by using properties on a SkillRequest. - /// A base set of properties based on the SkillRequest are applied to a new Activity object - /// for all request types with the activity type, and additional properties, set depending - /// on the specific request type. - /// - /// The SkillRequest to be used to create an Activity object. - /// Activity - public Activity RequestToActivity(SkillRequest skillRequest) - { - if (skillRequest.Request == null) - { - throw new ValidationException("Bad Request. Skill request missing Request property."); - } - - switch (skillRequest.Request) - { - case IntentRequest intentRequest: - if (intentRequest.Intent.Slots != null && intentRequest.Intent.Slots.ContainsKey(_options.DefaultIntentSlotName)) - { - return RequestToMessageActivity(skillRequest, intentRequest); - } - else - { - if (intentRequest.Intent.Name == "AMAZON.StopIntent") - { - return RequestToEndOfConversationActivity(skillRequest); - } - - return RequestToEventActivity(skillRequest); - } - case LaunchRequest launchRequest: - return RequestToConversationUpdateActivity(skillRequest); - case SessionEndedRequest sessionEndedRequest: - return RequestToEndOfConversationActivity(skillRequest); - default: - return RequestToEventActivity(skillRequest); - } - } - - /// - /// Creates a SkillResponse based on an Activity and original SkillRequest. - /// - /// The Activity to use to create the SkillResponse - /// Original SkillRequest received from Alexa Skills service. This is used - /// to check if the original request was a SessionEndedRequest which should not return a response. - /// SkillResponse - public SkillResponse ActivityToResponse(Activity activity, SkillRequest alexaRequest) - { - var response = new SkillResponse() - { - Version = "1.0", - Response = new ResponseBody() - }; - - if (activity == null || activity.Type != ActivityTypes.Message || alexaRequest.Request is SessionEndedRequest) - { - response.Response.ShouldEndSession = true; - response.Response.OutputSpeech = new PlainTextOutputSpeech - { - Text = string.Empty - }; - return response; - } - - if (!string.IsNullOrEmpty(activity.Speak)) - { - response.Response.OutputSpeech = new SsmlOutputSpeech(activity.Speak); - } - else - { - response.Response.OutputSpeech = new PlainTextOutputSpeech(activity.Text); - } - - ProcessActivityAttachments(activity, response); - - if (ShouldSetEndSession(response)) - { - switch (activity.InputHint) - { - case InputHints.IgnoringInput: - response.Response.ShouldEndSession = true; - break; - case InputHints.ExpectingInput: - response.Response.ShouldEndSession = false; - response.Response.Reprompt = new Reprompt(activity.Text); - break; - default: - response.Response.ShouldEndSession = _options.ShouldEndSessionByDefault; - break; - } - } - - return response; - } - - /// - /// Concatenates activities into a single activity. Uses the last activity in the list as the base activity. - /// If any of the activities being process contain an outer SSML speak tag within the value of the Speak property, - /// these are removed from the individual activities and a tag is wrapped around the resulting - /// concatenated string. An SSML strong break tag is added between activity content. For more infomation - /// about the supported SSML for Alexa see - /// https://developer.amazon.com/en-US/docs/alexa/custom-skills/speech-synthesis-markup-language-ssml-reference.html#break - /// - /// The list of one or more outgoing activities - /// Activity - public Activity MergeActivities(IList activities) - { - var messageActivities = activities?.Where(a => a.Type == ActivityTypes.Message).ToList(); - - if (messageActivities == null || messageActivities.Count == 0) - { - return null; - } - - var activity = messageActivities.Last(); - - if (messageActivities.Any(a => !string.IsNullOrEmpty(a.Speak))) - { - var speakText = string.Join("", messageActivities - .Select(a => !string.IsNullOrEmpty(a.Speak) ? StripSpeakTag(a.Speak) : NormalizeActivityText(a.TextFormat, a.Text, forSsml: true)) - .Where(s => !string.IsNullOrEmpty(s)) - .Select(s => s)); - - activity.Speak = $"{speakText}"; - } - - activity.Text = string.Join(". ", messageActivities - .Select(a => NormalizeActivityText(a.TextFormat, a.Text, forSsml: false)) - .Where(s => !string.IsNullOrEmpty(s)) - .Select(s => s.Trim(new char[] { ' ', '.' }))); - - activity.Attachments = messageActivities.Where(x => x.Attachments != null).SelectMany(x => x.Attachments).ToList(); - - return activity; - } - - private Activity RequestToEndOfConversationActivity(SkillRequest skillRequest) - { - var activity = Activity.CreateEndOfConversationActivity() as Activity; - activity = SetGeneralActivityProperties(activity, skillRequest); - return activity; - } - - private Activity RequestToConversationUpdateActivity(SkillRequest skillRequest) - { - var activity = Activity.CreateConversationUpdateActivity() as Activity; - activity = SetGeneralActivityProperties(activity, skillRequest); - activity.MembersAdded.Add(new ChannelAccount(id: skillRequest.Session.User.UserId)); - return activity; - } - - private Activity RequestToMessageActivity(SkillRequest skillRequest, IntentRequest intentRequest) - { - var activity = Activity.CreateMessageActivity() as Activity; - activity = SetGeneralActivityProperties(activity, skillRequest); - activity.Text = intentRequest.Intent.Slots[_options.DefaultIntentSlotName].Value; - activity.Locale = intentRequest.Locale; - return activity; - } - - private Activity RequestToEventActivity(SkillRequest skillRequest) - { - var activity = Activity.CreateEventActivity() as Activity; - activity = SetGeneralActivityProperties(activity, skillRequest); - activity.Name = skillRequest.Request.Type; - - switch (skillRequest.Request) - { - case IntentRequest skillIntentRequest: - activity.Value = skillIntentRequest; - break; - case AccountLinkSkillEventRequest accountLinkSkillEventRequest: - activity.Value = accountLinkSkillEventRequest; - break; - case AudioPlayerRequest audioPlayerRequest: - activity.Value = audioPlayerRequest; - break; - case DisplayElementSelectedRequest displayElementSelectedRequest: - activity.Value = displayElementSelectedRequest; - break; - case PermissionSkillEventRequest permissionSkillEventRequest: - activity.Value = permissionSkillEventRequest; - break; - case PlaybackControllerRequest playbackControllerRequest: - activity.Value = playbackControllerRequest; - break; - case SkillEventRequest skillEventRequest: - activity.Value = skillEventRequest; - break; - case SystemExceptionRequest systemExceptionRequest: - activity.Value = systemExceptionRequest; - break; - default: - activity.Value = skillRequest.Request; - break; - } - - return activity; - } - - /// - /// Set the general properties, based on an incoming SkillRequest that can be applied - /// irresepective of the resulting Activity type. - /// - /// The Activity on which to set the properties on. - /// The incoming SkillRequest - /// Activity - private Activity SetGeneralActivityProperties(Activity activity, SkillRequest skillRequest) - { - var alexaSystem = skillRequest.Context.System; - - activity.ChannelId = _options.ChannelId; - activity.Id = skillRequest.Request.RequestId; - activity.DeliveryMode = DeliveryModes.ExpectReplies; - activity.ServiceUrl = _options.ServiceUrl ?? $"{alexaSystem.ApiEndpoint}?token={alexaSystem.ApiAccessToken}"; - activity.Recipient = new ChannelAccount(alexaSystem.Application.ApplicationId); - activity.From = new ChannelAccount(alexaSystem.Person?.PersonId ?? alexaSystem.User.UserId); - activity.Conversation = new ConversationAccount(isGroup: false, id: skillRequest.Session?.SessionId ?? skillRequest.Request.RequestId); - activity.Timestamp = skillRequest.Request.Timestamp.ToUniversalTime(); - activity.ChannelData = skillRequest; - - return activity; - } - - /// - /// Checks a string to see if it is XML and if the outer tag is a speak tag - /// indicating it is SSML. If an outer speak tag is found, the inner XML is - /// returned, otherwise the original string is returned - /// - /// String to be checked for an outer speak XML tag and stripped if found - private string StripSpeakTag(string speakText) - { - try - { - var speakSsmlDoc = XDocument.Parse(speakText); - if (speakSsmlDoc != null && speakSsmlDoc.Root.Name.ToString().ToLowerInvariant() == "speak") - { - using (var reader = speakSsmlDoc.Root.CreateReader()) - { - reader.MoveToContent(); - return reader.ReadInnerXml(); - } - } - - return speakText; - } - catch (XmlException) - { - return speakText; - } - } - - private string NormalizeActivityText(string textFormat, string text, bool forSsml) - { - if (string.IsNullOrWhiteSpace(text)) - { - return string.Empty; - } - - // Default to markdown if it isn't specified. - if (textFormat == null) - { - textFormat = TextFormatTypes.Markdown; - } - - string plainText; - if (textFormat.Equals(TextFormatTypes.Plain, StringComparison.Ordinal)) - { - plainText = text; - } - else if (textFormat.Equals(TextFormatTypes.Markdown, StringComparison.Ordinal)) - { - plainText = AlexaMarkdownToPlaintextRenderer.Render(text); - } - else // xml format or other unknown and unsupported format. - { - plainText = string.Empty; - } - - if (forSsml && !SecurityElement.IsValidText(plainText)) - { - plainText = SecurityElement.Escape(plainText); - } - return plainText; - } - - /// - /// Under certain circumstances, such as the inclusion of certain types of directives - /// on a response, should force the 'ShouldEndSession' property not be included on - /// an outgoing response. This method determines if this property is allowed to have - /// a value assigned. - /// - /// Boolean indicating if the 'ShouldEndSession' property can be populated on the response.' - /// bool - private bool ShouldSetEndSession(SkillResponse response) - { - if (response.Response.Directives.Any(d => d is IEndSessionDirective)) - { - return false; - } - - return true; - } - - /// - /// Processes any attachments on the Activity in order to amend the SkillResponse appropriately. - /// The current process for processing activity attachments is; - /// 1. Check for an instance of a SigninCard. Set the Card property on the SkillResponse to a LinkAccountCard. - /// 2. If no SigninCard is found, check for an instance of a HeroCard. Transform the first instance of a HeroCard - /// into an Alexa Card and set the Card property on the response. - /// 3. If no Signin or HeroCard instances were found, check for Alexa specific CardAttachment and - /// set the content of the Card property on the response. - /// 4. Look for any instances of DirectiveAttachments and add the appropriate directives to the response. - /// - /// The Activity for which to process activities. - /// The SkillResponse to be modified based on the attachments on the Activity object. - private void ProcessActivityAttachments(Activity activity, SkillResponse response) - { - activity.ConvertAttachmentContent(); - - var bfCard = activity.Attachments?.FirstOrDefault(a => a.ContentType == HeroCard.ContentType || a.ContentType == SigninCard.ContentType); - - if (bfCard != null) - { - switch (bfCard.Content) - { - case SigninCard signinCard: - response.Response.Card = new LinkAccountCard(); - break; - case HeroCard heroCard: - response.Response.Card = CreateAlexaCardFromHeroCard(heroCard); - break; - } - } - else - { - var cardAttachment = activity.Attachments?.FirstOrDefault(a => a.ContentType == AlexaAttachmentContentTypes.Card); - if (cardAttachment != null) - { - response.Response.Card = cardAttachment.Content as ICard; - } - } - - var directiveAttachments = activity.Attachments?.Where(a => a.ContentType == AlexaAttachmentContentTypes.Directive).ToList(); - if (directiveAttachments != null && directiveAttachments.Any()) - { - response.Response.Directives = directiveAttachments.Select(d => d.Content as IDirective).ToList(); - } - } - - /// - /// Uses a HeroCard to create an instance of ICard (either StandardCard or SimpleCard). - /// - /// The HeroCard to be transformed. - /// An instance of ICard - either SimpleCard or StandardCard. - private ICard CreateAlexaCardFromHeroCard(HeroCard heroCard) - { - if (heroCard.Images != null && heroCard.Images.Any()) - { - return new StandardCard() - { - Content = heroCard.Text, - Image = new AlexaResponse.CardImage() - { - SmallImageUrl = heroCard.Images[0].Url, - LargeImageUrl = heroCard.Images.Count > 1 ? heroCard.Images[1].Url : null - }, - Title = heroCard.Title - }; - } - else - { - return new SimpleCard() - { - Content = heroCard.Text, - Title = heroCard.Title - }; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Xml; +using System.Xml.Linq; +using Alexa.NET.Request; +using Alexa.NET.Request.Type; +using Alexa.NET.Response; +using Bot.Builder.Community.Adapters.Alexa.Core.Attachments; +using Bot.Builder.Community.Adapters.Alexa.Core.Helpers; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Rest; +using AlexaResponse = Alexa.NET.Response; + +namespace Bot.Builder.Community.Adapters.Alexa.Core +{ + public class AlexaRequestMapper + { + private AlexaRequestMapperOptions _options; + private ILogger _logger; + + public AlexaRequestMapper(AlexaRequestMapperOptions options = null, ILogger logger = null) + { + _options = options ?? new AlexaRequestMapperOptions(); + _logger = logger ?? NullLogger.Instance; + } + + /// + /// Returns an Activity object created by using properties on a SkillRequest. + /// A base set of properties based on the SkillRequest are applied to a new Activity object + /// for all request types with the activity type, and additional properties, set depending + /// on the specific request type. + /// + /// The SkillRequest to be used to create an Activity object. + /// Activity + public Activity RequestToActivity(SkillRequest skillRequest) + { + if (skillRequest.Request == null) + { + throw new ValidationException("Bad Request. Skill request missing Request property."); + } + + switch (skillRequest.Request) + { + case IntentRequest intentRequest: + if (intentRequest.Intent.Slots != null && intentRequest.Intent.Slots.ContainsKey(_options.DefaultIntentSlotName)) + { + return RequestToMessageActivity(skillRequest, intentRequest); + } + else + { + if (intentRequest.Intent.Name == "AMAZON.StopIntent") + { + return RequestToEndOfConversationActivity(skillRequest); + } + + return RequestToEventActivity(skillRequest); + } + case LaunchRequest launchRequest: + return RequestToConversationUpdateActivity(skillRequest); + case SessionEndedRequest sessionEndedRequest: + return RequestToEndOfConversationActivity(skillRequest); + default: + return RequestToEventActivity(skillRequest); + } + } + + /// + /// Creates a SkillResponse based on an Activity and original SkillRequest. + /// + /// The Activity to use to create the SkillResponse + /// Original SkillRequest received from Alexa Skills service. This is used + /// to check if the original request was a SessionEndedRequest which should not return a response. + /// SkillResponse + public SkillResponse ActivityToResponse(Activity activity, SkillRequest alexaRequest) + { + var response = new SkillResponse() + { + Version = "1.0", + Response = new ResponseBody() + }; + + if (activity == null || activity.Type != ActivityTypes.Message || alexaRequest.Request is SessionEndedRequest) + { + response.Response.ShouldEndSession = true; + response.Response.OutputSpeech = new PlainTextOutputSpeech + { + Text = string.Empty + }; + return response; + } + + if (!string.IsNullOrEmpty(activity.Speak)) + { + response.Response.OutputSpeech = new SsmlOutputSpeech(activity.Speak); + } + else + { + response.Response.OutputSpeech = new PlainTextOutputSpeech(activity.Text); + } + + ProcessActivityAttachments(activity, response); + + if (ShouldSetEndSession(response)) + { + switch (activity.InputHint) + { + case InputHints.IgnoringInput: + response.Response.ShouldEndSession = true; + break; + case InputHints.ExpectingInput: + response.Response.ShouldEndSession = false; + response.Response.Reprompt = new Reprompt(activity.Text); + break; + default: + response.Response.ShouldEndSession = _options.ShouldEndSessionByDefault; + break; + } + } + + return response; + } + + /// + /// Concatenates activities into a single activity. Uses the last activity in the list as the base activity. + /// If any of the activities being process contain an outer SSML speak tag within the value of the Speak property, + /// these are removed from the individual activities and a tag is wrapped around the resulting + /// concatenated string. An SSML strong break tag is added between activity content. For more infomation + /// about the supported SSML for Alexa see + /// https://developer.amazon.com/en-US/docs/alexa/custom-skills/speech-synthesis-markup-language-ssml-reference.html#break + /// + /// The list of one or more outgoing activities + /// Activity + public Activity MergeActivities(IList activities) + { + var messageActivities = activities?.Where(a => a.Type == ActivityTypes.Message).ToList(); + + if (messageActivities == null || messageActivities.Count == 0) + { + return null; + } + + var activity = messageActivities.Last(); + + if (messageActivities.Any(a => !string.IsNullOrEmpty(a.Speak))) + { + var speakText = string.Join("", messageActivities + .Select(a => !string.IsNullOrEmpty(a.Speak) ? StripSpeakTag(a.Speak) : NormalizeActivityText(a.TextFormat, a.Text, forSsml: true)) + .Where(s => !string.IsNullOrEmpty(s)) + .Select(s => s)); + + activity.Speak = $"{speakText}"; + } + + activity.Text = string.Join(". ", messageActivities + .Select(a => NormalizeActivityText(a.TextFormat, a.Text, forSsml: false)) + .Where(s => !string.IsNullOrEmpty(s)) + .Select(s => s.Trim(new char[] { ' ', '.' }))); + + activity.Attachments = messageActivities.Where(x => x.Attachments != null).SelectMany(x => x.Attachments).ToList(); + + return activity; + } + + private Activity RequestToEndOfConversationActivity(SkillRequest skillRequest) + { + var activity = Activity.CreateEndOfConversationActivity() as Activity; + activity = SetGeneralActivityProperties(activity, skillRequest); + return activity; + } + + private Activity RequestToConversationUpdateActivity(SkillRequest skillRequest) + { + var activity = Activity.CreateConversationUpdateActivity() as Activity; + activity = SetGeneralActivityProperties(activity, skillRequest); + activity.MembersAdded.Add(new ChannelAccount(id: skillRequest.Session.User.UserId)); + return activity; + } + + private Activity RequestToMessageActivity(SkillRequest skillRequest, IntentRequest intentRequest) + { + var activity = Activity.CreateMessageActivity() as Activity; + activity = SetGeneralActivityProperties(activity, skillRequest); + activity.Text = intentRequest.Intent.Slots[_options.DefaultIntentSlotName].Value; + activity.Locale = intentRequest.Locale; + return activity; + } + + private Activity RequestToEventActivity(SkillRequest skillRequest) + { + var activity = Activity.CreateEventActivity() as Activity; + activity = SetGeneralActivityProperties(activity, skillRequest); + activity.Name = skillRequest.Request.Type; + + switch (skillRequest.Request) + { + case IntentRequest skillIntentRequest: + activity.Value = skillIntentRequest; + break; + case AccountLinkSkillEventRequest accountLinkSkillEventRequest: + activity.Value = accountLinkSkillEventRequest; + break; + case AudioPlayerRequest audioPlayerRequest: + activity.Value = audioPlayerRequest; + break; + case DisplayElementSelectedRequest displayElementSelectedRequest: + activity.Value = displayElementSelectedRequest; + break; + case PermissionSkillEventRequest permissionSkillEventRequest: + activity.Value = permissionSkillEventRequest; + break; + case PlaybackControllerRequest playbackControllerRequest: + activity.Value = playbackControllerRequest; + break; + case SkillEventRequest skillEventRequest: + activity.Value = skillEventRequest; + break; + case SystemExceptionRequest systemExceptionRequest: + activity.Value = systemExceptionRequest; + break; + default: + activity.Value = skillRequest.Request; + break; + } + + return activity; + } + + /// + /// Set the general properties, based on an incoming SkillRequest that can be applied + /// irresepective of the resulting Activity type. + /// + /// The Activity on which to set the properties on. + /// The incoming SkillRequest + /// Activity + private Activity SetGeneralActivityProperties(Activity activity, SkillRequest skillRequest) + { + var alexaSystem = skillRequest.Context.System; + + activity.ChannelId = _options.ChannelId; + activity.Id = skillRequest.Request.RequestId; + activity.DeliveryMode = DeliveryModes.ExpectReplies; + activity.ServiceUrl = _options.ServiceUrl ?? $"{alexaSystem.ApiEndpoint}?token={alexaSystem.ApiAccessToken}"; + activity.Recipient = new ChannelAccount(alexaSystem.Application.ApplicationId); + activity.From = new ChannelAccount(alexaSystem.Person?.PersonId ?? alexaSystem.User.UserId); + activity.Conversation = new ConversationAccount(isGroup: false, id: skillRequest.Session?.SessionId ?? skillRequest.Request.RequestId); + activity.Timestamp = skillRequest.Request.Timestamp.ToUniversalTime(); + activity.ChannelData = skillRequest; + + return activity; + } + + /// + /// Checks a string to see if it is XML and if the outer tag is a speak tag + /// indicating it is SSML. If an outer speak tag is found, the inner XML is + /// returned, otherwise the original string is returned + /// + /// String to be checked for an outer speak XML tag and stripped if found + private string StripSpeakTag(string speakText) + { + try + { + var speakSsmlDoc = XDocument.Parse(speakText); + if (speakSsmlDoc != null && speakSsmlDoc.Root.Name.ToString().ToLowerInvariant() == "speak") + { + using (var reader = speakSsmlDoc.Root.CreateReader()) + { + reader.MoveToContent(); + return reader.ReadInnerXml(); + } + } + + return speakText; + } + catch (XmlException) + { + return speakText; + } + } + + private string NormalizeActivityText(string textFormat, string text, bool forSsml) + { + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + // Default to markdown if it isn't specified. + if (textFormat == null) + { + textFormat = TextFormatTypes.Markdown; + } + + string plainText; + if (textFormat.Equals(TextFormatTypes.Plain, StringComparison.Ordinal)) + { + plainText = text; + } + else if (textFormat.Equals(TextFormatTypes.Markdown, StringComparison.Ordinal)) + { + plainText = AlexaMarkdownToPlaintextRenderer.Render(text); + } + else // xml format or other unknown and unsupported format. + { + plainText = string.Empty; + } + + if (forSsml && !SecurityElement.IsValidText(plainText)) + { + plainText = SecurityElement.Escape(plainText); + } + return plainText; + } + + /// + /// Under certain circumstances, such as the inclusion of certain types of directives + /// on a response, should force the 'ShouldEndSession' property not be included on + /// an outgoing response. This method determines if this property is allowed to have + /// a value assigned. + /// + /// Boolean indicating if the 'ShouldEndSession' property can be populated on the response.' + /// bool + private bool ShouldSetEndSession(SkillResponse response) + { + if (response.Response.Directives.Any(d => d is IEndSessionDirective)) + { + return false; + } + + return true; + } + + /// + /// Processes any attachments on the Activity in order to amend the SkillResponse appropriately. + /// The current process for processing activity attachments is; + /// 1. Check for an instance of a SigninCard. Set the Card property on the SkillResponse to a LinkAccountCard. + /// 2. If no SigninCard is found, check for an instance of a HeroCard. Transform the first instance of a HeroCard + /// into an Alexa Card and set the Card property on the response. + /// 3. If no Signin or HeroCard instances were found, check for Alexa specific CardAttachment and + /// set the content of the Card property on the response. + /// 4. Look for any instances of DirectiveAttachments and add the appropriate directives to the response. + /// + /// The Activity for which to process activities. + /// The SkillResponse to be modified based on the attachments on the Activity object. + private void ProcessActivityAttachments(Activity activity, SkillResponse response) + { + activity.ConvertAttachmentContent(); + + var bfCard = activity.Attachments?.FirstOrDefault(a => a.ContentType == HeroCard.ContentType || a.ContentType == SigninCard.ContentType); + + if (bfCard != null) + { + switch (bfCard.Content) + { + case SigninCard signinCard: + response.Response.Card = new LinkAccountCard(); + break; + case HeroCard heroCard: + response.Response.Card = CreateAlexaCardFromHeroCard(heroCard); + break; + } + } + else + { + var cardAttachment = activity.Attachments?.FirstOrDefault(a => a.ContentType == AlexaAttachmentContentTypes.Card); + if (cardAttachment != null) + { + response.Response.Card = cardAttachment.Content as ICard; + } + } + + var directiveAttachments = activity.Attachments?.Where(a => a.ContentType == AlexaAttachmentContentTypes.Directive).ToList(); + if (directiveAttachments != null && directiveAttachments.Any()) + { + response.Response.Directives = directiveAttachments.Select(d => d.Content as IDirective).ToList(); + } + } + + /// + /// Uses a HeroCard to create an instance of ICard (either StandardCard or SimpleCard). + /// + /// The HeroCard to be transformed. + /// An instance of ICard - either SimpleCard or StandardCard. + private ICard CreateAlexaCardFromHeroCard(HeroCard heroCard) + { + if (heroCard.Images != null && heroCard.Images.Any()) + { + return new StandardCard() + { + Content = heroCard.Text, + Image = new AlexaResponse.CardImage() + { + SmallImageUrl = heroCard.Images[0].Url, + LargeImageUrl = heroCard.Images.Count > 1 ? heroCard.Images[1].Url : null + }, + Title = heroCard.Title + }; + } + else + { + return new SimpleCard() + { + Content = heroCard.Text, + Title = heroCard.Title + }; + } + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Helpers/AlexaMarkdownToPlaintextRenderer.cs b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Helpers/AlexaMarkdownToPlaintextRenderer.cs index c439de72..bfc8fa88 100644 --- a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Helpers/AlexaMarkdownToPlaintextRenderer.cs +++ b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Helpers/AlexaMarkdownToPlaintextRenderer.cs @@ -1,70 +1,70 @@ -using Microsoft.MarkedNet; - -namespace Bot.Builder.Community.Adapters.Alexa.Core.Helpers -{ - /// - /// Simple Markdown renderer to turn markdown into plain text for Alexa. - /// - public static class AlexaMarkdownToPlaintextRenderer - { - private static readonly Marked _marked = new Marked(new Options { EscapeHtml = false, Sanitize = false, Mangle = false, Renderer = new RemoveMarkupRenderer() }); - - public static string Render(string source) => _marked.Parse(source); - - private class RemoveMarkupRenderer : MarkdownRenderer - { - private const string ListItemMarker = "$$ListItemMarker$$"; - - public RemoveMarkupRenderer() : base() { } - public RemoveMarkupRenderer(Options options) : base(options) { } - - public override string Blockquote(string quote) => string.Concat(quote, ". "); - public override string Br() => ". "; - public override string Code(string code, string lang, bool escaped) => code; - public override string Codespan(string text) => text; - public override string Del(string text) => text; - public override string Em(string text) => text; - public override string Heading(string text, int level, string raw) => string.Concat(text, ". "); - public override string Hr() => ". "; - public override string Html(string html) => string.Empty; - public override string Image(string href, string title, string text) => title ?? text; - public override string Link(string href, string title, string text) - { - if (title != null) - { - return $"{title} {href}"; - } - else if (text == href) - { - // For standard links the title and href will be the same. - return href; - } - return $"{text} {href}"; - } - public override string List(string body, bool ordered, int start) - { - if (ordered) - { - for (int marker = start, markerIndex = body.IndexOf(ListItemMarker); markerIndex >= 0; markerIndex = body.IndexOf(ListItemMarker), ++marker) - { - body = body.Substring(0, markerIndex) + marker + ". " + body.Substring(markerIndex + ListItemMarker.Length); - } - } - else - { - body = body.Replace(ListItemMarker, string.Empty); - } - return $"{body.Trim().TrimEnd(',')}. "; - } - public override string ListItem(string text) => $"{ListItemMarker}{text}, "; - public override string Paragraph(string text) => string.Concat(text.Replace("\n", " ").TrimEnd('.'), ". "); - public override string Strong(string text) => text; - public override string Table(string header, string body) => string.Empty; - public override string TableCell(string content, TableCellFlags flags) => string.Empty; - public override string TableRow(string content) => string.Empty; - public override string Text(string text) => text.TrimEnd('.'); - public override string Preprocess(string text) => text.Trim(); - public override string Postprocess(string text) => text.Trim(); - } - } -} +using Microsoft.MarkedNet; + +namespace Bot.Builder.Community.Adapters.Alexa.Core.Helpers +{ + /// + /// Simple Markdown renderer to turn markdown into plain text for Alexa. + /// + public static class AlexaMarkdownToPlaintextRenderer + { + private static readonly Marked _marked = new Marked(new Options { EscapeHtml = false, Sanitize = false, Mangle = false, Renderer = new RemoveMarkupRenderer() }); + + public static string Render(string source) => _marked.Parse(source); + + private class RemoveMarkupRenderer : MarkdownRenderer + { + private const string ListItemMarker = "$$ListItemMarker$$"; + + public RemoveMarkupRenderer() : base() { } + public RemoveMarkupRenderer(Options options) : base(options) { } + + public override string Blockquote(string quote) => string.Concat(quote, ". "); + public override string Br() => ". "; + public override string Code(string code, string lang, bool escaped) => code; + public override string Codespan(string text) => text; + public override string Del(string text) => text; + public override string Em(string text) => text; + public override string Heading(string text, int level, string raw) => string.Concat(text, ". "); + public override string Hr() => ". "; + public override string Html(string html) => string.Empty; + public override string Image(string href, string title, string text) => title ?? text; + public override string Link(string href, string title, string text) + { + if (title != null) + { + return $"{title} {href}"; + } + else if (text == href) + { + // For standard links the title and href will be the same. + return href; + } + return $"{text} {href}"; + } + public override string List(string body, bool ordered, int start) + { + if (ordered) + { + for (int marker = start, markerIndex = body.IndexOf(ListItemMarker); markerIndex >= 0; markerIndex = body.IndexOf(ListItemMarker), ++marker) + { + body = body.Substring(0, markerIndex) + marker + ". " + body.Substring(markerIndex + ListItemMarker.Length); + } + } + else + { + body = body.Replace(ListItemMarker, string.Empty); + } + return $"{body.Trim().TrimEnd(',')}. "; + } + public override string ListItem(string text) => $"{ListItemMarker}{text}, "; + public override string Paragraph(string text) => string.Concat(text.Replace("\n", " ").TrimEnd('.'), ". "); + public override string Strong(string text) => text; + public override string Table(string header, string body) => string.Empty; + public override string TableCell(string content, TableCellFlags flags) => string.Empty; + public override string TableRow(string content) => string.Empty; + public override string Text(string text) => text.TrimEnd('.'); + public override string Preprocess(string text) => text.Trim(); + public override string Postprocess(string text) => text.Trim(); + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/AttachmentHelper.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/AttachmentHelper.cs new file mode 100644 index 00000000..fe30745b --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/AttachmentHelper.cs @@ -0,0 +1,112 @@ +using System; +using Bot.Builder.Community.Adapters.Google.Core.Model.Response; +using Bot.Builder.Community.Adapters.Google.Core.Model.SystemIntents; +using Microsoft.Bot.Schema; +using Microsoft.Rest; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using BasicCard = Bot.Builder.Community.Adapters.Google.Core.Model.Response.BasicCard; + +namespace Bot.Builder.Community.Adapters.Google.Core.Attachments +{ + public static class AttachmentHelper + { + /// + /// Convert all Google specific attachments to their correct type. + /// + /// + public static void ConvertAttachmentContent(this Activity activity) + { + if (activity == null || activity.Attachments == null) + { + return; + } + + foreach (var attachment in activity.Attachments) + { + switch (attachment.ContentType) + { + case HeroCard.ContentType: + Convert(attachment); + break; + case SigninCard.ContentType: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.BasicCard: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.MediaResponse: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.TableCard: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.CarouselIntent: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.ListIntent: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.DateTimeIntent: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.PermissionsIntent: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.PlaceLocationIntent: + Convert(attachment); + break; + case GoogleAttachmentContentTypes.ConfirmationIntent: + Convert(attachment); + break; + } + } + } + + private static void Convert(Attachment attachment) + { + try + { + attachment.Content = attachment.ContentAs(); + } + catch (JsonException ex) + { + throw new ValidationException($"Failed to convert Google Attachment with ContentType {attachment?.ContentType} to {typeof(T).Name}", ex); + } + catch + { + } + } + + /// + /// Convert the Attachment Content field to the given type. An exception is thrown if the conversion fails. + /// + public static T ContentAs(this Attachment attachment) + { + if (attachment == null) + throw new ArgumentNullException(nameof(attachment)); + + if (attachment.Content == null) + { + return default; + } + if (typeof(T).IsValueType) + { + return (T)System.Convert.ChangeType(attachment.Content, typeof(T)); + } + if (attachment.Content is T) + { + return (T)attachment.Content; + } + if (typeof(T) == typeof(byte[])) + { + return (T)(object)System.Convert.FromBase64String(attachment.Content.ToString()); + } + if (attachment.Content is string) + { + return JsonConvert.DeserializeObject((string)attachment.Content); + } + return (T)((JObject)attachment.Content).ToObject(); + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/Extensions.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/Extensions.cs new file mode 100644 index 00000000..f65c0182 --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/Extensions.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Bot.Builder.Community.Adapters.Google.Core.Model.Response; +using Bot.Builder.Community.Adapters.Google.Core.Model.SystemIntents; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using BasicCard = Bot.Builder.Community.Adapters.Google.Core.Model.Response.BasicCard; + +namespace Bot.Builder.Community.Adapters.Google.Core.Attachments +{ + public static class Extensions + { + public static Attachment ToAttachment(this ResponseItem responseItem) + { + switch (responseItem) + { + case TableCard tableCard: + return CreateAttachment(tableCard, GoogleAttachmentContentTypes.TableCard); + case MediaResponse mediaResponse: + return CreateAttachment(mediaResponse, GoogleAttachmentContentTypes.MediaResponse); + case BasicCard basicCard: + return CreateAttachment(basicCard, GoogleAttachmentContentTypes.BasicCard); + default: + return null; + } + } + + public static Attachment ToAttachment(this SystemIntent intent) + { + switch (intent) + { + case CarouselIntent carouselIntent: + return CreateAttachment(carouselIntent, GoogleAttachmentContentTypes.CarouselIntent); + case ListIntent listIntent: + return CreateAttachment(listIntent, GoogleAttachmentContentTypes.ListIntent); + case ConfirmationIntent confirmationIntent: + return CreateAttachment(confirmationIntent, GoogleAttachmentContentTypes.ConfirmationIntent); + case DateTimeIntent dateTimeIntent: + return CreateAttachment(dateTimeIntent, GoogleAttachmentContentTypes.DateTimeIntent); + case PermissionsIntent permissionsIntent: + return CreateAttachment(permissionsIntent, GoogleAttachmentContentTypes.PermissionsIntent); + case PlaceLocationIntent placeLocationIntent: + return CreateAttachment(placeLocationIntent, GoogleAttachmentContentTypes.PlaceLocationIntent); + default: + return null; + } + } + + private static Attachment CreateAttachment(T card, string contentType) + { + return new Attachment + { + Content = JObject.FromObject(card, new JsonSerializer() { NullValueHandling = NullValueHandling.Ignore }), + ContentType = contentType, + }; + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/GoogleAttachmentContentTypes.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/GoogleAttachmentContentTypes.cs new file mode 100644 index 00000000..1a6c674a --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/Attachments/GoogleAttachmentContentTypes.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bot.Builder.Community.Adapters.Google.Core.Attachments +{ + public static class GoogleAttachmentContentTypes + { + public const string BasicCard = "application/vhd.bfgoogle.basicCard"; + public const string TableCard = "application/vhd.bfgoogle.tableCard"; + public const string MediaResponse = "application/vhd.bfgoogle.media"; + public const string PermissionsIntent = "application/vhd.bfgoogle.permissions"; + public const string SigninIntent = "application/vhd.bfgoogle.signin"; + public const string PlaceLocationIntent = "application/vhd.bfgoogle.placelocation"; + public const string ListIntent = "application/vhd.bfgoogle.list"; + public const string CarouselIntent = "application/vhd.bfgoogle.carousel"; + public const string DateTimeIntent = "application/vhd.bfgoogle.datetime"; + public const string ConfirmationIntent = "application/vhd.bfgoogle.confirm"; + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/Bot.Builder.Community.Adapters.Google.Core.csproj b/libraries/Bot.Builder.Community.Adapters.Google.Core/Bot.Builder.Community.Adapters.Google.Core.csproj new file mode 100644 index 00000000..43bf3360 --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/Bot.Builder.Community.Adapters.Google.Core.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/ConversationRequestMapper.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/ConversationRequestMapper.cs new file mode 100644 index 00000000..158e47de --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/ConversationRequestMapper.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using System.Linq; +using Bot.Builder.Community.Adapters.Google.Core.Attachments; +using Bot.Builder.Community.Adapters.Google.Core.Helpers; +using Bot.Builder.Community.Adapters.Google.Core.Model.Request; +using Bot.Builder.Community.Adapters.Google.Core.Model.Response; +using Bot.Builder.Community.Adapters.Google.Core.Model.SystemIntents; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; + +namespace Bot.Builder.Community.Adapters.Google.Core +{ + public class ConversationRequestMapper : GoogleRequestMapperBase + { + public ConversationRequestMapper(GoogleRequestMapperOptions options = null, ILogger logger = null) : base(options, logger) + { + } + + public Activity RequestToActivity(ConversationRequest request) + { + var activity = new Activity(); + + var actionIntent = + request.Inputs.FirstOrDefault(i => i.Intent.ToLowerInvariant().StartsWith("actions.intent"))?.Intent; + + switch (actionIntent?.ToLowerInvariant()) + { + case "actions.intent.permission": + case "actions.intent.datetime": + case "ask_for_sign_in_confirmation": + case "actions.intent.place": + case "actions.intent.confirmation": + activity.Type = ActivityTypes.Event; + activity = SetGeneralActivityProperties(activity, request); + activity.Name = actionIntent; + activity.Value = request; + return activity; + case "actions.intent.sign_in": + activity.Type = ActivityTypes.Event; + activity = SetGeneralActivityProperties(activity, request); + activity.Name = actionIntent; + var signinStatusArgument = request.Inputs.First()?.Arguments?.Where(a => a.Name == "SIGN_IN").FirstOrDefault(); + var argumentExtension = signinStatusArgument?.Extension; + activity.Value = argumentExtension?["status"]; + return activity; + case "actions.intent.option": + activity.Type = ActivityTypes.Message; + activity = SetGeneralActivityProperties(activity, request); + activity.Text = StripInvocation(request.Inputs[0]?.RawInputs[0]?.Query, + Options.ActionInvocationName); + return activity; + } + + var text = StripInvocation(request.Inputs[0]?.RawInputs[0]?.Query, Options.ActionInvocationName); + + if (string.IsNullOrEmpty(text)) + { + activity.Type = ActivityTypes.ConversationUpdate; + activity = SetGeneralActivityProperties(activity, request); + activity.MembersAdded = new List() { new ChannelAccount() { Id = activity.From.Id } }; + return activity; + } + + activity.Type = ActivityTypes.Message; + activity = SetGeneralActivityProperties(activity, request); + activity.Text = text; + return activity; + } + + public ConversationWebhookResponse ActivityToResponse(Activity activity, ConversationRequest request) + { + var response = new ConversationWebhookResponse + { + UserStorage = request.User.UserStorage + }; + + // Send default empty response if no activity or invalid activity type sent + if (activity == null || activity.Type != ActivityTypes.Message) + { + response.ExpectUserResponse = false; + return response; + } + + activity.ConvertAttachmentContent(); + + var simpleResponse = new SimpleResponse + { + Content = new SimpleResponseContent + { + DisplayText = activity.Text, + Ssml = activity.Speak, + TextToSpeech = activity.Text + } + }; + + var processedIntentStatus = ProcessHelperIntentAttachments(activity); + + // If we have a system intent to send - send it - with or without additional simple prompt + if (processedIntentStatus.Intent != null) + { + response.ExpectUserResponse = true; + response.ExpectedInputs = new ExpectedInput[] + { + new ExpectedInput() + { + PossibleIntents = new SystemIntent[] + { + processedIntentStatus.Intent + }, + InputPrompt = processedIntentStatus.AllowAdditionalInputPrompt + ? new InputPrompt() + { + RichInitialPrompt = new RichResponse() + { + Items = new ResponseItem[] { simpleResponse } + } + } + : null + } + }; + + return response; + }; + + // We haven't sent a response using a SystemIntent, so send simple response + // plus any card activities + var responseItems = new List { simpleResponse }; + responseItems.AddRange(GetResponseItemsFromActivityAttachments(activity)); + + // ensure InputHint is set as required for response + if (activity.InputHint == null || activity.InputHint == InputHints.AcceptingInput) + { + activity.InputHint = + Options.ShouldEndSessionByDefault ? InputHints.IgnoringInput : InputHints.ExpectingInput; + } + + // check if we should be listening for more input from the user + switch (activity.InputHint) + { + case InputHints.IgnoringInput: + response.ExpectUserResponse = false; + response.FinalResponse = new FinalResponse() + { + RichResponse = new RichResponse() { Items = responseItems.ToArray() } + }; + break; + case InputHints.ExpectingInput: + response.ExpectUserResponse = true; + response.ExpectedInputs = new ExpectedInput[] + { + new ExpectedInput() + { + PossibleIntents = new SystemIntent[] + { + new TextIntent(), + }, + InputPrompt = new InputPrompt() + { + RichInitialPrompt = new RichResponse() {Items = responseItems.ToArray()} + } + } + }; + response.ExpectedInputs.First().InputPrompt.RichInitialPrompt.Suggestions = ConvertSuggestedActionsToSuggestionChips(activity)?.ToArray(); + break; + } + + return response; + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/DialogFlowRequestMapper.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/DialogFlowRequestMapper.cs new file mode 100644 index 00000000..b307dffa --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/DialogFlowRequestMapper.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using System.Linq; +using Bot.Builder.Community.Adapters.Google.Core.Attachments; +using Bot.Builder.Community.Adapters.Google.Core.Model; +using Bot.Builder.Community.Adapters.Google.Core.Model.Request; +using Bot.Builder.Community.Adapters.Google.Core.Model.Response; +using Bot.Builder.Community.Adapters.Google.Core.Model.SystemIntents; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Bot.Builder.Community.Adapters.Google.Core +{ + public class DialogFlowRequestMapper : GoogleRequestMapperBase + { + public DialogFlowRequestMapper(GoogleRequestMapperOptions options = null, ILogger logger = null) : base(options, logger) + { + Options = options ?? new GoogleRequestMapperOptions(); + Logger = logger ?? NullLogger.Instance; + } + + public Activity RequestToActivity(DialogFlowRequest request) + { + var payload = request.OriginalDetectIntentRequest.Payload; + + var activity = new Activity(); + activity = SetGeneralActivityProperties(activity, payload); + var actionIntent = payload.Inputs.FirstOrDefault(i => i.Intent.ToLowerInvariant().StartsWith("actions.intent"))?.Intent; + var queryText = StripInvocation(payload.Inputs[0]?.RawInputs[0]?.Query, Options.ActionInvocationName); + + if (request.QueryResult.Intent.IsFallback || request.QueryResult.Intent.DisplayName.ToLowerInvariant() == "launch") + { + if (string.IsNullOrEmpty(queryText) || request.QueryResult.Intent.DisplayName.ToLowerInvariant() == "launch") + { + activity.Type = ActivityTypes.ConversationUpdate; + activity.MembersAdded = new List() { new ChannelAccount() { Id = activity.From.Id } }; + return activity; + } + activity.Type = ActivityTypes.Message; + activity.Text = queryText; + return activity; + } + + switch (actionIntent?.ToLowerInvariant()) + { + case "actions.intent.sign_in": + activity.Type = ActivityTypes.Event; + activity.Name = request.QueryResult.Intent.DisplayName; + var signinStatusArgument = request.OriginalDetectIntentRequest.Payload.Inputs.First()?.Arguments?.Where(a => a.Name == "SIGN_IN").FirstOrDefault(); + var argumentExtension = signinStatusArgument?.Extension; + activity.Value = argumentExtension?["status"]; + return activity; + case "actions.intent.option": + case "actions.intent.text": + activity.Type = ActivityTypes.Message; + activity.Text = queryText; + return activity; + case "actions.intent.permission": + case "actions.intent.datetime": + case "ask_for_sign_in_confirmation": + case "actions.intent.place": + case "actions.intent.confirmation": + default: + activity.Type = ActivityTypes.Event; + activity.Name = request.QueryResult.Intent.DisplayName; + activity.Value = request; + return activity; + } + } + + public DialogFlowResponse ActivityToResponse(Activity activity, DialogFlowRequest dialogFlowRequest) + { + var response = new DialogFlowResponse() + { + Payload = new ResponsePayload() + { + Google = new PayloadContent() + { + ExpectUserResponse = !Options.ShouldEndSessionByDefault, + UserStorage = dialogFlowRequest.OriginalDetectIntentRequest.Payload.User.UserStorage + } + } + }; + + // Send default empty response if no activity or invalid activity type sent + if (activity == null || activity.Type != ActivityTypes.Message) + { + response.Payload.Google.ExpectUserResponse = false; + return response; + } + + activity.ConvertAttachmentContent(); + + var simpleResponse = new SimpleResponse + { + Content = new SimpleResponseContent + { + DisplayText = activity.Text, + Ssml = activity.Speak, + TextToSpeech = activity.Text + } + }; + + var processedIntentStatus = ProcessHelperIntentAttachments(activity); + + // If we have a system intent to send - send it - with or without additional simple prompt + if (processedIntentStatus.Intent != null) + { + response.Payload.Google.ExpectUserResponse = true; + response.Payload.Google.SystemIntent = GetDialogFlowSystemIntentFromSystemIntent(processedIntentStatus); + + if (processedIntentStatus.AllowAdditionalInputPrompt) + { + response.Payload.Google.RichResponse = new RichResponse() + { + Items = new ResponseItem[] {simpleResponse} + }; + } + + return response; + } + + var responseItems = new List { simpleResponse }; + responseItems.AddRange(GetResponseItemsFromActivityAttachments(activity)); + + response.Payload.Google.RichResponse = new RichResponse() + { + Items = responseItems.ToArray() + }; + + // ensure InputHint is set as required for response + if (activity.InputHint == null || activity.InputHint == InputHints.AcceptingInput) + { + activity.InputHint = + Options.ShouldEndSessionByDefault ? InputHints.IgnoringInput : InputHints.ExpectingInput; + } + + // check if we should be listening for more input from the user + switch (activity.InputHint) + { + case InputHints.IgnoringInput: + response.Payload.Google.ExpectUserResponse = false; + break; + case InputHints.ExpectingInput: + response.Payload.Google.ExpectUserResponse = true; + + var suggestionChips = ConvertSuggestedActionsToSuggestionChips(activity); + if (suggestionChips.Any()) + { + response.Payload.Google.RichResponse.Suggestions = suggestionChips.ToArray(); + } + break; + } + + return response; + } + + private DialogFlowSystemIntent GetDialogFlowSystemIntentFromSystemIntent( + ProcessHelperIntentAttachmentsResult processedIntentStatus) + { + var dialogFlowSystemIntent = new DialogFlowSystemIntent() + { + Intent = processedIntentStatus.Intent.Intent + }; + + switch (processedIntentStatus.Intent) + { + case SigninIntent signinIntent: + dialogFlowSystemIntent.Data = signinIntent.InputValueData; + break; + case ListIntent listIntent: + dialogFlowSystemIntent.Data = listIntent.InputValueData; + break; + case CarouselIntent carouselIntent: + dialogFlowSystemIntent.Data = carouselIntent.InputValueData; + break; + case PermissionsIntent permissionsIntent: + dialogFlowSystemIntent.Data = permissionsIntent.InputValueData; + break; + case DateTimeIntent dateTimeIntent: + dialogFlowSystemIntent.Data = dateTimeIntent.InputValueData; + break; + case PlaceLocationIntent placeLocationIntent: + dialogFlowSystemIntent.Data = placeLocationIntent.InputValueData; + break; + case ConfirmationIntent confirmationIntent: + dialogFlowSystemIntent.Data = confirmationIntent.InputValueData; + break; + } + + return dialogFlowSystemIntent; + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleAuthorizationHandler.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleAuthorizationHandler.cs new file mode 100644 index 00000000..8e4d7d4b --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleAuthorizationHandler.cs @@ -0,0 +1,29 @@ +using System; +using JWT.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace Bot.Builder.Community.Adapters.Google.Core +{ + /// + /// Google Authorization Handler. + /// + public class GoogleAuthorizationHandler + { + private readonly ILogger _logger; + + public GoogleAuthorizationHandler(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public static bool ValidateActionProjectId(string authorizationHeader, string actionProjectId) + { + var payload = new JwtBuilder().Decode(authorizationHeader); + var payloadJObj = JObject.Parse(payload); + var aud = (string)payloadJObj["aud"]; + return aud.ToLowerInvariant() == actionProjectId.ToLowerInvariant(); + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleRequestMapperBase.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleRequestMapperBase.cs new file mode 100644 index 00000000..8705a4d9 --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleRequestMapperBase.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Bot.Builder.Community.Adapters.Google.Core.Attachments; +using Bot.Builder.Community.Adapters.Google.Core.Helpers; +using Bot.Builder.Community.Adapters.Google.Core.Model; +using Bot.Builder.Community.Adapters.Google.Core.Model.Request; +using Bot.Builder.Community.Adapters.Google.Core.Model.Response; +using Bot.Builder.Community.Adapters.Google.Core.Model.SystemIntents; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using BasicCard = Bot.Builder.Community.Adapters.Google.Core.Model.Response.BasicCard; + +namespace Bot.Builder.Community.Adapters.Google.Core +{ + public abstract class GoogleRequestMapperBase + { + public GoogleRequestMapperOptions Options { get; set; } + public ILogger Logger; + + public GoogleRequestMapperBase(GoogleRequestMapperOptions options = null, ILogger logger = null) + { + Options = options ?? new GoogleRequestMapperOptions(); + Logger = logger ?? NullLogger.Instance; + } + + public Activity SetGeneralActivityProperties(Activity activity, ConversationRequest request) + { + activity.DeliveryMode = DeliveryModes.ExpectReplies; + activity.ChannelId = Options.ChannelId; + activity.ServiceUrl = Options.ServiceUrl; + activity.Recipient = new ChannelAccount("", "action"); + activity.Conversation = new ConversationAccount(false, id: $"{request.Conversation.ConversationId}"); + activity.From = new ChannelAccount(request.GetUserIdFromUserStorage()); + activity.Id = Guid.NewGuid().ToString(); + activity.Timestamp = DateTime.UtcNow; + activity.Locale = request.User.Locale; + activity.ChannelData = request; + + return activity; + } + + public static Activity MergeActivities(IList activities) + { + return MappingHelper.MergeActivities(activities); + } + + public ProcessHelperIntentAttachmentsResult ProcessHelperIntentAttachments(Activity activity) + { + if (activity?.Attachments != null + && activity.Attachments.FirstOrDefault(a => a.ContentType == SigninCard.ContentType) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = new SigninIntent(), + AllowAdditionalInputPrompt = false + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.ConfirmationIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.ConfirmationIntent, + activity), + AllowAdditionalInputPrompt = false + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.DateTimeIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.DateTimeIntent, + activity), + AllowAdditionalInputPrompt = false + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.PermissionsIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.PermissionsIntent, + activity), + AllowAdditionalInputPrompt = false + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.PlaceLocationIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.PlaceLocationIntent, + activity), + AllowAdditionalInputPrompt = false + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.SigninIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.SigninIntent, + activity), + AllowAdditionalInputPrompt = false + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.CarouselIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.CarouselIntent, + activity), + AllowAdditionalInputPrompt = true + }; + } + + if (activity?.Attachments?.FirstOrDefault(a => + a.ContentType == GoogleAttachmentContentTypes.ListIntent) != null) + { + return new ProcessHelperIntentAttachmentsResult() + { + Intent = ProcessSystemIntentAttachment( + GoogleAttachmentContentTypes.ListIntent, + activity), + AllowAdditionalInputPrompt = true + }; + } + + return new ProcessHelperIntentAttachmentsResult() + { + Intent = null + }; + } + + public ResponseItem ProcessResponseItemAttachment(string contentType, Activity activity) where T : ResponseItem + { + return activity.Attachments? + .Where(a => a.ContentType == contentType) + .Select(a => (T)a.Content).FirstOrDefault(); + } + + public SystemIntent ProcessSystemIntentAttachment(string contentType, Activity activity) where T : SystemIntent + { + return activity.Attachments? + .Where(a => a.ContentType == contentType) + .Select(a => (T)a.Content).FirstOrDefault(); + } + + public List GetResponseItemsFromActivityAttachments(Activity activity) + { + var responseItems = new List(); + + activity.ConvertAttachmentContent(); + + var basicCardItem = ProcessResponseItemAttachment(GoogleAttachmentContentTypes.BasicCard, activity); + if (basicCardItem != null) + responseItems.Add(basicCardItem); + + var tableCardItem = ProcessResponseItemAttachment(GoogleAttachmentContentTypes.TableCard, activity); + if (tableCardItem != null) + responseItems.Add(tableCardItem); + + var mediaItem = ProcessResponseItemAttachment(GoogleAttachmentContentTypes.MediaResponse, activity); + if (mediaItem != null) + responseItems.Add(mediaItem); + + return responseItems; + } + + public string StripInvocation(string query, string invocationName) + { + if (!string.IsNullOrEmpty(query) && !string.IsNullOrEmpty(invocationName)) + { + invocationName = invocationName.ToLowerInvariant(); + query = query.ToLowerInvariant(); + + if (query.Contains(invocationName)) + { + var newStartPosition = query.IndexOf(invocationName, StringComparison.Ordinal); + query = query.Substring(newStartPosition + invocationName.Length); + } + } + + return query; + } + + public static List ConvertSuggestedActionsToSuggestionChips(Activity activity) + { + var suggestions = new List(); + + if (activity.SuggestedActions != null && activity.SuggestedActions.Actions != null && activity.SuggestedActions.Actions.Any()) + { + foreach (var suggestion in activity.SuggestedActions.Actions) + { + suggestions.Add(new Suggestion { Title = suggestion.Title }); + } + } + + return suggestions; + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleRequestMapperOptions.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleRequestMapperOptions.cs new file mode 100644 index 00000000..5cc36d79 --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/GoogleRequestMapperOptions.cs @@ -0,0 +1,12 @@ +using Bot.Builder.Community.Adapters.Google.Core.Model; + +namespace Bot.Builder.Community.Adapters.Google.Core +{ + public class GoogleRequestMapperOptions + { + public string ChannelId { get; set; } = "google"; + public string ServiceUrl { get; set; } = null; + public bool ShouldEndSessionByDefault { get; set; } = true; + public string ActionInvocationName { get; set; } = null; + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/Helpers/Extensions.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/Helpers/Extensions.cs new file mode 100644 index 00000000..514dff82 --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/Helpers/Extensions.cs @@ -0,0 +1,33 @@ +using System; +using Bot.Builder.Community.Adapters.Google.Core.Model; +using Bot.Builder.Community.Adapters.Google.Core.Model.Request; +using Newtonsoft.Json.Linq; + +namespace Bot.Builder.Community.Adapters.Google.Core.Helpers +{ + public static class Extensions + { + public static void EnsureUniqueUserIdInUserStorage(this ConversationRequest conversationRequest) + { + if (conversationRequest.User.UserStorage == null || !conversationRequest.User.UserStorage.ContainsKey("UserId")) + { + if (conversationRequest.User.UserStorage == null) + { + conversationRequest.User.UserStorage = new JObject(); + } + + conversationRequest.User.UserStorage.Add("UserId", Guid.NewGuid().ToString()); + } + } + + public static string GetUserIdFromUserStorage(this ConversationRequest payload) + { + if (payload.User.UserStorage != null && payload.User.UserStorage.ContainsKey("UserId")) + { + return payload.User.UserStorage["UserId"].ToString(); + } + + return null; + } + } +} diff --git a/libraries/Bot.Builder.Community.Adapters.Google.Core/Helpers/GoogleCardFactory.cs b/libraries/Bot.Builder.Community.Adapters.Google.Core/Helpers/GoogleCardFactory.cs new file mode 100644 index 00000000..47a3527a --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Google.Core/Helpers/GoogleCardFactory.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Bot.Builder.Community.Adapters.Google.Core.Model.Response; +using Bot.Builder.Community.Adapters.Google.Core.Model.SystemIntents; + +namespace Bot.Builder.Community.Adapters.Google.Core.Helpers +{ + public static class GoogleCardFactory + { + public static BasicCard CreateBasicCard(string title, string subtitle, string formattedText) + { + return new BasicCard() + { + Content = new BasicCardContent() + { + + Title = title, + Subtitle = subtitle, + FormattedText = formattedText + } + }; + } + + public static TableCard CreateTableCard(List columns, List rows, string title = null, + string subtitle = null, List