Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 51 additions & 0 deletions examples/meetings/README.md
Original file line number Diff line number Diff line change
@@ -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`:

```json
"bots": [
{
"botId": "",
"scopes": [
"team",
"personal",
"groupChat"
],
"isNotificationOnly": false
}
]
```

2) In the authorization section, make sure to specify the following resource-specific permissions:

```json
"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.
17 changes: 17 additions & 0 deletions examples/meetings/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 }
102 changes: 102 additions & 0 deletions examples/meetings/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
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.start_time.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.join_url, 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.end_time.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 if hasattr(meeting_data.members[0].meeting, "role") else "a participant"

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("Welcome to the meetings sample! This app will notify you for meeting events.")


if __name__ == "__main__":
asyncio.run(app.start())
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

meeting_type: str
meeting_type: str = Field(alias="MeetingType")
"""Type of the meeting"""

join_url: str
join_url: str = Field(alias="JoinUrl")
"""URL to join the meeting"""

title: str
title: str = Field(alias="Title")
"""Title of the meeting."""

end_time: datetime
end_time: datetime = Field(alias="EndTime")
"""Timestamp for meeting end, in UTC."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from datetime import datetime
from typing import Literal

from pydantic import Field

from ...models import ActivityBase, CustomBaseModel


Expand All @@ -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.
"""

meeting_type: str
meeting_type: str = Field(alias="MeetingType")
"""
Type of the meeting
"""

join_url: str
join_url: str = Field(alias="JoinUrl")
"""
URL to join the meeting
"""

title: str
title: str = Field(alias="Title")
"""
The title of the meeting
"""

start_time: datetime
start_time: datetime = Field(alias="StartTime")
"""
Timestamp for meeting start, in UTC.
"""
Expand Down
37 changes: 37 additions & 0 deletions packages/api/tests/unit/test_meeting_end_event.py
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions packages/api/tests/unit/test_meeting_start_event.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.