diff --git a/packages/api/src/microsoft_teams/api/activities/__init__.py b/packages/api/src/microsoft_teams/api/activities/__init__.py index be3f19c3..5c38aa53 100644 --- a/packages/api/src/microsoft_teams/api/activities/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/__init__.py @@ -8,7 +8,7 @@ from pydantic import Field, TypeAdapter from . import event, install_update, invoke, message -from .activity_params import ActivityParams +from .sendable_activity import SendableActivity from .command import CommandActivity, CommandResultActivity, CommandResultValue, CommandSendActivity, CommandSendValue from .conversation import ( ConversationActivity, @@ -68,7 +68,7 @@ "ConversationEventType", "InvokeActivity", "TraceActivity", - "ActivityParams", + "SendableActivity", "SentActivity", ] __all__.extend(event.__all__) diff --git a/packages/api/src/microsoft_teams/api/activities/activity_params.py b/packages/api/src/microsoft_teams/api/activities/activity_params.py deleted file mode 100644 index a566f263..00000000 --- a/packages/api/src/microsoft_teams/api/activities/activity_params.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -# Union of all activity input types (each defined next to their respective activities) -from typing import Annotated, Union - -from pydantic import Field - -from .command import CommandResultActivityInput, CommandSendActivityInput -from .conversation import ConversationUpdateActivityInput -from .handoff import HandoffActivityInput -from .message import ( - MessageActivityInput, - MessageDeleteActivityInput, - MessageReactionActivityInput, - MessageUpdateActivityInput, -) -from .trace import TraceActivityInput -from .typing import TypingActivityInput - -ActivityParams = Annotated[ - Union[ - # Simple activities - ConversationUpdateActivityInput, - HandoffActivityInput, - TraceActivityInput, - TypingActivityInput, - # Message activities - MessageActivityInput, - MessageDeleteActivityInput, - MessageReactionActivityInput, - MessageUpdateActivityInput, - # Command activities - CommandSendActivityInput, - CommandResultActivityInput, - ], - Field(discriminator="type"), -] diff --git a/packages/api/src/microsoft_teams/api/activities/command/__init__.py b/packages/api/src/microsoft_teams/api/activities/command/__init__.py index 1d8eea47..2b73f0b2 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/command/__init__.py @@ -7,17 +7,15 @@ from pydantic import Field -from .command_result import CommandResultActivity, CommandResultActivityInput, CommandResultValue -from .command_send import CommandSendActivity, CommandSendActivityInput, CommandSendValue +from .command_result import CommandResultActivity, CommandResultValue +from .command_send import CommandSendActivity, CommandSendValue CommandActivity = Annotated[Union[CommandSendActivity, CommandResultActivity], Field(discriminator="type")] __all__ = [ "CommandResultValue", "CommandResultActivity", - "CommandResultActivityInput", "CommandSendValue", "CommandSendActivity", - "CommandSendActivityInput", "CommandActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_result.py b/packages/api/src/microsoft_teams/api/activities/command/command_result.py index 5d32ceeb..82760101 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_result.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_result.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel +from ...models import ActivityBase, CustomBaseModel class CommandResultValue(CustomBaseModel): @@ -47,7 +47,3 @@ class CommandResultActivity(_CommandResultBase, ActivityBase): """The name of the event.""" -class CommandResultActivityInput(_CommandResultBase, ActivityInputBase): - """Input model for creating command result activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_send.py b/packages/api/src/microsoft_teams/api/activities/command/command_send.py index 0f5a17fd..f6a1f7e4 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_send.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_send.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel +from ...models import ActivityBase, CustomBaseModel class CommandSendValue(CustomBaseModel): @@ -43,7 +43,3 @@ class CommandSendActivity(_CommandSendBase, ActivityBase): """The name of the event.""" -class CommandSendActivityInput(_CommandSendBase, ActivityInputBase): - """Input model for creating command send activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py index d6aa67be..51b90e6a 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py @@ -7,7 +7,6 @@ ConversationChannelData, ConversationEventType, ConversationUpdateActivity, - ConversationUpdateActivityInput, ) ConversationActivity = ConversationUpdateActivity @@ -16,6 +15,5 @@ "ConversationEventType", "ConversationChannelData", "ConversationUpdateActivity", - "ConversationUpdateActivityInput", "ConversationActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py index c13aa338..072f02f0 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py @@ -5,7 +5,7 @@ from typing import List, Literal, Optional -from ...models import Account, ActivityBase, ActivityInputBase, ChannelData, CustomBaseModel +from ...models import Account, ActivityBase, ChannelData, CustomBaseModel ConversationEventType = Literal[ "channelCreated", @@ -55,7 +55,3 @@ class ConversationUpdateActivity(_ConversationUpdateBase, ActivityBase): """Channel data with event type information.""" -class ConversationUpdateActivityInput(_ConversationUpdateBase, ActivityInputBase): - """Input model for creating conversation update activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/handoff.py b/packages/api/src/microsoft_teams/api/activities/handoff.py index 065b538a..8a19e6bc 100644 --- a/packages/api/src/microsoft_teams/api/activities/handoff.py +++ b/packages/api/src/microsoft_teams/api/activities/handoff.py @@ -5,7 +5,7 @@ from typing import Literal -from ..models import ActivityBase, ActivityInputBase, CustomBaseModel +from ..models import ActivityBase, CustomBaseModel class _HandoffBase(CustomBaseModel): @@ -18,5 +18,3 @@ class HandoffActivity(_HandoffBase, ActivityBase): """Output model for received handoff activities with required fields and read-only properties.""" -class HandoffActivityInput(_HandoffBase, ActivityInputBase): - """Input model for creating handoff activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/__init__.py b/packages/api/src/microsoft_teams/api/activities/message/__init__.py index a6ad3a6c..2bbfffa1 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/message/__init__.py @@ -8,12 +8,11 @@ from pydantic import Field from .message import MessageActivity, MessageActivityInput -from .message_delete import MessageDeleteActivity, MessageDeleteActivityInput, MessageDeleteChannelData +from .message_delete import MessageDeleteActivity, MessageDeleteChannelData from .message_reaction import MessageReactionActivity, MessageReactionActivityInput from .message_update import ( MessageEventType, MessageUpdateActivity, - MessageUpdateActivityInput, MessageUpdateChannelData, ) @@ -32,12 +31,10 @@ "MessageActivity", "MessageActivityInput", "MessageDeleteActivity", - "MessageDeleteActivityInput", "MessageDeleteChannelData", "MessageReactionActivity", "MessageReactionActivityInput", "MessageUpdateActivity", - "MessageUpdateActivityInput", "MessageUpdateChannelData", "MessageEventType", ] diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_delete.py b/packages/api/src/microsoft_teams/api/activities/message/message_delete.py index 7f7272f0..ee203ccc 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_delete.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_delete.py @@ -5,7 +5,7 @@ from typing import Literal, Optional -from ...models import ActivityBase, ActivityInputBase, ChannelData +from ...models import ActivityBase, ChannelData from ...models.custom_base_model import CustomBaseModel @@ -30,7 +30,3 @@ class MessageDeleteActivity(_MessageDeleteBase, ActivityBase): channel_data: MessageDeleteChannelData # pyright: ignore [reportGeneralTypeIssues] """Channel-specific data for message delete events.""" - - -class MessageDeleteActivityInput(_MessageDeleteBase, ActivityInputBase): - """Input model for creating message delete activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_update.py b/packages/api/src/microsoft_teams/api/activities/message/message_update.py index cb342e9d..74e11f31 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_update.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_update.py @@ -4,9 +4,9 @@ """ from datetime import datetime -from typing import Any, Literal, Optional, Self +from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, ChannelData +from ...models import ActivityBase, ChannelData from ...models.custom_base_model import CustomBaseModel MessageEventType = Literal["undeleteMessage", "editMessage"] @@ -56,57 +56,3 @@ class MessageUpdateActivity(_MessageUpdateBase, ActivityBase): """Channel-specific data for message update events.""" -class MessageUpdateActivityInput(_MessageUpdateBase, ActivityInputBase): - """Input model for creating message update activities with builder methods.""" - - def with_text(self, text: str) -> Self: - """ - Set the text content of the message. - - Args: - text: The text content to set - - Returns: - Self for method chaining - """ - self.text = text - return self - - def with_speak(self, speak: str) -> Self: - """ - Set the text to speak. - - Args: - speak: The text to speak - - Returns: - Self for method chaining - """ - self.speak = speak - return self - - def with_summary(self, summary: str) -> Self: - """ - Set the summary text. - - Args: - summary: The summary text to set - - Returns: - Self for method chaining - """ - self.summary = summary - return self - - def with_expiration(self, expiration: datetime) -> Self: - """ - Set the expiration time for the activity. - - Args: - expiration: The expiration datetime to set - - Returns: - Self for method chaining - """ - self.expiration = expiration - return self diff --git a/packages/api/src/microsoft_teams/api/activities/sendable_activity.py b/packages/api/src/microsoft_teams/api/activities/sendable_activity.py new file mode 100644 index 00000000..167cffe5 --- /dev/null +++ b/packages/api/src/microsoft_teams/api/activities/sendable_activity.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +# Union of activity input types that can be sent via the +# /v3/conversations/{id}/activities create/update endpoints. +# Other *ActivityInput classes exist for model symmetry but represent +# inbound-only event notifications (messageDelete, messageUpdate, etc.). +# NOTE: MessageReactionActivityInput is temporarily included here even +# though reactions have a dedicated /activities/{id}/reactions endpoint +# exposed via ReactionClient. +from typing import Annotated, Union + +from pydantic import Field + +from .message import ( + MessageActivityInput, + MessageReactionActivityInput, +) +from .typing import TypingActivityInput + +SendableActivity = Annotated[ + Union[ + MessageActivityInput, + MessageReactionActivityInput, + TypingActivityInput, + ], + Field(discriminator="type"), +] diff --git a/packages/api/src/microsoft_teams/api/activities/sent_activity.py b/packages/api/src/microsoft_teams/api/activities/sent_activity.py index f28eaec6..1543e7a4 100644 --- a/packages/api/src/microsoft_teams/api/activities/sent_activity.py +++ b/packages/api/src/microsoft_teams/api/activities/sent_activity.py @@ -4,7 +4,7 @@ """ from ..models import CustomBaseModel -from . import ActivityParams +from . import SendableActivity class SentActivity(CustomBaseModel): @@ -13,10 +13,10 @@ class SentActivity(CustomBaseModel): id: str """Id of the activity.""" - activity_params: ActivityParams + activity_params: SendableActivity """Additional parameters for the activity.""" @classmethod - def merge(cls, activity_params: ActivityParams, curr_activity: "SentActivity") -> "SentActivity": + def merge(cls, activity_params: SendableActivity, curr_activity: "SentActivity") -> "SentActivity": merged_data = {**activity_params.model_dump(), **curr_activity.model_dump()} return cls(**merged_data) diff --git a/packages/api/src/microsoft_teams/api/activities/trace.py b/packages/api/src/microsoft_teams/api/activities/trace.py index ff12876a..aa144d41 100644 --- a/packages/api/src/microsoft_teams/api/activities/trace.py +++ b/packages/api/src/microsoft_teams/api/activities/trace.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ..models import ActivityBase, ActivityInputBase, CustomBaseModel +from ..models import ActivityBase, CustomBaseModel class _TraceBase(CustomBaseModel): @@ -48,5 +48,3 @@ class TraceActivity(_TraceBase, ActivityBase): """ -class TraceActivityInput(_TraceBase, ActivityInputBase): - """Input model for creating trace activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py index a1ee86ef..e6746d91 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py @@ -8,7 +8,7 @@ from microsoft_teams.common.experimental import experimental from microsoft_teams.common.http import Client -from ...activities import ActivityParams, SentActivity +from ...activities import SendableActivity, SentActivity from ...models import TeamsChannelAccount from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -38,7 +38,7 @@ def __init__( super().__init__(http_client, api_client_settings) self.service_url = service_url.rstrip("/") - async def create(self, conversation_id: str, activity: ActivityParams) -> SentActivity: + async def create(self, conversation_id: str, activity: SendableActivity) -> SentActivity: """ Create a new activity in a conversation. @@ -61,7 +61,7 @@ async def create(self, conversation_id: str, activity: ActivityParams) -> SentAc id = response.json().get("id", _PLACEHOLDER_ACTIVITY_ID) return SentActivity(id=id, activity_params=activity) - async def update(self, conversation_id: str, activity_id: str, activity: ActivityParams) -> SentActivity: + async def update(self, conversation_id: str, activity_id: str, activity: SendableActivity) -> SentActivity: """ Update an existing activity in a conversation. @@ -80,7 +80,7 @@ async def update(self, conversation_id: str, activity_id: str, activity: Activit id = response.json()["id"] return SentActivity(id=id, activity_params=activity) - async def reply(self, conversation_id: str, activity_id: str, activity: ActivityParams) -> SentActivity: + async def reply(self, conversation_id: str, activity_id: str, activity: SendableActivity) -> SentActivity: """ Reply to an activity in a conversation. @@ -128,7 +128,7 @@ async def get_members(self, conversation_id: str, activity_id: str) -> List[Team return [TeamsChannelAccount.model_validate(member) for member in response.json()] @experimental("ExperimentalTeamsTargeted") - async def create_targeted(self, conversation_id: str, activity: ActivityParams) -> SentActivity: + async def create_targeted(self, conversation_id: str, activity: SendableActivity) -> SentActivity: """ Create a new targeted activity in a conversation. @@ -153,7 +153,7 @@ async def create_targeted(self, conversation_id: str, activity: ActivityParams) return SentActivity(id=id, activity_params=activity) @experimental("ExperimentalTeamsTargeted") - async def update_targeted(self, conversation_id: str, activity_id: str, activity: ActivityParams) -> SentActivity: + async def update_targeted(self, conversation_id: str, activity_id: str, activity: SendableActivity) -> SentActivity: """ Update an existing targeted activity in a conversation. diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/client.py b/packages/api/src/microsoft_teams/api/clients/conversation/client.py index 1c42a103..0422a551 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/client.py @@ -10,7 +10,7 @@ from ...models import ConversationResource from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient -from .activity import ActivityParams, ConversationActivityClient +from .activity import SendableActivity, ConversationActivityClient from .member import ConversationMemberClient from .params import CreateConversationParams @@ -26,13 +26,13 @@ def __init__(self, client: "ConversationClient", conversation_id: str) -> None: class ActivityOperations(ConversationOperations): """Operations for managing activities in a conversation.""" - async def create(self, activity: ActivityParams): + async def create(self, activity: SendableActivity): return await self._client.activities_client.create(self._conversation_id, activity) - async def update(self, activity_id: str, activity: ActivityParams): + async def update(self, activity_id: str, activity: SendableActivity): return await self._client.activities_client.update(self._conversation_id, activity_id, activity) - async def reply(self, activity_id: str, activity: ActivityParams): + async def reply(self, activity_id: str, activity: SendableActivity): return await self._client.activities_client.reply(self._conversation_id, activity_id, activity) async def delete(self, activity_id: str): @@ -41,11 +41,11 @@ async def delete(self, activity_id: str): async def get_members(self, activity_id: str): return await self._client.activities_client.get_members(self._conversation_id, activity_id) - async def create_targeted(self, activity: ActivityParams): + async def create_targeted(self, activity: SendableActivity): """Create a new targeted activity visible only to the specified recipient.""" return await self._client.activities_client.create_targeted(self._conversation_id, activity) - async def update_targeted(self, activity_id: str, activity: ActivityParams): + async def update_targeted(self, activity_id: str, activity: SendableActivity): """Update an existing targeted activity.""" return await self._client.activities_client.update_targeted(self._conversation_id, activity_id, activity) diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/params.py b/packages/api/src/microsoft_teams/api/clients/conversation/params.py index 3d35407c..5997c202 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/params.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/params.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional from ...models import Account, CustomBaseModel -from .activity import ActivityParams +from .activity import SendableActivity class CreateConversationParams(CustomBaseModel): @@ -20,7 +20,7 @@ class CreateConversationParams(CustomBaseModel): """ The tenant ID for the conversation. """ - activity: Optional[ActivityParams] = None + activity: Optional[SendableActivity] = None """ The initial activity to post in the conversation. """ diff --git a/packages/api/tests/unit/test_sent_activity.py b/packages/api/tests/unit/test_sent_activity.py index 549ed97c..b7ed7a6a 100644 --- a/packages/api/tests/unit/test_sent_activity.py +++ b/packages/api/tests/unit/test_sent_activity.py @@ -5,19 +5,19 @@ # pyright: basic import pytest -from microsoft_teams.api.activities import ActivityParams, MessageActivityInput, SentActivity +from microsoft_teams.api.activities import SendableActivity, MessageActivityInput, SentActivity @pytest.fixture -def mock_new_activity_params() -> ActivityParams: - """Create a mock ActivityParams for testing.""" +def mock_new_activity_params() -> SendableActivity: + """Create a mock SendableActivity for testing.""" return MessageActivityInput( id="updated-id", type="message", text="updated message", locale="en-US", reply_to_id="activity-3" ) @pytest.fixture -def mock_sent_activity(mock_new_activity_params: ActivityParams) -> SentActivity: +def mock_sent_activity(mock_new_activity_params: SendableActivity) -> SentActivity: """Create a mock SentActivity for testing.""" return SentActivity( id="sent-1", diff --git a/packages/apps/src/microsoft_teams/apps/activity_sender.py b/packages/apps/src/microsoft_teams/apps/activity_sender.py index 78d02ae8..124fbe18 100644 --- a/packages/apps/src/microsoft_teams/apps/activity_sender.py +++ b/packages/apps/src/microsoft_teams/apps/activity_sender.py @@ -7,10 +7,10 @@ from typing import cast from microsoft_teams.api import ( - ActivityParams, ApiClient, ConversationReference, MessageActivityInput, + SendableActivity, SentActivity, ) from microsoft_teams.common import Client @@ -36,7 +36,7 @@ def __init__(self, client: Client): """ self._client = client - async def send(self, activity: ActivityParams, ref: ConversationReference) -> SentActivity: + async def send(self, activity: SendableActivity, ref: ConversationReference) -> SentActivity: """ Send an activity to the Bot Framework. @@ -76,6 +76,22 @@ async def send(self, activity: ActivityParams, ref: ConversationReference) -> Se res = await activities.create(activity) return SentActivity.merge(activity, res) + async def delete(self, activity_id: str, ref: ConversationReference, targeted: bool = False) -> None: + """ + Delete an activity from a conversation. + + Args: + activity_id: The ID of the activity to delete + ref: The conversation reference + targeted: If True, deletes a targeted (ephemeral) activity + """ + api = ApiClient(service_url=ref.service_url, options=self._client) + activities = api.conversations.activities(ref.conversation.id) + if targeted: + await activities.delete_targeted(activity_id) + else: + await activities.delete(activity_id) + def create_stream(self, ref: ConversationReference) -> StreamerProtocol: """ Create a new activity stream for real-time updates. diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index f678910d..85f8ed20 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -14,7 +14,7 @@ from microsoft_teams.api import ( Account, ActivityBase, - ActivityParams, + SendableActivity, ApiClient, ClientCredentials, ConversationAccount, @@ -277,7 +277,7 @@ async def stop(self) -> None: self._events.emit("error", ErrorEvent(error, context={"method": "stop"})) raise - async def send(self, conversation_id: str, activity: str | ActivityParams | AdaptiveCard): + async def send(self, conversation_id: str, activity: str | SendableActivity | AdaptiveCard): """Send an activity proactively.""" if not self._initialized: diff --git a/packages/apps/src/microsoft_teams/apps/app_process.py b/packages/apps/src/microsoft_teams/apps/app_process.py index 9b048113..78f99ed9 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -8,7 +8,7 @@ from microsoft_teams.api import ( ActivityBase, - ActivityParams, + SendableActivity, ActivityTypeAdapter, ApiClient, ApiClientSettings, @@ -131,7 +131,7 @@ async def _build_context( send = activityCtx.send async def updated_send( - message: str | ActivityParams | AdaptiveCard, + message: str | SendableActivity | AdaptiveCard, conversation_ref: Optional[ConversationReference] = None, ) -> SentActivity: res = await send(message, conversation_ref) diff --git a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py index 4ce90e9e..28f316b7 100644 --- a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py +++ b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py @@ -11,7 +11,7 @@ from microsoft_teams.api import ( Account, - ActivityParams, + SendableActivity, ApiClient, ConversationAccount, ConversationReference, @@ -50,7 +50,7 @@ class FunctionContext(ClientContext, Generic[T]): data: T """The function payload.""" - async def send(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[SentActivity]: + async def send(self, activity: str | SendableActivity | AdaptiveCard) -> Optional[SentActivity]: """ Send an activity to the current conversation. @@ -81,7 +81,7 @@ async def send(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[ return await self.activity_sender.send(activity, conversation_ref) - async def _resolve_conversation_id(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[str]: + async def _resolve_conversation_id(self, activity: str | SendableActivity | AdaptiveCard) -> Optional[str]: """Resolve or create a conversation ID for the current user/context. Args: diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index 9f273a19..39f4153a 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -6,12 +6,12 @@ import base64 import json import logging +import warnings from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generic, Optional, TypeVar, cast from microsoft_teams.api import ( ActivityBase, - ActivityParams, ApiClient, CardAction, CardActionType, @@ -21,6 +21,7 @@ GetUserTokenParams, JsonWebToken, MessageActivityInput, + SendableActivity, SentActivity, SignOutUserParams, TokenExchangeResource, @@ -34,6 +35,7 @@ from microsoft_teams.api.models.oauth import OAuthCard from microsoft_teams.cards import AdaptiveCard from microsoft_teams.common import Storage +from microsoft_teams.common.experimental import ExperimentalWarning from microsoft_teams.common.http.client_token import Token from ..activity_sender import ActivitySender @@ -71,7 +73,7 @@ class SignInOptions: Optional[TokenPostResource], Optional[str], ], - ActivityParams, + SendableActivity, ] ] = None @@ -165,14 +167,14 @@ def app_graph(self) -> "GraphServiceClient": async def send( self, - message: str | ActivityParams | AdaptiveCard, + message: str | SendableActivity | AdaptiveCard, conversation_ref: Optional[ConversationReference] = None, ) -> SentActivity: """ Send a message to the conversation. Args: - message: The message to send, can be a string, ActivityParams, or AdaptiveCard + message: The message to send, can be a string, SendableActivity, or AdaptiveCard conversation_ref: Optional conversation reference to override the current conversation reference """ if isinstance(message, str): @@ -186,7 +188,36 @@ async def send( res = await self._activity_sender.send(activity, ref) return res - async def reply(self, input: str | ActivityParams) -> SentActivity: + async def delete( + self, + activity_id: str, + conversation_ref: Optional[ConversationReference] = None, + targeted: bool = False, + ) -> None: + """ + Delete an activity from the conversation. + + Args: + activity_id: The ID of the activity to delete + conversation_ref: Optional conversation reference to override the current one, + enabling proactive deletion from a different conversation + targeted: If True, deletes a targeted (ephemeral) activity + + .. warning:: Preview + The ``targeted`` parameter is in preview and may change or be + removed in future versions. Diagnostic: ExperimentalTeamsTargeted + """ + if targeted: + warnings.warn( + "The targeted parameter of delete is in preview and may change " + "or be removed in future versions. Diagnostic: ExperimentalTeamsTargeted", + ExperimentalWarning, + stacklevel=2, + ) + ref = conversation_ref or self.conversation_ref + await self._activity_sender.delete(activity_id, ref, targeted=targeted) + + async def reply(self, input: str | SendableActivity) -> SentActivity: """Send a reply to the activity.""" activity = MessageActivityInput(text=input) if isinstance(input, str) else input if isinstance(activity, MessageActivityInput): diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index 96fdfbd9..85a33564 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -5,12 +5,14 @@ # pyright: basic +import warnings from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from microsoft_teams.api import Account, MessageActivityInput, SentActivity +from microsoft_teams.api import Account, ConversationReference, MessageActivityInput, SentActivity from microsoft_teams.apps.routing.activity_context import ActivityContext +from microsoft_teams.common.experimental import ExperimentalWarning def _create_activity_context( @@ -217,8 +219,8 @@ async def test_reply_with_string(self) -> None: assert "My reply" in (sent_activity.text or "") @pytest.mark.asyncio - async def test_reply_with_activity_params(self) -> None: - """reply() with an ActivityParams instance sets reply_to_id and delegates to send.""" + async def test_reply_with_sendable_activity(self) -> None: + """reply() with a SendableActivity instance sets reply_to_id and delegates to send.""" mock_activity = MagicMock() mock_activity.type = "event" mock_activity.id = "evt-id-999" @@ -387,3 +389,65 @@ async def test_sign_out_logs_error_and_does_not_raise_on_failure(self) -> None: mock_log_error.assert_called_once() logged_message = mock_log_error.call_args[0][0] assert "Failed to sign out user" in logged_message + + +class TestActivityContextDelete: + """Tests for ActivityContext.delete().""" + + @pytest.mark.asyncio + async def test_delete_delegates_to_sender(self) -> None: + """delete() calls ActivitySender.delete with current conversation_ref.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + await ctx.delete("msg-123") + + mock_sender.delete.assert_called_once_with("msg-123", ctx.conversation_ref, targeted=False) + + @pytest.mark.asyncio + async def test_delete_with_custom_conversation_ref(self) -> None: + """delete() uses provided conversation_ref for proactive deletion.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + custom_ref = MagicMock(spec=ConversationReference) + await ctx.delete("msg-123", conversation_ref=custom_ref) + + mock_sender.delete.assert_called_once_with("msg-123", custom_ref, targeted=False) + + @pytest.mark.asyncio + async def test_delete_targeted_passes_flag(self) -> None: + """delete() with targeted=True passes targeted=True to sender.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + await ctx.delete("msg-123", targeted=True) + + mock_sender.delete.assert_called_once_with("msg-123", ctx.conversation_ref, targeted=True) + + @pytest.mark.asyncio + async def test_delete_targeted_emits_experimental_warning(self) -> None: + """delete() with targeted=True emits ExperimentalWarning.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + await ctx.delete("msg-123", targeted=True) + + experimental = [w for w in caught if issubclass(w.category, ExperimentalWarning)] + assert len(experimental) == 1 + assert "ExperimentalTeamsTargeted" in str(experimental[0].message) + + @pytest.mark.asyncio + async def test_delete_non_targeted_no_warning(self) -> None: + """delete() without targeted=True does not emit ExperimentalWarning.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + await ctx.delete("msg-123") + + experimental = [w for w in caught if issubclass(w.category, ExperimentalWarning)] + assert len(experimental) == 0 diff --git a/packages/apps/tests/test_activity_sender.py b/packages/apps/tests/test_activity_sender.py index 384edbb9..ce3890f7 100644 --- a/packages/apps/tests/test_activity_sender.py +++ b/packages/apps/tests/test_activity_sender.py @@ -170,3 +170,50 @@ async def test_update_non_targeted_message_calls_update(self, sender, conversati mock_activities.update.assert_called_once_with("existing-msg-id", activity) mock_activities.update_targeted.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_calls_delete(self, sender, conversation_ref): + """Test that delete() calls the underlying delete method.""" + mock_activities = MagicMock() + mock_activities.delete = AsyncMock(return_value=None) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.delete("msg-123", conversation_ref) + + mock_activities.delete.assert_called_once_with("msg-123") + mock_activities.delete_targeted.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_targeted_calls_delete_targeted(self, sender, conversation_ref): + """Test that delete() with targeted=True calls delete_targeted.""" + mock_activities = MagicMock() + mock_activities.delete_targeted = AsyncMock(return_value=None) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.delete("msg-123", conversation_ref, targeted=True) + + mock_activities.delete_targeted.assert_called_once_with("msg-123") + mock_activities.delete.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_uses_correct_conversation_id(self, sender, conversation_ref): + """Test that delete() uses conversation id from ref.""" + mock_activities = MagicMock() + mock_activities.delete = AsyncMock(return_value=None) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.delete("msg-123", conversation_ref) + + mock_api.conversations.activities.assert_called_once_with(conversation_ref.conversation.id)