microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
aamirj/minimal

Branches

Tags

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

Clone

HTTPS

Download ZIP

Samples/Samples.Cards/Program.cs

687lines · modecode

1using System.Text.Json;
2
3using Microsoft.Teams.Api.Activities.Invokes;
4using Microsoft.Teams.Api.AdaptiveCards;
5using Microsoft.Teams.Apps;
6using Microsoft.Teams.Apps.Activities;
7using Microsoft.Teams.Apps.Annotations;
8using Microsoft.Teams.Apps.Extensions;
9using Microsoft.Teams.Apps.Plugins;
10using Microsoft.Teams.Cards;
11using Microsoft.Teams.Common;
12using System.Text.Json;
13using Microsoft.Teams.Plugins.AspNetCore.DevTools.Extensions;
14using Microsoft.Teams.Plugins.AspNetCore.Extensions;
15
16using AdaptiveCardInvokes = Microsoft.Teams.Apps.Activities.Invokes;
17
18namespace Samples.Cards;
19
20public static partial class Program
21{
22 public static void Main(string[] args)
23 {
24 var builder = WebApplication.CreateBuilder(args);
25 // Bind the application to localhost:3978 when run with `dotnet run`
26 builder.WebHost.UseUrls("http://localhost:3978");
27 builder.Services.AddOpenApi();
28
29 // Use environment variable to choose between Controller and Minimal API approaches
30 var useMinimalApi = builder.Configuration.GetValue<bool>("USE_MINIMAL_API", false);
31 if (useMinimalApi)
32 {
33 Console.WriteLine("Using Minimal API approach");
34 }
35 else
36 {
37 Console.WriteLine("Using Controller approach");
38 }
39
40 if (!useMinimalApi)
41 {
42 builder.Services.AddTransient<Controller>();
43 }
44
45 builder.AddTeams().AddTeamsDevTools();
46
47 var app = builder.Build();
48
49 if (app.Environment.IsDevelopment())
50 {
51 app.MapOpenApi();
52 }
53
54 app.UseHttpsRedirection();
55 var teams = app.UseTeams();
56
57 if (useMinimalApi)
58 {
59 RegisterMinimalApiHandlers(teams);
60 }
61
62 app.Run();
63 }
64
65 private static void RegisterMinimalApiHandlers(Microsoft.Teams.Apps.App teams)
66 {
67 // Handle messages using minimal API
68 teams.OnMessage(async context =>
69 {
70 var activity = context.Activity;
71
72 context.Log.Info($"[MESSAGE] Received: {SanitizeForLog(activity.Text)}");
73 context.Log.Info($"[MESSAGE] From: {SanitizeForLog(activity.From?.Name ?? "unknown")}");
74
75 var text = activity.Text?.ToLowerInvariant() ?? "";
76
77 if (text.Contains("card"))
78 {
79 context.Log.Info("[CARD] Basic card requested");
80 var card = CreateBasicAdaptiveCard();
81 await context.Send(card);
82 }
83 else if (text.Contains("profile"))
84 {
85 context.Log.Info("[PROFILE] Profile card requested");
86 var card = CreateProfileCard();
87 await context.Send(card);
88 }
89 else if (text.Contains("validation"))
90 {
91 context.Log.Info("[VALIDATION] Validation card requested");
92 var card = CreateProfileCardWithValidation();
93 await context.Send(card);
94 }
95 else if (text.Contains("feedback"))
96 {
97 context.Log.Info("[FEEDBACK] Feedback card requested");
98 var card = CreateFeedbackCard();
99 await context.Send(card);
100 }
101 else if (text.Contains("form"))
102 {
103 context.Log.Info("[FORM] Task form card requested");
104 var card = CreateTaskFormCard();
105 await context.Send(card);
106 }
107 else if (text.Contains("json"))
108 {
109 context.Log.Info("[JSON] JSON deserialization card requested");
110 var card = CreateCardFromJson();
111 await context.Send(card);
112 }
113 else if (text.Contains("reply"))
114 {
115 await context.Send("Hello! How can I assist you today?");
116 }
117 else
118 {
119 await context.Typing();
120 await context.Send($"You said '{activity.Text}'. Try typing: card, profile, validation, feedback, form, json, or reply");
121 }
122 });
123
124 // Handle adaptive card actions using minimal API
125 AdaptiveCardInvokes.AppInvokeActivityExtensions.OnAdaptiveCardAction(teams, async context =>
126 {
127 var activity = context.Activity;
128
129 context.Log.Info("[CARD_ACTION] Card action received");
130
131 var data = activity.Value?.Action?.Data;
132
133 // Let's log the actual data structure to understand what we're working with
134 context.Log.Info($"[CARD_ACTION] Raw data: {System.Text.Json.JsonSerializer.Serialize(data)}");
135
136 if (data == null)
137 {
138 context.Log.Error("[CARD_ACTION] No data in card action");
139 return new Microsoft.Teams.Api.AdaptiveCards.ActionResponse.Message("No data specified") { StatusCode = 400 };
140 }
141
142 // Extract action from the Value property
143 string? action = data.TryGetValue("action", out var actionObj) ? actionObj?.ToString() : null;
144
145 if (string.IsNullOrEmpty(action))
146 {
147 context.Log.Error("[CARD_ACTION] No action specified in card data");
148 return new Microsoft.Teams.Api.AdaptiveCards.ActionResponse.Message("No action specified") { StatusCode = 400 };
149 }
150 context.Log.Info($"[CARD_ACTION] Processing action: {action}");
151
152 // Helper method to extract form field values (they're at root level, not in Value)
153 string? GetFormValue(string key)
154 {
155 if (data.TryGetValue(key, out var val))
156 {
157 if (val is System.Text.Json.JsonElement element)
158 return element.GetString();
159 return val?.ToString();
160 }
161 return null;
162 }
163
164 switch (action)
165 {
166 case "submit_basic":
167 var notifyValue = GetFormValue("notify") ?? "false";
168 await context.Send($"Basic card submitted! Notify setting: {notifyValue}");
169 break;
170
171 case "submit_feedback":
172 var feedbackText = GetFormValue("feedback") ?? "No feedback provided";
173 await context.Send($"Feedback received: {feedbackText}");
174 break;
175
176 case "create_task":
177 var title = GetFormValue("title") ?? "Untitled";
178 var priority = GetFormValue("priority") ?? "medium";
179 var dueDate = GetFormValue("due_date") ?? "No date";
180 await context.Send($"Task created!\nTitle: {title}\nPriority: {priority}\nDue: {dueDate}");
181 break;
182
183 case "save_profile":
184 var name = GetFormValue("name") ?? "Unknown";
185 var email = GetFormValue("email") ?? "No email";
186 var subscribe = GetFormValue("subscribe") ?? "false";
187 var age = GetFormValue("age");
188 var location = GetFormValue("location") ?? "Not specified";
189
190 var response = $"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}";
191 if (!string.IsNullOrEmpty(age))
192 response += $"\nAge: {age}";
193 if (location != "Not specified")
194 response += $"\nLocation: {location}";
195
196 await context.Send(response);
197 break;
198
199 case "test_json":
200 await context.Send("✅ JSON deserialization test successful! The card was properly created from JSON and the action was processed correctly.");
201 break;
202
203 default:
204 context.Log.Error($"[CARD_ACTION] Unknown action: {action}");
205 return new Microsoft.Teams.Api.AdaptiveCards.ActionResponse.Message("Unknown action") { StatusCode = 400 };
206 }
207
208 return new Microsoft.Teams.Api.AdaptiveCards.ActionResponse.Message("Action processed successfully") { StatusCode = 200 };
209 });
210 }
211
212 // Helper method to sanitize user input for logging
213 private static string SanitizeForLog(string input)
214 {
215 if (input == null) return "";
216 // Remove carriage returns and line feeds to prevent log forging
217 return input.Replace("\r", "").Replace("\n", "");
218 }
219
220 private static AdaptiveCard CreateBasicAdaptiveCard()
221 {
222 return new AdaptiveCard
223 {
224 Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
225 Body = new List<CardElement>
226 {
227 new TextBlock("Hello world")
228 {
229 Wrap = true,
230 Weight = TextWeight.Bolder
231 },
232 new ToggleInput("Notify me")
233 {
234 Id = "notify"
235 }
236 },
237 Actions = new List<Microsoft.Teams.Cards.Action>
238 {
239 new ExecuteAction
240 {
241 Title = "Submit",
242 Data = new Union<string, SubmitActionData>(new SubmitActionData { NonSchemaProperties = new Dictionary<string, object?> { { "action", "submit_basic" } } }),
243 AssociatedInputs = AssociatedInputs.Auto
244 }
245 }
246 };
247 }
248
249 private static AdaptiveCard CreateProfileCard()
250 {
251 return new AdaptiveCard
252 {
253 Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
254 Body = new List<CardElement>
255 {
256 new TextBlock("User Profile")
257 {
258 Weight = TextWeight.Bolder,
259 Size = TextSize.Large
260 },
261 new TextInput
262 {
263 Id = "name",
264 Label = "Name",
265 Value = "John Doe"
266 },
267 new TextInput
268 {
269 Id = "email",
270 Label = "Email",
271 Value = "john@contoso.com"
272 },
273 new ToggleInput("Subscribe to newsletter")
274 {
275 Id = "subscribe",
276 Value = "false"
277 }
278 },
279 Actions = new List<Microsoft.Teams.Cards.Action>
280 {
281 new ExecuteAction
282 {
283 Title = "Save",
284 Data = new Union<string, SubmitActionData>(new SubmitActionData { NonSchemaProperties = new Dictionary<string, object?> { { "action", "save_profile" }, { "entity_id", "12345" } } }),
285 AssociatedInputs = AssociatedInputs.Auto
286 },
287 new OpenUrlAction("https://adaptivecards.microsoft.com")
288 {
289 Title = "Learn More"
290 }
291 }
292 };
293 }
294
295 private static AdaptiveCard CreateProfileCardWithValidation()
296 {
297 return new AdaptiveCard
298 {
299 Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
300 Body = new List<CardElement>
301 {
302 new TextBlock("Profile with Validation")
303 {
304 Weight = TextWeight.Bolder,
305 Size = TextSize.Large
306 },
307 new NumberInput
308 {
309 Id = "age",
310 Label = "Age",
311 IsRequired = true,
312 Min = 0,
313 Max = 120
314 },
315 new TextInput
316 {
317 Id = "name",
318 Label = "Name",
319 IsRequired = true,
320 ErrorMessage = "Name is required"
321 },
322 new TextInput
323 {
324 Id = "location",
325 Label = "Location"
326 }
327 },
328 Actions = new List<Microsoft.Teams.Cards.Action>
329 {
330 new ExecuteAction
331 {
332 Title = "Save",
333 Data = new Union<string, SubmitActionData>(new SubmitActionData { NonSchemaProperties = new Dictionary<string, object?> { { "action", "save_profile" } } }),
334 AssociatedInputs = AssociatedInputs.Auto
335 }
336 }
337 };
338 }
339
340 private static AdaptiveCard CreateFeedbackCard()
341 {
342 return new AdaptiveCard
343 {
344 Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
345 Body = new List<CardElement>
346 {
347 new TextBlock("Feedback Form")
348 {
349 Weight = TextWeight.Bolder,
350 Size = TextSize.Large
351 },
352 new TextInput
353 {
354 Id = "feedback",
355 Label = "Your Feedback",
356 Placeholder = "Please share your thoughts...",
357 IsMultiline = true,
358 IsRequired = true
359 }
360 },
361 Actions = new List<Microsoft.Teams.Cards.Action>
362 {
363 new ExecuteAction
364 {
365 Title = "Submit Feedback",
366 Data = new Union<string, SubmitActionData>(new SubmitActionData { NonSchemaProperties = new Dictionary<string, object?> { { "action", "submit_feedback" } } }),
367 AssociatedInputs = AssociatedInputs.Auto
368 }
369 }
370 };
371 }
372
373 private static AdaptiveCard CreateTaskFormCard()
374 {
375 return new AdaptiveCard
376 {
377 Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
378 Body = new List<CardElement>
379 {
380 new TextBlock("Create New Task")
381 {
382 Weight = TextWeight.Bolder,
383 Size = TextSize.Large
384 },
385 new TextInput
386 {
387 Id = "title",
388 Label = "Task Title",
389 Placeholder = "Enter task title"
390 },
391 new TextInput
392 {
393 Id = "description",
394 Label = "Description",
395 Placeholder = "Enter task details",
396 IsMultiline = true
397 },
398 new ChoiceSetInput
399 {
400 Id = "priority",
401 Label = "Priority",
402 Value = "medium",
403 Choices = new List<Choice>
404 {
405 new() { Title = "High", Value = "high" },
406 new() { Title = "Medium", Value = "medium" },
407 new() { Title = "Low", Value = "low" }
408 }
409 },
410 new DateInput
411 {
412 Id = "due_date",
413 Label = "Due Date",
414 Value = DateTime.Now.ToString("yyyy-MM-dd")
415 }
416 },
417 Actions = new List<Microsoft.Teams.Cards.Action>
418 {
419 new ExecuteAction
420 {
421 Title = "Create Task",
422 Data = new Union<string, SubmitActionData>(new SubmitActionData { NonSchemaProperties = new Dictionary<string, object?> { { "action", "create_task" } } }),
423 AssociatedInputs = AssociatedInputs.Auto,
424 Style = ActionStyle.Positive
425 }
426 }
427 };
428 }
429
430 private static AdaptiveCard CreateCardFromJson()
431 {
432 // JSON similar to the Python create_model_validate_card example
433 var cardJson = @"{
434 ""type"": ""AdaptiveCard"",
435 ""body"": [
436 {
437 ""type"": ""ColumnSet"",
438 ""columns"": [
439 {
440 ""type"": ""Column"",
441 ""verticalContentAlignment"": ""center"",
442 ""items"": [
443 {
444 ""type"": ""Image"",
445 ""style"": ""Person"",
446 ""url"": ""https://aka.ms/AAp9xo4"",
447 ""size"": ""Small"",
448 ""altText"": ""Portrait of David Claux""
449 }
450 ],
451 ""width"": ""auto""
452 },
453 {
454 ""type"": ""Column"",
455 ""spacing"": ""medium"",
456 ""verticalContentAlignment"": ""center"",
457 ""items"": [
458 {
459 ""type"": ""TextBlock"",
460 ""weight"": ""Bolder"",
461 ""text"": ""David Claux"",
462 ""wrap"": true
463 }
464 ],
465 ""width"": ""auto""
466 },
467 {
468 ""type"": ""Column"",
469 ""spacing"": ""medium"",
470 ""verticalContentAlignment"": ""center"",
471 ""items"": [
472 {
473 ""type"": ""TextBlock"",
474 ""text"": ""Principal Platform Architect at Microsoft"",
475 ""isSubtle"": true,
476 ""wrap"": true
477 }
478 ],
479 ""width"": ""stretch""
480 }
481 ]
482 },
483 {
484 ""type"": ""TextBlock"",
485 ""text"": ""This card was created from JSON deserialization!"",
486 ""wrap"": true,
487 ""color"": ""good"",
488 ""spacing"": ""medium""
489 }
490 ],
491 ""actions"": [
492 {
493 ""type"": ""Action.Execute"",
494 ""title"": ""Test JSON Action"",
495 ""data"": {
496 ""Value"": {
497 ""action"": ""test_json""
498 }
499 },
500 ""associatedInputs"": ""auto""
501 }
502 ],
503 ""version"": ""1.5"",
504 ""schema"": ""http://adaptivecards.io/schemas/adaptive-card.json""
505 }";
506
507 try
508 {
509 var card = JsonSerializer.Deserialize<AdaptiveCard>(cardJson);
510
511 return card ?? throw new InvalidOperationException("Failed to deserialize card");
512 }
513 catch (Exception ex)
514 {
515 Console.WriteLine($"Error deserializing card JSON: {ex.Message}");
516 // If deserialization fails, return a fallback card with error info
517 return new AdaptiveCard
518 {
519 Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
520 Body = new List<CardElement>
521 {
522 new TextBlock("JSON Deserialization Test")
523 {
524 Weight = TextWeight.Bolder,
525 Size = TextSize.Large,
526 Color = TextColor.Attention
527 },
528 new TextBlock($"Deserialization failed: {ex.Message}")
529 {
530 Wrap = true,
531 Color = TextColor.Attention
532 }
533 }
534 };
535 }
536 }
537
538 [TeamsController]
539 public class Controller
540 {
541 [Message]
542 public async Task OnMessage([Context] Microsoft.Teams.Api.Activities.MessageActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log)
543 {
544 log.Info($"[MESSAGE] Received: {SanitizeForLog(activity.Text)}");
545 log.Info($"[MESSAGE] From: {SanitizeForLog(activity.From?.Name ?? "unknown")}");
546
547 var text = activity.Text?.ToLowerInvariant() ?? "";
548
549 if (text.Contains("card"))
550 {
551 log.Info("[CARD] Basic card requested");
552 var card = CreateBasicAdaptiveCard();
553 await client.Send(card);
554 }
555 else if (text.Contains("profile"))
556 {
557 log.Info("[PROFILE] Profile card requested");
558 var card = CreateProfileCard();
559 await client.Send(card);
560 }
561 else if (text.Contains("validation"))
562 {
563 log.Info("[VALIDATION] Validation card requested");
564 var card = CreateProfileCardWithValidation();
565 await client.Send(card);
566 }
567 else if (text.Contains("feedback"))
568 {
569 log.Info("[FEEDBACK] Feedback card requested");
570 var card = CreateFeedbackCard();
571 await client.Send(card);
572 }
573 else if (text.Contains("form"))
574 {
575 log.Info("[FORM] Task form card requested");
576 var card = CreateTaskFormCard();
577 await client.Send(card);
578 }
579 else if (text.Contains("json"))
580 {
581 log.Info("[JSON] JSON deserialization card requested");
582 var card = CreateCardFromJson();
583 await client.Send(card);
584 }
585 else if (text.Contains("reply"))
586 {
587 await client.Send("Hello! How can I assist you today?");
588 }
589 else
590 {
591 await client.Typing();
592 await client.Send($"You said '{activity.Text}'. Try typing: card, profile, validation, feedback, form, json, or reply");
593 }
594 }
595
596 [Microsoft.Teams.Apps.Activities.Invokes.AdaptiveCard.Action]
597 public async Task<ActionResponse> OnCardAction([Context] AdaptiveCards.ActionActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log)
598 {
599 log.Info("[CARD_ACTION] Card action received");
600
601 var data = activity.Value?.Action?.Data;
602
603 // Let's log the actual data structure to understand what we're working with
604 log.Info($"[CARD_ACTION] Raw data: {System.Text.Json.JsonSerializer.Serialize(data)}");
605
606 if (data == null)
607 {
608 log.Error("[CARD_ACTION] No data in card action");
609 return new ActionResponse.Message("No data specified") { StatusCode = 400 };
610 }
611
612 // Extract action from the Value property
613 string? action = data.TryGetValue("action", out var actionObj) ? actionObj?.ToString() : null;
614
615 if (string.IsNullOrEmpty(action))
616 {
617 log.Error("[CARD_ACTION] No action specified in card data");
618 return new ActionResponse.Message("No action specified") { StatusCode = 400 };
619 }
620 log.Info($"[CARD_ACTION] Processing action: {action}");
621
622 // Helper method to extract form field values (they're at root level, not in Value)
623 string? GetFormValue(string key)
624 {
625 if (data.TryGetValue(key, out var val))
626 {
627 if (val is System.Text.Json.JsonElement element)
628 return element.GetString();
629 return val?.ToString();
630 }
631 return null;
632 }
633
634 switch (action)
635 {
636 case "submit_basic":
637 var notifyValue = GetFormValue("notify") ?? "false";
638 await client.Send($"Basic card submitted! Notify setting: {notifyValue}");
639 break;
640
641 case "submit_feedback":
642 var feedbackText = GetFormValue("feedback") ?? "No feedback provided";
643 await client.Send($"Feedback received: {feedbackText}");
644 break;
645
646 case "create_task":
647 var title = GetFormValue("title") ?? "Untitled";
648 var priority = GetFormValue("priority") ?? "medium";
649 var dueDate = GetFormValue("due_date") ?? "No date";
650 await client.Send($"Task created!\nTitle: {title}\nPriority: {priority}\nDue: {dueDate}");
651 break;
652
653 case "save_profile":
654 var name = GetFormValue("name") ?? "Unknown";
655 var email = GetFormValue("email") ?? "No email";
656 var subscribe = GetFormValue("subscribe") ?? "false";
657 var age = GetFormValue("age");
658 var location = GetFormValue("location") ?? "Not specified";
659
660 var response = $"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}";
661 if (!string.IsNullOrEmpty(age))
662 response += $"\nAge: {age}";
663 if (location != "Not specified")
664 response += $"\nLocation: {location}";
665
666 await client.Send(response);
667 break;
668
669 case "test_json":
670 await client.Send("✅ JSON deserialization test successful! The card was properly created from JSON and the action was processed correctly.");
671 break;
672
673 default:
674 log.Error($"[CARD_ACTION] Unknown action: {action}");
675 return new ActionResponse.Message("Unknown action") { StatusCode = 400 };
676 }
677
678 return new ActionResponse.Message("Action processed successfully") { StatusCode = 200 };
679 }
680
681 [Microsoft.Teams.Apps.Events.Event("activity")]
682 public void OnEvent(IPlugin plugin, Microsoft.Teams.Apps.Events.Event @event)
683 {
684 Console.WriteLine("!!HIT!!");
685 }
686 }
687}