8000 Port: Send targeted meeting notification in Teams meeting (#2172) · sahilparekh/botbuilder-python@0f2cb2a · GitHub
[go: up one dir, main page]

Skip to content

Commit 0f2cb2a

Browse files
Port: Send targeted meeting notification in Teams meeting (microsoft#2172)
* Send targeted meeting notification in Teams meeting * test cases --------- Co-authored-by: tracyboehrer <tracyboehrer@users.noreply.github.com>
1 parent d4a1867 commit 0f2cb2a

File tree

5 files changed

+522
-0
lines changed

5 files changed

+522
-0
lines changed

libraries/botbuilder-core/botbuilder/core/teams/teams_info.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
TeamsChannelAccount,
2626
TeamsPagedMembersResult,
2727
TeamsMeetingParticipant,
28+
MeetingNotificationBase,
29+
MeetingNotificationResponse,
2830
)
2931

3032

@@ -100,6 +102,31 @@ async def _legacy_send_message_to_teams_channel(
100102
)
101103
return (result[0], result[1])
102104

105+
@staticmethod
106+
async def send_meeting_notification(
107+
turn_context: TurnContext,
108+
notification: MeetingNotificationBase,
109+
meeting_id: str = None,
110+
) -> MeetingNotificationResponse:
111+
meeting_id = (
112+
meeting_id
113+
if meeting_id
114+
else teams_get_meeting_info(turn_context.activity).id
115+
)
116+
if meeting_id is None:
117+
raise TypeError(
118+
"TeamsInfo._send_meeting_notification: method requires a meeting_id or "
119+
"TurnContext that contains a meeting id"
120+
)
121+
122+
if notification is None:
123+
raise TypeError("notification is required.")
124+
125+
connector_client = await TeamsInfo.get_teams_connector_client(turn_context)
126+
return await connector_client.teams.send_meeting_notification(
127+
meeting_id, notification
128+
)
129+
103130
@staticmethod
104131
async def _create_conversation_callback(
105132
new_turn_context,

libraries/botbuilder-core/tests/teams/test_teams_info.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4+
import json
45
import aiounittest
6+
from botbuilder.schema.teams._models_py3 import (
7+
ContentType,
8+
MeetingNotificationChannelData,
9+
MeetingStageSurface,
10+
MeetingTabIconSurface,
11+
OnBehalfOf,
12+
TargetedMeetingNotification,
13+
TargetedMeetingNotificationValue,
14+
TaskModuleContinueResponse,
15+
TaskModuleTaskInfo,
16+
)
517
from botframework.connector import Channels
618

719
from botbuilder.core import TurnContext, MessageFactory
@@ -234,13 +246,62 @@ async def test_get_meeting_info(self):
234246
handler = TeamsActivityHandler()
235247
await handler.on_turn(turn_context)
236248

249+
async def test_send_meeting_notificationt(self):
250+
test_cases = [
251+
("202", "accepted"),
252+
(
253+
"207",
254+
"if the notifications are sent only to parital number of recipients\
255+
because the validation on some recipients' ids failed or some\
256+
recipients were not found in the roster. In this case, \
257+
SMBA will return the user MRIs of those failed recipients\
258+
in a format that was given to a bot (ex: if a bot sent \
259+
encrypted user MRIs, return encrypted one).",
260+
),
261+
(
262+
"400",
263+
"when Meeting Notification request payload validation fails. For instance,\
264+
Recipients: # of recipients is greater than what the API allows ||\
265+
all of recipients' user ids were invalid, Surface: Surface list\
266+
is empty or null, Surface type is invalid, Duplicative \
267+
surface type exists in one payload",
268+
),
269+
(
270+
"403",
271+
"if the bot is not allowed to send the notification. In this case,\
272+
the payload should contain more detail error message. \
273+
There can be many reasons: bot disabled by tenant admin,\
274+
blocked during live site mitigation, the bot does not\
275+
have a correct RSC permission for a specific surface type, etc",
276+
),
277+
]
278+
for status_code, expected_message in test_cases:
279+
adapter = SimpleAdapterWithCreateConversation()
280+
281+
activity = Activity(
282+
type="targetedMeetingNotification",
283+
text="Test-send_meeting_notificationt",
284+
channel_id=Channels.ms_teams,
285+
from_property=ChannelAccount(
286+
aad_object_id="participantId-1", name=status_code
287+
),
288+
service_url="https://test.coffee",
289+
conversation=ConversationAccount(id="conversation-id"),
290+
)
291+
292+
turn_context = TurnContext(adapter, activity)
293+
handler = TeamsActivityHandler()
294+
await handler.on_turn(turn_context)
295+
237296

238297
class TestTeamsActivityHandler(TeamsActivityHandler):
239298
async def on_turn(self, turn_context: TurnContext):
240299
await super().on_turn(turn_context)
241300

242301
if turn_context.activity.text == "test_send_message_to_teams_channel":
243302
await self.call_send_message_to_teams(turn_context)
303+
elif turn_context.activity.text == "test_send_meeting_notification":
304+
await self.call_send_meeting_notification(turn_context)
244305

245306
async def call_send_message_to_teams(self, turn_context: TurnContext):
246307
msg = MessageFactory.text("call_send_message_to_teams")
@@ -251,3 +312,71 @@ async def call_send_message_to_teams(self, turn_context: TurnContext):
251312

252313
assert reference[0].activity_id == "new_conversation_id"
253314
assert reference[1] == "reference123"
315+
316+
async def call_send_meeting_notification(self, turn_context: TurnContext):
317+
from_property = turn_context.activity.from_property
318+
try:
319+
# Send the meeting notification asynchronously
320+
failed_participants = await TeamsInfo.send_meeting_notification(
321+
turn_context,
322+
self.get_targeted_meeting_notification(from_property),
323+
"meeting-id",
324+
)
325+
326+
# Handle based on the 'from_property.name'
327+
if from_property.name == "207":
328+
self.assertEqual(
329+
"failingid",
330+
failed_participants.recipients_failure_info[0].recipient_mri,
331+
)
332+
elif from_property.name == "202":
333+
assert failed_participants is None
334+
else:
335+
raise TypeError(
336+
f"Expected HttpOperationException with response status code {from_property.name}."
337+
)
338+
339+
except ValueError as ex:
340+
# Assert that the response status code matches the from_property.name
341+
assert from_property.name == str(int(ex.response.status_code))
342+
343+
# Deserialize the error response content to an ErrorResponse object
344+
error_response = json.loads(ex.response.content)
345+
346+
# Handle based on error codes
347+
if from_property.name == "400":
348+
assert error_response["error"]["code"] == "BadSyntax"
349+
elif from_property.name == "403":
350+
assert error_response["error"]["code"] == "BotNotInConversationRoster"
351+
else:
352+
raise TypeError(
353+
f"Expected HttpOperationException with response status code {from_property.name}."
354+
)
355+
356+
def get_targeted_meeting_notification(self, from_account: ChannelAccount):
357+
recipients = [from_account.id]
358+
359+
if from_account.name == "207":
360+
recipients.append("failingid")
361+
362+
meeting_stage_surface = MeetingStageSurface(
363+
content=TaskModuleContinueResponse(
364+
value=TaskModuleTaskInfo(title="title here", height=3, width=2)
365+
),
366+
content_type=ContentType.Task,
367+
)
368+
369+
meeting_tab_icon_surface = MeetingTabIconSurface(
370+
tab_entity_id="test tab entity id"
371+
)
372+
373+
value = TargetedMeetingNotificationValue(
374+
recipients=recipients,
375+
surfaces=[meeting_stage_surface, meeting_tab_icon_surface],
376+
)
377+
378+
obo = OnBehalfOf(display_name=from_account.name, mri=from_account.id)
379+
380+
channel_data = MeetingNotificationChannelData(on_behalf_of_list=[obo])
381+
382+
return TargetedMeetingNotification(value=value, channel_data=channel_data)

libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
from ._models_py3 import ConfigAuthResponse
8787
from ._models_py3 import ConfigResponse
8888
from ._models_py3 import ConfigTaskResponse
89+
from ._models_py3 import MeetingNotificationBase
90+
from ._models_py3 import MeetingNotificationResponse
8991

9092
__all__ = [
9193
"AppBasedLinkQuery",
@@ -173,4 +175,6 @@
173175
"ConfigAuthResponse",
174176
"ConfigResponse",
175177
"ConfigTaskResponse",
178+
"MeetingNotificationBase",
179+
"MeetingNotificationResponse",
176180
]

0 commit comments

Comments
 (0)
0