Skip to content

Commit 9bab0e8

Browse files
authored
refactor(vs): update vs notification timer template (#14233)
* refactor: update vs notification templates
1 parent f3948d0 commit 9bab0e8

File tree

61 files changed

+4722
-102
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4722
-102
lines changed

templates/vs/csharp/notification-http-timer-trigger/MessageHandler.cs.tpl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@ using Microsoft.Azure.Functions.Worker;
22
using Microsoft.AspNetCore.Http;
33
using Microsoft.AspNetCore.Mvc;
44
using Microsoft.Extensions.Logging;
5-
using Microsoft.TeamsFx.Conversation;
65
using Microsoft.Agents.Builder;
76
using Microsoft.Agents.Hosting.AspNetCore;
7+
using {{SafeProjectName}}.Notification;
88

99
namespace {{SafeProjectName}}
1010
{
1111
public sealed class MessageHandler
1212
{
13-
private readonly ConversationBot _conversation;
13+
private readonly NotificationBot _notification;
1414
private readonly IAgent _bot;
1515
private readonly ILogger<MessageHandler> _log;
1616
17-
public MessageHandler(ConversationBot conversation, IAgent bot, ILogger<MessageHandler> log)
17+
public MessageHandler(NotificationBot notification, IAgent bot, ILogger<MessageHandler> log)
1818
{
19-
_conversation = conversation;
19+
_notification = notification;
2020
_bot = bot;
2121
_log = log;
2222
}
@@ -26,7 +26,7 @@ namespace {{SafeProjectName}}
2626
{
2727
_log.LogInformation("MessageHandler processes a request.");
2828
29-
await (_conversation.Adapter as CloudAdapter).ProcessAsync(req, req.HttpContext.Response, _bot, req.HttpContext.RequestAborted);
29+
await (_notification.Adapter as CloudAdapter).ProcessAsync(req, req.HttpContext.Response, _bot, req.HttpContext.RequestAborted);
3030
3131
return new EmptyResult();
3232
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using Microsoft.Agents.Builder;
2+
using Microsoft.Agents.Core.Models;
3+
using Microsoft.Agents.Extensions.Teams.Models;
4+
5+
namespace {{SafeProjectName}}.Notification
6+
{
7+
public class Channel
8+
{
9+
/// <summary>
10+
/// Constructor.
11+
/// </summary>
12+
/// <param name="parent">The parent <see cref="TeamsBotInstallation"/> where this channel is created from.</param>
13+
/// <param name="info">Detailed channel information.</param>
14+
/// <exception cref="ArgumentNullException">Throws if provided parameter is null.</exception>
15+
/// <remarks>
16+
/// It's recommended to get channels from <see cref="TeamsBotInstallation.GetChannelsAsync"/>.
17+
/// </remarks>
18+
public Channel(TeamsBotInstallation parent, ChannelInfo info)
19+
{
20+
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
21+
Info = info ?? throw new ArgumentNullException(nameof(info));
22+
}
23+
24+
/// <summary>
25+
/// The parent <see cref="TeamsBotInstallation"/> where this channel is created from.
26+
/// </summary>
27+
public TeamsBotInstallation Parent { get; private set; }
28+
29+
/// <summary>
30+
/// Detailed channel information.
31+
/// </summary>
32+
public ChannelInfo Info { get; private set; }
33+
34+
/// <summary>
35+
/// The type of target. For channel it's always <see cref="NotificationTargetType.Channel"/>.
36+
/// </summary>
37+
public string Type { get => "channel"; }
38+
39+
/// <inheritdoc/>
40+
public async Task<string> SendMessage(string message, CancellationToken cancellationToken = default)
41+
{
42+
var response = "";
43+
await Parent.Adapter.ContinueConversationAsync
44+
(
45+
Parent.BotAppId,
46+
Parent.ConversationReference,
47+
async (context1, ct1) => {
48+
var conversation = NewConversation(context1);
49+
await Parent.Adapter.ContinueConversationAsync
50+
(
51+
Parent.BotAppId,
52+
conversation,
53+
async (context2, ct2) => {
54+
var res = await context2.SendActivityAsync(message, cancellationToken: ct2).ConfigureAwait(false);
55+
response = res?.Id;
56+
},
57+
ct1
58+
).ConfigureAwait(false);
59+
},
60+
cancellationToken
61+
).ConfigureAwait(false);
62+
return response;
63+
}
64+
65+
/// <inheritdoc/>
66+
public async Task<string> SendAdaptiveCard(object card, CancellationToken cancellationToken = default)
67+
{
68+
var response = "";
69+
await Parent.Adapter.ContinueConversationAsync
70+
(
71+
Parent.BotAppId,
72+
Parent.ConversationReference,
73+
async (context1, ct1) => {
74+
var conversation = NewConversation(context1);
75+
await Parent.Adapter.ContinueConversationAsync
76+
(
77+
Parent.BotAppId,
78+
conversation,
79+
async (context2, ct2) => {
80+
var res = await context2.SendActivityAsync
81+
(
82+
MessageFactory.Attachment
83+
(
84+
new Attachment
85+
{
86+
ContentType = "application/vnd.microsoft.card.adaptive",
87+
Content = card,
88+
}
89+
),
90+
cancellationToken: ct2
91+
).ConfigureAwait(false);
92+
response = res?.Id;
93+
},
94+
ct1
95+
).ConfigureAwait(false);
96+
},
97+
cancellationToken
98+
).ConfigureAwait(false);
99+
return response;
100+
}
101+
102+
private ConversationReference NewConversation(ITurnContext context)
103+
{
104+
var reference = context.Activity.GetConversationReference();
105+
var channelConversation = reference.Clone();
106+
channelConversation.Conversation.Id = Info.Id ?? string.Empty;
107+
return channelConversation;
108+
}
109+
}
110+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace {{SafeProjectName}}.Notification
2+
{
3+
using Microsoft.Agents.Core.Models;
4+
using System.Text.Json;
5+
static internal class ConversationReferenceExtensions
6+
{
7+
static internal ConversationReference Clone(this ConversationReference reference)
8+
{
9+
if (reference == null)
10+
{
11+
return null;
12+
}
13+
14+
return JsonSerializer.Deserialize<ConversationReference>(JsonSerializer.Serialize(reference));
15+
}
16+
17+
static internal string GetKey(this ConversationReference reference)
18+
{
19+
return $"_{reference.Conversation?.TenantId}_{reference.Conversation?.Id}";
20+
}
21+
22+
static internal string GetTargetType(this ConversationReference reference)
23+
{
24+
return reference?.Conversation?.ConversationType;
25+
}
26+
}
27+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace {{SafeProjectName}}.Notification
2+
{
3+
using Microsoft.Agents.Core.Models;
4+
using Microsoft.Agents.Storage;
5+
6+
public interface IConversationReferenceStorage: IStorage
7+
{
8+
Task<PagedData<ConversationReference>> ListAsync(int? pageSize = null, string continuationToken = null, CancellationToken cancellationToken = default);
9+
}
10+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace {{SafeProjectName}}.Notification
2+
{
3+
using Microsoft.Agents.Builder;
4+
using Microsoft.Agents.Core.Models;
5+
using Microsoft.Agents.Extensions.Teams.Models;
6+
7+
static internal class ITurnContextExtensions
8+
{
9+
static internal string GetTeamsBotInstallationId(this ITurnContext context)
10+
{
11+
string result = null;
12+
var activity = context?.Activity;
13+
if (activity != null)
14+
{
15+
var channelData = activity.GetChannelData<TeamsChannelData>();
16+
if (channelData != null)
17+
{
18+
result = channelData?.Team?.Id;
19+
}
20+
21+
// Fallback to use conversation id.
22+
// The conversation id is equal to team id only when the bot app is installed into the General channel.
23+
if (result == null && activity.Conversation?.Name == null)
24+
{
25+
result = activity.Conversation?.Id;
26+
}
27+
}
28+
29+
return result;
30+
}
31+
}
32+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
namespace {{SafeProjectName}}.Notification
2+
{
3+
using Microsoft.Agents.Core.Models;
4+
using System.Text.Json;
5+
internal class LocalFileStorage: IConversationReferenceStorage
6+
{
7+
private readonly string _filePath;
8+
9+
public LocalFileStorage(string dirName)
10+
{
11+
var localFileName = Environment.GetEnvironmentVariable("TEAMSFX_NOTIFICATION_STORE_FILENAME") ?? ".notification.localstore.json";
12+
_filePath = Path.Combine(dirName, localFileName);
13+
}
14+
15+
public async Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken = default)
16+
{
17+
if (!File.Exists(_filePath))
18+
{
19+
return null;
20+
}
21+
22+
var result = new Dictionary<string, object>();
23+
var allData = await ReadFromFile(cancellationToken).ConfigureAwait(false);
24+
foreach (var key in keys)
25+
{
26+
if (allData.ContainsKey(key))
27+
{
28+
result[key] = allData[key];
29+
}
30+
}
31+
return result;
32+
}
33+
34+
public async Task<IDictionary<string, TStoreItem>> ReadAsync<TStoreItem>(string[] keys, CancellationToken cancellationToken = default) where TStoreItem : class
35+
{
36+
var storeItems = await ReadAsync(keys, cancellationToken).ConfigureAwait(false);
37+
var values = new Dictionary<string, TStoreItem>(keys.Length);
38+
foreach (var entry in storeItems)
39+
{
40+
if (entry.Value is TStoreItem valueAsType)
41+
{
42+
values.Add(entry.Key, valueAsType);
43+
}
44+
}
45+
return values;
46+
}
47+
48+
public async Task<PagedData<ConversationReference>> ListAsync(int? pageSize = null, string continuationToken = null, CancellationToken cancellationToken = default)
49+
{
50+
if (!File.Exists(_filePath))
51+
{
52+
return new PagedData<ConversationReference>
53+
{
54+
Data = Array.Empty<ConversationReference>(),
55+
ContinuationToken = null
56+
};
57+
}
58+
59+
var allData = await ReadFromFile(cancellationToken).ConfigureAwait(false);
60+
var allValues = allData.Values.ToList();
61+
62+
int skip = 0;
63+
if (!string.IsNullOrEmpty(continuationToken) && int.TryParse(continuationToken, out int tokenValue))
64+
{
65+
skip = tokenValue;
66+
}
67+
68+
int take = pageSize ?? allValues.Count;
69+
var page = allValues.Skip(skip).Take(take).ToArray();
70+
71+
string nextToken = (skip + take) < allValues.Count ? (skip + take).ToString() : null;
72+
73+
return new PagedData<ConversationReference>
74+
{
75+
Data = page.Cast<ConversationReference>().ToArray(),
76+
ContinuationToken = nextToken
77+
};
78+
}
79+
80+
public async Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken = default)
81+
{
82+
if (!File.Exists(_filePath))
83+
{
84+
var allData = new Dictionary<string, ConversationReference>(changes.Count);
85+
foreach (var kvp in changes)
86+
{
87+
if (!(kvp.Value is ConversationReference))
88+
{
89+
throw new ArgumentException($"Value for key '{kvp.Key}' is not of type 'ConversationReference'.");
90+
}
91+
allData[kvp.Key] = (ConversationReference)kvp.Value;
92+
}
93+
await WriteToFile(allData, cancellationToken).ConfigureAwait(false);
94+
}
95+
else
96+
{
97+
var allData = await ReadFromFile(cancellationToken).ConfigureAwait(false);
98+
foreach (var kvp in changes)
99+
{
100+
allData[kvp.Key] = (ConversationReference)kvp.Value;
101+
}
102+
await WriteToFile(allData, cancellationToken).ConfigureAwait(false);
103+
}
104+
}
105+
106+
public Task WriteAsync<TStoreItem>(IDictionary<string, TStoreItem> changes, CancellationToken cancellationToken = default) where TStoreItem : class
107+
{
108+
Dictionary<string, object> changesAsObject = new(changes.Count);
109+
foreach (var change in changes)
110+
{
111+
changesAsObject.Add(change.Key, change.Value);
112+
}
113+
return WriteAsync(changesAsObject, cancellationToken);
114+
}
115+
116+
public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken = default)
117+
{
118+
if (File.Exists(_filePath))
119+
{
120+
var allData = await ReadFromFile(cancellationToken).ConfigureAwait(false);
121+
foreach (var key in keys)
122+
{
123+
if (allData.ContainsKey(key))
124+
{
125+
allData.Remove(key);
126+
}
127+
}
128+
await WriteToFile(allData, cancellationToken).ConfigureAwait(false);
129+
}
130+
}
131+
132+
private async Task<IDictionary<string, ConversationReference>> ReadFromFile(CancellationToken cancellationToken = default)
133+
{
134+
var fileInfo = new FileInfo(_filePath);
135+
if (!fileInfo.Exists || fileInfo.Length == 0)
136+
{
137+
// return empty map
138+
return new Dictionary<string, ConversationReference>();
139+
}
140+
141+
using var file = File.OpenRead(_filePath);
142+
return await JsonSerializer.DeserializeAsync<IDictionary<string, ConversationReference>>(file, cancellationToken: cancellationToken).ConfigureAwait(false);
143+
}
144+
145+
private async Task WriteToFile(IDictionary<string, ConversationReference> data, CancellationToken cancellationToken = default)
146+
{
147+
using var file = File.Create(_filePath);
148+
await JsonSerializer.SerializeAsync(file, data, new JsonSerializerOptions { WriteIndented = true }, cancellationToken).ConfigureAwait(false);
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)