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

microsoft / botbuilder-js / 12302726031

12 Dec 2024 06:40PM UTC coverage: 84.055% (-0.6%) from 84.691%
12302726031

Pull #4806

github

web-flow
Merge 87fc178f5 into 3b8fcab21
Pull Request #4806: feat: Support Sso for SharePoint bot ACEs

8149 of 10862 branches covered (75.02%)

Branch coverage included in aggregate %.

7 of 57 new or added lines in 5 files covered. (12.28%)

153 existing lines in 14 files now uncovered.

20438 of 23148 relevant lines covered (88.29%)

3457.65 hits per line

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

98.11
/libraries/botbuilder/src/fileTranscriptStore.ts
1
/**
2
 * @module botbuilder
3
 */
4
/**
5
 * Copyright (c) Microsoft Corporation. All rights reserved.
6
 * Licensed under the MIT License.
7
 */
8
import { join, parse } from 'path';
1✔
9
import { mkdirp, pathExists, readdir, readFile, remove, writeFile } from 'fs-extra';
1✔
10
import { Activity, PagedResult, TranscriptInfo, TranscriptStore } from 'botbuilder-core';
11
import filenamify from '../vendors/filenamify/index';
1✔
12

13
/**
14
 * @private
15
 * The number of .net ticks at the unix epoch.
16
 */
17
const epochTicks = 621355968000000000;
1✔
18

19
/**
20
 * @private
21
 * There are 10000 .net ticks per millisecond.
22
 */
23
const ticksPerMillisecond = 10000;
1✔
24

25
/**
26
 * @private
27
 * @param timestamp A date used to calculate future ticks.
28
 */
29
function getTicks(timestamp: Date): string {
30
    const ticks: number = epochTicks + timestamp.getTime() * ticksPerMillisecond;
332✔
31

32
    return ticks.toString(16);
332✔
33
}
34

35
/**
36
 * @private
37
 * @param ticks A string containing ticks.
38
 */
39
function readDate(ticks: string): Date {
40
    const t: number = Math.round((parseInt(ticks, 16) - epochTicks) / ticksPerMillisecond);
730✔
41

42
    return new Date(t);
730✔
43
}
44

45
/**
46
 * @private
47
 * @param date A date used to create a filter.
48
 * @param fileName The filename containing the timestamp string
49
 */
50
function withDateFilter(date: Date, fileName: string): any {
51
    if (!date) {
1,722✔
52
        return true;
992✔
53
    }
54

55
    const ticks: string = fileName.split('-')[0];
730✔
56
    return readDate(ticks) >= date;
730✔
57
}
58

59
/**
60
 * @private
61
 * @param expression A function that will be used to test items.
62
 */
63
function includeWhen(expression: any): any {
64
    let shouldInclude = false;
25✔
65

66
    return (item: any): boolean => {
25✔
67
        return shouldInclude || (shouldInclude = expression(item));
2,202✔
68
    };
69
}
70

71
/**
72
 * @private
73
 * @param json A JSON string to be parsed into an activity.
74
 */
75
function parseActivity(json: string): Activity {
76
    const activity: Activity = JSON.parse(json);
312✔
77
    activity.timestamp = new Date(activity.timestamp);
312✔
78

79
    return activity;
312✔
80
}
81

82
/**
83
 * The file transcript store stores transcripts in file system with each activity as a file.
84
 *
85
 * @remarks
86
 * This class provides an interface to log all incoming and outgoing activities to the filesystem.
87
 * It implements the features necessary to work alongside the TranscriptLoggerMiddleware plugin.
88
 * When used in concert, your bot will automatically log all conversations.
89
 *
90
 * Below is the boilerplate code needed to use this in your app:
91
 * ```javascript
92
 * const { FileTranscriptStore, TranscriptLoggerMiddleware } = require('botbuilder');
93
 *
94
 * adapter.use(new TranscriptLoggerMiddleware(new FileTranscriptStore(__dirname + '/transcripts/')));
95
 * ```
96
 */
