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
61 changes: 61 additions & 0 deletions examples/reactions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Example: Message Reactions

A bot that demonstrates adding and removing reactions to messages in Microsoft Teams.

This example shows how to use the `ReactionClient` to programmatically add and remove reactions (like, heart, laugh, etc.) on messages.

## Commands

| Command | Behavior |
|---------|----------|
| `react <type>` | Adds a reaction to your message (e.g., `react like`, `react heart`) |
| `unreact <type>` | Removes a reaction from your message |
| `help` | Shows available commands |

## Supported Reaction Types

- `like` - 👍 Like
Comment thread
lilyydu marked this conversation as resolved.
- `heart` - ❤️ Heart
- `laugh` - 😂 Laugh
- `surprised` - 😮 Surprised
- `sad` - 😢 Sad
- `angry` - 😠 Angry
- `plusOne` - ➕ Plus one

## How It Works

The bot listens for incoming messages and:
1. When you send `react <type>`, it adds that reaction to your message
2. When you send `unreact <type>`, it removes that reaction from your message

The reactions are added/removed using the Bot Framework v3 API:
- **Add**: `PUT /v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}`
- **Remove**: `DELETE /v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}`

## Testing

1. Add the bot to a Teams chat or channel
2. Send a message: `react like`
3. **Expected result**: A 👍 reaction appears on your message
4. Send another message: `unreact like`
5. **Expected result**: The 👍 reaction is removed from that message

## Run

```bash
cd examples/reactions

# Activate venv
.venv\Scripts\activate

python src/main.py
```

## Environment Variables

Create a `.env` file:

```env
CLIENT_ID=<your-azure-bot-app-id>
CLIENT_SECRET=<your-azure-bot-app-secret>
```
14 changes: 14 additions & 0 deletions examples/reactions/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "reactions"
version = "0.1.0"
description = "Message reactions example"
readme = "README.md"
requires-python = ">=3.12,<3.15"
dependencies = [
"dotenv>=0.9.9",
"microsoft-teams-apps",
"microsoft-teams-api",
]

[tool.uv.sources]
microsoft-teams-apps = { workspace = true }
83 changes: 83 additions & 0 deletions examples/reactions/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

import asyncio

from microsoft_teams.api import MessageActivity
from microsoft_teams.api.activities.typing import TypingActivityInput
from microsoft_teams.apps import ActivityContext, App

"""
Example: Message Reactions

A bot that demonstrates adding and removing reactions to messages in Microsoft Teams.
This example shows how to use the ReactionClient to programmatically add and remove
reactions (like, heart, laugh, etc.) on messages.
"""

app = App()


@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
"""Handle message activities."""
await ctx.reply(TypingActivityInput())

text = (ctx.activity.text or "").lower().strip()
conversation_id = ctx.activity.conversation.id
activity_id = ctx.activity.id

# ============================================
# Add Reaction
# ============================================
if text.startswith("react "):
reaction_type = text[6:].strip()

await ctx.api.reactions.add(
conversation_id=conversation_id,
activity_id=activity_id,
reaction_type=reaction_type,
)

await ctx.reply(f"✅ Added {reaction_type} reaction to your message!")
print(f"[REACTION] Added '{reaction_type}' to activity {activity_id}")
return

# ============================================
# Remove Reaction
# ============================================
if text.startswith("unreact "):
reaction_type = text[8:].strip()

await ctx.api.reactions.delete(
conversation_id=conversation_id,
activity_id=activity_id,
reaction_type=reaction_type,
)

await ctx.reply(f"✅ Removed {reaction_type} reaction from your message!")
print(f"[REACTION] Removed '{reaction_type}' from activity {activity_id}")
return

# ============================================
# Help / Default
# ============================================
if "help" in text:
await ctx.reply(
"**Message Reactions Bot**\n\n"
"**Commands:**\n"
"- `react <type>` - Add a reaction to your message\n"
"- `unreact <type>` - Remove a reaction from your message\n\n"
"- `react like` - Adds a 👍 to your message\n"
"- `unreact like` - Removes the 👍 from your message"
)
return

# Default
await ctx.reply('Say "help" for available commands, or try "react like"!')
Comment thread
lilyydu marked this conversation as resolved.


if __name__ == "__main__":
asyncio.run(app.start())
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from typing import List, Literal, Optional, Self

from typing_extensions import deprecated

from ...models import ActivityBase, ActivityInputBase, MessageReaction
from ...models.custom_base_model import CustomBaseModel

Expand All @@ -28,6 +30,7 @@ class MessageReactionActivity(_MessageReactionBase, ActivityBase):
class MessageReactionActivityInput(_MessageReactionBase, ActivityInputBase):
"""Input model for creating message reaction activities with builder methods."""

