• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 388636

03 May 2024 02:02PM UTC coverage: 78.171% (+0.02%) from 78.153%
388636

push

CI-PR build

web-flow
Microsoft.Identity.Client bump (#6779)

* Microsoft.Identity.Client bump

* Compensated for new ManagedIdentityClient auto-retrying requests

---------

Co-authored-by: Tracy Boehrer <trboehre@microsoft.com>

26189 of 33502 relevant lines covered (78.17%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.24
/libraries/Microsoft.Bot.Builder/FileTranscriptLogger.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.IO;
7
using System.Linq;
8
using System.Text;
9
using System.Threading.Tasks;
10
using Microsoft.Bot.Schema;
11
using Newtonsoft.Json;
12

13
namespace Microsoft.Bot.Builder
14
{
15
    /// <summary>
16
    /// FileTranscriptLogger which creates a .transcript file for each conversationId.
17
    /// </summary>
18
    /// <remarks>
19
    /// This is a useful class for unit tests.
20
    /// </remarks>
21
    public class FileTranscriptLogger : ITranscriptStore
22
    {
23
        private static readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings()
1✔
24
        {
1✔
25
            Formatting = Formatting.Indented,
1✔
26
            NullValueHandling = NullValueHandling.Ignore,
1✔
27
            MaxDepth = null
1✔
28
        };
1✔
29

30
        private readonly string _folder;
31
        private readonly bool _unitTestMode;
32
        private readonly HashSet<string> _started = new HashSet<string>();
1✔
33

34
        /// <summary>
35
        /// Initializes a new instance of the <see cref="FileTranscriptLogger"/> class.
36
        /// </summary>
37
        /// <param name="folder">folder to place the transcript files (Default current folder).</param>
38
        /// <param name="unitTestMode">unitTestMode will overwrite transcript files.</param>
39
        public FileTranscriptLogger(string folder = null, bool unitTestMode = true)
1✔
40
        {
41
            if (folder == null)
1✔
42
            {
43
                folder = Environment.CurrentDirectory;
×
44
            }
45

46
            folder = PathUtils.NormalizePath(folder);
1✔
47

48
            if (!Directory.Exists(folder))
1✔
49
            {
50
                Directory.CreateDirectory(folder);
1✔
51
            }
52

53
            this._folder = folder;
1✔
54
            this._unitTestMode = unitTestMode;
1✔
55
        }
1✔
56

57
        /// <summary>
58
        /// Log an activity to the transcript.
59
        /// </summary>
60
        /// <param name="activity">The activity to transcribe.</param>
61
        /// <returns>A task that represents the work queued to execute.</returns>
62
        public async Task LogActivityAsync(IActivity activity)
63
        {
64
            if (activity == null)
1✔
65
            {
66
                throw new ArgumentNullException(nameof(activity));
1✔
67
            }
68

69
            var transcriptFile = GetTranscriptFile(activity.ChannelId, activity.Conversation.Id);
1✔
70

71
            if (System.Diagnostics.Debugger.IsAttached && activity.Type == ActivityTypes.Message)
×
72
            {
73
                System.Diagnostics.Trace.TraceInformation($"{activity.From.Name ?? activity.From.Id ?? activity.From.Role} [{activity.Type}] {activity.AsMessageActivity()?.Text}");
×
74
            }
75
            else
76
            {
77
                System.Diagnostics.Trace.TraceInformation($"{activity.From.Name ?? activity.From.Id ?? activity.From.Role} [{activity.Type}]");
×
78
            }
79

80
            // try 3 times
81
            for (int i = 0; i < 3; i++)
×
82
            {
83
                try
84
                {
85
                    if ((this._unitTestMode == true && !_started.Contains(transcriptFile)) || !File.Exists(transcriptFile))
1✔
86
                    {
87
                        System.Diagnostics.Trace.TraceInformation($"file://{transcriptFile.Replace("\\", "/")}");
1✔
88
                        _started.Add(transcriptFile);
1✔
89

90
                        using (var stream = File.OpenWrite(transcriptFile))
1✔
91
                        {
92
                            using (var writer = new StreamWriter(stream) as TextWriter)
1✔
93
                            {
94
                                await writer.WriteAsync($"[{JsonConvert.SerializeObject(activity, _jsonSettings)}]").ConfigureAwait(false);
1✔
95
                                return;
1✔
96
                            }
97
                        }
98
                    }
99

100
                    switch (activity.Type)
1✔
101
                    {
102
                        case ActivityTypes.MessageDelete:
103
                            await MessageDeleteAsync(activity, transcriptFile).ConfigureAwait(false);
1✔
104
                            return;
1✔
105

106
                        case ActivityTypes.MessageUpdate:
107
                            await MessageUpdateAsync(activity, transcriptFile).ConfigureAwait(false);
1✔
108
                            return;
1✔
109

110
                        default:
111
                            // append
112
                            await LogActivityAsync(activity, transcriptFile).ConfigureAwait(false);
1✔
113
                            return;
1✔
114
                    }
115
                }
116
#pragma warning disable CA1031 // Do not catch general exception types (we ignore the exception and we retry)
117
                catch (Exception e)
×
118
#pragma warning restore CA1031 // Do not catch general exception types
119
                {
120
                    // try again
121
                    System.Diagnostics.Trace.TraceError($"Try {i + 1} - Failed to log activity because: {e.GetType()} : {e.Message}");
×
122
                }
×
123
            }
124
        }
1✔
125

126
        /// <summary>
127
        /// Gets from the store activities that match a set of criteria.
128
        /// </summary>
129
        /// <param name="channelId">The ID of the channel the conversation is in.</param>
130
        /// <param name="conversationId">The ID of the conversation.</param>
131
        /// <param name="continuationToken">The continuation token (if available).</param>
132
        /// <param name="startDate">A cutoff date. Activities older than this date are not included.</param>
133
        /// <returns>A task that represents the work queued to execute.</returns>
134
        /// <remarks>If the task completes successfully, the result contains the matching activities.</remarks>
135
        public async Task<PagedResult<IActivity>> GetTranscriptActivitiesAsync(string channelId, string conversationId, string continuationToken = null, DateTimeOffset startDate = default(DateTimeOffset))
136
        {
137
            var transcriptFile = GetTranscriptFile(channelId, conversationId);
1✔
138

139
            var transcript = await LoadTranscriptAsync(transcriptFile).ConfigureAwait(false);
1✔
140
            var result = new PagedResult<IActivity>();
1✔
141
            result.ContinuationToken = null;
1✔
142
            result.Items = transcript.Where(activity => activity.Timestamp >= startDate).Cast<IActivity>().ToArray();
1✔
143
            return result;
1✔
144
        }
1✔
145

146
        /// <summary>
147
        /// Gets the conversations on a channel from the store.
148
        /// </summary>
149
        /// <param name="channelId">The ID of the channel.</param>
150
        /// <param name="continuationToken">Continuation token (if available).</param>
151
        /// <returns>A task that represents the work queued to execute.</returns>
152
        /// <remarks>List all transcripts for given ChannelID.</remarks>
153
        public Task<PagedResult<TranscriptInfo>> ListTranscriptsAsync(string channelId, string continuationToken = null)
154
        {
155
            List<TranscriptInfo> transcripts = new List<TranscriptInfo>();
1✔
156
            var channelFolder = GetChannelFolder(channelId);
1✔
157

158
            foreach (var file in Directory.EnumerateFiles(channelFolder, "*.transcript"))
1✔
159
            {
160
                transcripts.Add(new TranscriptInfo()
1✔
161
                {
1✔
162
                    ChannelId = channelId,
1✔
163
                    Id = Path.GetFileNameWithoutExtension(file),
1✔
164
                    Created = File.GetCreationTime(file),
1✔
165
                });
1✔
166
            }
167

168
            return Task.FromResult(new PagedResult<TranscriptInfo>()
1✔
169
            {
1✔
170
                Items = transcripts.ToArray(),
1✔
171
                ContinuationToken = null,
1✔
172
            });
1✔
173
        }
174

175
        /// <summary>
176
        /// Deletes conversation data from the store.
177
        /// </summary>
178
        /// <param name="channelId">The ID of the channel the conversation is in.</param>
179
        /// <param name="conversationId">The ID of the conversation to delete.</param>
180
        /// <returns>A task that represents the work queued to execute.</returns>
181
        public Task DeleteTranscriptAsync(string channelId, string conversationId)
182
        {
183
            var transcriptFile = GetTranscriptFile(channelId, conversationId);
1✔
184
            File.Delete(transcriptFile);
1✔
185
            return Task.CompletedTask;
1✔
186
        }
187

188
        private static async Task<Activity[]> LoadTranscriptAsync(string transcriptFile)
189
        {
190
            if (File.Exists(transcriptFile))
1✔
191
            {
192
                using (var stream = File.OpenRead(transcriptFile))
1✔
193
                {
194
                    using (var reader = new StreamReader(stream) as TextReader)
1✔
195
                    {
196
                        var json = await reader.ReadToEndAsync().ConfigureAwait(false);
1✔
197
                        return JsonConvert.DeserializeObject<Activity[]>(json, new JsonSerializerSettings { MaxDepth = null });
1✔
198
                    }
199
                }
200
            }
201

202
            return Array.Empty<Activity>();
1✔
203
        }
1✔
204

205
        private static async Task LogActivityAsync(IActivity activity, string transcriptFile)
206
        {
207
            var json = $",\n{JsonConvert.SerializeObject(activity, _jsonSettings)}]";
1✔
208

209
            using (var stream = File.Open(transcriptFile, FileMode.OpenOrCreate))
1✔
210
            {
211
                if (stream.Length > 0)
1✔
212
                {
213
                    stream.Seek(-1, SeekOrigin.End);
1✔
214
                }
215

216
                using (TextWriter writer = new StreamWriter(stream))
1✔
217
                {
218
                    await writer.WriteAsync(json).ConfigureAwait(false);
1✔
219
                }
1✔
220
            }
1✔
221
        }
1✔
222

223
        private static async Task MessageUpdateAsync(IActivity activity, string transcriptFile)
224
        {
225
            // load all activities
226
            var transcript = await LoadTranscriptAsync(transcriptFile).ConfigureAwait(false);
1✔
227

228
            for (int i = 0; i < transcript.Length; i++)
1✔
229
            {
230
                var originalActivity = transcript[i];
1✔
231
                if (originalActivity.Id == activity.Id)
1✔
232
                {
233
                    var serializerSettings = new JsonSerializerSettings { MaxDepth = null };
1✔
234
                    var updatedActivity = JsonConvert.DeserializeObject<Activity>(JsonConvert.SerializeObject(activity, serializerSettings), serializerSettings);
1✔
235
                    updatedActivity.Type = originalActivity.Type; // fixup original type (should be Message)
1✔
236
                    updatedActivity.LocalTimestamp = originalActivity.LocalTimestamp;
1✔
237
                    updatedActivity.Timestamp = originalActivity.Timestamp;
1✔
238
                    transcript[i] = updatedActivity;
1✔
239
                    var json = JsonConvert.SerializeObject(transcript, _jsonSettings);
1✔
240
                    using (var stream = File.OpenWrite(transcriptFile))
1✔
241
                    {
242
                        using (var writer = new StreamWriter(stream) as TextWriter)
1✔
243
                        {
244
                            await writer.WriteAsync(json).ConfigureAwait(false);
1✔
245
                            return;
1✔
246
                        }
247
                    }
248
                }
249
            }
250
        }
1✔
251

252
        private static async Task MessageDeleteAsync(IActivity activity, string transcriptFile)
253
        {
254
            // load all activities
255
            var transcript = await LoadTranscriptAsync(transcriptFile).ConfigureAwait(false);
1✔
256

257
            // if message delete comes in, delete the message from the transcript
258
            for (int index = 0; index < transcript.Length; index++)
1✔
259
            {
260
                var originalActivity = transcript[index];
1✔
261
                if (originalActivity.Id == activity.Id)
1✔
262
                {
263
                    // tombstone the original message
264
                    transcript[index] = new Activity()
1✔
265
                    {
1✔
266
                        Type = ActivityTypes.MessageDelete,
1✔
267
                        Id = originalActivity.Id,
1✔
268
                        From = new ChannelAccount(id: "deleted", role: originalActivity.From.Role),
1✔
269
                        Recipient = new ChannelAccount(id: "deleted", role: originalActivity.Recipient.Role),
1✔
270
                        Locale = originalActivity.Locale,
1✔
271
                        LocalTimestamp = originalActivity.Timestamp,
1✔
272
                        Timestamp = originalActivity.Timestamp,
1✔
273
                        ChannelId = originalActivity.ChannelId,
1✔
274
                        Conversation = originalActivity.Conversation,
1✔
275
                        ServiceUrl = originalActivity.ServiceUrl,
1✔
276
                        ReplyToId = originalActivity.ReplyToId,
1✔
277
                    };
1✔
278
                    var json = JsonConvert.SerializeObject(transcript, _jsonSettings);
1✔
279
                    using (var stream = File.OpenWrite(transcriptFile))
1✔
280
                    {
281
                        using (var writer = new StreamWriter(stream) as TextWriter)
1✔
282
                        {
283
                            await writer.WriteAsync(json).ConfigureAwait(false);
1✔
284
                            return;
1✔
285
                        }
286
                    }
287
                }
288
            }
289
        }
1✔
290

291
        private static string SanitizeString(string str, char[] invalidChars)
292
        {
293
            var sb = new StringBuilder(str);
1✔
294

295
            foreach (var invalidChar in invalidChars)
1✔
296
            {
297
                sb.Replace(invalidChar.ToString(), string.Empty);
1✔
298
            }
299

300
            return sb.ToString();
1✔
301
        }
302

303
        private string GetTranscriptFile(string channelId, string conversationId)
304
        {
305
            if (channelId == null)
1✔
306
            {
307
                throw new ArgumentNullException(channelId);
1✔
308
            }
309

310
            if (conversationId == null)
1✔
311
            {
312
                throw new ArgumentNullException(nameof(conversationId));
1✔
313
            }
314

315
            var channelFolder = GetChannelFolder(channelId);
1✔
316

317
            var fileName = SanitizeString(conversationId, Path.GetInvalidFileNameChars());
1✔
318

319
            return Path.Combine(channelFolder, fileName + ".transcript");
1✔
320
        }
321

322
        private string GetChannelFolder(string channelId)
323
        {
324
            if (channelId == null)
1✔
325
            {
326
                throw new ArgumentNullException(channelId);
1✔
327
            }
328

329
            var folderName = SanitizeString(channelId, Path.GetInvalidPathChars());
1✔
330

331
            var channelFolder = Path.Combine(_folder, folderName);
1✔
332
            if (!Directory.Exists(channelFolder))
1✔
333
            {
334
                Directory.CreateDirectory(channelFolder);
1✔
335
            }
336

337
            return channelFolder;
1✔
338
        }
339
    }
340
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc