From 90fd485b897a7a992a32c03e3b77befaa149dbde Mon Sep 17 00:00:00 2001 From: lilydu Date: Thu, 22 Jan 2026 15:35:35 -0800 Subject: [PATCH 1/7] add meetings sample and update casing --- examples/meetings/README.md | 51 +++++++++ examples/meetings/pyproject.toml | 17 +++ examples/meetings/src/main.py | 103 ++++++++++++++++++ .../api/activities/event/meeting_end.py | 10 +- .../activities/event/meeting_participant.py | 6 +- .../api/activities/event/meeting_start.py | 10 +- 6 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 examples/meetings/README.md create mode 100644 examples/meetings/pyproject.toml create mode 100644 examples/meetings/src/main.py diff --git a/examples/meetings/README.md b/examples/meetings/README.md new file mode 100644 index 00000000..e540eaec --- /dev/null +++ b/examples/meetings/README.md @@ -0,0 +1,51 @@ +# Sample: Meetings + +This sample demonstrates how to handle real-time updates for meeting events and meeting participant events. + +## Manifest Requirements + +There are a few requirements in the Teams app manifest (manifest.json) to support these events. + +1) The `scopes` section must include `team`, and `groupChat`: + +```csharp + "bots": [ + { + "botId": "", + "scopes": [ + "team", + "personal", + "groupChat" + ], + "isNotificationOnly": false + } + ] +``` + +2) In the authorization section, make sure to specify the following resource-specific permissions: + +```csharp + "authorization":{ + "permissions":{ + "resourceSpecific":[ + { + "name":"OnlineMeetingParticipant.Read.Chat", + "type":"Application" + }, + { + "name":"ChannelMeeting.ReadBasic.Group", + "type":"Application" + }, + { + "name":"OnlineMeeting.ReadBasic.Chat", + "type":"Application" + } + ] + } + } +``` + +### Teams Developer Portal: Bot Configuration + +For your Bot, make sure the [Meeting Event Subscriptions](https://learn.microsoft.com/en-us/microsoftteams/platform/apps-in-teams-meetings/meeting-apps-apis?branch=pr-en-us-8455&tabs=channel-meeting%2Cguest-user%2Cone-on-one-call%2Cdotnet3%2Cdotnet2%2Cdotnet%2Cparticipant-join-event%2Cparticipant-join-event1#receive-meeting-participant-events) are checked. +This enables you to receive the Meeting Participant events. \ No newline at end of file diff --git a/examples/meetings/pyproject.toml b/examples/meetings/pyproject.toml new file mode 100644 index 00000000..3dc36e81 --- /dev/null +++ b/examples/meetings/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "meetings" +version = "0.1.0" +description = "Meetings app" +readme = "README.md" +requires-python = ">=3.12,<3.14" +dependencies = [ + "dotenv>=0.9.9", + "microsoft-teams-apps", + "microsoft-teams-api", + "microsoft-teams-cards", +] + +[tool.uv.sources] +microsoft-teams-apps = { workspace = true } +microsoft-teams-api = { workspace = true } +microsoft-teams-cards = { workspace = true } diff --git a/examples/meetings/src/main.py b/examples/meetings/src/main.py new file mode 100644 index 00000000..ab7178f1 --- /dev/null +++ b/examples/meetings/src/main.py @@ -0,0 +1,103 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio + +from microsoft_teams.api.activities.event import ( + MeetingEndEventActivity, + MeetingParticipantJoinEventActivity, + MeetingParticipantLeaveEventActivity, + MeetingStartEventActivity, +) +from microsoft_teams.api.activities.message import MessageActivity +from microsoft_teams.api.activities.typing import TypingActivityInput +from microsoft_teams.apps import ActivityContext, App +from microsoft_teams.cards import AdaptiveCard, OpenUrlAction, TextBlock + +app = App() + + +@app.on_meeting_start +async def handle_meeting_start(ctx: ActivityContext[MeetingStartEventActivity]): + meeting_data = ctx.activity.value + start_time = meeting_data.StartTime.strftime("%c") + + card = AdaptiveCard( + body=[ + TextBlock( + text=f"'{meeting_data.Title}' has started at {start_time}.", + wrap=True, + weight="Bolder", + ) + ], + actions=[OpenUrlAction(url=meeting_data.JoinUrl, title="Join the meeting")], + ) + + await ctx.send(card) + + +@app.on_meeting_end +async def handle_meeting_end(ctx: ActivityContext[MeetingEndEventActivity]): + meeting_data = ctx.activity.value + end_time = meeting_data.EndTime.strftime("%c") + + card = AdaptiveCard( + body=[ + TextBlock( + text=f"'{meeting_data.Title}' has ended at {end_time}.", + wrap=True, + weight="Bolder", + ) + ] + ) + + await ctx.send(card) + + +@app.on_meeting_participant_join +async def handle_meeting_participant_join(ctx: ActivityContext[MeetingParticipantJoinEventActivity]): + meeting_data = ctx.activity.value + member = meeting_data.members[0].user.name + role = meeting_data.members[0].meeting.role + + card = AdaptiveCard( + body=[ + TextBlock( + text=f"{member} has joined the meeting as {role}.", + wrap=True, + weight="Bolder", + ) + ] + ) + + await ctx.send(card) + + +@app.on_meeting_participant_leave +async def handle_meeting_participant_leave(ctx: ActivityContext[MeetingParticipantLeaveEventActivity]): + meeting_data = ctx.activity.value + member = meeting_data.members[0].user.name + + card = AdaptiveCard( + body=[ + TextBlock( + text=f"{member} has left the meeting.", + wrap=True, + weight="Bolder", + ) + ] + ) + + await ctx.send(card) + + +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + await ctx.reply(TypingActivityInput()) + await ctx.send(f'you said "{ctx.activity.text}"') + + +if __name__ == "__main__": + asyncio.run(app.start()) diff --git a/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py b/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py index 31610c03..b73af679 100644 --- a/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py +++ b/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py @@ -10,19 +10,19 @@ class MeetingEndEventValue(CustomBaseModel): - id: str + Id: str """The meeting's Id, encoded as a BASE64 string.""" - meeting_type: str + MeetingType: str """Type of the meeting""" - join_url: str + JoinUrl: str """URL to join the meeting""" - title: str + Title: str """Title of the meeting.""" - end_time: datetime + EndTime: datetime """Timestamp for meeting end, in UTC.""" diff --git a/packages/api/src/microsoft_teams/api/activities/event/meeting_participant.py b/packages/api/src/microsoft_teams/api/activities/event/meeting_participant.py index cdc28424..1bb55ba6 100644 --- a/packages/api/src/microsoft_teams/api/activities/event/meeting_participant.py +++ b/packages/api/src/microsoft_teams/api/activities/event/meeting_participant.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from typing import List, Literal +from typing import List, Literal, Optional from ...models import ActivityBase, CustomBaseModel, TeamsChannelAccount @@ -16,8 +16,8 @@ class MeetingParticipantInfo(CustomBaseModel): in_meeting: bool """Indicates whether the participant is currently in the meeting.""" - role: str - """The role of the participant in the meeting""" + role: Optional[str] = None + """The role of the participant in the meeting. Optional field.""" class MeetingParticipant(CustomBaseModel): diff --git a/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py b/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py index 69c1efde..f4be5923 100644 --- a/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py +++ b/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py @@ -14,27 +14,27 @@ class MeetingStartEventValue(CustomBaseModel): The value associated with a meeting start event in Microsoft Teams. """ - id: str + Id: str """ The meeting's Id, encoded as a BASE64 string. """ - meeting_type: str + MeetingType: str """ Type of the meeting """ - join_url: str + JoinUrl: str """ URL to join the meeting """ - title: str + Title: str """ The title of the meeting """ - start_time: datetime + StartTime: datetime """ Timestamp for meeting start, in UTC. """ From a94dd4bf6519fc4e604ae3f859b293aade17392a Mon Sep 17 00:00:00 2001 From: lilydu Date: Thu, 22 Jan 2026 16:32:00 -0800 Subject: [PATCH 2/7] update general README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 68162493..00ccba10 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ cookiecutter templates/test -o tests - [`@examples/ai-test`](./examples/ai-test/README.md) - [`@examples/stream`](./examples/stream/README.md) - [`@examples/oauth`](./examples/oauth/README.md) +- [`@examples/meetings`](./examples//meetings/README.md) ## Links From 1d4255f078cadd08abc7a14772da660f11bcf998 Mon Sep 17 00:00:00 2001 From: lilydu Date: Thu, 22 Jan 2026 16:35:03 -0800 Subject: [PATCH 3/7] remove extra slash --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00ccba10..80d99c4c 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ cookiecutter templates/test -o tests - [`@examples/ai-test`](./examples/ai-test/README.md) - [`@examples/stream`](./examples/stream/README.md) - [`@examples/oauth`](./examples/oauth/README.md) -- [`@examples/meetings`](./examples//meetings/README.md) +- [`@examples/meetings`](./examples/meetings/README.md) ## Links From 93952a846ece6e97edf2c8041ddd30b2d28e683f Mon Sep 17 00:00:00 2001 From: lilydu Date: Fri, 23 Jan 2026 10:08:04 -0800 Subject: [PATCH 4/7] update README and optional role param --- examples/meetings/README.md | 4 ++-- examples/meetings/src/main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/meetings/README.md b/examples/meetings/README.md index e540eaec..064bddb5 100644 --- a/examples/meetings/README.md +++ b/examples/meetings/README.md @@ -8,7 +8,7 @@ There are a few requirements in the Teams app manifest (manifest.json) to suppor 1) The `scopes` section must include `team`, and `groupChat`: -```csharp +```json "bots": [ { "botId": "", @@ -24,7 +24,7 @@ There are a few requirements in the Teams app manifest (manifest.json) to suppor 2) In the authorization section, make sure to specify the following resource-specific permissions: -```csharp +```json "authorization":{ "permissions":{ "resourceSpecific":[ diff --git a/examples/meetings/src/main.py b/examples/meetings/src/main.py index ab7178f1..1619bc0e 100644 --- a/examples/meetings/src/main.py +++ b/examples/meetings/src/main.py @@ -60,7 +60,7 @@ async def handle_meeting_end(ctx: ActivityContext[MeetingEndEventActivity]): async def handle_meeting_participant_join(ctx: ActivityContext[MeetingParticipantJoinEventActivity]): meeting_data = ctx.activity.value member = meeting_data.members[0].user.name - role = meeting_data.members[0].meeting.role + role = meeting_data.members[0].meeting.role if hasattr(meeting_data.members[0].meeting, "role") else "a participant" card = AdaptiveCard( body=[ From 7a28484c5bc29317991bc0c416d1b6c0da0a1f40 Mon Sep 17 00:00:00 2001 From: lilydu Date: Mon, 26 Jan 2026 13:54:34 -0800 Subject: [PATCH 5/7] update msg handler and use Field from pydantic --- examples/meetings/src/main.py | 13 ++++++------ .../api/activities/event/meeting_end.py | 12 ++++++----- .../api/activities/event/meeting_start.py | 14 +++++++------ uv.lock | 20 +++++++++++++++++++ 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/examples/meetings/src/main.py b/examples/meetings/src/main.py index 1619bc0e..a6fa97ed 100644 --- a/examples/meetings/src/main.py +++ b/examples/meetings/src/main.py @@ -22,17 +22,17 @@ @app.on_meeting_start async def handle_meeting_start(ctx: ActivityContext[MeetingStartEventActivity]): meeting_data = ctx.activity.value - start_time = meeting_data.StartTime.strftime("%c") + start_time = meeting_data.start_time.strftime("%c") card = AdaptiveCard( body=[ TextBlock( - text=f"'{meeting_data.Title}' has started at {start_time}.", + text=f"'{meeting_data.title}' has started at {start_time}.", wrap=True, weight="Bolder", ) ], - actions=[OpenUrlAction(url=meeting_data.JoinUrl, title="Join the meeting")], + actions=[OpenUrlAction(url=meeting_data.join_url, title="Join the meeting")], ) await ctx.send(card) @@ -41,12 +41,11 @@ async def handle_meeting_start(ctx: ActivityContext[MeetingStartEventActivity]): @app.on_meeting_end async def handle_meeting_end(ctx: ActivityContext[MeetingEndEventActivity]): meeting_data = ctx.activity.value - end_time = meeting_data.EndTime.strftime("%c") - + end_time = meeting_data.end_time.strftime("%c") card = AdaptiveCard( body=[ TextBlock( - text=f"'{meeting_data.Title}' has ended at {end_time}.", + text=f"'{meeting_data.title}' has ended at {end_time}.", wrap=True, weight="Bolder", ) @@ -96,7 +95,7 @@ async def handle_meeting_participant_leave(ctx: ActivityContext[MeetingParticipa @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.reply(TypingActivityInput()) - await ctx.send(f'you said "{ctx.activity.text}"') + await ctx.send("Welcome to the meetings sample! This app will notify you for meeting events.") if __name__ == "__main__": diff --git a/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py b/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py index b73af679..779b8876 100644 --- a/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py +++ b/packages/api/src/microsoft_teams/api/activities/event/meeting_end.py @@ -6,23 +6,25 @@ from datetime import datetime from typing import Literal +from pydantic import Field + from ...models import ActivityBase, CustomBaseModel class MeetingEndEventValue(CustomBaseModel): - Id: str + id: str = Field(alias="Id") """The meeting's Id, encoded as a BASE64 string.""" - MeetingType: str + meeting_type: str = Field(alias="MeetingType") """Type of the meeting""" - JoinUrl: str + join_url: str = Field(alias="JoinUrl") """URL to join the meeting""" - Title: str + title: str = Field(alias="Title") """Title of the meeting.""" - EndTime: datetime + end_time: datetime = Field(alias="EndTime") """Timestamp for meeting end, in UTC.""" diff --git a/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py b/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py index f4be5923..a16434da 100644 --- a/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py +++ b/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py @@ -6,6 +6,8 @@ from datetime import datetime from typing import Literal +from pydantic import Field + from ...models import ActivityBase, CustomBaseModel @@ -14,27 +16,27 @@ class MeetingStartEventValue(CustomBaseModel): The value associated with a meeting start event in Microsoft Teams. """ - Id: str + id: str = Field(alias="Id") """ The meeting's Id, encoded as a BASE64 string. """ - MeetingType: str + meeting_type: str = Field(alias="MeetingType") """ Type of the meeting """ - JoinUrl: str + join_url: str = Field(alias="JoinUrl") """ - URL to join the meeting + URL to join the meetng """ - Title: str + title: str = Field(alias="Title") """ The title of the meeting """ - StartTime: datetime + start_time: datetime = Field(alias="StartTime") """ Timestamp for meeting start, in UTC. """ diff --git a/uv.lock b/uv.lock index 9a30754a..aa67582e 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,7 @@ members = [ "graph", "mcp-client", "mcp-server", + "meetings", "message-extensions", "microsoft-teams-a2a", "microsoft-teams-ai", @@ -1333,6 +1334,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "meetings" +version = "0.1.0" +source = { virtual = "examples/meetings" } +dependencies = [ + { name = "dotenv" }, + { name = "microsoft-teams-api" }, + { name = "microsoft-teams-apps" }, + { name = "microsoft-teams-cards" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "microsoft-teams-api", editable = "packages/api" }, + { name = "microsoft-teams-apps", editable = "packages/apps" }, + { name = "microsoft-teams-cards", editable = "packages/cards" }, +] + [[package]] name = "message-extensions" version = "0.1.0" From 592c10f5ea7cd9101e510b7433648a74f0c26a1f Mon Sep 17 00:00:00 2001 From: lilydu Date: Mon, 26 Jan 2026 13:56:18 -0800 Subject: [PATCH 6/7] fix tpo --- .../src/microsoft_teams/api/activities/event/meeting_start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py b/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py index a16434da..3e898d50 100644 --- a/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py +++ b/packages/api/src/microsoft_teams/api/activities/event/meeting_start.py @@ -28,7 +28,7 @@ class MeetingStartEventValue(CustomBaseModel): join_url: str = Field(alias="JoinUrl") """ - URL to join the meetng + URL to join the meeting """ title: str = Field(alias="Title") From 075fe2dd4616a825363f60987dee9c88802e3f76 Mon Sep 17 00:00:00 2001 From: lilydu Date: Mon, 26 Jan 2026 15:42:06 -0800 Subject: [PATCH 7/7] added tests for deserialization --- .../api/tests/unit/test_meeting_end_event.py | 37 ++++++++++++++++++ .../tests/unit/test_meeting_start_event.py | 38 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 packages/api/tests/unit/test_meeting_end_event.py create mode 100644 packages/api/tests/unit/test_meeting_start_event.py diff --git a/packages/api/tests/unit/test_meeting_end_event.py b/packages/api/tests/unit/test_meeting_end_event.py new file mode 100644 index 00000000..a6877b8e --- /dev/null +++ b/packages/api/tests/unit/test_meeting_end_event.py @@ -0,0 +1,37 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" +# pyright: basic + +from datetime import datetime + +import pytest +from microsoft_teams.api.activities.event.meeting_end import ( + MeetingEndEventValue, +) + + +@pytest.mark.unit +class TestMeetingEndEventValue: + """Unit tests for MeetingEndEventValue serialization.""" + + def test_deserialization_from_aliased_fields(self): + """Test that MeetingEndEventValue correctly deserializes from aliased field names""" + data = { + "Id": "meeting-123-base64", + "MeetingType": "Scheduled", + "JoinUrl": "https://teams.microsoft.com/join/meeting-123", + "Title": "Sprint Planning Meeting", + "EndTime": "2024-01-15T15:30:00Z", + } + + event_value = MeetingEndEventValue.model_validate(data) + assert event_value.id == "meeting-123-base64" + assert event_value.meeting_type == "Scheduled" + assert event_value.join_url == "https://teams.microsoft.com/join/meeting-123" + assert event_value.title == "Sprint Planning Meeting" + assert isinstance(event_value.end_time, datetime) + assert event_value.end_time.year == 2024 + assert event_value.end_time.month == 1 + assert event_value.end_time.day == 15 diff --git a/packages/api/tests/unit/test_meeting_start_event.py b/packages/api/tests/unit/test_meeting_start_event.py new file mode 100644 index 00000000..da82ea02 --- /dev/null +++ b/packages/api/tests/unit/test_meeting_start_event.py @@ -0,0 +1,38 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" +# pyright: basic + +from datetime import datetime + +import pytest +from microsoft_teams.api.activities.event.meeting_start import ( + MeetingStartEventValue, +) + + +@pytest.mark.unit +class TestMeetingStartEventValue: + """Unit tests for MeetingStartEventValue serialization.""" + + def test_deserialization_from_aliased_fields(self): + """Test that MeetingStartEventValue correctly deserializes from aliased field names""" + data = { + "Id": "meeting-123-base64", + "MeetingType": "Scheduled", + "JoinUrl": "https://teams.microsoft.com/join/meeting-123", + "Title": "Sprint Planning Meeting", + "StartTime": "2024-01-15T14:30:00Z", + } + + event_value = MeetingStartEventValue.model_validate(data) + + assert event_value.id == "meeting-123-base64" + assert event_value.meeting_type == "Scheduled" + assert event_value.join_url == "https://teams.microsoft.com/join/meeting-123" + assert event_value.title == "Sprint Planning Meeting" + assert isinstance(event_value.start_time, datetime) + assert event_value.start_time.year == 2024 + assert event_value.start_time.month == 1 + assert event_value.start_time.day == 15