@deprecated("MessageReactionActivityInput is deprecated and will be removed in a future release. ")
def add_reaction(self, reaction: MessageReaction) -> Self:
"""
Add a message reaction to the added reactions list.
Expand All @@ -44,6 +47,7 @@ def add_reaction(self, reaction: MessageReaction) -> Self:
self.reactions_added.append(reaction)
return self

@deprecated("MessageReactionActivityInput is deprecated and will be removed in a future release.")
def remove_reaction(self, reaction: MessageReaction) -> Self:
"""
Remove a message reaction and add it to the removed reactions list.
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/microsoft_teams/api/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
Licensed under the MIT License.
"""

from . import bot, conversation, meeting, team, user
from . import bot, conversation, meeting, reaction, team, user
from .api_client import ApiClient
from .api_client_settings import DEFAULT_API_CLIENT_SETTINGS, ApiClientSettings, merge_api_client_settings
from .bot import * # noqa: F403
from .conversation import * # noqa: F403
from .meeting import * # noqa: F403
from .reaction import * # noqa: F403
from .team import * # noqa: F403
from .user import * # noqa: F403

Expand All @@ -22,5 +23,6 @@
__all__.extend(bot.__all__)
__all__.extend(conversation.__all__)
__all__.extend(meeting.__all__)
__all__.extend(reaction.__all__)
__all__.extend(team.__all__)
__all__.extend(user.__all__)
3 changes: 3 additions & 0 deletions packages/api/src/microsoft_teams/api/clients/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .bot import BotClient
from .conversation import ConversationClient
from .meeting import MeetingClient
from .reaction import ReactionClient
from .team import TeamClient
from .user import UserClient

Expand Down Expand Up @@ -42,6 +43,7 @@ def __init__(
self.conversations = ConversationClient(service_url, self._http, self._api_client_settings)
self.teams = TeamClient(service_url, self._http, self._api_client_settings)
self.meetings = MeetingClient(service_url, self._http, self._api_client_settings)
self.reactions = ReactionClient(service_url, self._http, self._api_client_settings)

@property
def http(self) -> HttpClient:
Expand All @@ -56,4 +58,5 @@ def http(self, value: HttpClient) -> None:
self.users.http = value
self.teams.http = value
self.meetings.http = value
self.reactions.http = value
self._http = value
10 changes: 10 additions & 0 deletions packages/api/src/microsoft_teams/api/clients/reaction/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

from .client import ReactionClient

__all__ = [
"ReactionClient",
]
73 changes: 73 additions & 0 deletions packages/api/src/microsoft_teams/api/clients/reaction/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

from typing import Optional

from microsoft_teams.common.http import Client

from ...models.message import MessageReactionType
from ..api_client_settings import ApiClientSettings
from ..base_client import BaseClient


class ReactionClient(BaseClient):
"""
Client for working with app message reactions for a given conversation/activity.
"""

def __init__(
self,
service_url: str,
http_client: Optional[Client] = None,
api_client_settings: Optional[ApiClientSettings] = None,
):
"""
Initialize the reaction client.

Args:
service_url: The base URL for the Teams service
http_client: Optional HTTP client to use. If not provided, a new one will be created.
api_client_settings: Optional API client settings.
"""
super().__init__(http_client, api_client_settings)
self.service_url = service_url

async def add(
self,
conversation_id: str,
activity_id: str,
reaction_type: MessageReactionType,
) -> None:
"""
Adds a reaction on an activity in a conversation.

Args:
conversation_id: The conversation id.
activity_id: The id of the activity to react to.
reaction_type: The reaction type (for example: "like", "heart", "laugh", etc.).
"""
url = (
f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}/reactions/{reaction_type}"
)
await self.http.put(url)

async def delete(
self,
conversation_id: str,
activity_id: str,
reaction_type: MessageReactionType,
) -> None:
"""
Removes a reaction from an activity in a conversation.

Args:
conversation_id: The conversation id.
activity_id: The id of the activity the reaction is on.
reaction_type: The reaction type to remove (for example: "like", "heart", "laugh", etc.).
"""
url = (
f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}/reactions/{reaction_type}"
)
await self.http.delete(url)
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@

from typing import Literal, Union

MessageReactionType = Union[Literal["like", "heart", "laugh", "surprised", "sad", "angry", "plusOne"], str]
MessageReactionType = Union[
Literal["like", "heart", "1f440_eyes", "2705_whiteheavycheckmark", "launch", "1f4cc_pushpin"], str
Comment thread
lilyydu marked this conversation as resolved.
]
4 changes: 4 additions & 0 deletions packages/api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ def handler(request: httpx.Request) -> httpx.Response:
"channelCount": 5,
"memberCount": 15,
}
elif "/reactions/" in str(request.url):
# Handle reaction endpoints - both PUT (add) and DELETE (remove)
# These endpoints typically return 200 with empty response or minimal response
response_data = {"ok": True}

return httpx.Response(
status_code=200,
Expand Down
Loading