Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

port: [#4276] Add Teams read receipt event (#6356) #4297

Merged
merged 2 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
});