microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/core

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Bot.Core/BotApplication.cs

170lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Diagnostics;
5using Microsoft.AspNetCore.Http;
6using Microsoft.Extensions.Logging;
7using Microsoft.Extensions.Logging.Abstractions;
8using Microsoft.Teams.Bot.Core.Hosting;
9using Microsoft.Teams.Bot.Core.Schema;
10
11namespace Microsoft.Teams.Bot.Core;
12
13/// <summary>
14/// Represents a bot application.
15/// </summary>
16public class BotApplication
17{
18 private readonly ILogger<BotApplication> _logger;
19 private readonly ConversationClient? _conversationClient;
20 private readonly UserTokenClient? _userTokenClient;
21 private readonly TimeSpan _processActivityTimeout = TimeSpan.FromMinutes(5);
22 internal TurnMiddleware MiddleWare { get; }
23
24 /// <summary>
25 /// Creates a default instance, primarily for testing purposes. The ConversationClient and UserTokenClient properties will not be initialized
26 /// </summary>
27 protected BotApplication()
28 {
29 _logger = NullLogger<BotApplication>.Instance;
30 AppId = string.Empty;
31 MiddleWare = new TurnMiddleware();
32 }
33
34 /// <summary>
35 /// Initializes a new instance of the BotApplication class with the specified conversation client, app ID,
36 /// and logger.
37 /// Initializes a new instance of the BotApplication class with the specified conversation client, app ID,
38 /// and logger.
39 /// </summary>
40 /// <param name="conversationClient">The client used to manage and interact with conversations for the bot.</param>
41 /// <param name="userTokenClient">The client used to manage user tokens for authentication.</param>
42 /// <param name="logger">The logger used to record operational and diagnostic information for the bot application.</param>
43 /// <param name="options">Options containing the application (client) ID, used for logging and diagnostics. Defaults to an empty instance if not provided.</param>
44 public BotApplication(ConversationClient conversationClient, UserTokenClient userTokenClient, ILogger<BotApplication> logger, BotApplicationOptions? options = null)
45 {
46 options ??= new();
47 _logger = logger;
48 AppId = options.AppId;
49 MiddleWare = new TurnMiddleware();
50 MiddleWare.SetLogger(logger);
51 _conversationClient = conversationClient;
52 _userTokenClient = userTokenClient;
53 _processActivityTimeout = options.ProcessActivityTimeout;
54 logger.LogInformationGuarded("Started {ThisType} listener for AppID:{AppId} with SDK version {SdkVersion}", GetType().Name, options.AppId, Version);
55 }
56
57
58 /// <summary>
59 /// Gets the application (client) ID configured for this bot.
60 /// </summary>
61 public string AppId { get; }
62
63 /// <summary>
64 /// Gets the client used to manage and interact with conversations.
65 /// </summary>
66 /// <remarks>Accessing this property before the client is initialized will result in an exception. Ensure
67 /// that the client is properly configured before use.</remarks>
68 public ConversationClient ConversationClient => _conversationClient ?? throw new InvalidOperationException("ConversationClient not initialized");
69
70 /// <summary>
71 /// Gets the client used to manage user tokens for authentication.
72 /// </summary>
73 /// <remarks>Accessing this property before the client is initialized will result in an exception. Ensure
74 /// that the client is properly configured before use.</remarks>
75 public UserTokenClient UserTokenClient => _userTokenClient ?? throw new InvalidOperationException("UserTokenClient not registered");
76
77 /// <summary>
78 /// Gets or sets the delegate that is invoked to handle an incoming activity asynchronously.
79 /// </summary>
80 /// <remarks>Assign a delegate to process activities as they are received. The delegate should accept an
81 /// <see cref="CoreActivity"/> and a <see cref="CancellationToken"/>, and return a <see cref="Task"/> representing the
82 /// asynchronous operation. If <see langword="null"/>, incoming activities will not be handled.</remarks>
83 public virtual Func<CoreActivity, CancellationToken, Task>? OnActivity { get; set; }
84
85 /// <summary>
86 /// Processes an incoming HTTP request containing a bot activity.
87 /// </summary>
88 /// <param name="httpContext"></param>
89 /// <param name="cancellationToken"></param>
90 /// <returns></returns>
91 /// <exception cref="InvalidOperationException"></exception>
92 /// <exception cref="BotHandlerException"></exception>
93 public virtual async Task ProcessAsync(HttpContext httpContext, CancellationToken cancellationToken = default)
94 {
95 ArgumentNullException.ThrowIfNull(httpContext);
96 ArgumentNullException.ThrowIfNull(_conversationClient);
97
98 _logger.LogDebug("Start processing HTTP request for activity");
99
100 CoreActivity activity = await CoreActivity.FromJsonStreamAsync(httpContext.Request.Body, cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Invalid Activity");
101
102 _logger.LogInformationGuarded("Activity received: Type={Type} Id={Id} ServiceUrl={ServiceUrl} MSCV={MSCV}",
103 activity.Type,
104 activity.Id,
105 activity.ServiceUrl,
106 httpContext.Request.GetCorrelationVector());
107
108 _logger.LogTraceGuarded("Received activity: {Activity}", activity.ToJson());
109
110 // TODO: Replace with structured scope data, ensure it works with OpenTelemetry and other logging providers
111 using (_logger.BeginScope("ActivityType={ActivityType} ActivityId={ActivityId} ServiceUrl={ServiceUrl} MSCV={MSCV}",
112 activity.Type, activity.Id, activity.ServiceUrl, httpContext.Request.GetCorrelationVector()))
113 {
114 // Use a dedicated timeout instead of the HTTP request's cancellation token.
115 // The HTTP token fires when the client disconnects, which is expected for
116 // streaming handlers that outlive the original request.
117 using var cts = new CancellationTokenSource(_processActivityTimeout);
118 try
119 {
120 CancellationToken token = Debugger.IsAttached ? CancellationToken.None : cts.Token;
121 await MiddleWare.RunPipelineAsync(this, activity, this.OnActivity, 0, token).ConfigureAwait(false);
122 }
123 catch (OperationCanceledException) when (cts.IsCancellationRequested)
124 {
125 _logger.LogWarning("Activity processing timed out after {Timeout}: Id={Id}", _processActivityTimeout, activity.Id);
126 }
127 catch (Exception ex)
128 {
129 _logger.LogError(ex, "Error processing activity: Id={Id}", activity.Id);
130 throw new BotHandlerException("Error processing activity", ex, activity);
131 }
132 finally
133 {
134 _logger.LogInformationGuarded("Finished processing activity: Id={Id}", activity.Id);
135 }
136 }
137 }
138
139 /// <summary>
140 /// Adds the specified turn middleware to the middleware pipeline.
141 /// </summary>
142 /// <param name="middleware">The middleware component to add to the pipeline. Cannot be null.</param>
143 /// <returns>An ITurnMiddleWare instance representing the updated middleware pipeline.</returns>
144 public ITurnMiddleware UseMiddleware(ITurnMiddleware middleware)
145 {
146 ArgumentNullException.ThrowIfNull(middleware);
147 MiddleWare.Use(middleware);
148 return MiddleWare;
149 }
150
151 /// <summary>
152 /// Sends the specified activity to the conversation asynchronously.
153 /// </summary>
154 /// <param name="activity">The activity to send to the conversation. Cannot be null. Must have Conversation.Id set.</param>
155 /// <param name="cancellationToken">A cancellation token that can be used to cancel the send operation.</param>
156 /// <returns>A task that represents the asynchronous operation. The task result contains the identifier of the sent activity.</returns>
157 /// <exception cref="Exception">Thrown if the conversation client has not been initialized.</exception>
158 public async Task<SendActivityResponse?> SendActivityAsync(CoreActivity activity, CancellationToken cancellationToken = default)
159 {
160 ArgumentNullException.ThrowIfNull(activity);
161 ArgumentNullException.ThrowIfNull(_conversationClient, "ConversationClient not initialized");
162
163 return await _conversationClient.SendActivityAsync(activity, cancellationToken: cancellationToken).ConfigureAwait(false);
164 }
165
166 /// <summary>
167 /// Gets the version of the SDK.
168 /// </summary>
169 public static string Version => ThisAssembly.NuGetPackageVersion;
170}
171