microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/update-release-process

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Api/Activities/Activity.cs

569lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4using System.Diagnostics.CodeAnalysis;
5using System.Text.Json;
6using System.Text.Json.Serialization;
7
8using Microsoft.Teams.Api.Entities;
9using Microsoft.Teams.Common;
10
11namespace Microsoft.Teams.Api.Activities;
12
13[JsonConverter(typeof(JsonConverter<ActivityType>))]
14public partial class ActivityType(string value) : StringEnum(value)
15{
16 public Type ToType()
17 {
18 if (IsTyping) return typeof(TypingActivity);
19 if (IsCommand) return typeof(CommandActivity);
20 if (IsCommandResult) return typeof(CommandResultActivity);
21 if (IsConversationUpdate) return typeof(ConversationUpdateActivity);
22 #pragma warning disable CS0618
23 if (IsEndOfConversation) return typeof(EndOfConversationActivity);
24 #pragma warning restore CS0618
25 if (IsInstallUpdate) return typeof(InstallUpdateActivity);
26 if (IsMessage) return typeof(MessageActivity);
27 if (IsMessageUpdate) return typeof(MessageUpdateActivity);
28 if (IsMessageDelete) return typeof(MessageDeleteActivity);
29 if (IsMessageReaction) return typeof(MessageReactionActivity);
30 if (IsEvent) return typeof(EventActivity);
31 if (IsInvoke) return typeof(InvokeActivity);
32 return typeof(Activity);
33 }
34
35 public string ToPrettyString()
36 {
37 var value = ToString();
38 return $"{value.First().ToString().ToUpper()}{value.AsSpan(1).ToString()}";
39 }
40}
41
42[JsonConverter(typeof(ActivityJsonConverter))]
43public partial interface IActivity : IConvertible, ICloneable
44{
45 public string Id { get; set; }
46
47 public ActivityType Type { get; set; }
48
49 public string? ReplyToId { get; set; }
50
51 public ChannelId ChannelId { get; set; }
52
53 public Account From { get; set; }
54
55 public Account Recipient { get; set; }
56
57 public Conversation Conversation { get; set; }
58
59 /// <summary>
60 /// A reference to another conversation or activity.
61 /// </summary>
62 [Obsolete("This will be removed by end of summer 2026.")]
63 public ConversationReference? RelatesTo { get; set; }
64
65 public string? ServiceUrl { get; set; }
66
67 public string? Locale { get; set; }
68
69 public DateTime? Timestamp { get; set; }
70
71 public DateTime? LocalTimestamp { get; set; }
72
73 public IList<IEntity>? Entities { get; set; }
74
75 public ChannelData? ChannelData { get; set; }
76
77 public IDictionary<string, object?> Properties { get; set; }
78
79 /// <summary>
80 /// is this a streaming activity
81 /// </summary>
82 [JsonIgnore]
83 public bool IsStreaming { get; }
84
85 /// <summary>
86 /// get the activity type/name path
87 /// </summary>
88 public string GetPath();
89
90 /// <summary>
91 /// Generates a quoted reply placeholder for the current activity.
92 /// See <see cref="MessageActivity.AddQuote(string, string?)"/> for the recommended approach.
93 /// </summary>
94 [Obsolete("Use MessageActivity.AddQuote() instead.")]
95 public string ToQuoteReply();
96}
97
98[JsonConverter(typeof(ActivityJsonConverter))]
99public partial class Activity : IActivity
100{
101 [JsonPropertyName("id")]
102 [JsonPropertyOrder(0)]
103 public string Id { get; set; }
104
105 [JsonPropertyName("type")]
106 [JsonPropertyOrder(10)]
107 public ActivityType Type { get; set; }
108
109 [JsonPropertyName("replyToId")]
110 [JsonPropertyOrder(20)]
111 public string? ReplyToId { get; set; }
112
113 [JsonPropertyName("channelId")]
114 [JsonPropertyOrder(30)]
115 public ChannelId ChannelId { get; set; } = ChannelId.MsTeams;
116
117 [JsonPropertyName("from")]
118 [JsonPropertyOrder(40)]
119 public Account From { get; set; }
120
121 [JsonPropertyName("recipient")]
122 [JsonPropertyOrder(50)]
123 public Account Recipient { get; set; }
124
125 [JsonPropertyName("conversation")]
126 [JsonPropertyOrder(60)]
127 public Conversation Conversation { get; set; }
128
129 /// <summary>
130 /// A reference to another conversation or activity.
131 /// </summary>
132 [JsonPropertyName("relatesTo")]
133 [JsonPropertyOrder(70)]
134 [Obsolete("This will be removed by end of summer 2026.")]
135 public ConversationReference? RelatesTo { get; set; }
136
137 [JsonPropertyName("serviceUrl")]
138 [JsonPropertyOrder(80)]
139 public string? ServiceUrl { get; set; }
140
141 [JsonPropertyName("locale")]
142 [JsonPropertyOrder(90)]
143 public string? Locale { get; set; }
144
145 [JsonPropertyName("timestamp")]
146 [JsonPropertyOrder(100)]
147 public DateTime? Timestamp { get; set; }
148
149 [JsonPropertyName("localTimestamp")]
150 [JsonPropertyOrder(110)]
151 public DateTime? LocalTimestamp { get; set; }
152
153 [JsonPropertyName("entities")]
154 [JsonPropertyOrder(120)]
155 public IList<IEntity>? Entities { get; set; }
156
157 [JsonPropertyName("channelData")]
158 [JsonPropertyOrder(130)]
159 public ChannelData? ChannelData { get; set; }
160
161 [JsonExtensionData]
162 public IDictionary<string, object?> Properties { get; set; } = new Dictionary<string, object?>();
163
164 [JsonConstructor]
165 public Activity(string type)
166 {
167 Type = new(type);
168 }
169
170 public Activity(ActivityType type)
171 {
172 Type = type;
173 }
174
175 public Activity(IActivity activity)
176 {
177 Id = activity.Id;
178 Type = activity.Type;
179 ReplyToId = activity.ReplyToId;
180 ChannelId = activity.ChannelId;
181 From = activity.From;
182 Recipient = activity.Recipient;
183 Conversation = activity.Conversation;
184 #pragma warning disable CS0618
185 RelatesTo = activity.RelatesTo;
186 #pragma warning restore CS0618
187 ServiceUrl = activity.ServiceUrl;
188 Locale = activity.Locale;
189 Timestamp = activity.Timestamp;
190 LocalTimestamp = activity.LocalTimestamp;
191 Entities = activity.Entities;
192 ChannelData = activity.ChannelData;
193 Properties = activity.Properties;
194 }
195
196 [JsonIgnore]
197 public bool IsStreaming => Entities?.Any(entity => entity.Type == "streaminfo" && entity is StreamInfoEntity) ?? false;
198
199 public object Clone() => MemberwiseClone();
200 public virtual Activity Copy() => (Activity)Clone();
201 public virtual string GetPath() => string.Join(".", ["Activity", Type.ToPrettyString()]);
202
203 public virtual Activity WithId(string value)
204 {
205 Id = value;
206 return this;
207 }
208
209 public virtual Activity WithChannelId(ChannelId value)
210 {
211 ChannelId = value;
212 return this;
213 }
214
215 public virtual Activity WithFrom(Account value)
216 {
217 From = value;
218 return this;
219 }
220
221 public virtual Activity WithConversation(Conversation value)
222 {
223 Conversation = value;
224 return this;
225 }
226
227 [Obsolete("This will be removed by end of summer 2026.")]
228 public virtual Activity WithRelatesTo(ConversationReference value)
229 {
230 #pragma warning disable CS0618
231 RelatesTo = value;
232 #pragma warning restore CS0618
233 return this;
234 }
235
236 public virtual Activity WithRecipient(Account value)
237 {
238 Recipient = value;
239#pragma warning disable ExperimentalTeamsTargeted
240 Recipient.IsTargeted = null;
241#pragma warning restore ExperimentalTeamsTargeted
242 return this;
243 }
244
245 [Experimental("ExperimentalTeamsTargeted")]
246 public virtual Activity WithRecipient(Account value, bool isTargeted)
247 {
248 Recipient = value;
249#pragma warning disable ExperimentalTeamsTargeted
250 Recipient.IsTargeted = isTargeted ? true : null;
251#pragma warning restore ExperimentalTeamsTargeted
252 return this;
253 }
254
255 public virtual Activity WithServiceUrl(string value)
256 {
257 ServiceUrl = value;
258 return this;
259 }
260
261 public virtual Activity WithTimestamp(DateTime value)
262 {
263 Timestamp = value;
264 return this;
265 }
266
267 public virtual Activity WithLocale(string value)
268 {
269 Locale = value;
270 return this;
271 }
272
273 public virtual Activity WithLocalTimestamp(DateTime value)
274 {
275 LocalTimestamp = value;
276 return this;
277 }
278
279 public virtual Activity WithData(ChannelData value)
280 {
281 ChannelData ??= new();
282 ChannelData.Merge(value);
283 NormalizeFeedback();
284 return this;
285 }
286
287 /// <summary>
288 /// The Teams service rejects <c>feedbackLoop</c> and <c>feedbackLoopEnabled</c>
289 /// set at the same time. When <see cref="ChannelData.FeedbackLoop"/> is set it
290 /// wins; otherwise a legacy <c>FeedbackLoopEnabled = true</c> is upgraded to
291 /// <see cref="FeedbackType.Default"/>.
292 /// </summary>
293 private void NormalizeFeedback()
294 {
295 if (ChannelData is null) return;
296
297 if (ChannelData.FeedbackLoop is not null)
298 {
299 ChannelData.FeedbackLoopEnabled = null;
300 }
301 else if (ChannelData.FeedbackLoopEnabled == true)
302 {
303 ChannelData.FeedbackLoop = new FeedbackLoop(FeedbackType.Default);
304 ChannelData.FeedbackLoopEnabled = null;
305 }
306 }
307
308 public virtual Activity WithData(string key, object? value)
309 {
310 ChannelData ??= new();
311 ChannelData.Properties[key] = value;
312 return this;
313 }
314
315 public virtual Activity WithAppId(string value)
316 {
317 ChannelData ??= new();
318 ChannelData.App ??= new App() { Id = value };
319 return this;
320 }
321
322 /// <summary>
323 /// add an entity
324 /// </summary>
325 public virtual Activity AddEntity(params IEntity[] entities)
326 {
327 Entities ??= [];
328
329 foreach (var entity in entities)
330 {
331 Entities.Add(entity);
332 }
333
334 return this;
335 }
336
337 public virtual Activity UpdateEntity(IEntity oldEntity, IEntity newEntity)
338 {
339 if (Entities != null)
340 {
341 Entities.Remove(oldEntity);
342 }
343 else
344 {
345 Entities = [];
346 }
347
348 Entities.Add(newEntity);
349 return this;
350 }
351
352 /// <summary>
353 /// ensures a single root level message entity exists
354 /// </summary>
355 private IMessageEntity GetRootLevelMessageEntity()
356 {
357 var messageEntity = Entities?.FirstOrDefault(
358 e => e.Type == "https://schema.org/Message" && e.OType == "Message"
359 ) as IMessageEntity;
360
361 if (messageEntity is null)
362 {
363 messageEntity = new MessageEntity()
364 {
365 Type = "https://schema.org/Message",
366 OType = "Message",
367 OContext = "https://schema.org"
368 };
369
370 AddEntity(messageEntity);
371 }
372
373 return messageEntity;
374 }
375
376 /// <summary>
377 /// add the `Generated By AI` label
378 /// </summary>
379 public virtual Activity AddAIGenerated()
380 {
381 var messageEntity = GetRootLevelMessageEntity();
382 messageEntity.AdditionalType ??= [];
383
384 if (!messageEntity.AdditionalType.Contains("AIGeneratedContent"))
385 {
386 messageEntity.AdditionalType.Add("AIGeneratedContent");
387 }
388
389 return this;
390 }
391
392 /// <summary>
393 /// add content sensitivity label
394 /// </summary>
395 /// <param name="name">the content title</param>
396 /// <param name="description">the content description</param>
397 /// <param name="pattern">the pattern</param>
398 public virtual Activity AddSensitivityLabel(string name, string? description = null, DefinedTerm? pattern = null)
399 {
400 return AddEntity(new SensitiveUsageEntity()
401 {
402 Name = name,
403 Description = description,
404 Pattern = pattern
405 });
406 }
407
408 /// <summary>
409 /// Legacy builder method of enabling default message feedback.
410 /// </summary>
411 /// <param name="value">Whether to enable default message feedback.</param>
412 public virtual Activity AddFeedback(bool value = true)
413 {
414 ChannelData ??= new();
415
416 if (value)
417 {
418 ChannelData.FeedbackLoop = new FeedbackLoop(FeedbackType.Default);
419 }
420 else
421 {
422 ChannelData.FeedbackLoop = null;
423 }
424
425 ChannelData.FeedbackLoopEnabled = null;
426 return this;
427 }
428
429 /// <summary>
430 /// Enable message feedback with an explicit mode (default or custom).
431 /// </summary>
432 /// <param name="mode">
433 /// <see cref="FeedbackType.Default"/> shows Teams' built-in thumbs up/down UI.
434 /// <see cref="FeedbackType.Custom"/> triggers a <c>message/fetchTask</c> invoke
435 /// so the bot can return its own task module dialog.
436 /// </param>
437 public virtual Activity AddFeedback(FeedbackType mode)
438 {
439 ChannelData ??= new();
440 ChannelData.FeedbackLoop = new FeedbackLoop(mode);
441 ChannelData.FeedbackLoopEnabled = null;
442 return this;
443 }
444
445 /// <summary>
446 /// add a targeted message info entity for prompt preview.
447 /// If an entity with type "targetedMessageInfo" already exists, it is not added again.
448 /// Any existing "quotedReply" entities are always removed from <see cref="Entities"/>
449 /// and matching &lt;quoted messageId="..."/&gt; placeholders are always stripped
450 /// from <see cref="MessageActivity.Text"/> to prevent collision between
451 /// quoted replies and prompt preview.
452 /// </summary>
453 /// <param name="messageId">the message ID of the targeted message</param>
454 [Experimental("ExperimentalTeamsTargeted")]
455 public virtual Activity AddTargetedMessageInfo(string messageId)
456 {
457 // Always strip quotedReply entities and matching <quoted .../> placeholder
458 // to avoid collision with prompt preview
459 if (Entities is not null)
460 {
461 for (var i = Entities.Count - 1; i >= 0; i--)
462 {
463 if (Entities[i].Type == "quotedReply") Entities.RemoveAt(i);
464 }
465 }
466
467 if (this is MessageActivity message && message.Text is not null)
468 {
469 message.Text = message.Text.Replace($"<quoted messageId=\"{messageId}\"/>", string.Empty).Trim();
470 }
471
472 // Only add entity if not already present
473 var hasEntity = Entities?.Any(e => e.Type == "targetedMessageInfo") ?? false;
474 if (!hasEntity)
475 {
476 AddEntity(new TargetedMessageInfoEntity { MessageId = messageId });
477 }
478
479 return this;
480 }
481
482 /// <summary>
483 /// add a citation
484 /// </summary>
485 public virtual Activity AddCitation(int position, CitationAppearance appearance)
486 {
487 var messageEntity = GetRootLevelMessageEntity();
488 var citationEntity = new CitationEntity(messageEntity);
489 citationEntity.Citation ??= [];
490 citationEntity.Citation.Add(new CitationEntity.Claim()
491 {
492 Position = position,
493 Appearance = appearance.ToDocument()
494 });
495
496 UpdateEntity(messageEntity, citationEntity);
497 return this;
498 }
499
500 public CommandActivity ToCommand() => (CommandActivity)this;
501 public CommandResultActivity ToCommandResult() => (CommandResultActivity)this;
502 public TypingActivity ToTyping() => (TypingActivity)this;
503 public InstallUpdateActivity ToInstallUpdate() => (InstallUpdateActivity)this;
504 public MessageActivity ToMessage() => (MessageActivity)this;
505 public MessageUpdateActivity ToMessageUpdate() => (MessageUpdateActivity)this;
506 public MessageDeleteActivity ToMessageDelete() => (MessageDeleteActivity)this;
507 public MessageReactionActivity ToMessageReaction() => (MessageReactionActivity)this;
508 public ConversationUpdateActivity ToConversationUpdate() => (ConversationUpdateActivity)this;
509 #pragma warning disable CS0618
510 public EndOfConversationActivity ToEndOfConversation() => (EndOfConversationActivity)this;
511 #pragma warning restore CS0618
512 public EventActivity ToEvent() => (EventActivity)this;
513 public InvokeActivity ToInvoke() => (InvokeActivity)this;
514
515 public Activity Merge(Activity from)
516 {
517 Id ??= from.Id;
518 ReplyToId ??= from.ReplyToId;
519 ChannelId ??= from.ChannelId;
520 From ??= from.From;
521 Recipient ??= from.Recipient;
522 Conversation ??= from.Conversation;
523 #pragma warning disable CS0618
524 RelatesTo ??= from.RelatesTo;
525 #pragma warning restore CS0618
526 ServiceUrl ??= from.ServiceUrl;
527 Locale ??= from.Locale;
528 Timestamp ??= from.Timestamp;
529 LocalTimestamp ??= from.LocalTimestamp;
530 AddEntity(from.Entities?.ToArray() ?? []);
531
532 if (from.ChannelData is not null)
533 {
534 WithData(from.ChannelData);
535 }
536
537 if (from.Properties is not null)
538 {
539 Properties ??= new Dictionary<string, object?>();
540
541 foreach (var kv in from.Properties)
542 {
543 Properties[kv.Key] = kv.Value;
544 }
545 }
546
547 return this;
548 }
549
550 /// <summary>
551 /// Generates a quoted reply placeholder for the current activity.
552 /// See <see cref="MessageActivity.AddQuote(string, string?)"/> for the recommended approach.
553 /// </summary>
554 [Obsolete("Use MessageActivity.AddQuote() instead.")]
555 public virtual string ToQuoteReply()
556 {
557 if (Id == null) return string.Empty;
558 return $"<quoted messageId=\"{Id}\"/>";
559 }
560
561 public override string ToString()
562 {
563 return JsonSerializer.Serialize(this, new JsonSerializerOptions()
564 {
565 WriteIndented = true,
566 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
567 });
568 }
569}