Skip to content
Open
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
36 changes: 36 additions & 0 deletions examples/quoting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Example: Quoting

A bot that demonstrates various ways to quote previous messages in Microsoft Teams.

## Commands

| Command | Behavior |
|---------|----------|
| `test reply` | `reply()` — auto-quotes the inbound message |
| `test quote` | `quote()` — sends a message, then quotes it by ID |
| `test add` | `add_quote()` — sends a message, then quotes it with builder + response |
| `test multi` | Sends two messages, then quotes both with interleaved responses |
| `test manual` | `add_quote()` + `add_text()` — manual control |
| `help` | Shows available commands |
| *(quote a message)* | Bot reads and displays the quoted reply metadata |

## Run

```bash
cd examples/quoting
pip install -e .
python src/main.py

# Or with uv:
uv run 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>
TENANT_ID=<your-tenant-id>
```
14 changes: 14 additions & 0 deletions examples/quoting/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "quoting"
version = "0.1.0"
description = "Quoting example - demonstrates various ways to quote previous messages in conversations"
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 }
109 changes: 109 additions & 0 deletions examples/quoting/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

import asyncio

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

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()

# ============================================
# Read inbound quoted replies
# ============================================
quotes = ctx.activity.get_quoted_messages()
if quotes:
quote = quotes[0].quoted_reply
info_parts = [f"Quoted message ID: {quote.message_id}"]
if quote.sender_name:
info_parts.append(f"From: {quote.sender_name}")
if quote.preview:
info_parts.append(f'Preview: "{quote.preview}"')
if quote.is_reply_deleted:
info_parts.append("(deleted)")
if quote.validated_message_reference:
info_parts.append("(validated)")

await ctx.send("You sent a message with a quoted reply:\n\n" + "\n".join(info_parts))

# ============================================
# reply() — auto-quotes the inbound message
# ============================================
if "test reply" in text:
await ctx.reply("Thanks for your message! This reply auto-quotes it using reply().")
return

# ============================================
# quote() — quote a previously sent message by ID
# ============================================
if "test quote" in text:
sent = await ctx.send("The meeting has been moved to 3 PM tomorrow.")
await ctx.quote(sent.id, "Just to confirm — does the new time work for everyone?")
return

# ============================================
# add_quote() — builder with response
# ============================================
if "test add" in text:
sent = await ctx.send("Please review the latest PR before end of day.")
msg = MessageActivityInput().add_quote(sent.id, "Done! Left my comments on the PR.")
await ctx.send(msg)
return

# ============================================
# Multi-quote with mixed responses
# ============================================
if "test multi" in text:
sent_a = await ctx.send("We need to update the API docs before launch.")
sent_b = await ctx.send("The design mockups are ready for review.")
sent_c = await ctx.send("CI pipeline is green on main.")
msg = (
MessageActivityInput()
.add_quote(sent_a.id, "I can take the docs — will have a draft by Thursday.")
.add_quote(sent_b.id, "Looks great, approved!")
.add_quote(sent_c.id)
)
await ctx.send(msg)
return

# ============================================
# add_quote() + add_text() — manual control
# ============================================
if "test manual" in text:
sent = await ctx.send("Deployment to staging is complete.")
msg = MessageActivityInput().add_quote(sent.id).add_text(" Verified — all smoke tests passing.")
await ctx.send(msg)
return

# ============================================
# Help / Default
# ============================================
if "help" in text:
await ctx.reply(
"**Quoting Test Bot**\n\n"
"**Commands:**\n"
"- `test reply` - reply() auto-quotes your message\n"
"- `test quote` - quote() quotes a previously sent message\n"
"- `test add` - add_quote() builder with response\n"
"- `test multi` - Multi-quote with mixed responses (one bare quote with no response)\n"
"- `test manual` - add_quote() + add_text() manual control\n\n"
"Quote any message to me to see the parsed metadata!"
)
return

await ctx.reply('Say "help" for available commands.')


if __name__ == "__main__":
asyncio.run(app.start())
67 changes: 67 additions & 0 deletions packages/api/src/microsoft_teams/api/activities/message/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Any, List, Literal, Optional, Self

from microsoft_teams.cards import AdaptiveCard
from microsoft_teams.common.experimental import experimental

