microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
mehak/state

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Apps/Context.cs

340lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Diagnostics.CodeAnalysis;
5using Microsoft.Extensions.Logging;
6using Microsoft.Teams.Apps.Api.Clients;
7using Microsoft.Teams.Apps.OAuth;
8using Microsoft.Teams.Apps.Schema;
9using Microsoft.Teams.Apps.Schema.Entities;
10using Microsoft.Teams.Apps.State;
11using Microsoft.Teams.Core;
12
13namespace Microsoft.Teams.Apps;
14
15
16/// <summary>
17/// Context for a bot turn.
18/// </summary>
19/// <param name="botApplication">The bot application instance that owns this context.</param>
20/// <param name="activity">The incoming activity for this turn.</param>
21/// <param name="turnState">The turn state for this turn, or <see langword="null"/> when state is not enabled.</param>
22public class Context<TActivity>(TeamsBotApplication botApplication, TActivity activity, TurnState? turnState = null) where TActivity : TeamsActivity
23{
24 /// <summary>
25 /// Base bot application.
26 /// </summary>
27 public TeamsBotApplication TeamsBotApplication { get; } = botApplication;
28
29 /// <summary>
30 /// Current activity.
31 /// </summary>
32 public TActivity Activity { get; } = activity;
33
34 /// <summary>
35 /// Gets the application (client) ID configured for this bot.
36 /// </summary>
37 public string AppId => TeamsBotApplication.AppId;
38
39 /// <summary>
40 /// Gets the turn state for the current turn, or <see langword="null"/> if state is not enabled
41 /// (see <c>UseState</c>). Use <c>ctx.State?.Conversation</c> and <c>ctx.State?.User</c> to read and
42 /// write scoped values.
43 /// </summary>
44 public TurnState? State { get; } = turnState;
45
46 private ContextLogger? _log;
47
48 /// <summary>
49 /// Gets the logger for this context, providing <c>.Info()</c>, <c>.Error()</c>, <c>.Debug()</c>,
50 /// and <c>.Warn()</c> convenience methods that delegate to the underlying <see cref="ILogger"/>.
51 /// </summary>
52 public ContextLogger Log => _log ??= new ContextLogger(TeamsBotApplication.Logger);
53
54 private ApiClient? _api;
55
56 /// <summary>
57 /// Gets the <see cref="ApiClient"/> scoped to the current activity's service URL.
58 /// </summary>
59 public ApiClient Api => _api ??= TeamsBotApplication.Api.ForServiceUrl(
60 Activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."));
61
62 // ==================== Convenience Send/Reply/Typing ====================
63
64 /// <summary>
65 /// Sends a text message to the conversation.
66 /// </summary>
67 /// <param name="text">The text to send.</param>
68 /// <param name="cancellationToken">A cancellation token.</param>
69 /// <returns>The response from the send operation.</returns>
70 public Task<SendActivityResponse?> SendAsync(string text, CancellationToken cancellationToken = default)
71 => SendActivityAsync(text, cancellationToken);
72
73 /// <summary>
74 /// Sends an activity to the conversation.
75 /// </summary>
76 /// <param name="activity">The activity to send.</param>
77 /// <param name="cancellationToken">A cancellation token.</param>
78 /// <returns>The response from the send operation.</returns>
79 public Task<SendActivityResponse?> SendAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
80 => SendActivityAsync(activity, cancellationToken);
81
82 /// <summary>
83 /// Sends a text message as a threaded reply to the current activity. When the inbound activity
84 /// has an id, the response auto-quotes it (rendered as a quote bubble above the response in Teams);
85 /// otherwise sends without quoting.
86 /// </summary>
87 /// <param name="text">The text to send.</param>
88 /// <param name="cancellationToken">A cancellation token.</param>
89 /// <returns>The response from the send operation.</returns>
90 public Task<SendActivityResponse?> ReplyAsync(string text, CancellationToken cancellationToken = default)
91 => ReplyAsync(new MessageActivity(text), cancellationToken);
92
93 /// <summary>
94 /// Sends an activity to the conversation. When the inbound activity has an id, the response
95 /// auto-quotes it (rendered as a quote bubble above the response in Teams). Otherwise sends
96 /// without quoting. To send without quoting unconditionally, use <see cref="Send(TeamsActivity, CancellationToken)"/>.
97 /// </summary>
98 /// <param name="activity">The activity to send.</param>
99 /// <param name="cancellationToken">A cancellation token.</param>
100 /// <returns>The response from the send operation.</returns>
101 public Task<SendActivityResponse?> ReplyAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
102 {
103 ArgumentNullException.ThrowIfNull(activity);
104 if (!string.IsNullOrWhiteSpace(Activity.Id))
105 {
106 return Quote(Activity.Id, activity, cancellationToken);
107 }
108
109 return SendActivityAsync(activity, cancellationToken);
110 }
111
112 /// <summary>
113 /// Sends a typing indicator to the conversation.
114 /// </summary>
115 /// <param name="cancellationToken">A cancellation token.</param>
116 /// <returns>The response from the send operation.</returns>
117 public Task<SendActivityResponse?> TypingAsync(CancellationToken cancellationToken = default)
118 => SendTypingActivityAsync(cancellationToken);
119
120 /// <summary>
121 /// Send a message to the conversation with a quoted message reference prepended to the text.
122 /// Teams renders the quoted message as a preview bubble above the response text.
123 /// </summary>
124 /// <param name="messageId">The ID of the message to quote.</param>
125 /// <param name="text">The response text, appended to the quoted message placeholder.</param>
126 /// <param name="cancellationToken">Optional cancellation token.</param>
127 /// <returns>The response from sending the activity.</returns>
128 [Experimental("ExperimentalTeamsQuotedReplies")]
129 public Task<SendActivityResponse?> Quote(string messageId, string text, CancellationToken cancellationToken = default)
130 => Quote(messageId, new MessageActivity(text), cancellationToken);
131
132 /// <summary>
133 /// Send a message to the conversation with a quoted message reference prepended to the text.
134 /// Teams renders the quoted message as a preview bubble above the response text.
135 /// </summary>
136 /// <param name="messageId">The ID of the message to quote.</param>
137 /// <param name="activity">The activity to send. For <see cref="MessageActivity"/>, a quote placeholder for messageId is prepended to its text. Other activity types are sent as-is without quoting.</param>
138 /// <param name="cancellationToken">Optional cancellation token.</param>
139 /// <returns>The response from sending the activity.</returns>
140 [Experimental("ExperimentalTeamsQuotedReplies")]
141 public Task<SendActivityResponse?> Quote(string messageId, TeamsActivity activity, CancellationToken cancellationToken = default)
142 {
143 ArgumentNullException.ThrowIfNull(activity);
144 ArgumentException.ThrowIfNullOrWhiteSpace(messageId);
145 if (activity is MessageActivity message)
146 {
147 message.PrependQuote(messageId);
148 }
149 return SendActivityAsync(activity, cancellationToken);
150 }
151
152 /// <inheritdoc cref="SendAsync(string, CancellationToken)"/>
153 public Task<SendActivityResponse?> Send(string text, CancellationToken cancellationToken = default)
154 => SendAsync(text, cancellationToken);
155
156 /// <inheritdoc cref="SendAsync(TeamsActivity, CancellationToken)"/>
157 public Task<SendActivityResponse?> Send(TeamsActivity activity, CancellationToken cancellationToken = default)
158 => SendAsync(activity, cancellationToken);
159
160 /// <inheritdoc cref="ReplyAsync(string, CancellationToken)"/>
161 public Task<SendActivityResponse?> Reply(string text, CancellationToken cancellationToken = default)
162 => ReplyAsync(text, cancellationToken);
163
164 /// <inheritdoc cref="ReplyAsync(TeamsActivity, CancellationToken)"/>
165 public Task<SendActivityResponse?> Reply(TeamsActivity activity, CancellationToken cancellationToken = default)
166 => ReplyAsync(activity, cancellationToken);
167
168 /// <inheritdoc cref="TypingAsync(CancellationToken)"/>
169 public Task<SendActivityResponse?> Typing(CancellationToken cancellationToken = default)
170 => TypingAsync(cancellationToken);
171
172 // ==================== Core Send Methods ====================
173
174 /// <summary>
175 /// Sends a message activity as a reply.
176 /// </summary>
177 /// <param name="text">The text to send.</param>
178 /// <param name="cancellationToken">A cancellation token.</param>
179 /// <returns>The response from the send operation.</returns>
180 public Task<SendActivityResponse?> SendActivityAsync(string text, CancellationToken cancellationToken = default)
181 => SendActivityAsync(new MessageActivity(text) { TextFormat = TextFormats.Plain }, cancellationToken);
182
183 /// <summary>
184 /// Sends an activity to the conversation.
185 /// </summary>
186 /// <param name="activity">The activity to send.</param>
187 /// <param name="cancellationToken">A cancellation token.</param>
188 /// <returns>The response from the send operation.</returns>
189 public Task<SendActivityResponse?> SendActivityAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
190 {
191 ArgumentNullException.ThrowIfNull(activity);
192
193 bool isTargeted = activity.Recipient?.IsTargeted == true;
194
195 if (isTargeted && Activity.Conversation?.ConversationType == ConversationType.Personal)
196 {
197 throw new InvalidOperationException(
198 "Targeted messages are not supported in personal (1:1) chats.");
199 }
200
201 if (activity.Type == TeamsActivityType.Message
202 && Activity.Recipient?.IsTargeted == true
203 && Activity.Id is not null)
204 {
205 TargetedMessageInfoEntityExtensions.AddToActivity(activity, Activity.Id);
206 }
207
208 TeamsActivity reply = new TeamsActivityBuilder(activity)
209 .WithConversationReference(Activity)
210 .Build();
211 return TeamsBotApplication.SendActivityAsync(reply, cancellationToken: cancellationToken);
212 }
213
214 /// <summary>
215 /// Sends a typing activity to the conversation asynchronously.
216 /// </summary>
217 /// <param name="cancellationToken">A cancellation token.</param>
218 /// <returns>The response from the send operation.</returns>
219 public Task<SendActivityResponse?> SendTypingActivityAsync(CancellationToken cancellationToken = default)
220 {
221 TeamsActivity reply = new TeamsActivityBuilder()
222 .WithType(TeamsActivityType.Typing)
223 .WithConversationReference(Activity)
224 .Build();
225 return TeamsBotApplication.SendActivityAsync(reply, cancellationToken: cancellationToken);
226 }
227
228 // ==================== OAuth Sign-In ====================
229
230 /// <summary>
231 /// Trigger user OAuth sign-in flow for the activity sender.
232 /// Attempts silent token acquisition first; if no token is cached, sends an OAuthCard.
233 /// </summary>
234 /// <param name="options">OAuth options including connection name and card text.</param>
235 /// <param name="cancellationToken">A cancellation token.</param>
236 /// <returns>The existing user token if found, or null if the sign-in flow was initiated.</returns>
237 public Task<string?> SignInAsync(OAuthOptions? options = null, CancellationToken cancellationToken = default)
238 {
239 OAuthFlow flow = ResolveOAuthFlow(options?.ConnectionName);
240 return flow.SignInAsync(this, options, cancellationToken);
241 }
242
243 /// <summary>
244 /// Sign the user out, revoking their token from the Bot Framework Token Store.
245 /// </summary>
246 /// <param name="connectionName">The connection name to sign out from. If null, uses the default registered connection.</param>
247 /// <param name="cancellationToken">A cancellation token.</param>
248 public Task SignOutAsync(string? connectionName = null, CancellationToken cancellationToken = default)
249 {
250 OAuthFlow flow = ResolveOAuthFlow(connectionName);
251 return flow.SignOutAsync(this, cancellationToken);
252 }
253
254 /// <inheritdoc cref="SignInAsync(OAuthOptions?, CancellationToken)"/>
255 public Task<string?> SignIn(OAuthOptions? options = null, CancellationToken cancellationToken = default)
256 => SignInAsync(options, cancellationToken);
257
258 /// <inheritdoc cref="SignOutAsync(string?, CancellationToken)"/>
259 public Task SignOut(string? connectionName = null, CancellationToken cancellationToken = default)
260 => SignOutAsync(connectionName, cancellationToken);
261
262 /// <summary>
263 /// Whether the activity sender has a valid cached token.
264 /// When a single OAuthFlow is registered, checks that connection.
265 /// When multiple are registered, checks the first one and logs a warning;
266 /// prefer <see cref="IsSignedInAsync"/> with an explicit connection name instead.
267 /// Returns false if no OAuthFlow is registered.
268 /// </summary>
269 /// <remarks>
270 /// This property blocks the calling thread (sync-over-async) while querying
271 /// the Bot Framework Token Service. Under high concurrency this can cause
272 /// thread-pool starvation. Prefer <see cref="IsSignedInAsync"/> in new code.
273 /// </remarks>
274 [Obsolete("Use IsSignedInAsync() instead. This property blocks the calling thread and can cause thread-pool starvation under load.")]
275 public bool IsSignedIn
276 {
277 get
278 {
279 OAuthFlowRegistry? registry = TeamsBotApplication.OAuthRegistry;
280 if (registry is null) return false;
281
282 OAuthFlow? flow = registry.ResolveSingleWithWarning();
283 if (flow is null) return false;
284
285 return flow.GetTokenAsync(this).GetAwaiter().GetResult() is not null;
286 }
287 }
288
289 /// <summary>
290 /// Check whether the user has a valid cached token for a given OAuth connection.
291 /// </summary>
292 /// <param name="connectionName">The connection name to check. If null, uses the single registered connection.</param>
293 /// <param name="cancellationToken">A cancellation token.</param>
294 /// <returns>True if the user has a valid token; false otherwise.</returns>
295 public Task<bool> IsSignedInAsync(string? connectionName = null, CancellationToken cancellationToken = default)
296 {
297 OAuthFlow flow = ResolveOAuthFlow(connectionName);
298 return flow.IsSignedInAsync(this, cancellationToken);
299 }
300
301 /// <summary>
302 /// Get the token status for all configured OAuth connections.
303 /// Returns every connection registered on the bot, so the developer
304 /// never needs to enumerate connection names manually.
305 /// </summary>
306 /// <param name="cancellationToken">A cancellation token.</param>
307 /// <returns>A list of token status results for all configured connections.</returns>
308 public Task<IList<GetTokenStatusResult>> GetConnectionStatusAsync(CancellationToken cancellationToken = default)
309 {
310 OAuthFlowRegistry registry = TeamsBotApplication.OAuthRegistry
311 ?? throw new InvalidOperationException("No OAuthFlow registered. Call AddOAuthFlow(connectionName) on the TeamsBotApplication first.");
312
313 // Use any flow -- GetConnectionStatusAsync returns all connections regardless
314 OAuthFlow flow = registry.ResolveSingle()
315 ?? registry.GetAllFlows().First();
316
317 return flow.GetConnectionStatusAsync(this, cancellationToken);
318 }
319
320 private OAuthFlow ResolveOAuthFlow(string? connectionName)
321 {
322 OAuthFlowRegistry registry = TeamsBotApplication.OAuthRegistry
323 ?? throw new InvalidOperationException("No OAuthFlow registered. Call AddOAuthFlow(connectionName) on the TeamsBotApplication first.");
324
325 if (connectionName is not null)
326 {
327 OAuthFlow? flow = registry.Resolve(connectionName);
328 if (flow is not null) return flow;
329
330 string registered = string.Join(", ", registry.GetRegisteredConnectionNames().Select(n => $"'{n}'"));
331 throw new InvalidOperationException(
332 $"No OAuthFlow registered for connection '{connectionName}'. " +
333 $"Registered connections: {(registered.Length > 0 ? registered : "(none)")}.");
334 }
335
336 return registry.ResolveSingle()
337 ?? throw new InvalidOperationException(
338 "Multiple OAuthFlow instances registered. Specify a connection name in OAuthOptions or SignOut(connectionName).");
339 }
340}
341