97
export class FileTranscriptStore implements TranscriptStore {
1✔
98
    private static readonly PageSize: number = 20;
1✔
99

100
    private readonly rootFolder: string;
101

102
    /**
103
     * Creates an instance of FileTranscriptStore.
104
     *
105
     * @param folder Root folder where transcript will be stored.
106
     */
107
    constructor(folder: string) {
108
        if (!folder) {
4✔
109
            throw new Error('Missing folder.');
1✔
110
        }
111

112
        this.rootFolder = folder;
3✔
113
    }
114

115
    /**
116
     * Log an activity to the transcript.
117
     *
118
     * @param activity Activity being logged.
119
     * @returns {Promise<void>} a promise representing the asynchronous operation.
120
     */
121
    async logActivity(activity: Activity): Promise<void> {
122
        if (!activity) {
333✔
123
            throw new Error('activity cannot be null for logActivity()');
1✔
124
        }
125

126
        const conversationFolder: string = this.getTranscriptFolder(activity.channelId, activity.conversation.id);
332✔
127
        const activityFileName: string = this.getActivityFilename(activity);
332✔
128

129
        return this.saveActivity(activity, conversationFolder, activityFileName);
332✔
130
    }
131

132
    /**
133
     * Get all activities associated with a conversation id (aka get the transcript).
134
     *
135
     * @param channelId Channel Id.
136
     * @param conversationId Conversation Id.
137
     * @param continuationToken (Optional) Continuation token to page through results.
138
     * @param startDate (Optional) Earliest time to include.
139
     * @returns {Promise<PagedResult<Activity>>} PagedResult of activities.
140
     */
141
    async getTranscriptActivities(
142
        channelId: string,
143
        conversationId: string,
144
        continuationToken?: string,
145
        startDate?: Date
146
    ): Promise<PagedResult<Activity>> {
147
        if (!channelId) {
25✔
148
            throw new Error('Missing channelId');
1✔
149
        }
150

151
        if (!conversationId) {
24✔
152
            throw new Error('Missing conversationId');
1✔
153
        }
154

155
        const pagedResult: PagedResult<Activity> = { items: [], continuationToken: undefined };
23✔
156
        const transcriptFolder: string = this.getTranscriptFolder(channelId, conversationId);
23✔
157

158
        const exists = await pathExists(transcriptFolder);
23✔
159
        if (!exists) {
23✔
160
            return pagedResult;
3✔
161
        }
162

163
        const transcriptFolderContents = await readdir(transcriptFolder);
20✔
164
        const include = includeWhen((fileName) => !continuationToken || parse(fileName).name === continuationToken);
655✔
165
        const items = transcriptFolderContents.filter(
20✔
166
            (transcript) => transcript.endsWith('.json') && withDateFilter(startDate, transcript) && include(transcript)
1,722✔
167
        );
168

169
        pagedResult.items = await Promise.all(
20✔
170
            items
171
                .slice(0, FileTranscriptStore.PageSize)
172
                .sort()
173
                .map(async (activityFilename) => {
312✔
174
                    const json = await readFile(join(transcriptFolder, activityFilename), 'utf8');
312✔
175
                    return parseActivity(json);
312✔
176
                })
177
        );
178
        const { length } = pagedResult.items;
20✔
179
        if (length === FileTranscriptStore.PageSize && items[length]) {
20✔
180
            pagedResult.continuationToken = parse(items[length]).name;
12✔
181
        }
182
        return pagedResult;
20✔
183
    }
184

185
    /**
186
     * List all the logged conversations for a given channelId.
187
     *
188
     * @param channelId Channel Id.
189
     * @param continuationToken (Optional) Continuation token to page through results.
190
     * @returns {Promise<PagedResult<TranscriptInfo>>} PagedResult of transcripts.
191
     */
192
    async listTranscripts(channelId: string, continuationToken?: string): Promise<PagedResult<TranscriptInfo>> {
193
        if (!channelId) {
6✔
194
            throw new Error('Missing channelId');
1✔
195
        }
196

197
        const pagedResult: PagedResult<TranscriptInfo> = { items: [], continuationToken: undefined };
5✔
198
        const channelFolder: string = this.getChannelFolder(channelId);
5✔
199

200
        const exists = await pathExists(channelFolder);
5✔
201
        if (!exists) {
5!
UNCOV
202
            return pagedResult;
×
203
        }
204
        const channels = await readdir(channelFolder);
5✔
205
        const items = channels.filter(includeWhen((di) => !continuationToken || di === continuationToken));
205✔
206
        pagedResult.items = items
5✔
207
            .slice(0, FileTranscriptStore.PageSize)
208
            .map((i) => ({ channelId: channelId, id: i, created: null }));
100✔
209
        const { length } = pagedResult.items;
5✔
210
        if (length === FileTranscriptStore.PageSize && items[length]) {
5✔
211
            pagedResult.continuationToken = items[length];
4✔
212
        }
213

214
        return pagedResult;
5✔
215
    }
216

217
    /**
218
     * Delete a conversation and all of it's activities.
219
     *
220
     * @param channelId Channel Id where conversation took place.
221
     * @param conversationId Id of the conversation to delete.
222
     * @returns {Promise<void>} A promise representing the asynchronous operation.
223
     */
224
    async deleteTranscript(channelId: string, conversationId: string): Promise<void> {
225
        if (!channelId) {
4✔
226
            throw new Error('Missing channelId');
1✔
227
        }
228

229
        if (!conversationId) {
3✔
230
            throw new Error('Missing conversationId');
1✔
231
        }
232

233
        const transcriptFolder: string = this.getTranscriptFolder(channelId, conversationId);
2✔
234

235
        return remove(transcriptFolder);
2✔
236
    }
237

238
    /**
239
     * Saves the [Activity](xref:botframework-schema.Activity) as a JSON file.
240
     *
241
     * @param activity The [Activity](xref:botframework-schema.Activity) to transcript.
242
     * @param transcriptPath The path where the transcript will be saved.
243
     * @param activityFilename The name for the file.
244
     * @returns {Promise<void>} A promise representing the asynchronous operation.
245
     */
246
    private async saveActivity(activity: Activity, transcriptPath: string, activityFilename: string): Promise<void> {
247
        const json: string = JSON.stringify(activity, null, '\t');
332✔
248

249
        const exists = await pathExists(transcriptPath);
332✔
250
        if (!exists) {
332✔
251
            await mkdirp(transcriptPath);
332✔
252
        }
253
        return writeFile(join(transcriptPath, activityFilename), json, 'utf8');
332✔
254
    }
255

256
    /**
257
     * @private
258
     */
259
    private getActivityFilename(activity: Activity): string {
260
        return `${getTicks(activity.timestamp)}-${this.sanitizeKey(activity.id)}.json`;
332✔
261
    }
262

263
    /**
264
     * @private
265
     */
266
    private getChannelFolder(channelId: string): string {
267
        return join(this.rootFolder, this.sanitizeKey(channelId));
5✔
268
    }
269

270
    /**
271
     * @private
272
     */
273
    private getTranscriptFolder(channelId: string, conversationId: string): string {
274
        return join(this.rootFolder, this.sanitizeKey(channelId), this.sanitizeKey(conversationId));
357✔
275
    }
276

277
    /**
278
     * @private
279
     */
280
    private sanitizeKey(key: string): string {
281
        return filenamify(key);
1,051✔
282
    }
283
}
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

© 2025 Coveralls, Inc