from ...models import (
Account,
Expand All @@ -21,6 +22,8 @@
Importance,
InputHint,
MentionEntity,
QuotedReplyData,
QuotedReplyEntity,
StreamInfoEntity,
SuggestedActions,
TextFormat,
Expand Down Expand Up @@ -82,6 +85,20 @@ class MessageActivity(_MessageBase, ActivityBase):
text: str = "" # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride]
"""The text content of the message."""

@experimental("ExperimentalTeamsQuotedReplies")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want quote/quoting for this too?

def get_quoted_messages(self) -> list[QuotedReplyEntity]:
"""
Get all quoted reply entities from this message.

Returns:
List of quoted reply entities, empty if none

.. warning:: Coming Soon
This API is coming soon and may change in the future.
Diagnostic: ExperimentalTeamsQuotedReplies
"""
return [e for e in (self.entities or []) if isinstance(e, QuotedReplyEntity)]

def is_recipient_mentioned(self) -> bool:
"""
Check if the recipient account is mentioned in the message.
Expand Down Expand Up @@ -397,6 +414,56 @@ def add_stream_final(self) -> Self:

return self.add_entity(stream_entity)

@experimental("ExperimentalTeamsQuotedReplies")
def prepend_quote(self, message_id: str) -> Self:
"""
Prepend a quotedReply entity and placeholder before existing text.
Used by reply()/quote() for quote-above-response.

Args:
message_id: The IC3 message ID of the message to quote

Returns:
Self for method chaining

.. warning:: Coming Soon
This API is coming soon and may change in the future.
Diagnostic: ExperimentalTeamsQuotedReplies
"""
if not self.entities:
self.entities = []
self.entities.append(QuotedReplyEntity(quoted_reply=QuotedReplyData(message_id=message_id)))
placeholder = f'<quoted messageId="{message_id}"/>'
has_text = bool((self.text or "").strip())
self.text = f"{placeholder} {self.text}" if has_text else placeholder
return self

@experimental("ExperimentalTeamsQuotedReplies")
def add_quote(self, message_id: str, text: str | None = None) -> Self:
"""
Add a quoted message reference and append a placeholder to text.
Teams renders the quoted message as a preview bubble above the response text.
If text is provided, it is appended to the quoted message placeholder.

Args:
message_id: The ID of the message to quote
text: Optional text, appended to the quoted message placeholder

Returns:
Self for method chaining

.. warning:: Coming Soon
This API is coming soon and may change in the future.
Diagnostic: ExperimentalTeamsQuotedReplies
"""
if not self.entities:
self.entities = []
self.entities.append(QuotedReplyEntity(quoted_reply=QuotedReplyData(message_id=message_id)))
self.add_text(f'<quoted messageId="{message_id}"/>')
if text:
self.add_text(f" {text}")
return self

def with_recipient(self, value: Account, is_targeted: Optional[bool] = None) -> Self:
"""
Set the recipient.
Expand Down
5 changes: 0 additions & 5 deletions packages/api/src/microsoft_teams/api/models/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,6 @@ def with_id(self, value: str) -> Self:
self.id = value
return self

def with_reply_to_id(self, value: str) -> Self:
"""Set the reply_to_id."""
self.reply_to_id = value
return self

def with_channel_id(self, value: ChannelID) -> Self:
"""Set the channel_id."""
self.channel_id = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .mention_entity import MentionEntity
from .message_entity import MessageEntity
from .product_info_entity import ProductInfoEntity
from .quoted_reply_entity import QuotedReplyData, QuotedReplyEntity
from .sensitive_usage_entity import SensitiveUsage, SensitiveUsageEntity, SensitiveUsagePattern
from .stream_info_entity import StreamInfoEntity

Expand All @@ -34,6 +35,8 @@
"MentionEntity",
"MessageEntity",
"ProductInfoEntity",
"QuotedReplyData",
"QuotedReplyEntity",
"SensitiveUsageEntity",
"SensitiveUsage",
"SensitiveUsagePattern",
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/microsoft_teams/api/models/entity/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .mention_entity import MentionEntity
from .message_entity import MessageEntity
from .product_info_entity import ProductInfoEntity
from .quoted_reply_entity import QuotedReplyEntity
from .sensitive_usage_entity import SensitiveUsageEntity
from .stream_info_entity import StreamInfoEntity

Expand All @@ -23,4 +24,5 @@
CitationEntity,
SensitiveUsageEntity,
ProductInfoEntity,
QuotedReplyEntity,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

from typing import Literal, Optional

from microsoft_teams.common.experimental import experimental

from ..custom_base_model import CustomBaseModel


@experimental("ExperimentalTeamsQuotedReplies")
class QuotedReplyData(CustomBaseModel):
"""Data for a quoted reply entity

.. warning:: Coming Soon
This API is coming soon and may change in the future.
Diagnostic: ExperimentalTeamsQuotedReplies
"""

message_id: str
"ID of the message being quoted"

sender_id: Optional[str] = None
"ID of the sender of the quoted message"

sender_name: Optional[str] = None
"Name of the sender of the quoted message"

preview: Optional[str] = None
"Preview text of the quoted message"

time: Optional[str] = None
"Timestamp of the quoted message (IC3 epoch value, e.g. '1772050244572'). Inbound only."

is_reply_deleted: Optional[bool] = None
"Whether the quoted reply has been deleted"

validated_message_reference: Optional[bool] = None
"Whether the message reference has been validated"


@experimental("ExperimentalTeamsQuotedReplies")
class QuotedReplyEntity(CustomBaseModel):
"""Entity containing quoted reply information

.. warning:: Coming Soon
This API is coming soon and may change in the future.
Diagnostic: ExperimentalTeamsQuotedReplies
"""

type: Literal["quotedReply"] = "quotedReply"
"Type identifier for quoted reply"

quoted_reply: QuotedReplyData
"The quoted reply data"
2 changes: 0 additions & 2 deletions packages/api/tests/unit/test_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ def test_should_build(
)
)
.with_recipient(bot)
.with_reply_to_id("3")
.with_service_url("http://localhost")
.with_timestamp(datetime.now())
.with_local_timestamp(datetime.now())
Expand All @@ -93,7 +92,6 @@ def test_should_build(
conversation=chat,
)
assert activity.recipient == bot
assert activity.reply_to_id == "3"
assert activity.service_url == "http://localhost"
assert activity.timestamp is not None
assert activity.local_timestamp is not None
Expand Down
Loading
Loading