├── .gitignore ├── Documentation └── Screenshots │ ├── connect-to-channels.png │ ├── msteams-1-watch.png │ ├── msteams-2-accept-connection-request.png │ ├── msteams-3-conversation.png │ ├── slack-1-connection-request.png │ └── slack-2-conversation.png ├── IntermediatorBotSample.sln ├── IntermediatorBotSample ├── Bot │ └── IntermediatorBot.cs ├── CommandHandling │ ├── Command.cs │ ├── CommandCardFactory.cs │ └── CommandHandler.cs ├── ConversationHistory │ ├── MessageLog.cs │ ├── MessageLogEntity.cs │ └── MessageLogs.cs ├── IntermediatorBotSample.csproj ├── Logging │ └── AggregationChannelLogger.cs ├── MessageRouting │ ├── ConnectionRequestHandler.cs │ └── MessageRouterResultHandler.cs ├── Middleware │ ├── CatchExceptionMiddleware.cs │ └── HandoffMiddleware.cs ├── Pages │ ├── Index.cshtml │ └── Index.cshtml.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Resources │ ├── StringConstants.cs │ ├── Strings.Designer.cs │ └── Strings.resx ├── Startup.cs ├── TeamsManifest │ ├── color.png │ ├── manifest.json │ └── outline.png └── appsettings.json ├── LICENSE ├── README.md └── appveyor.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | # Package and web configs 255 | #*.config 256 | -------------------------------------------------------------------------------- /Documentation/Screenshots/connect-to-channels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/Documentation/Screenshots/connect-to-channels.png -------------------------------------------------------------------------------- /Documentation/Screenshots/msteams-1-watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/Documentation/Screenshots/msteams-1-watch.png -------------------------------------------------------------------------------- /Documentation/Screenshots/msteams-2-accept-connection-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/Documentation/Screenshots/msteams-2-accept-connection-request.png -------------------------------------------------------------------------------- /Documentation/Screenshots/msteams-3-conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/Documentation/Screenshots/msteams-3-conversation.png -------------------------------------------------------------------------------- /Documentation/Screenshots/slack-1-connection-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/Documentation/Screenshots/slack-1-connection-request.png -------------------------------------------------------------------------------- /Documentation/Screenshots/slack-2-conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/Documentation/Screenshots/slack-2-conversation.png -------------------------------------------------------------------------------- /IntermediatorBotSample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2037 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntermediatorBotSample", "IntermediatorBotSample\IntermediatorBotSample.csproj", "{C5442A0C-E1AB-4237-B069-097A470AF818}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C5442A0C-E1AB-4237-B069-097A470AF818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C5442A0C-E1AB-4237-B069-097A470AF818}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C5442A0C-E1AB-4237-B069-097A470AF818}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C5442A0C-E1AB-4237-B069-097A470AF818}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {4EAC083F-2C41-4CBF-895D-24097084BDA7} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Bot/IntermediatorBot.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.CommandHandling; 2 | using Microsoft.Bot; 3 | using Microsoft.Bot.Builder; 4 | using Microsoft.Bot.Schema; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace IntermediatorBotSample.Bot 10 | { 11 | public class IntermediatorBot : IBot 12 | { 13 | private const string SampleUrl = "https://github.com/tompaana/intermediator-bot-sample"; 14 | 15 | public async Task OnTurnAsync(ITurnContext context, CancellationToken ct) 16 | { 17 | Command showOptionsCommand = new Command(Commands.ShowOptions); 18 | 19 | HeroCard heroCard = new HeroCard() 20 | { 21 | Title = "Hello!", 22 | Subtitle = "I am Intermediator Bot", 23 | Text = $"My purpose is to serve as a sample on how to implement the human hand-off. Click/tap the button below or type \"{new Command(Commands.ShowOptions).ToString()}\" to see all possible commands. To learn more visit {SampleUrl}.", 24 | Buttons = new List() 25 | { 26 | new CardAction() 27 | { 28 | Title = "Show options", 29 | Value = showOptionsCommand.ToString(), 30 | Type = ActionTypes.ImBack 31 | } 32 | } 33 | }; 34 | 35 | Activity replyActivity = context.Activity.CreateReply(); 36 | replyActivity.Attachments = new List() { heroCard.ToAttachment() }; 37 | await context.SendActivityAsync(replyActivity); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /IntermediatorBotSample/CommandHandling/Command.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Schema; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using Underscore.Bot.MessageRouting.DataStore; 8 | using Underscore.Bot.MessageRouting.Models; 9 | 10 | namespace IntermediatorBotSample.CommandHandling 11 | { 12 | /// 13 | /// The commands. 14 | /// 15 | public enum Commands 16 | { 17 | Undefined = 0, 18 | ShowOptions, 19 | Watch, // Adds aggregation channel 20 | Unwatch, // Removes aggregation channel 21 | GetRequests, 22 | CreateRequest, 23 | AcceptRequest, 24 | RejectRequest, 25 | GetHistory, 26 | Disconnect 27 | } 28 | 29 | /// 30 | /// Command representation. 31 | /// 32 | public class Command 33 | { 34 | public const string CommandKeyword = "command"; // Used if the channel does not support mentions 35 | public const string CommandParameterAll = "*"; 36 | 37 | /// 38 | /// The actual command such as "watch" or "unwatch". 39 | /// 40 | public Commands BaseCommand 41 | { 42 | get; 43 | protected set; 44 | } 45 | 46 | /// 47 | /// The command parameters. 48 | /// 49 | public IList Parameters 50 | { 51 | get; 52 | protected set; 53 | } 54 | 55 | /// 56 | /// The bot name. 57 | /// 58 | public string BotName 59 | { 60 | get; 61 | set; 62 | } 63 | 64 | /// 65 | /// Constructor. 66 | /// 67 | /// The actual command. 68 | /// The command parameters. 69 | /// The bot name (optional). 70 | public Command(Commands baseCommand, string[] parameters = null, string botName = null) 71 | { 72 | if (baseCommand == Commands.Undefined) 73 | { 74 | throw new ArgumentNullException("The base command must be defined"); 75 | } 76 | 77 | BaseCommand = baseCommand; 78 | 79 | if (parameters != null) 80 | { 81 | Parameters = parameters.ToList(); 82 | } 83 | else 84 | { 85 | Parameters = new List(); 86 | } 87 | 88 | BotName = botName; 89 | } 90 | 91 | /// 92 | /// Creates an accept/reject connection request command. 93 | /// 94 | /// The connection request to accept/reject. 95 | /// If true, will create an accept command. Reject command, if false. 96 | /// The bot name (optional). 97 | /// A newly created accept/reject connection request command. 98 | public static Command CreateAcceptOrRejectConnectionRequestCommand( 99 | ConnectionRequest connectionRequest, bool doAccept, string botName = null) 100 | { 101 | ChannelAccount requestorChannelAccount = 102 | RoutingDataManager.GetChannelAccount(connectionRequest.Requestor); 103 | 104 | return new Command( 105 | doAccept ? Commands.AcceptRequest : Commands.RejectRequest, 106 | new string[] { requestorChannelAccount?.Id, connectionRequest.Requestor.Conversation?.Id }, 107 | botName); 108 | } 109 | 110 | /// 111 | /// Tries to parse the given JSON string into a command object. 112 | /// 113 | /// The command as JSON string. 114 | /// A newly created command instance or null, if failed to parse. 115 | public static Command FromJson(string commandAsJsonString) 116 | { 117 | Command command = null; 118 | 119 | if (!string.IsNullOrWhiteSpace(commandAsJsonString)) 120 | { 121 | try 122 | { 123 | command = JsonConvert.DeserializeObject(commandAsJsonString); 124 | } 125 | catch (Exception) 126 | { 127 | } 128 | } 129 | 130 | return command; 131 | } 132 | 133 | /// 134 | /// Serializes this object into a JSON string. 135 | /// 136 | /// A newly created JSON string based on this object instance. 137 | public string ToJson() 138 | { 139 | return JsonConvert.SerializeObject(this); 140 | } 141 | 142 | /// 143 | /// Tries to parse the given string into a command object. 144 | /// 145 | /// The command as string. 146 | /// A newly created command instance based on the string content or null, if no command found. 147 | public static Command FromString(string commandAsString) 148 | { 149 | if (string.IsNullOrWhiteSpace(commandAsString)) 150 | { 151 | return null; 152 | } 153 | 154 | string[] commandAsStringArray = commandAsString.Split(' '); 155 | 156 | Command command = null; 157 | int baseCommandIndex = -1; 158 | 159 | for (int i = 1; i < commandAsStringArray.Length; ++i) 160 | { 161 | Commands baseCommand = StringToCommand(commandAsStringArray[i].Trim()); 162 | 163 | if (baseCommand != Commands.Undefined) 164 | { 165 | command = new Command(baseCommand); 166 | baseCommandIndex = i; 167 | break; 168 | } 169 | } 170 | 171 | if (command != null) 172 | { 173 | if (baseCommandIndex == 1 174 | && !string.IsNullOrWhiteSpace(commandAsStringArray[baseCommandIndex - 1]) 175 | && !commandAsStringArray[baseCommandIndex - 1].Equals(CommandKeyword)) 176 | { 177 | command.BotName = commandAsStringArray[baseCommandIndex - 1]; 178 | command.BotName = command.BotName.Replace('@', ' ').Trim(); 179 | } 180 | 181 | for (int i = baseCommandIndex + 1; i < commandAsStringArray.Length; ++i) 182 | { 183 | if (!string.IsNullOrWhiteSpace(commandAsStringArray[i])) 184 | { 185 | command.Parameters.Add(commandAsStringArray[i].Trim()); 186 | } 187 | } 188 | } 189 | 190 | return command; 191 | } 192 | 193 | /// 194 | /// For convenience. 195 | /// Tries to parse the text content in the given message activity to a command object. 196 | /// 197 | /// The message activity whose text content to parse. 198 | /// A newly created command instance or null, if no command found. 199 | public static Command FromMessageActivity(IMessageActivity messageActivity) 200 | { 201 | return FromString(messageActivity.Text?.Trim()); 202 | } 203 | 204 | /// 205 | /// Tries to parse the string in the channel data of the given activity to a command object. 206 | /// 207 | /// The activity whose channel data to parse. 208 | /// A newly created command instance or null, if no command found. 209 | public static Command FromChannelData(Activity activity) 210 | { 211 | return FromJson(activity.ChannelData as string); 212 | } 213 | 214 | public string ToString(bool addCommandKeywordOrBotName = true) 215 | { 216 | StringBuilder stringBuilder = new StringBuilder(); 217 | 218 | if (addCommandKeywordOrBotName) 219 | { 220 | if (string.IsNullOrWhiteSpace(BotName)) 221 | { 222 | stringBuilder.Append(CommandKeyword); 223 | } 224 | else 225 | { 226 | stringBuilder.Append("@"); 227 | stringBuilder.Append(BotName); 228 | } 229 | 230 | stringBuilder.Append(' '); 231 | } 232 | 233 | stringBuilder.Append(CommandToString(BaseCommand)); 234 | 235 | if (Parameters != null && Parameters.Count > 0) 236 | { 237 | foreach (string parameter in Parameters) 238 | { 239 | stringBuilder.Append(' '); 240 | stringBuilder.Append(parameter); 241 | } 242 | } 243 | 244 | return stringBuilder.ToString(); 245 | } 246 | 247 | public override string ToString() 248 | { 249 | return ToString(true); 250 | } 251 | 252 | /// The command (enum). 253 | /// The command as string. 254 | public static string CommandToString(Commands command) 255 | { 256 | return command.ToString(); 257 | } 258 | 259 | /// The command as string. 260 | /// The command (enum). 261 | public static Commands StringToCommand(string commandAsString) 262 | { 263 | if (Enum.TryParse(commandAsString, out Commands result)) 264 | { 265 | return result; 266 | } 267 | 268 | foreach (Commands command in Enum.GetValues(typeof(Commands))) 269 | { 270 | if (command.ToString().ToLower().Equals(commandAsString.ToLower())) 271 | { 272 | return command; 273 | } 274 | } 275 | 276 | return Commands.Undefined; 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /IntermediatorBotSample/CommandHandling/CommandCardFactory.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.Resources; 2 | using Microsoft.Bot.Schema; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using Underscore.Bot.MessageRouting.DataStore; 7 | using Underscore.Bot.MessageRouting.Models; 8 | 9 | namespace IntermediatorBotSample.CommandHandling 10 | { 11 | /// 12 | /// An utility class for creating command related cards. 13 | /// 14 | public class CommandCardFactory 15 | { 16 | /// 17 | /// Creates a card with all command options. 18 | /// 19 | /// The name of the bot (optional). 20 | /// A newly created command options card. 21 | public static HeroCard CreateCommandOptionsCard(string botName) 22 | { 23 | HeroCard card = new HeroCard() 24 | { 25 | Title = Strings.CommandMenuTitle, 26 | Subtitle = Strings.CommandMenuDescription, 27 | 28 | Text = string.Format( 29 | Strings.CommandMenuInstructions, 30 | Command.CommandKeyword, 31 | botName, 32 | new Command( 33 | Commands.AcceptRequest, 34 | new string[] { "(user ID)", "(user conversation ID)" }, 35 | botName).ToString()), 36 | 37 | Buttons = new List() 38 | { 39 | new CardAction() 40 | { 41 | Title = Command.CommandToString(Commands.Watch), 42 | Type = ActionTypes.ImBack, 43 | Value = new Command(Commands.Watch, null, botName).ToString() 44 | }, 45 | new CardAction() 46 | { 47 | Title = Command.CommandToString(Commands.Unwatch), 48 | Type = ActionTypes.ImBack, 49 | Value = new Command(Commands.Unwatch, null, botName).ToString() 50 | }, 51 | new CardAction() 52 | { 53 | Title = Command.CommandToString(Commands.GetRequests), 54 | Type = ActionTypes.ImBack, 55 | Value = new Command(Commands.GetRequests, null, botName).ToString() 56 | }, 57 | new CardAction() 58 | { 59 | Title = Command.CommandToString(Commands.AcceptRequest), 60 | Type = ActionTypes.ImBack, 61 | Value = new Command(Commands.AcceptRequest, null, botName).ToString() 62 | }, 63 | new CardAction() 64 | { 65 | Title = Command.CommandToString(Commands.RejectRequest), 66 | Type = ActionTypes.ImBack, 67 | Value = new Command(Commands.RejectRequest, null, botName).ToString() 68 | }, 69 | new CardAction() 70 | { 71 | Title = Command.CommandToString(Commands.GetHistory), 72 | Type = ActionTypes.ImBack, 73 | Value = new Command(Commands.GetHistory, null, botName).ToString() 74 | }, 75 | new CardAction() 76 | { 77 | Title = Command.CommandToString(Commands.Disconnect), 78 | Type = ActionTypes.ImBack, 79 | Value = new Command(Commands.Disconnect, null, botName).ToString() 80 | } 81 | } 82 | }; 83 | 84 | return card; 85 | } 86 | 87 | /// 88 | /// Creates a large connection request card. 89 | /// 90 | /// The connection request. 91 | /// The name of the bot (optional). 92 | /// A newly created request card. 93 | public static HeroCard CreateConnectionRequestCard( 94 | ConnectionRequest connectionRequest, string botName = null) 95 | { 96 | if (connectionRequest == null || connectionRequest.Requestor == null) 97 | { 98 | throw new ArgumentNullException("The connection request or the conversation reference of the requestor is null"); 99 | } 100 | 101 | ChannelAccount requestorChannelAccount = 102 | RoutingDataManager.GetChannelAccount(connectionRequest.Requestor); 103 | 104 | if (requestorChannelAccount == null) 105 | { 106 | throw new ArgumentNullException("The channel account of the requestor is null"); 107 | } 108 | 109 | string requestorChannelAccountName = string.IsNullOrEmpty(requestorChannelAccount.Name) 110 | ? StringConstants.NoUserNamePlaceholder : requestorChannelAccount.Name; 111 | string requestorChannelId = 112 | CultureInfo.CurrentCulture.TextInfo.ToTitleCase(connectionRequest.Requestor.ChannelId); 113 | 114 | Command acceptCommand = 115 | Command.CreateAcceptOrRejectConnectionRequestCommand(connectionRequest, true, botName); 116 | Command rejectCommand = 117 | Command.CreateAcceptOrRejectConnectionRequestCommand(connectionRequest, false, botName); 118 | 119 | HeroCard card = new HeroCard() 120 | { 121 | Title = Strings.ConnectionRequestTitle, 122 | Subtitle = string.Format(Strings.RequestorDetailsTitle, requestorChannelAccountName, requestorChannelId), 123 | Text = string.Format(Strings.AcceptRejectConnectionHint, acceptCommand.ToString(), rejectCommand.ToString()), 124 | 125 | Buttons = new List() 126 | { 127 | new CardAction() 128 | { 129 | Title = Strings.AcceptButtonTitle, 130 | Type = ActionTypes.ImBack, 131 | Value = acceptCommand.ToString() 132 | }, 133 | new CardAction() 134 | { 135 | Title = Strings.RejectButtonTitle, 136 | Type = ActionTypes.ImBack, 137 | Value = rejectCommand.ToString() 138 | } 139 | } 140 | }; 141 | 142 | return card; 143 | } 144 | 145 | /// 146 | /// Creates multiple large connection request cards. 147 | /// 148 | /// The connection requests. 149 | /// The name of the bot (optional). 150 | /// A list of request cards as attachments. 151 | public static IList CreateMultipleConnectionRequestCards( 152 | IList connectionRequests, string botName = null) 153 | { 154 | IList attachments = new List(); 155 | 156 | foreach (ConnectionRequest connectionRequest in connectionRequests) 157 | { 158 | attachments.Add(CreateConnectionRequestCard(connectionRequest, botName).ToAttachment()); 159 | } 160 | 161 | return attachments; 162 | } 163 | 164 | /// 165 | /// Creates a compact card for accepting/rejecting multiple requests. 166 | /// 167 | /// The connection requests. 168 | /// If true, will create an accept card. If false, will create a reject card. 169 | /// The name of the bot (optional). 170 | /// The newly created card. 171 | public static HeroCard CreateMultiConnectionRequestCard( 172 | IList connectionRequests, bool doAccept, string botName = null) 173 | { 174 | HeroCard card = new HeroCard() 175 | { 176 | Title = (doAccept 177 | ? Strings.AcceptConnectionRequestsCardTitle 178 | : Strings.RejectConnectionRequestCardTitle), 179 | Subtitle = (doAccept 180 | ? Strings.AcceptConnectionRequestsCardInstructions 181 | : Strings.RejectConnectionRequestsCardInstructions), 182 | }; 183 | 184 | card.Buttons = new List(); 185 | 186 | if (!doAccept && connectionRequests.Count > 1) 187 | { 188 | card.Buttons.Add(new CardAction() 189 | { 190 | Title = Strings.RejectAll, 191 | Type = ActionTypes.ImBack, 192 | Value = new Command(Commands.RejectRequest, new string[] { Command.CommandParameterAll }, botName).ToString() 193 | }); 194 | } 195 | 196 | foreach (ConnectionRequest connectionRequest in connectionRequests) 197 | { 198 | ChannelAccount requestorChannelAccount = 199 | RoutingDataManager.GetChannelAccount(connectionRequest.Requestor, out bool isBot); 200 | 201 | if (requestorChannelAccount == null) 202 | { 203 | throw new ArgumentNullException("The channel account of the requestor is null"); 204 | } 205 | 206 | string requestorChannelAccountName = string.IsNullOrEmpty(requestorChannelAccount.Name) 207 | ? StringConstants.NoUserNamePlaceholder : requestorChannelAccount.Name; 208 | string requestorChannelId = 209 | CultureInfo.CurrentCulture.TextInfo.ToTitleCase(connectionRequest.Requestor.ChannelId); 210 | string requestorChannelAccountId = requestorChannelAccount.Id; 211 | 212 | Command command = 213 | Command.CreateAcceptOrRejectConnectionRequestCommand(connectionRequest, doAccept, botName); 214 | 215 | card.Buttons.Add(new CardAction() 216 | { 217 | Title = string.Format( 218 | Strings.RequestorDetailsItem, 219 | requestorChannelAccountName, 220 | requestorChannelId, 221 | requestorChannelAccountId), 222 | Type = ActionTypes.ImBack, 223 | Value = command.ToString() 224 | }); 225 | } 226 | 227 | return card; 228 | } 229 | 230 | /// 231 | /// Adds the given card into the given activity as an attachment. 232 | /// 233 | /// The activity to add the card into. 234 | /// The card to add. 235 | /// The given activity with the card added. 236 | public static Activity AddCardToActivity(Activity activity, HeroCard card) 237 | { 238 | activity.Attachments = new List() { card.ToAttachment() }; 239 | return activity; 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /IntermediatorBotSample/CommandHandling/CommandHandler.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.MessageRouting; 2 | using IntermediatorBotSample.Resources; 3 | using Microsoft.Bot.Builder; 4 | using Microsoft.Bot.Connector; 5 | using Microsoft.Bot.Schema; 6 | using Newtonsoft.Json; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using Underscore.Bot.MessageRouting; 11 | using Underscore.Bot.MessageRouting.DataStore; 12 | using Underscore.Bot.MessageRouting.Models; 13 | using Underscore.Bot.MessageRouting.Results; 14 | 15 | namespace IntermediatorBotSample.CommandHandling 16 | { 17 | /// 18 | /// Handler for bot commands related to message routing. 19 | /// 20 | public class CommandHandler 21 | { 22 | private MessageRouter _messageRouter; 23 | private MessageRouterResultHandler _messageRouterResultHandler; 24 | private ConnectionRequestHandler _connectionRequestHandler; 25 | private IList _permittedAggregationChannels; 26 | 27 | /// 28 | /// Constructor. 29 | /// 30 | /// The message router. 31 | /// A MessageRouterResultHandler instance for 32 | /// handling possible routing actions such as accepting connection requests. 33 | /// The connection request handler. 34 | /// Permitted aggregation channels. 35 | /// Null list means all channels are allowed. 36 | public CommandHandler( 37 | MessageRouter messageRouter, 38 | MessageRouterResultHandler messageRouterResultHandler, 39 | ConnectionRequestHandler connectionRequestHandler, 40 | IList permittedAggregationChannels = null) 41 | { 42 | _messageRouter = messageRouter; 43 | _messageRouterResultHandler = messageRouterResultHandler; 44 | _connectionRequestHandler = connectionRequestHandler; 45 | _permittedAggregationChannels = permittedAggregationChannels; 46 | } 47 | 48 | /// 49 | /// Checks the given activity for a possible command. 50 | /// 51 | /// The context containing the activity, which in turn may contain a possible command. 52 | /// True, if a command was detected and handled. False otherwise. 53 | public async virtual Task HandleCommandAsync(ITurnContext context) 54 | { 55 | Activity activity = context.Activity; 56 | Command command = Command.FromMessageActivity(activity); 57 | 58 | if (command == null) 59 | { 60 | // Check for back channel command 61 | command = Command.FromChannelData(activity); 62 | } 63 | 64 | if (command == null) 65 | { 66 | return false; 67 | } 68 | 69 | bool wasHandled = false; 70 | Activity replyActivity = null; 71 | ConversationReference sender = MessageRouter.CreateSenderConversationReference(activity); 72 | 73 | switch (command.BaseCommand) 74 | { 75 | case Commands.ShowOptions: 76 | // Present all command options in a card 77 | replyActivity = CommandCardFactory.AddCardToActivity( 78 | activity.CreateReply(), CommandCardFactory.CreateCommandOptionsCard(activity.Recipient?.Name)); 79 | wasHandled = true; 80 | break; 81 | 82 | case Commands.Watch: 83 | // Add the sender's channel/conversation into the list of aggregation channels 84 | bool isPermittedAggregationChannel = false; 85 | 86 | if (_permittedAggregationChannels != null && _permittedAggregationChannels.Count > 0) 87 | { 88 | foreach (string permittedAggregationChannel in _permittedAggregationChannels) 89 | { 90 | if (!string.IsNullOrWhiteSpace(activity.ChannelId) 91 | && activity.ChannelId.ToLower().Equals(permittedAggregationChannel.ToLower())) 92 | { 93 | isPermittedAggregationChannel = true; 94 | break; 95 | } 96 | } 97 | } 98 | else 99 | { 100 | isPermittedAggregationChannel = true; 101 | } 102 | 103 | if (isPermittedAggregationChannel) 104 | { 105 | ConversationReference aggregationChannelToAdd = new ConversationReference( 106 | null, null, null, 107 | activity.Conversation, activity.ChannelId, activity.ServiceUrl); 108 | 109 | ModifyRoutingDataResult modifyRoutingDataResult = 110 | _messageRouter.RoutingDataManager.AddAggregationChannel(aggregationChannelToAdd); 111 | 112 | if (modifyRoutingDataResult.Type == ModifyRoutingDataResultType.Added) 113 | { 114 | replyActivity = activity.CreateReply(Strings.AggregationChannelSet); 115 | } 116 | else if (modifyRoutingDataResult.Type == ModifyRoutingDataResultType.AlreadyExists) 117 | { 118 | replyActivity = activity.CreateReply(Strings.AggregationChannelAlreadySet); 119 | } 120 | else if (modifyRoutingDataResult.Type == ModifyRoutingDataResultType.Error) 121 | { 122 | replyActivity = activity.CreateReply( 123 | string.Format(Strings.FailedToSetAggregationChannel, modifyRoutingDataResult.ErrorMessage)); 124 | } 125 | } 126 | else 127 | { 128 | replyActivity = activity.CreateReply( 129 | string.Format(Strings.NotPermittedAggregationChannel, activity.ChannelId)); 130 | } 131 | 132 | wasHandled = true; 133 | break; 134 | 135 | case Commands.Unwatch: 136 | // Remove the sender's channel/conversation from the list of aggregation channels 137 | if (_messageRouter.RoutingDataManager.IsAssociatedWithAggregation(sender)) 138 | { 139 | ConversationReference aggregationChannelToRemove = new ConversationReference( 140 | null, null, null, 141 | activity.Conversation, activity.ChannelId, activity.ServiceUrl); 142 | 143 | if (_messageRouter.RoutingDataManager.RemoveAggregationChannel(aggregationChannelToRemove)) 144 | { 145 | replyActivity = activity.CreateReply(Strings.AggregationChannelRemoved); 146 | } 147 | else 148 | { 149 | replyActivity = activity.CreateReply(Strings.FailedToRemoveAggregationChannel); 150 | } 151 | 152 | wasHandled = true; 153 | } 154 | 155 | break; 156 | 157 | case Commands.GetRequests: 158 | IList connectionRequests = 159 | _messageRouter.RoutingDataManager.GetConnectionRequests(); 160 | 161 | replyActivity = activity.CreateReply(); 162 | 163 | if (connectionRequests.Count == 0) 164 | { 165 | replyActivity.Text = Strings.NoPendingRequests; 166 | } 167 | else 168 | { 169 | replyActivity.Attachments = CommandCardFactory.CreateMultipleConnectionRequestCards( 170 | connectionRequests, activity.Recipient?.Name); 171 | } 172 | 173 | replyActivity.ChannelData = JsonConvert.SerializeObject(connectionRequests); 174 | wasHandled = true; 175 | break; 176 | 177 | case Commands.AcceptRequest: 178 | case Commands.RejectRequest: 179 | // Accept/reject connection request 180 | bool doAccept = (command.BaseCommand == Commands.AcceptRequest); 181 | 182 | if (_messageRouter.RoutingDataManager.IsAssociatedWithAggregation(sender)) 183 | { 184 | // The sender is associated with the aggregation and has the right to accept/reject 185 | if (command.Parameters.Count == 0) 186 | { 187 | replyActivity = activity.CreateReply(); 188 | 189 | connectionRequests = 190 | _messageRouter.RoutingDataManager.GetConnectionRequests(); 191 | 192 | if (connectionRequests.Count == 0) 193 | { 194 | replyActivity.Text = Strings.NoPendingRequests; 195 | } 196 | else 197 | { 198 | replyActivity = CommandCardFactory.AddCardToActivity( 199 | replyActivity, CommandCardFactory.CreateMultiConnectionRequestCard( 200 | connectionRequests, doAccept, activity.Recipient?.Name)); 201 | } 202 | } 203 | else if (!doAccept 204 | && command.Parameters[0].Equals(Command.CommandParameterAll)) 205 | { 206 | // Reject all pending connection requests 207 | if (!await _connectionRequestHandler.RejectAllPendingRequestsAsync( 208 | _messageRouter, _messageRouterResultHandler)) 209 | { 210 | replyActivity = activity.CreateReply(); 211 | replyActivity.Text = Strings.FailedToRejectPendingRequests; 212 | } 213 | } 214 | else if (command.Parameters.Count > 1) 215 | { 216 | // Try to accept/reject the specified connection request 217 | ChannelAccount requestorChannelAccount = 218 | new ChannelAccount(command.Parameters[0]); 219 | ConversationAccount requestorConversationAccount = 220 | new ConversationAccount(null, null, command.Parameters[1]); 221 | 222 | AbstractMessageRouterResult messageRouterResult = 223 | await _connectionRequestHandler.AcceptOrRejectRequestAsync( 224 | _messageRouter, _messageRouterResultHandler, sender, doAccept, 225 | requestorChannelAccount, requestorConversationAccount); 226 | 227 | await _messageRouterResultHandler.HandleResultAsync(messageRouterResult); 228 | } 229 | else 230 | { 231 | replyActivity = activity.CreateReply(Strings.InvalidOrMissingCommandParameter); 232 | } 233 | } 234 | #if DEBUG 235 | // We shouldn't respond to command attempts by regular users, but I guess 236 | // it's okay when debugging 237 | else 238 | { 239 | replyActivity = activity.CreateReply(Strings.ConnectionRequestResponseNotAllowed); 240 | } 241 | #endif 242 | 243 | wasHandled = true; 244 | break; 245 | 246 | case Commands.Disconnect: 247 | // End the 1:1 conversation(s) 248 | IList disconnectResults = _messageRouter.Disconnect(sender); 249 | 250 | if (disconnectResults != null && disconnectResults.Count > 0) 251 | { 252 | foreach (ConnectionResult disconnectResult in disconnectResults) 253 | { 254 | await _messageRouterResultHandler.HandleResultAsync(disconnectResult); 255 | } 256 | 257 | wasHandled = true; 258 | } 259 | 260 | break; 261 | 262 | default: 263 | replyActivity = activity.CreateReply(string.Format(Strings.CommandNotRecognized, command.BaseCommand)); 264 | break; 265 | } 266 | 267 | if (replyActivity != null) 268 | { 269 | await context.SendActivityAsync(replyActivity); 270 | } 271 | 272 | return wasHandled; 273 | } 274 | 275 | /// 276 | /// Checks the given activity and determines whether the message was addressed directly to 277 | /// the bot or not. 278 | /// 279 | /// Note: Only mentions are inspected at the moment. 280 | /// 281 | /// The message activity. 282 | /// Use false for channels that do not properly support mentions. 283 | /// True, if the message was address directly to the bot. False otherwise. 284 | public bool WasBotAddressedDirectly(IMessageActivity messageActivity, bool strict = true) 285 | { 286 | bool botWasMentioned = false; 287 | 288 | if (strict) 289 | { 290 | Mention[] mentions = messageActivity.GetMentions(); 291 | 292 | foreach (Mention mention in mentions) 293 | { 294 | foreach (ConversationReference bot in _messageRouter.RoutingDataManager.GetBotInstances()) 295 | { 296 | if (mention.Mentioned.Id.Equals(RoutingDataManager.GetChannelAccount(bot).Id)) 297 | { 298 | botWasMentioned = true; 299 | break; 300 | } 301 | } 302 | } 303 | } 304 | else 305 | { 306 | // Here we assume the message starts with the bot name, for instance: 307 | // 308 | // * "@..." 309 | // * ": ..." 310 | string botName = messageActivity.Recipient?.Name; 311 | string message = messageActivity.Text?.Trim(); 312 | 313 | if (!string.IsNullOrEmpty(botName) && !string.IsNullOrEmpty(message) && message.Length > botName.Length) 314 | { 315 | try 316 | { 317 | message = message.Remove(botName.Length + 1, message.Length - botName.Length - 1); 318 | botWasMentioned = message.Contains(botName); 319 | } 320 | catch (ArgumentOutOfRangeException e) 321 | { 322 | System.Diagnostics.Debug.WriteLine($"Failed to check if bot was mentioned: {e.Message}"); 323 | } 324 | } 325 | } 326 | 327 | return botWasMentioned; 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /IntermediatorBotSample/ConversationHistory/MessageLog.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Schema; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace IntermediatorBotSample.ConversationHistory 7 | { 8 | public class MessageLog 9 | { 10 | /// 11 | /// The messages in the log. 12 | /// 13 | public IList Activities 14 | { 15 | get; 16 | private set; 17 | } 18 | 19 | /// 20 | /// The user associated with this message log. 21 | /// 22 | public ConversationReference User 23 | { 24 | get; 25 | private set; 26 | } 27 | 28 | /// 29 | /// Constructor. 30 | /// 31 | /// The user associated with this message log. 32 | public MessageLog(ConversationReference user) 33 | { 34 | User = user; 35 | Activities = new List(); 36 | } 37 | 38 | /// 39 | /// Adds a message (an activity) to the log. 40 | /// 41 | /// The activity to add. 42 | public void AddMessage(Activity activity) 43 | { 44 | Activities.Add(activity); 45 | } 46 | 47 | public string ToJson() 48 | { 49 | return JsonConvert.SerializeObject(this); 50 | } 51 | 52 | public static MessageLog FromJson(string messageLogAsJsonString) 53 | { 54 | MessageLog messageLog = null; 55 | 56 | try 57 | { 58 | messageLog = JsonConvert.DeserializeObject(messageLogAsJsonString); 59 | } 60 | catch (Exception e) 61 | { 62 | System.Diagnostics.Debug.WriteLine($"Failed to deserialize message log: {e.Message}"); 63 | } 64 | 65 | return messageLog; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /IntermediatorBotSample/ConversationHistory/MessageLogEntity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.WindowsAzure.Storage.Table; 2 | using System; 3 | 4 | namespace IntermediatorBotSample.ConversationHistory 5 | { 6 | public class MessageLogEntity : TableEntity 7 | { 8 | public string Body 9 | { 10 | get; 11 | set; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /IntermediatorBotSample/ConversationHistory/MessageLogs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Schema; 2 | using Microsoft.WindowsAzure.Storage; 3 | using Microsoft.WindowsAzure.Storage.Table; 4 | using Newtonsoft.Json; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Underscore.Bot.MessageRouting.DataStore; 8 | using Underscore.Bot.MessageRouting.DataStore.Azure; 9 | 10 | namespace IntermediatorBotSample.ConversationHistory 11 | { 12 | public class MessageLogs 13 | { 14 | private const string MessageLogsTableName = "MessageLogs"; 15 | private const string PartitionKey = "IntermediatorBot"; 16 | 17 | private CloudTable _messageLogsTable; 18 | private readonly IList _inMemoryMessageLogs; 19 | 20 | /// 21 | /// Constructor. 22 | /// 23 | /// The connection string for Azure Table Storage. 24 | public MessageLogs(string connectionString) 25 | { 26 | if (string.IsNullOrEmpty(connectionString)) 27 | { 28 | System.Diagnostics.Debug.WriteLine("WARNING!!! No connection string - storing message logs in memory"); 29 | _inMemoryMessageLogs = new List(); 30 | } 31 | else 32 | { 33 | System.Diagnostics.Debug.WriteLine("Using Azure Table Storage for storing message logs"); 34 | _messageLogsTable = AzureStorageHelper.GetTable(connectionString, MessageLogsTableName); 35 | MakeSureConversationHistoryTableExistsAsync().Wait(); 36 | } 37 | } 38 | 39 | /// All the message logs. 40 | public IList GetMessageLogs() 41 | { 42 | if (_messageLogsTable != null) 43 | { 44 | var entities = GetAllEntitiesFromTable(_messageLogsTable).Result; 45 | return GetAllMessageLogsFromEntities(entities); 46 | } 47 | 48 | return _inMemoryMessageLogs; 49 | } 50 | 51 | /// 52 | /// Finds the message log associated with the given user. 53 | /// 54 | /// The user whose message log to find. 55 | /// The message log of the user or null, if not found. 56 | public MessageLog GetMessageLog(ConversationReference user) 57 | { 58 | var messageLogs = GetMessageLogs(); 59 | 60 | foreach (MessageLog messageLog in messageLogs) 61 | { 62 | if (RoutingDataManager.Match(user, messageLog.User)) 63 | { 64 | return messageLog; 65 | } 66 | } 67 | return null; 68 | } 69 | 70 | /// 71 | /// Adds a message (an activity) to the log associated with the given user. 72 | /// 73 | /// The activity to add. 74 | /// The user associated with the message. 75 | public void AddMessageLog(Microsoft.Bot.Schema.Activity activity, ConversationReference user) 76 | { 77 | if (_messageLogsTable != null) 78 | { 79 | // Add to AzureTable 80 | } 81 | 82 | else 83 | { 84 | // Add to InMemory storage 85 | } 86 | } 87 | 88 | /// 89 | /// 90 | /// 91 | /// The user whose message log to delete. 92 | public void DeleteMessageLog(ConversationReference user) 93 | { 94 | // TODO 95 | } 96 | 97 | /// 98 | /// Makes sure the cloud table for storing message logs exists. 99 | /// A table is created, if one doesn't exist. 100 | /// 101 | private async Task MakeSureConversationHistoryTableExistsAsync() 102 | { 103 | try 104 | { 105 | await _messageLogsTable.CreateIfNotExistsAsync(); 106 | System.Diagnostics.Debug.WriteLine($"Table '{_messageLogsTable.Name}' created or did already exist"); 107 | } 108 | catch (StorageException e) 109 | { 110 | System.Diagnostics.Debug.WriteLine($"Failed to create table '{_messageLogsTable.Name}' (perhaps it already exists): {e.Message}"); 111 | } 112 | } 113 | 114 | private async Task> GetAllEntitiesFromTable(CloudTable table) 115 | { 116 | var query = new TableQuery() 117 | .Where(TableQuery.GenerateFilterCondition( 118 | "PartitionKey", QueryComparisons.Equal, PartitionKey)); 119 | 120 | return await table.ExecuteTableQueryAsync(query); 121 | } 122 | 123 | private IList GetAllMessageLogsFromEntities(IList entities) 124 | { 125 | IList messageLogs = new List(); 126 | 127 | foreach (var entity in entities) 128 | { 129 | var messageLog = JsonConvert.DeserializeObject(entity.Body); 130 | messageLogs.Add(messageLog); 131 | } 132 | 133 | return messageLogs; 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /IntermediatorBotSample/IntermediatorBotSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | True 29 | True 30 | Strings.resx 31 | 32 | 33 | 34 | 35 | 36 | ResXFileCodeGenerator 37 | Strings.Designer.cs 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Logging/AggregationChannelLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Schema; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using Underscore.Bot.MessageRouting; 5 | using Underscore.Bot.MessageRouting.Logging; 6 | 7 | namespace IntermediatorBotSample.Logging 8 | { 9 | /// 10 | /// Logger that outputs all log messages to all aggregation channel conversations. 11 | /// 12 | public class AggregationChannelLogger : ILogger 13 | { 14 | private MessageRouter _messageRouter; 15 | 16 | public AggregationChannelLogger(MessageRouter messageRouter) 17 | { 18 | _messageRouter = messageRouter; 19 | } 20 | 21 | public async void Log(string message, [CallerMemberName] string methodName = "") 22 | { 23 | if (!string.IsNullOrWhiteSpace(methodName)) 24 | { 25 | message = $"{DateTime.Now}> {methodName}: {message}"; 26 | } 27 | 28 | bool wasSent = false; 29 | 30 | foreach (ConversationReference aggregationChannel in 31 | _messageRouter.RoutingDataManager.GetAggregationChannels()) 32 | { 33 | ResourceResponse resourceResponse = 34 | await _messageRouter.SendMessageAsync(aggregationChannel, message); 35 | 36 | if (resourceResponse != null) 37 | { 38 | wasSent = true; 39 | } 40 | } 41 | 42 | if (!wasSent) 43 | { 44 | System.Diagnostics.Debug.WriteLine(message); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /IntermediatorBotSample/MessageRouting/ConnectionRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.Resources; 2 | using Microsoft.Bot.Schema; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Underscore.Bot.MessageRouting; 6 | using Underscore.Bot.MessageRouting.DataStore; 7 | using Underscore.Bot.MessageRouting.Models; 8 | using Underscore.Bot.MessageRouting.Results; 9 | 10 | namespace IntermediatorBotSample.MessageRouting 11 | { 12 | /// 13 | /// Contains utility methods for accepting and rejecting connection requests. 14 | /// 15 | public class ConnectionRequestHandler 16 | { 17 | private IList _noDirectConversationsWithChannels; 18 | 19 | /// 20 | /// Constructor. 21 | /// 22 | /// Does not try to create direct 23 | /// conversations when the agent is on one of the channels on this list. 24 | public ConnectionRequestHandler(IList noDirectConversationsWithChannels) 25 | { 26 | _noDirectConversationsWithChannels = noDirectConversationsWithChannels; 27 | } 28 | 29 | /// 30 | /// Tries to accept/reject a pending connection request. 31 | /// 32 | /// The message router. 33 | /// The message router result handler. 34 | /// The sender party (accepter/rejecter). 35 | /// If true, will try to accept the request. If false, will reject. 36 | /// The channel account ID of the user/bot whose request to accept/reject. 37 | /// The conversation account ID of the user/bot whose request to accept/reject. 38 | /// The result. 39 | public async Task AcceptOrRejectRequestAsync( 40 | MessageRouter messageRouter, MessageRouterResultHandler messageRouterResultHandler, 41 | ConversationReference sender, bool doAccept, 42 | ChannelAccount requestorChannelAccountId, ConversationAccount requestorConversationAccountId) 43 | { 44 | AbstractMessageRouterResult messageRouterResult = new ConnectionRequestResult() 45 | { 46 | Type = ConnectionRequestResultType.Error 47 | }; 48 | 49 | ConversationReference requestor = 50 | new ConversationReference( 51 | null, requestorChannelAccountId, null, requestorConversationAccountId); 52 | 53 | ConnectionRequest connectionRequest = 54 | messageRouter.RoutingDataManager.FindConnectionRequest(requestor); 55 | 56 | if (connectionRequest == null) 57 | { 58 | // Try bot 59 | requestor.Bot = requestor.User; 60 | requestor.User = null; 61 | 62 | connectionRequest = 63 | messageRouter.RoutingDataManager.FindConnectionRequest(requestor); 64 | } 65 | 66 | if (connectionRequest != null) 67 | { 68 | Connection connection = null; 69 | 70 | if (sender != null) 71 | { 72 | connection = messageRouter.RoutingDataManager.FindConnection(sender); 73 | } 74 | 75 | ConversationReference senderInConnection = null; 76 | ConversationReference counterpart = null; 77 | 78 | if (connection != null && connection.ConversationReference1 != null) 79 | { 80 | if (RoutingDataManager.Match(sender, connection.ConversationReference1)) 81 | { 82 | senderInConnection = connection.ConversationReference1; 83 | counterpart = connection.ConversationReference2; 84 | } 85 | else 86 | { 87 | senderInConnection = connection.ConversationReference2; 88 | counterpart = connection.ConversationReference1; 89 | } 90 | } 91 | 92 | if (doAccept) 93 | { 94 | if (senderInConnection != null) 95 | { 96 | // The sender (accepter/rejecter) is ALREADY connected to another party 97 | if (counterpart != null) 98 | { 99 | messageRouterResult.ErrorMessage = string.Format( 100 | Strings.AlreadyConnectedWithUser, 101 | RoutingDataManager.GetChannelAccount(counterpart)?.Name); 102 | } 103 | else 104 | { 105 | messageRouterResult.ErrorMessage = Strings.ErrorOccured; 106 | } 107 | } 108 | else 109 | { 110 | bool createNewDirectConversation = 111 | (_noDirectConversationsWithChannels == null 112 | || !(_noDirectConversationsWithChannels.Contains(sender.ChannelId.ToLower()))); 113 | 114 | // Try to accept 115 | messageRouterResult = await messageRouter.ConnectAsync( 116 | sender, 117 | connectionRequest.Requestor, 118 | createNewDirectConversation); 119 | } 120 | } 121 | else 122 | { 123 | // Note: Rejecting is OK even if the sender is alreay connected 124 | messageRouterResult = messageRouter.RejectConnectionRequest(connectionRequest.Requestor, sender); 125 | } 126 | } 127 | else 128 | { 129 | messageRouterResult.ErrorMessage = Strings.FailedToFindPendingRequest; 130 | } 131 | 132 | return messageRouterResult; 133 | } 134 | 135 | /// 136 | /// Tries to reject all pending requests. 137 | /// 138 | /// The message router. 139 | /// The message router result handler. 140 | /// True, if successful. False otherwise. 141 | public async Task RejectAllPendingRequestsAsync( 142 | MessageRouter messageRouter, MessageRouterResultHandler messageRouterResultHandler) 143 | { 144 | bool wasSuccessful = false; 145 | IList connectionRequests = messageRouter.RoutingDataManager.GetConnectionRequests(); 146 | 147 | if (connectionRequests.Count > 0) 148 | { 149 | IList connectionRequestResults = 150 | new List(); 151 | 152 | foreach (ConnectionRequest connectionRequest in connectionRequests) 153 | { 154 | connectionRequestResults.Add( 155 | messageRouter.RejectConnectionRequest(connectionRequest.Requestor)); 156 | } 157 | 158 | foreach (ConnectionRequestResult connectionRequestResult in connectionRequestResults) 159 | { 160 | await messageRouterResultHandler.HandleResultAsync(connectionRequestResult); 161 | } 162 | 163 | wasSuccessful = true; 164 | } 165 | 166 | return wasSuccessful; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /IntermediatorBotSample/MessageRouting/MessageRouterResultHandler.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.CommandHandling; 2 | using IntermediatorBotSample.Resources; 3 | using Microsoft.Bot.Schema; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Underscore.Bot.MessageRouting; 8 | using Underscore.Bot.MessageRouting.DataStore; 9 | using Underscore.Bot.MessageRouting.Models; 10 | using Underscore.Bot.MessageRouting.Results; 11 | 12 | namespace IntermediatorBotSample.MessageRouting 13 | { 14 | /// 15 | /// Handles the message router results. 16 | /// 17 | public class MessageRouterResultHandler 18 | { 19 | private MessageRouter _messageRouter; 20 | 21 | public MessageRouterResultHandler(MessageRouter messageRouter) 22 | { 23 | _messageRouter = messageRouter 24 | ?? throw new ArgumentNullException( 25 | $"({nameof(messageRouter)}) cannot be null"); 26 | } 27 | 28 | /// 29 | /// Handles the given message router result. 30 | /// 31 | /// The result to handle. 32 | /// True, if the result was handled. False, if no action was taken. 33 | public virtual async Task HandleResultAsync(AbstractMessageRouterResult messageRouterResult) 34 | { 35 | if (messageRouterResult != null) 36 | { 37 | if (messageRouterResult is ConnectionRequestResult) 38 | { 39 | return await HandleConnectionRequestResultAsync(messageRouterResult as ConnectionRequestResult); 40 | } 41 | 42 | if (messageRouterResult is ConnectionResult) 43 | { 44 | return await HandleConnectionResultAsync(messageRouterResult as ConnectionResult); 45 | } 46 | 47 | if (messageRouterResult is MessageRoutingResult) 48 | { 49 | return await HandleMessageRoutingResultAsync(messageRouterResult as MessageRoutingResult); 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /// 57 | /// Handles the given connection request result. 58 | /// 59 | /// The result to handle. 60 | /// True, if the result was handled. False, if no action was taken. 61 | protected virtual async Task HandleConnectionRequestResultAsync( 62 | ConnectionRequestResult connectionRequestResult) 63 | { 64 | ConnectionRequest connectionRequest = connectionRequestResult?.ConnectionRequest; 65 | 66 | if (connectionRequest == null || connectionRequest.Requestor == null) 67 | { 68 | System.Diagnostics.Debug.WriteLine("No client to inform about the connection request result"); 69 | return false; 70 | } 71 | 72 | switch (connectionRequestResult.Type) 73 | { 74 | case ConnectionRequestResultType.Created: 75 | foreach (ConversationReference aggregationChannel 76 | in _messageRouter.RoutingDataManager.GetAggregationChannels()) 77 | { 78 | ConversationReference botConversationReference = 79 | _messageRouter.RoutingDataManager.FindConversationReference( 80 | aggregationChannel.ChannelId, aggregationChannel.Conversation.Id, null, true); 81 | 82 | if (botConversationReference != null) 83 | { 84 | IMessageActivity messageActivity = Activity.CreateMessageActivity(); 85 | messageActivity.Conversation = aggregationChannel.Conversation; 86 | messageActivity.Recipient = RoutingDataManager.GetChannelAccount(aggregationChannel); 87 | messageActivity.Attachments = new List 88 | { 89 | CommandCardFactory.CreateConnectionRequestCard( 90 | connectionRequest, 91 | RoutingDataManager.GetChannelAccount( 92 | botConversationReference)?.Name).ToAttachment() 93 | }; 94 | 95 | await _messageRouter.SendMessageAsync(aggregationChannel, messageActivity); 96 | } 97 | } 98 | 99 | await _messageRouter.SendMessageAsync( 100 | connectionRequest.Requestor, Strings.NotifyClientWaitForRequestHandling); 101 | return true; 102 | 103 | case ConnectionRequestResultType.AlreadyExists: 104 | await _messageRouter.SendMessageAsync( 105 | connectionRequest.Requestor, Strings.NotifyClientDuplicateRequest); 106 | return true; 107 | 108 | case ConnectionRequestResultType.Rejected: 109 | if (connectionRequestResult.Rejecter != null) 110 | { 111 | await _messageRouter.SendMessageAsync( 112 | connectionRequestResult.Rejecter, 113 | string.Format(Strings.NotifyOwnerRequestRejected, GetNameOrId(connectionRequest.Requestor))); 114 | } 115 | 116 | await _messageRouter.SendMessageAsync( 117 | connectionRequest.Requestor, Strings.NotifyClientRequestRejected); 118 | return true; 119 | 120 | case ConnectionRequestResultType.NotSetup: 121 | await _messageRouter.SendMessageAsync( 122 | connectionRequest.Requestor, Strings.NoAgentsAvailable); 123 | return true; 124 | 125 | case ConnectionRequestResultType.Error: 126 | if (connectionRequestResult.Rejecter != null) 127 | { 128 | await _messageRouter.SendMessageAsync( 129 | connectionRequestResult.Rejecter, 130 | string.Format(Strings.ConnectionRequestResultErrorWithResult, connectionRequestResult.ErrorMessage)); 131 | } 132 | 133 | return true; 134 | 135 | default: 136 | break; 137 | } 138 | 139 | return false; 140 | } 141 | 142 | /// 143 | /// Handles the given connection result. 144 | /// 145 | /// The result to handle. 146 | /// True, if the result was handled. False, if no action was taken. 147 | protected virtual async Task HandleConnectionResultAsync(ConnectionResult connectionResult) 148 | { 149 | Connection connection = connectionResult.Connection; 150 | 151 | switch (connectionResult.Type) 152 | { 153 | case ConnectionResultType.Connected: 154 | if (connection != null) 155 | { 156 | if (connection.ConversationReference1 != null) 157 | { 158 | await _messageRouter.SendMessageAsync( 159 | connection.ConversationReference1, 160 | string.Format(Strings.NotifyOwnerConnected, 161 | GetNameOrId(connection.ConversationReference2))); 162 | } 163 | 164 | if (connection.ConversationReference2 != null) 165 | { 166 | await _messageRouter.SendMessageAsync( 167 | connection.ConversationReference2, 168 | string.Format(Strings.NotifyOwnerConnected, 169 | GetNameOrId(connection.ConversationReference1))); 170 | } 171 | } 172 | 173 | return true; 174 | 175 | case ConnectionResultType.Disconnected: 176 | if (connection != null) 177 | { 178 | if (connection.ConversationReference1 != null) 179 | { 180 | await _messageRouter.SendMessageAsync( 181 | connection.ConversationReference1, 182 | string.Format(Strings.NotifyOwnerDisconnected, 183 | GetNameOrId(connection.ConversationReference2))); 184 | } 185 | 186 | if (connection.ConversationReference2 != null) 187 | { 188 | await _messageRouter.SendMessageAsync( 189 | connection.ConversationReference2, 190 | string.Format(Strings.NotifyClientDisconnected, 191 | GetNameOrId(connection.ConversationReference1))); 192 | } 193 | } 194 | 195 | return true; 196 | 197 | case ConnectionResultType.Error: 198 | if (connection.ConversationReference1 != null) 199 | { 200 | await _messageRouter.SendMessageAsync( 201 | connection.ConversationReference1, 202 | string.Format(Strings.ConnectionResultErrorWithResult, connectionResult.ErrorMessage)); 203 | } 204 | 205 | return true; 206 | 207 | default: 208 | break; 209 | } 210 | 211 | return false; 212 | } 213 | 214 | /// 215 | /// Handles the given message routing result. 216 | /// 217 | /// The result to handle. 218 | /// True, if the result was handled. False, if no action was taken. 219 | protected virtual async Task HandleMessageRoutingResultAsync( 220 | MessageRoutingResult messageRoutingResult) 221 | { 222 | ConversationReference agent = messageRoutingResult?.Connection?.ConversationReference1; 223 | 224 | switch (messageRoutingResult.Type) 225 | { 226 | case MessageRoutingResultType.NoActionTaken: 227 | case MessageRoutingResultType.MessageRouted: 228 | // No need to do anything 229 | break; 230 | 231 | case MessageRoutingResultType.FailedToRouteMessage: 232 | case MessageRoutingResultType.Error: 233 | if (agent != null) 234 | { 235 | string errorMessage = string.IsNullOrWhiteSpace(messageRoutingResult.ErrorMessage) 236 | ? Strings.FailedToForwardMessage 237 | : messageRoutingResult.ErrorMessage; 238 | 239 | 240 | await _messageRouter.SendMessageAsync(agent, errorMessage); 241 | } 242 | 243 | return true; 244 | 245 | default: 246 | break; 247 | } 248 | 249 | return false; 250 | } 251 | 252 | /// 253 | /// Tries to resolve the name of the given user/bot instance. 254 | /// Will fallback to ID, if no name specified. 255 | /// 256 | /// The conversation reference, whose details to resolve. 257 | /// The name or the ID of the given user/bot instance. 258 | protected virtual string GetNameOrId(ConversationReference conversationReference) 259 | { 260 | if (conversationReference != null) 261 | { 262 | ChannelAccount channelAccount = 263 | RoutingDataManager.GetChannelAccount(conversationReference); 264 | 265 | if (channelAccount != null) 266 | { 267 | if (!string.IsNullOrWhiteSpace(channelAccount.Name)) 268 | { 269 | return channelAccount.Name; 270 | } 271 | 272 | if (!string.IsNullOrWhiteSpace(channelAccount.Id)) 273 | { 274 | return channelAccount.Id; 275 | } 276 | } 277 | } 278 | 279 | return StringConstants.NoNameOrId; 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Middleware/CatchExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Microsoft.Bot.Builder.Core.Extensions 6 | { 7 | /// 8 | /// This piece of middleware can be added to allow you to handle exceptions when they are thrown 9 | /// within your bot's code or middleware further down the pipeline. Using this handler you might 10 | /// send an appropriate message to the user to let them know that something has gone wrong. 11 | /// You can specify the type of exception the middleware should catch and this middleware can be added 12 | /// multiple times to allow you to handle different exception types in different ways. 13 | /// 14 | /// 15 | /// The type of the exception that you want to catch. This can be 'Exception' to 16 | /// catch all or a specific type of exception 17 | /// 18 | public class CatchExceptionMiddleware : IMiddleware where T : Exception 19 | { 20 | private readonly Func _handler; 21 | 22 | public CatchExceptionMiddleware(Func callOnException) 23 | { 24 | _handler = callOnException; 25 | } 26 | 27 | public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken)) 28 | { 29 | try 30 | { 31 | // Continue to route the activity through the pipeline 32 | // any errors further down the pipeline will be caught by 33 | // this try / catch 34 | await next(cancellationToken).ConfigureAwait(false); 35 | } 36 | catch (T ex) 37 | { 38 | // If an error is thrown and the exception is of type T then invoke the handler 39 | await _handler.Invoke(context, ex).ConfigureAwait(false); 40 | } 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Middleware/HandoffMiddleware.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.CommandHandling; 2 | using IntermediatorBotSample.ConversationHistory; 3 | using IntermediatorBotSample.MessageRouting; 4 | using Microsoft.Bot.Builder; 5 | using Microsoft.Bot.Connector.Authentication; 6 | using Microsoft.Bot.Schema; 7 | using Microsoft.Extensions.Configuration; 8 | using System.Collections.Generic; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Underscore.Bot.MessageRouting; 12 | using Underscore.Bot.MessageRouting.DataStore; 13 | using Underscore.Bot.MessageRouting.DataStore.Azure; 14 | using Underscore.Bot.MessageRouting.DataStore.Local; 15 | using Underscore.Bot.MessageRouting.Results; 16 | 17 | namespace IntermediatorBotSample.Middleware 18 | { 19 | public class HandoffMiddleware : IMiddleware 20 | { 21 | private const string KeyAzureTableStorageConnectionString = "AzureTableStorageConnectionString"; 22 | private const string KeyRejectConnectionRequestIfNoAggregationChannel = "RejectConnectionRequestIfNoAggregationChannel"; 23 | private const string KeyPermittedAggregationChannels = "PermittedAggregationChannels"; 24 | private const string KeyNoDirectConversationsWithChannels = "NoDirectConversationsWithChannels"; 25 | 26 | public IConfiguration Configuration 27 | { 28 | get; 29 | protected set; 30 | } 31 | 32 | public MessageRouter MessageRouter 33 | { 34 | get; 35 | protected set; 36 | } 37 | 38 | public MessageRouterResultHandler MessageRouterResultHandler 39 | { 40 | get; 41 | protected set; 42 | } 43 | 44 | public CommandHandler CommandHandler 45 | { 46 | get; 47 | protected set; 48 | } 49 | 50 | public MessageLogs MessageLogs 51 | { 52 | get; 53 | protected set; 54 | } 55 | 56 | public HandoffMiddleware(IConfiguration configuration) 57 | { 58 | Configuration = configuration; 59 | string connectionString = Configuration[KeyAzureTableStorageConnectionString]; 60 | IRoutingDataStore routingDataStore = null; 61 | 62 | if (string.IsNullOrEmpty(connectionString)) 63 | { 64 | System.Diagnostics.Debug.WriteLine($"WARNING!!! No connection string found - using {nameof(InMemoryRoutingDataStore)}"); 65 | routingDataStore = new InMemoryRoutingDataStore(); 66 | } 67 | else 68 | { 69 | System.Diagnostics.Debug.WriteLine($"Found a connection string - using {nameof(AzureTableRoutingDataStore)}"); 70 | routingDataStore = new AzureTableRoutingDataStore(connectionString); 71 | } 72 | 73 | MessageRouter = new MessageRouter( 74 | routingDataStore, 75 | new MicrosoftAppCredentials(Configuration["MicrosoftAppId"], Configuration["MicrosoftAppPassword"])); 76 | 77 | //MessageRouter.Logger = new Logging.AggregationChannelLogger(MessageRouter); 78 | 79 | MessageRouterResultHandler = new MessageRouterResultHandler(MessageRouter); 80 | 81 | ConnectionRequestHandler connectionRequestHandler = 82 | new ConnectionRequestHandler(GetChannelList(KeyNoDirectConversationsWithChannels)); 83 | 84 | CommandHandler = new CommandHandler( 85 | MessageRouter, 86 | MessageRouterResultHandler, 87 | connectionRequestHandler, 88 | GetChannelList(KeyPermittedAggregationChannels)); 89 | 90 | MessageLogs = new MessageLogs(connectionString); 91 | } 92 | 93 | public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken ct) 94 | { 95 | Activity activity = context.Activity; 96 | 97 | if (activity.Type is ActivityTypes.Message) 98 | { 99 | bool.TryParse( 100 | Configuration[KeyRejectConnectionRequestIfNoAggregationChannel], 101 | out bool rejectConnectionRequestIfNoAggregationChannel); 102 | 103 | // Store the conversation references (identities of the sender and the recipient [bot]) 104 | // in the activity 105 | MessageRouter.StoreConversationReferences(activity); 106 | 107 | AbstractMessageRouterResult messageRouterResult = null; 108 | 109 | // Check the activity for commands 110 | if (await CommandHandler.HandleCommandAsync(context) == false) 111 | { 112 | // No command detected/handled 113 | 114 | // Let the message router route the activity, if the sender is connected with 115 | // another user/bot 116 | messageRouterResult = await MessageRouter.RouteMessageIfSenderIsConnectedAsync(activity); 117 | 118 | if (messageRouterResult is MessageRoutingResult 119 | && (messageRouterResult as MessageRoutingResult).Type == MessageRoutingResultType.NoActionTaken) 120 | { 121 | // No action was taken by the message router. This means that the user 122 | // is not connected (in a 1:1 conversation) with a human 123 | // (e.g. customer service agent) yet. 124 | 125 | // Check for cry for help (agent assistance) 126 | if (!string.IsNullOrWhiteSpace(activity.Text) 127 | && activity.Text.ToLower().Contains("human")) 128 | { 129 | // Create a connection request on behalf of the sender 130 | // Note that the returned result must be handled 131 | messageRouterResult = MessageRouter.CreateConnectionRequest( 132 | MessageRouter.CreateSenderConversationReference(activity), 133 | rejectConnectionRequestIfNoAggregationChannel); 134 | } 135 | else 136 | { 137 | // No action taken - this middleware did not consume the activity so let it propagate 138 | await next(ct).ConfigureAwait(false); 139 | } 140 | } 141 | } 142 | 143 | // Uncomment to see the result in a reply (may be useful for debugging) 144 | //if (messageRouterResult != null) 145 | //{ 146 | // await MessageRouter.ReplyToActivityAsync(activity, messageRouterResult.ToString()); 147 | //} 148 | 149 | // Handle the result, if necessary 150 | await MessageRouterResultHandler.HandleResultAsync(messageRouterResult); 151 | } 152 | else 153 | { 154 | // No action taken - this middleware did not consume the activity so let it propagate 155 | await next(ct).ConfigureAwait(false); 156 | } 157 | } 158 | 159 | /// 160 | /// Extracts the channel list from the settings matching the given key. 161 | /// 162 | /// The list of channels or null, if none found. 163 | private IList GetChannelList(string key) 164 | { 165 | IList channelList = null; 166 | 167 | string channels = Configuration[key]; 168 | 169 | if (!string.IsNullOrWhiteSpace(channels)) 170 | { 171 | System.Diagnostics.Debug.WriteLine($"Channels by key \"{key}\": {channels}"); 172 | string[] channelArray = channels.Split(','); 173 | 174 | if (channelArray.Length > 0) 175 | { 176 | channelList = new List(); 177 | 178 | foreach (string channel in channelArray) 179 | { 180 | channelList.Add(channel.Trim()); 181 | } 182 | } 183 | } 184 | else 185 | { 186 | System.Diagnostics.Debug.WriteLine($"No channels defined by key \"{key}\" in app settings"); 187 | } 188 | 189 | return channelList; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using IntermediatorBotSample.Pages 3 | @model IndexModel 4 | @{ 5 | } 6 | 7 |

Intermediator Bot Sample

8 | 9 |

10 | The purpose is to serve as a sample on how to implement the human hand-off. 11 | Visit https://github.com/tompaana/intermediator-bot-sample to learn more. 12 |

13 | 14 |

The bot endpoint URL is:

15 | 16 | 21 | 22 |

The bot credentials are:

23 |
    24 |
  • 25 |
  • 26 |
27 | 28 | 34 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace IntermediatorBotSample.Pages 6 | { 7 | public class IndexModel : PageModel 8 | { 9 | [BindProperty] 10 | public string BotEndpointPath 11 | { 12 | get 13 | { 14 | return $"{Configuration["BotBasePath"]}{Configuration["BotMessagesPath"]}"; 15 | } 16 | } 17 | 18 | [BindProperty] 19 | public string BotAppId 20 | { 21 | get 22 | { 23 | #if DEBUG 24 | return Configuration["MicrosoftAppId"]; 25 | #else 26 | return "None of your business"; 27 | #endif 28 | } 29 | } 30 | 31 | [BindProperty] 32 | public string BotAppPassword 33 | { 34 | get 35 | { 36 | #if DEBUG 37 | return Configuration["MicrosoftAppPassword"]; 38 | #else 39 | return "Also none of your business"; 40 | #endif 41 | } 42 | } 43 | 44 | private IConfiguration Configuration 45 | { 46 | get; 47 | } 48 | 49 | public IndexModel(IConfiguration configuration) 50 | { 51 | Configuration = configuration; 52 | } 53 | 54 | public void OnGet() 55 | { 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace IntermediatorBotSample 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:29210/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "IntermediatorBotSample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:29211/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Resources/StringConstants.cs: -------------------------------------------------------------------------------- 1 | namespace IntermediatorBotSample.Resources 2 | { 3 | public class StringConstants 4 | { 5 | public static readonly string NoUserNamePlaceholder = "(no user name)"; 6 | public static readonly string NoNameOrId = "(no name or ID)"; 7 | 8 | public static readonly string LineBreak = "\n\r"; 9 | public static readonly char QuotationMark = '"'; 10 | 11 | // For parsing JSON 12 | public static readonly string EndOfLineInJsonResponse = "\\r\\n"; 13 | public static readonly char BackslashInJsonResponse = '\\'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Resources/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace IntermediatorBotSample.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IntermediatorBotSample.Resources.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Accept. 65 | /// 66 | internal static string AcceptButtonTitle { 67 | get { 68 | return ResourceManager.GetString("AcceptButtonTitle", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Choose the user whose request to accept. 74 | /// 75 | internal static string AcceptConnectionRequestsCardInstructions { 76 | get { 77 | return ResourceManager.GetString("AcceptConnectionRequestsCardInstructions", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Accept request. 83 | /// 84 | internal static string AcceptConnectionRequestsCardTitle { 85 | get { 86 | return ResourceManager.GetString("AcceptConnectionRequestsCardTitle", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Accept or reject the request: Type "{0}" to accept, "{1}" to reject or use the buttons if enabled. 92 | /// 93 | internal static string AcceptRejectConnectionHint { 94 | get { 95 | return ResourceManager.GetString("AcceptRejectConnectionHint", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to To set up an aggregation channel, type "{0}". 101 | /// 102 | internal static string AddAggregationChannelCommandHint { 103 | get { 104 | return ResourceManager.GetString("AddAggregationChannelCommandHint", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to This channel/conversation is already receiving requests. 110 | /// 111 | internal static string AggregationChannelAlreadySet { 112 | get { 113 | return ResourceManager.GetString("AggregationChannelAlreadySet", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to This channel/conversation will no longer receive requests. 119 | /// 120 | internal static string AggregationChannelRemoved { 121 | get { 122 | return ResourceManager.GetString("AggregationChannelRemoved", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to This channel/conversation is now where the requests are aggregated. 128 | /// 129 | internal static string AggregationChannelSet { 130 | get { 131 | return ResourceManager.GetString("AggregationChannelSet", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to You are already connected with user "{0}". 137 | /// 138 | internal static string AlreadyConnectedWithUser { 139 | get { 140 | return ResourceManager.GetString("AlreadyConnectedWithUser", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to Apply to all. 146 | /// 147 | internal static string ApplyToAll { 148 | get { 149 | return ResourceManager.GetString("ApplyToAll", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to Bots. 155 | /// 156 | internal static string Bots { 157 | get { 158 | return ResourceManager.GetString("Bots", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to Administrative options for controlling the end user bot conversations. 164 | /// 165 | internal static string CommandMenuDescription { 166 | get { 167 | return ResourceManager.GetString("CommandMenuDescription", resourceCulture); 168 | } 169 | } 170 | 171 | /// 172 | /// Looks up a localized string similar to Select the command using the buttons below or type the command keyword ("{0}")/bot handle ("@{1}") followed by the command and its parameters (if any), for example: "{2}".. 173 | /// 174 | internal static string CommandMenuInstructions { 175 | get { 176 | return ResourceManager.GetString("CommandMenuInstructions", resourceCulture); 177 | } 178 | } 179 | 180 | /// 181 | /// Looks up a localized string similar to Commands. 182 | /// 183 | internal static string CommandMenuTitle { 184 | get { 185 | return ResourceManager.GetString("CommandMenuTitle", resourceCulture); 186 | } 187 | } 188 | 189 | /// 190 | /// Looks up a localized string similar to Command "{0}" not recognized. 191 | /// 192 | internal static string CommandNotRecognized { 193 | get { 194 | return ResourceManager.GetString("CommandNotRecognized", resourceCulture); 195 | } 196 | } 197 | 198 | /// 199 | /// Looks up a localized string similar to A connection request was made, but the requestor party is null. 200 | /// 201 | internal static string ConnectionRequestMadeButRequestorIsNull { 202 | get { 203 | return ResourceManager.GetString("ConnectionRequestMadeButRequestorIsNull", resourceCulture); 204 | } 205 | } 206 | 207 | /// 208 | /// Looks up a localized string similar to Sorry, you are not allowed to accept or reject requests. 209 | /// 210 | internal static string ConnectionRequestResponseNotAllowed { 211 | get { 212 | return ResourceManager.GetString("ConnectionRequestResponseNotAllowed", resourceCulture); 213 | } 214 | } 215 | 216 | /// 217 | /// Looks up a localized string similar to A connection request error occurred: {0}. 218 | /// 219 | internal static string ConnectionRequestResultErrorWithResult { 220 | get { 221 | return ResourceManager.GetString("ConnectionRequestResultErrorWithResult", resourceCulture); 222 | } 223 | } 224 | 225 | /// 226 | /// Looks up a localized string similar to Assistance request. 227 | /// 228 | internal static string ConnectionRequestTitle { 229 | get { 230 | return ResourceManager.GetString("ConnectionRequestTitle", resourceCulture); 231 | } 232 | } 233 | 234 | /// 235 | /// Looks up a localized string similar to A connection error occurred: {0}. 236 | /// 237 | internal static string ConnectionResultErrorWithResult { 238 | get { 239 | return ResourceManager.GetString("ConnectionResultErrorWithResult", resourceCulture); 240 | } 241 | } 242 | 243 | /// 244 | /// Looks up a localized string similar to To initiate a conversation with an agent, type "{0}". 245 | /// 246 | internal static string ConnectRequestCommandHint { 247 | get { 248 | return ResourceManager.GetString("ConnectRequestCommandHint", resourceCulture); 249 | } 250 | } 251 | 252 | /// 253 | /// Looks up a localized string similar to Deleting all data.... 254 | /// 255 | internal static string DeletingAllData { 256 | get { 257 | return ResourceManager.GetString("DeletingAllData", resourceCulture); 258 | } 259 | } 260 | 261 | /// 262 | /// Looks up a localized string similar to Deleting all data as requested by "0".... 263 | /// 264 | internal static string DeletingAllDataWithCommandIssuer { 265 | get { 266 | return ResourceManager.GetString("DeletingAllDataWithCommandIssuer", resourceCulture); 267 | } 268 | } 269 | 270 | /// 271 | /// Looks up a localized string similar to You said: {0}. 272 | /// 273 | internal static string EchoMessage { 274 | get { 275 | return ResourceManager.GetString("EchoMessage", resourceCulture); 276 | } 277 | } 278 | 279 | /// 280 | /// Looks up a localized string similar to An error occurred. 281 | /// 282 | internal static string ErrorOccured { 283 | get { 284 | return ResourceManager.GetString("ErrorOccured", resourceCulture); 285 | } 286 | } 287 | 288 | /// 289 | /// Looks up a localized string similar to An error occurred: {0}. 290 | /// 291 | internal static string ErrorOccuredWithResult { 292 | get { 293 | return ResourceManager.GetString("ErrorOccuredWithResult", resourceCulture); 294 | } 295 | } 296 | 297 | /// 298 | /// Looks up a localized string similar to Failed to find the bot instance on aggregation channel "{0}". 299 | /// 300 | internal static string FailedToFindBotOnAggregationChannel { 301 | get { 302 | return ResourceManager.GetString("FailedToFindBotOnAggregationChannel", resourceCulture); 303 | } 304 | } 305 | 306 | /// 307 | /// Looks up a localized string similar to Failed to find the pending request. 308 | /// 309 | internal static string FailedToFindPendingRequest { 310 | get { 311 | return ResourceManager.GetString("FailedToFindPendingRequest", resourceCulture); 312 | } 313 | } 314 | 315 | /// 316 | /// Looks up a localized string similar to Failed to find a pending request for user "0": {1}. 317 | /// 318 | internal static string FailedToFindPendingRequestForUserWithErrorMessage { 319 | get { 320 | return ResourceManager.GetString("FailedToFindPendingRequestForUserWithErrorMessage", resourceCulture); 321 | } 322 | } 323 | 324 | /// 325 | /// Looks up a localized string similar to Failed to forward the message. 326 | /// 327 | internal static string FailedToForwardMessage { 328 | get { 329 | return ResourceManager.GetString("FailedToForwardMessage", resourceCulture); 330 | } 331 | } 332 | 333 | /// 334 | /// Looks up a localized string similar to Failed to reject pending requests. 335 | /// 336 | internal static string FailedToRejectPendingRequests { 337 | get { 338 | return ResourceManager.GetString("FailedToRejectPendingRequests", resourceCulture); 339 | } 340 | } 341 | 342 | /// 343 | /// Looks up a localized string similar to Failed to remove aggregation channel. 344 | /// 345 | internal static string FailedToRemoveAggregationChannel { 346 | get { 347 | return ResourceManager.GetString("FailedToRemoveAggregationChannel", resourceCulture); 348 | } 349 | } 350 | 351 | /// 352 | /// Looks up a localized string similar to Failed to set the aggregation channel: {0}. 353 | /// 354 | internal static string FailedToSetAggregationChannel { 355 | get { 356 | return ResourceManager.GetString("FailedToSetAggregationChannel", resourceCulture); 357 | } 358 | } 359 | 360 | /// 361 | /// Looks up a localized string similar to Invalid command. 362 | /// 363 | internal static string InvalidCommand { 364 | get { 365 | return ResourceManager.GetString("InvalidCommand", resourceCulture); 366 | } 367 | } 368 | 369 | /// 370 | /// Looks up a localized string similar to Command parameter invalid or missing. 371 | /// 372 | internal static string InvalidOrMissingCommandParameter { 373 | get { 374 | return ResourceManager.GetString("InvalidOrMissingCommandParameter", resourceCulture); 375 | } 376 | } 377 | 378 | /// 379 | /// Looks up a localized string similar to Sorry, there are no agents available right now. 380 | /// 381 | internal static string NoAgentsAvailable { 382 | get { 383 | return ResourceManager.GetString("NoAgentsAvailable", resourceCulture); 384 | } 385 | } 386 | 387 | /// 388 | /// Looks up a localized string similar to No aggregation channel set up. 389 | /// 390 | internal static string NoAggregationChannel { 391 | get { 392 | return ResourceManager.GetString("NoAggregationChannel", resourceCulture); 393 | } 394 | } 395 | 396 | /// 397 | /// Looks up a localized string similar to No bots stored. 398 | /// 399 | internal static string NoBotsStored { 400 | get { 401 | return ResourceManager.GetString("NoBotsStored", resourceCulture); 402 | } 403 | } 404 | 405 | /// 406 | /// Looks up a localized string similar to No conversations. 407 | /// 408 | internal static string NoConversations { 409 | get { 410 | return ResourceManager.GetString("NoConversations", resourceCulture); 411 | } 412 | } 413 | 414 | /// 415 | /// Looks up a localized string similar to No pending requests. 416 | /// 417 | internal static string NoPendingRequests { 418 | get { 419 | return ResourceManager.GetString("NoPendingRequests", resourceCulture); 420 | } 421 | } 422 | 423 | /// 424 | /// Looks up a localized string similar to No results. 425 | /// 426 | internal static string NoResults { 427 | get { 428 | return ResourceManager.GetString("NoResults", resourceCulture); 429 | } 430 | } 431 | 432 | /// 433 | /// Looks up a localized string similar to Your request was accepted and you are now chatting with {0}. 434 | /// 435 | internal static string NotifyClientConnected { 436 | get { 437 | return ResourceManager.GetString("NotifyClientConnected", resourceCulture); 438 | } 439 | } 440 | 441 | /// 442 | /// Looks up a localized string similar to Your conversation with {0} has ended. 443 | /// 444 | internal static string NotifyClientDisconnected { 445 | get { 446 | return ResourceManager.GetString("NotifyClientDisconnected", resourceCulture); 447 | } 448 | } 449 | 450 | /// 451 | /// Looks up a localized string similar to Your request has already been received, please wait for an agent to respond. 452 | /// 453 | internal static string NotifyClientDuplicateRequest { 454 | get { 455 | return ResourceManager.GetString("NotifyClientDuplicateRequest", resourceCulture); 456 | } 457 | } 458 | 459 | /// 460 | /// Looks up a localized string similar to Unfortunately your request could not be processed. 461 | /// 462 | internal static string NotifyClientRequestRejected { 463 | get { 464 | return ResourceManager.GetString("NotifyClientRequestRejected", resourceCulture); 465 | } 466 | } 467 | 468 | /// 469 | /// Looks up a localized string similar to Please wait for your request to be accepted. 470 | /// 471 | internal static string NotifyClientWaitForRequestHandling { 472 | get { 473 | return ResourceManager.GetString("NotifyClientWaitForRequestHandling", resourceCulture); 474 | } 475 | } 476 | 477 | /// 478 | /// Looks up a localized string similar to You are now connected to user "{0}". 479 | /// 480 | internal static string NotifyOwnerConnected { 481 | get { 482 | return ResourceManager.GetString("NotifyOwnerConnected", resourceCulture); 483 | } 484 | } 485 | 486 | /// 487 | /// Looks up a localized string similar to You are now disconnected from the conversation with user "{0}". 488 | /// 489 | internal static string NotifyOwnerDisconnected { 490 | get { 491 | return ResourceManager.GetString("NotifyOwnerDisconnected", resourceCulture); 492 | } 493 | } 494 | 495 | /// 496 | /// Looks up a localized string similar to Request from user "{0}" rejected. 497 | /// 498 | internal static string NotifyOwnerRequestRejected { 499 | get { 500 | return ResourceManager.GetString("NotifyOwnerRequestRejected", resourceCulture); 501 | } 502 | } 503 | 504 | /// 505 | /// Looks up a localized string similar to {0} is not a permitted aggregation channel. 506 | /// 507 | internal static string NotPermittedAggregationChannel { 508 | get { 509 | return ResourceManager.GetString("NotPermittedAggregationChannel", resourceCulture); 510 | } 511 | } 512 | 513 | /// 514 | /// Looks up a localized string similar to No users stored. 515 | /// 516 | internal static string NoUsersStored { 517 | get { 518 | return ResourceManager.GetString("NoUsersStored", resourceCulture); 519 | } 520 | } 521 | 522 | /// 523 | /// Looks up a localized string similar to To see the available commands, type "{0}". 524 | /// 525 | internal static string OptionsCommandHint { 526 | get { 527 | return ResourceManager.GetString("OptionsCommandHint", resourceCulture); 528 | } 529 | } 530 | 531 | /// 532 | /// Looks up a localized string similar to Party "{0}" removed. 533 | /// 534 | internal static string PartyRemoved { 535 | get { 536 | return ResourceManager.GetString("PartyRemoved", resourceCulture); 537 | } 538 | } 539 | 540 | /// 541 | /// Looks up a localized string similar to {0} pending request(s) found. 542 | /// 543 | internal static string PendingRequestsFoundWithCount { 544 | get { 545 | return ResourceManager.GetString("PendingRequestsFoundWithCount", resourceCulture); 546 | } 547 | } 548 | 549 | /// 550 | /// Looks up a localized string similar to Reject all. 551 | /// 552 | internal static string RejectAll { 553 | get { 554 | return ResourceManager.GetString("RejectAll", resourceCulture); 555 | } 556 | } 557 | 558 | /// 559 | /// Looks up a localized string similar to Reject. 560 | /// 561 | internal static string RejectButtonTitle { 562 | get { 563 | return ResourceManager.GetString("RejectButtonTitle", resourceCulture); 564 | } 565 | } 566 | 567 | /// 568 | /// Looks up a localized string similar to Reject request. 569 | /// 570 | internal static string RejectConnectionRequestCardTitle { 571 | get { 572 | return ResourceManager.GetString("RejectConnectionRequestCardTitle", resourceCulture); 573 | } 574 | } 575 | 576 | /// 577 | /// Looks up a localized string similar to Choose the user whose request to reject. 578 | /// 579 | internal static string RejectConnectionRequestsCardInstructions { 580 | get { 581 | return ResourceManager.GetString("RejectConnectionRequestsCardInstructions", resourceCulture); 582 | } 583 | } 584 | 585 | /// 586 | /// Looks up a localized string similar to "{0}" on channel "{1}" ({2}). 587 | /// 588 | internal static string RequestorDetailsItem { 589 | get { 590 | return ResourceManager.GetString("RequestorDetailsItem", resourceCulture); 591 | } 592 | } 593 | 594 | /// 595 | /// Looks up a localized string similar to Request was made, but the details of the requestor are missing. 596 | /// 597 | internal static string RequestorDetailsMissing { 598 | get { 599 | return ResourceManager.GetString("RequestorDetailsMissing", resourceCulture); 600 | } 601 | } 602 | 603 | /// 604 | /// Looks up a localized string similar to Requested by user "{0}" on channel "{1}". 605 | /// 606 | internal static string RequestorDetailsTitle { 607 | get { 608 | return ResourceManager.GetString("RequestorDetailsTitle", resourceCulture); 609 | } 610 | } 611 | 612 | /// 613 | /// Looks up a localized string similar to Data of user "{0}" deleted. 614 | /// 615 | internal static string UserDataDeleted { 616 | get { 617 | return ResourceManager.GetString("UserDataDeleted", resourceCulture); 618 | } 619 | } 620 | 621 | /// 622 | /// Looks up a localized string similar to User name missing. 623 | /// 624 | internal static string UserNameMissing { 625 | get { 626 | return ResourceManager.GetString("UserNameMissing", resourceCulture); 627 | } 628 | } 629 | 630 | /// 631 | /// Looks up a localized string similar to Users. 632 | /// 633 | internal static string Users { 634 | get { 635 | return ResourceManager.GetString("Users", resourceCulture); 636 | } 637 | } 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Resources/Strings.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Accept 122 | 123 | 124 | Accept or reject the request: Type "{0}" to accept, "{1}" to reject or use the buttons if enabled 125 | 126 | 127 | To set up an aggregation channel, type "{0}" 128 | 129 | 130 | This channel/conversation is already receiving requests 131 | 132 | 133 | This channel/conversation will no longer receive requests 134 | 135 | 136 | This channel/conversation is now where the requests are aggregated 137 | 138 | 139 | You are already connected with user "{0}" 140 | 141 | 142 | Administrative options for controlling the end user bot conversations 143 | 144 | 145 | Select the command using the buttons below or type the command keyword ("{0}")/bot handle ("@{1}") followed by the command and its parameters (if any), for example: "{2}". 146 | 147 | 148 | Commands 149 | 150 | 151 | Command "{0}" not recognized 152 | 153 | 154 | A connection request was made, but the requestor party is null 155 | 156 | 157 | Sorry, you are not allowed to accept or reject requests 158 | 159 | 160 | Assistance request 161 | 162 | 163 | To initiate a conversation with an agent, type "{0}" 164 | 165 | 166 | Deleting all data... 167 | 168 | 169 | Deleting all data as requested by "0"... 170 | 171 | 172 | You said: {0} 173 | 174 | 175 | An error occurred 176 | 177 | 178 | An error occurred: {0} 179 | 180 | 181 | Failed to find the bot instance on aggregation channel "{0}" 182 | 183 | 184 | Failed to find a pending request for user "0": {1} 185 | 186 | 187 | Failed to forward the message 188 | 189 | 190 | Failed to remove aggregation channel 191 | 192 | 193 | Sorry, there are no agents available right now 194 | 195 | 196 | No aggregation channel set up 197 | 198 | 199 | No pending requests 200 | 201 | 202 | Your request was accepted and you are now chatting with {0} 203 | 204 | 205 | Your conversation with {0} has ended 206 | 207 | 208 | Your request has already been received, please wait for an agent to respond 209 | 210 | 211 | Unfortunately your request could not be processed 212 | 213 | 214 | Please wait for your request to be accepted 215 | 216 | 217 | You are now connected to user "{0}" 218 | 219 | 220 | You are now disconnected from the conversation with user "{0}" 221 | 222 | 223 | Request from user "{0}" rejected 224 | 225 | 226 | To see the available commands, type "{0}" 227 | 228 | 229 | Party "{0}" removed 230 | 231 | 232 | Reject 233 | 234 | 235 | Requested by user "{0}" on channel "{1}" 236 | 237 | 238 | Data of user "{0}" deleted 239 | 240 | 241 | User name missing 242 | 243 | 244 | Choose the user whose request to accept 245 | 246 | 247 | Accept request 248 | 249 | 250 | Apply to all 251 | 252 | 253 | Bots 254 | 255 | 256 | Failed to find the pending request 257 | 258 | 259 | Failed to reject pending requests 260 | 261 | 262 | Invalid command 263 | 264 | 265 | Command parameter invalid or missing 266 | 267 | 268 | No bots stored 269 | 270 | 271 | No conversations 272 | 273 | 274 | No results 275 | 276 | 277 | No users stored 278 | 279 | 280 | {0} pending request(s) found 281 | 282 | 283 | Reject all 284 | 285 | 286 | Reject request 287 | 288 | 289 | Choose the user whose request to reject 290 | 291 | 292 | "{0}" on channel "{1}" ({2}) 293 | 294 | 295 | Users 296 | 297 | 298 | Request was made, but the details of the requestor are missing 299 | 300 | 301 | A connection request error occurred: {0} 302 | 303 | 304 | A connection error occurred: {0} 305 | 306 | 307 | Failed to set the aggregation channel: {0} 308 | 309 | 310 | {0} is not a permitted aggregation channel 311 | 312 | -------------------------------------------------------------------------------- /IntermediatorBotSample/Startup.cs: -------------------------------------------------------------------------------- 1 | using IntermediatorBotSample.Bot; 2 | using IntermediatorBotSample.Middleware; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Bot.Builder; 6 | using Microsoft.Bot.Builder.Core.Extensions; 7 | using Microsoft.Bot.Builder.BotFramework; 8 | using Microsoft.Bot.Builder.Integration; 9 | using Microsoft.Bot.Builder.Integration.AspNet.Core; 10 | using Microsoft.Bot.Builder.TraceExtensions; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using System; 14 | 15 | namespace IntermediatorBotSample 16 | { 17 | public class Startup 18 | { 19 | public IConfiguration Configuration 20 | { 21 | get; 22 | } 23 | 24 | public Startup(IHostingEnvironment env) 25 | { 26 | var builder = new ConfigurationBuilder() 27 | .SetBasePath(env.ContentRootPath) 28 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 29 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 30 | .AddEnvironmentVariables(); 31 | 32 | Configuration = builder.Build(); 33 | } 34 | 35 | /// 36 | /// This method gets called by the runtime. Use this method to add services to the container. 37 | /// 38 | /// 39 | public void ConfigureServices(IServiceCollection services) 40 | { 41 | services.AddMvc().AddControllersAsServices(); 42 | services.AddSingleton(_ => Configuration); 43 | 44 | services.AddBot(options => 45 | { 46 | options.CredentialProvider = new ConfigurationCredentialProvider(Configuration); 47 | 48 | // The CatchExceptionMiddleware provides a top-level exception handler for your bot. 49 | // Any exceptions thrown by other Middleware, or by your OnTurn method, will be 50 | // caught here. To facillitate debugging, the exception is sent out, via Trace, 51 | // to the emulator. Trace activities are NOT displayed to users, so in addition 52 | // an "Ooops" message is sent. 53 | options.Middleware.Add(new CatchExceptionMiddleware(async (context, exception) => 54 | { 55 | await context.TraceActivityAsync("Bot Exception", exception); 56 | await context.SendActivityAsync($"Sorry, it looks like something went wrong: {exception.Message}"); 57 | })); 58 | 59 | // The Memory Storage used here is for local bot debugging only. When the bot 60 | // is restarted, anything stored in memory will be gone. 61 | IStorage dataStore = new MemoryStorage(); 62 | 63 | // The File data store, shown here, is suitable for bots that run on 64 | // a single machine and need durable state across application restarts. 65 | // IStorage dataStore = new FileStorage(System.IO.Path.GetTempPath()); 66 | 67 | // For production bots use the Azure Table Store, Azure Blob, or 68 | // Azure CosmosDB storage provides, as seen below. To include any of 69 | // the Azure based storage providers, add the Microsoft.Bot.Builder.Azure 70 | // Nuget package to your solution. That package is found at: 71 | // https://www.nuget.org/packages/Microsoft.Bot.Builder.Azure/ 72 | 73 | // IStorage dataStore = new Microsoft.Bot.Builder.Azure.AzureTableStorage("AzureTablesConnectionString", "TableName"); 74 | // IStorage dataStore = new Microsoft.Bot.Builder.Azure.AzureBlobStorage("AzureBlobConnectionString", "containerName"); 75 | 76 | // Handoff middleware 77 | options.Middleware.Add(new HandoffMiddleware(Configuration)); 78 | }); 79 | 80 | services.AddMvc(); // Required Razor pages 81 | } 82 | 83 | /// 84 | /// This method gets called by the runtime. 85 | /// Use this method to configure the HTTP request pipeline. 86 | /// 87 | /// 88 | /// 89 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 90 | { 91 | if (env.IsDevelopment()) 92 | { 93 | app.UseDeveloperExceptionPage(); 94 | } 95 | 96 | app.UseDefaultFiles() 97 | .UseStaticFiles() 98 | .UseMvc() // Required Razor pages 99 | .UseBotFramework(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /IntermediatorBotSample/TeamsManifest/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/IntermediatorBotSample/TeamsManifest/color.png -------------------------------------------------------------------------------- /IntermediatorBotSample/TeamsManifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://statics.teams.microsoft.com/sdk/v1.2/manifest/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.2", 4 | "version": "0.0.1", 5 | "id": "INSERT YOUR TEAMS APP ID HERE", 6 | "packageName": "com.underscore.intermediator.bot.sample", 7 | "developer": { 8 | "name": "Tomi", 9 | "websiteUrl": "https://github.com/tompaana/intermediator-bot-sample", 10 | "privacyUrl": "https://github.com/tompaana/intermediator-bot-sample/README.md", 11 | "termsOfUseUrl": "https://github.com/tompaana/intermediator-bot-sample/LICENSE" 12 | }, 13 | "icons": { 14 | "color": "color.png", 15 | "outline": "outline.png" 16 | }, 17 | "name": { 18 | "short": "Intermediator Bot", 19 | "full": "Intermediator Bot Sample" 20 | }, 21 | "description": { 22 | "short": "Intermediator Bot Sample", 23 | "full": "A sample bot, that routes messages between two users on different channels." 24 | }, 25 | "accentColor": "#00FF00", 26 | "configurableTabs": [ 27 | ], 28 | "staticTabs": [ 29 | ], 30 | "bots": [ 31 | { 32 | "botId": "INSERT YOUR BOT'S MICROSOFT APP ID HERE", 33 | "scopes": [ 34 | "personal", 35 | "team" 36 | ] 37 | } 38 | ], 39 | "permissions": [ 40 | "identity", 41 | "messageTeamMembers" 42 | ], 43 | "validDomains": [ 44 | "*.azurewebsites.net" 45 | ] 46 | } -------------------------------------------------------------------------------- /IntermediatorBotSample/TeamsManifest/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/intermediator-bot-sample/d6448978d2f8991b6c26d3a178a1fca29ea42ad5/IntermediatorBotSample/TeamsManifest/outline.png -------------------------------------------------------------------------------- /IntermediatorBotSample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MicrosoftAppId": "", 3 | "MicrosoftAppPassword": "", 4 | "BotBasePath": "/api", 5 | "BotMessagesPath": "/messages", 6 | "AzureTableStorageConnectionString": "", 7 | "RejectConnectionRequestIfNoAggregationChannel": true, 8 | "PermittedAggregationChannels": "", 9 | "NoDirectConversationsWithChannels": "emulator, facebook, skype, msteams, webchat" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2019 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intermediator Bot Sample # 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/i1u8puahyxl79ha6?svg=true)](https://ci.appveyor.com/project/tompaana/intermediator-bot-sample) 4 | 5 | This is a sample bot, built with the [Microsoft Bot Framework](https://dev.botframework.com/) (v4), 6 | that routes messages between two users on different channels. This sample utilizes the 7 | [Bot Message Routing (component) project](https://github.com/tompaana/bot-message-routing). 8 | The general gist of the message routing is explained in this article: 9 | [Chatbots as Middlemen blog post](https://tompaana.github.io/content/chatbots_as_middlemen.html). 10 | 11 | A possible use case for this type of a bot would be a customer service scenario where the bot relays 12 | the messages between a customer and a customer service agent. 13 | 14 | This is a C# sample targeting the latest version (v4) of the Microsoft Bot Framework. The sample 15 | did previously target the v3.x and you can find that last release 16 | [here](https://github.com/tompaana/intermediator-bot-sample/releases/tag/v1.1). 17 | 18 | If you prefer **Node.js**, fear not, there are these two great samples to look into: 19 | 20 | * [botframework-v4-handoff](https://github.com/GeekTrainer/botframework-v4-handoff) 21 | * [Bot-HandOff (v3)](https://github.com/palindromed/Bot-HandOff) 22 | 23 | #### Contents #### 24 | 25 | * [Getting started](#getting-started) 26 | * [Deploying the bot](#deploying-the-bot) 27 | * [App settings and credentials](#app-settings-and-credentials) 28 | * [Testing the hand-off](#testing-the-hand-off) 29 | * [About the implementation](#implementation) 30 | * [Custom agent portal](#what-if-i-want-to-have-a-custom-agent-portalchannel) 31 | * [Helpful links](#see-also) 32 | 33 | ## Getting started ## 34 | 35 | Since this is an advanced bot scenario, the prerequisites include that you are familiar with the 36 | basic concepts of the Microsoft Bot Framework and you know the C# programming language. Before 37 | getting started it is recommended that you have the following tools installed: 38 | 39 | * [Visual Studio IDE](https://www.visualstudio.com/vs/) 40 | * [ngrok](https://ngrok.com/) 41 | * [Bot Framework Emulator](https://github.com/Microsoft/BotFramework-Emulator) ([download](https://github.com/Microsoft/BotFramework-Emulator/releases)) 42 | * [Debug bots with the Bot Framework Emulator](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-debug-emulator?view=azure-bot-service-4.0) 43 | 44 | Altough the bot can be practically hosted anywhere, the deployment instructions (below) are for 45 | Azure. If you don't have an Azure subscription yet, you can get one for free here: 46 | [Create your Azure free account today](https://azure.microsoft.com/en-us/free/). 47 | 48 | ## Deploying the bot ## 49 | 50 | This sample demonstrates routing messages between different users on different channels. Hence, 51 | using only the emulator to test the sample may prove difficult. To utilize other channels, you must 52 | first compile and publish the bot: 53 | 54 | 1. Open the solution (`IntermediatorBotSample.sln`) in Visual Studio/your IDE and make sure it 55 | compiles without any errors (or warnings) 56 | 2. Follow the steps in this article carefully: 57 | [Deploy your bot to Azure](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0) 58 | * Top tip: Create a new [Azure resource group](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-overview#resource-groups) 59 | for the app so that if stuff goes wrong, it's really easy to just delete the whole group and 60 | start over 61 | * Having issues testing the bot (as in "The dang thing doesn't work!!") - check the following: 62 | * Did you remember to include `/api/messages` in the messaging endpoint 63 | (Bot Channels Registration/Settings)? 64 | * Did you remember to create and add the credentials (`MicrosoftAppId` and `MicrosoftAppPassword`)? 65 | 3. Add the credentials (`MicrosoftAppId` and `MicrosoftAppPassword`) to the 66 | [`appsettings.json` file](/IntermediatorBotSample/appsettings.json) and republish the bot - now 67 | all you need to do to republish is to right-click the app project in the **Solution Explorer** in 68 | Visual Studio, select **Publish...** and click the **Publish** button on the tab (named in the 69 | sample "IntermediatorBotSample"). 70 | 71 | ## App settings and credentials ## 72 | 73 | App settings and credentials are available in the 74 | [`appsettings.json`](/IntermediatorBotSample/appsettings.json) 75 | file of this sample. The file contains both bot and storage credentials as well as the settings that 76 | can be used to tailor the experience. The default content of the file looks something like this: 77 | 78 | ```json 79 | { 80 | "MicrosoftAppId": "", 81 | "MicrosoftAppPassword": "", 82 | "BotBasePath": "/api", 83 | "BotMessagesPath": "/messages", 84 | "AzureTableStorageConnectionString": "", 85 | "RejectConnectionRequestIfNoAggregationChannel": true, 86 | "PermittedAggregationChannels": "", 87 | "NoDirectConversationsWithChannels": "emulator, facebook, skype, msteams, webchat" 88 | } 89 | ``` 90 | 91 | ### Settings ### 92 | 93 | * `BotBasePath` and `BotMessagesPath` can be used to define the messaging endpoint of the bot. The 94 | endpoint by default is `http(s):///api/messages`. 95 | * `RejectConnectionRequestIfNoAggregationChannel` defines whether to reject connection requests 96 | automatically if no aggregation channel is set or not. Pretty straightforward, don't you think? 97 | * `PermittedAggregationChannels` can be used to rule out certain channels as aggregation channels. 98 | If, for instance, the list contains `facebook`, the bot will refuse to set any Facebook 99 | conversation as an aggregation channel. 100 | * The solution allows the bot to try and create direct conversations with the accepted "customers". 101 | `NoDirectConversationsWithChannels` defines the channels where the bot should not try to do this. 102 | 103 | ### Credentials ### 104 | 105 | * `MicrosoftAppId` and `MicrosoftAppPassword` should contain the bot's credentials, which you 106 | acquire from the [Azure portal](https://portal.azure.com) when you publish the bot. 107 | * The bot needs to have a centralized storage for routing data. When you insert a valid Azure Table 108 | Storage connection string as the value of the `AzureTableStorageConnectionString` property, the 109 | storage automatically taken in use. 110 | 111 | ## Testing the hand-off ## 112 | 113 | This scenario utilizes an aggregation concept (see the terminology table in this document). One or 114 | more channels act as aggregated channels where the customer requests (for human assistance) are 115 | sent. The conversation owners (e.g. customer service agents) then accept or reject the requests. 116 | 117 | Once you have published the bot, go to the channel you want to receive the requests and issue the 118 | following command to the bot (given that you haven't changed the default bot command handler or the 119 | command itself): 120 | 121 | ``` 122 | @ watch 123 | ``` 124 | 125 | In case mentions are not supported, you can also use the command keyword: 126 | 127 | ``` 128 | command watch 129 | ``` 130 | 131 | Now all the requests from another channels are forwarded to this channel. 132 | See the default flow below: 133 | 134 | | Teams | Slack | 135 | | ----- | ----- | 136 | | ![Setting the aggregation channel](Documentation/Screenshots/msteams-1-watch.png?raw=true) | | 137 | | | ![Connection request sent](/Documentation/Screenshots/slack-1-connection-request.png?raw=true) | | 138 | | ![Connection request accepted](/Documentation/Screenshots/msteams-2-accept-connection-request.png?raw=true) | | 139 | | ![Conversation in Teams](/Documentation/Screenshots/msteams-3-conversation.png?raw=true) | ![Conversation in Slack](/Documentation/Screenshots/slack-2-conversation.png?raw=true) | 140 | 141 | ### Commands ### 142 | 143 | The bot comes with a simple command handling mechanism, which supports the commands in the table 144 | below. 145 | 146 | | Command | Description | 147 | | ------- | ----------- | 148 | | `showOptions` | Displays the command options as a card with buttons (convenient!) | 149 | | `Watch` | Marks the current channel as **aggregation** channel (where requests are sent). | 150 | | `Unwatch` | Removes the current channel from the list of aggregation channels. | 151 | | `GetRequests` | Lists all pending connection requests. | 152 | | `AcceptRequest ` | Accepts the conversation connection request of the given user. If no user ID is entered, the bot will render a nice card with accept/reject buttons given that pending connection requests exist. | 153 | | `RejectRequest ` | Rejects the conversation connection request of the given user. If no user ID is entered, the bot will render a nice card with accept/reject buttons given that pending connection requests exist. | 154 | | `Disconnect` | Ends the current conversation with a user. | 155 | 156 | To issue a command use the bot name: 157 | 158 | ``` 159 | @ 160 | ``` 161 | 162 | In case mentions are not supported, you can also use the command keyword: 163 | 164 | ``` 165 | command 166 | ``` 167 | 168 | Although not an actual command, typing `human` will initiate a connection request, which an agent 169 | can then reject or accept. 170 | 171 | ## Implementation ## 172 | 173 | The core message routing functionality comes from the 174 | [Bot Message Routing (component)](https://github.com/tompaana/bot-message-routing) project. 175 | This sample demonstrates how to use the component and provides the necessary "plumbing" such as 176 | command handling. Here are the main classes of the sample: 177 | 178 | * **[HandoffMiddleware](/IntermediatorBotSample/Middleware/HandoffMiddleware.cs)**: Contains all the 179 | components (class instances) required by the hand-off and implements the main logic flow. This 180 | middleware class will check every incoming message for hand-off related actions. 181 | * **[CommandHandler](/IntermediatorBotSample/CommandHandling/CommandHandler.cs)**: 182 | Provides implementation for checking and acting on commands in messages before they are passed to 183 | a dialog etc. 184 | * **[MessageRouterResultHandler](/IntermediatorBotSample/MessageRouting/MessageRouterResultHandler.cs)**: 185 | Handles the results of the operations executed by the **`MessageRouter`** of the Bot Message 186 | Routing component. 187 | * **[ConnectionRequestHandler](/IntermediatorBotSample/MessageRouting/ConnectionRequestHandler.cs)**: 188 | Implements the main logic for accepting or rejecting connection requests. 189 | 190 | ## What if I want to have a custom agent portal/channel? ## 191 | 192 | Well, right now you have to implement it. There are couple of different ways to go about it. It's 193 | hard to say which one is the best, but if I were to do it, I'd propably start by... 194 | 195 | 1. ...implementing a REST API endpoint 196 | (see for instance [Create a Web API with ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-2.1)). 197 | Then I'd hook the REST API into the message routing code and finally removed the text based 198 | command handling altogether. 199 | 2. Another options is to use back channel messages and hook them up into the current command 200 | pipeline. Granted some changes would need to be made to separate the direct commands from the 201 | back channel ones. Also, the response would likely need to be (or recommended to be) in JSON. 202 | 3. Something else - remember, it's just a web app! 203 | 204 | ## See also ## 205 | 206 | * [Bot Message Routing (component) project](https://github.com/tompaana/bot-message-routing) 207 | * [NuGet package](https://www.nuget.org/packages/BotMessageRouting) 208 | * [Chatbots as Middlemen blog post](https://tompaana.github.io/content/chatbots_as_middlemen.html) 209 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.9.{build} 2 | image: Visual Studio 2017 3 | configuration: Release 4 | platform: Any CPU 5 | before_build: 6 | - cmd: dotnet --version 7 | - cmd: nuget restore 8 | build: 9 | verbosity: minimal 10 | test: 11 | assemblies: 12 | only: 13 | - '**\*.Tests.dll' 14 | --------------------------------------------------------------------------------