Skip to content

Commit

Permalink
port: [#4276] Add Teams read receipt event (#6356) (#4297)
Browse files Browse the repository at this point in the history
* Add support for Teams Read Receipt event

* Update botbuilder.api.md
  • Loading branch information
ceciliaavila authored Aug 2, 2022
1 parent 2e4b73a commit 5ee4dac
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 0 deletions.
3 changes: 3 additions & 0 deletions libraries/botbuilder/etc/botbuilder.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { NodeWebSocketFactoryBase } from 'botframework-streaming';
import { O365ConnectorCardActionQuery } from 'botbuilder-core';
import { PagedMembersResult } from 'botbuilder-core';
import { PagedResult } from 'botbuilder-core';
import { ReadReceiptInfo } from 'botframework-connector';
import { RequestHandler } from 'botframework-streaming';
import { ResourceResponse } from 'botbuilder-core';
import { SigninStateVerificationQuery } from 'botbuilder-core';
Expand Down Expand Up @@ -395,6 +396,8 @@ export class TeamsActivityHandler extends ActivityHandler {
onTeamsMembersAddedEvent(handler: (membersAdded: TeamsChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise<void>) => Promise<void>): this;
protected onTeamsMembersRemoved(context: TurnContext): Promise<void>;
onTeamsMembersRemovedEvent(handler: (membersRemoved: TeamsChannelAccount[], teamInfo: TeamInfo, context: TurnContext, next: () => Promise<void>) => Promise<void>): this;
protected onTeamsReadReceipt(context: TurnContext): Promise<void>;
onTeamsReadReceiptEvent(handler: (receiptInfo: ReadReceiptInfo, context: TurnContext, next: () => Promise<void>) => Promise<void>): this;
protected onTeamsTeamArchived(context: any): Promise<void>;
onTeamsTeamArchivedEvent(handler: (teamInfo: TeamInfo, context: TurnContext, next: () => Promise<void>) => Promise<void>): this;
protected onTeamsTeamDeleted(context: any): Promise<void>;
Expand Down
29 changes: 29 additions & 0 deletions libraries/botbuilder/src/teamsActivityHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
tokenExchangeOperationName,
verifyStateOperationName,
} from 'botbuilder-core';
import { ReadReceiptInfo } from 'botframework-connector';
import { TeamsInfo } from './teamsInfo';
import * as z from 'zod';

Expand Down Expand Up @@ -1001,6 +1002,8 @@ export class TeamsActivityHandler extends ActivityHandler {
protected async dispatchEventActivity(context: TurnContext): Promise<void> {
if (context.activity.channelId === Channels.Msteams) {
switch (context.activity.name) {
case 'application/vnd.microsoft.readReceipt':
return this.onTeamsReadReceipt(context);
case 'application/vnd.microsoft.meetingStart':
return this.onTeamsMeetingStart(context);
case 'application/vnd.microsoft.meetingEnd':
Expand Down Expand Up @@ -1033,6 +1036,17 @@ export class TeamsActivityHandler extends ActivityHandler {
await this.handle(context, 'TeamsMeetingEnd', this.defaultNextEvent(context));
}

/**
* Invoked when a read receipt for a previously sent message is received from the connector.
* Override this in a derived class to provide logic for when the bot receives a read receipt event.
*
* @param context The context for this turn.
* @returns A promise that represents the work queued.
*/
protected async onTeamsReadReceipt(context: TurnContext): Promise<void> {
await this.handle(context, 'TeamsReadReceipt', this.defaultNextEvent(context));
}

/**
* Registers a handler for when a Teams meeting starts.
*
Expand Down Expand Up @@ -1082,4 +1096,19 @@ export class TeamsActivityHandler extends ActivityHandler {
);
});
}

/**
* Registers a handler for when a Read Receipt is sent.
*
* @param handler A callback that handles Read Receipt events.
* @returns A promise that represents the work queued.
*/
onTeamsReadReceiptEvent(
handler: (receiptInfo: ReadReceiptInfo, context: TurnContext, next: () => Promise<void>) => Promise<void>
): this {
return this.on('TeamsReadReceipt', async (context, next) => {
const receiptInfo = context.activity.value;
await handler(new ReadReceiptInfo(receiptInfo.lastReadMessageId), context, next);
});
}
}
47 changes: 47 additions & 0 deletions libraries/botbuilder/tests/teamsActivityHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2350,5 +2350,52 @@ describe('TeamsActivityHandler', function () {
})
.startTest();
});

it('onTeamsReadReceipt routed activity', async function () {
let onTeamsReadReceiptCalled = false;
const bot = new TeamsActivityHandler();
const activity = {
channelId: Channels.Msteams,
type: 'event',
name: 'application/vnd.microsoft.readReceipt',
value: JSON.parse('{ "lastReadMessageId": 10101010}'),
};

bot.onEvent(async (context, next) => {
assert(context, 'context not found');
assert(next, 'next not found');
onEventCalled = true;
await next();
});

bot.onTeamsReadReceiptEvent(async (receiptInfo, context, next) => {
assert(receiptInfo, 'receiptInfo not found');
assert(context, 'context not found');
assert(next, 'next not found');
assert.strictEqual(receiptInfo.lastReadMessageId, activity.value.lastReadMessageId);
onTeamsReadReceiptCalled = true;
await next();
});

bot.onDialog(async (context, next) => {
assert(context, 'context not found');
assert(next, 'next not found');
onDialogCalled = true;
await next();
});

const adapter = new TestAdapter(async (context) => {
await bot.run(context);
});

await adapter
.send(activity)
.then(() => {
assert(onTeamsReadReceiptCalled);
assert(onEventCalled, 'onConversationUpdate handler not called');
assert(onDialogCalled, 'onDialog handler not called');
})
.startTest();
});
});
});
1 change: 1 addition & 0 deletions libraries/botframework-connector/src/teams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
export * from './teamsConnectorClient';
export * from './teamsConnectorClientContext';
export * from './models';
export * from './readReceiptInfo';
60 changes: 60 additions & 0 deletions libraries/botframework-connector/src/teams/readReceiptInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/**
* General information about a read receipt.
*/
export class ReadReceiptInfo {
/**
* The id of the last read message.
*/
lastReadMessageId: string;

/**
* Initializes a new instance of the ReadReceiptInfo class.
*
* @param lastReadMessageId Optional. The id of the last read message.
*/
constructor(lastReadMessageId?: string) {
this.lastReadMessageId = lastReadMessageId;
}

/**
* Helper method useful for determining if a message has been read. This method
* converts the strings to numbers. If the compareMessageId is less than or equal to
* the lastReadMessageId, then the message has been read.
*
* @param compareMessageId The id of the message to compare.
* @param lastReadMessageId The id of the last message read by the user.
* @returns True if the compareMessageId is less than or equal to the lastReadMessageId.
*/
static isMessageRead(compareMessageId: string, lastReadMessageId: string): boolean {
if (
compareMessageId &&
compareMessageId.trim().length > 0 &&
lastReadMessageId &&
lastReadMessageId.trim().length > 0
) {
const compareMessageIdNum = Number(compareMessageId);
const lastReadMessageIdNum = Number(lastReadMessageId);

if (compareMessageIdNum && lastReadMessageIdNum) {
return compareMessageIdNum <= lastReadMessageIdNum;
}
}
return false;
}

/**
* Helper method useful for determining if a message has been read.
* If the compareMessageId is less than or equal to the lastReadMessageId, then the message has been read.
*
* @param compareMessageId The id of the message to compare.
* @returns True if the compareMessageId is less than or equal to the lastReadMessageId.
*/
isMessageRead(compareMessageId: string): boolean {
return ReadReceiptInfo.isMessageRead(compareMessageId, this.lastReadMessageId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const assert = require('assert');
const { ReadReceiptInfo } = require('../..');

describe('ReadReceiptInfo', function () {
const testCases = [
{ title: 'compare msg equal to last', compare: '1000', lastRead: '1000', isRead: true },
{ title: 'compare msg < than last', compare: '1000', lastRead: '1001', isRead: true },
{ title: 'compare msg > than last', compare: '1001', lastRead: '1000', isRead: false },
{ title: 'null compare msg', compare: null, lastRead: '1000', isRead: false },
{ title: 'null last msg', compare: '1000', lastRead: null, isRead: false },
];

testCases.map((testData) => {
it(testData.title, function () {
const readReceipt = new ReadReceiptInfo(testData.lastRead);

assert.strictEqual(readReceipt.lastReadMessageId, testData.lastRead);
assert.strictEqual(readReceipt.isMessageRead(testData.compare), testData.isRead);
assert.strictEqual(ReadReceiptInfo.isMessageRead(testData.compare, testData.lastRead), testData.isRead);
});
});
});

0 comments on commit 5ee4dac

Please sign in to comment.