Skip to content
Draft
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
17 changes: 13 additions & 4 deletions packages/app/src/microsoft/teams/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import os
from dataclasses import dataclass
from logging import Logger
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast, overload
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union, cast, overload

from dotenv import find_dotenv, load_dotenv
from microsoft.teams.api import (
Activity,
ActivityBase,
ActivityTypeAdapter,
ApiClient,
Expand All @@ -22,6 +23,7 @@
JsonWebToken,
TokenProtocol,
)
from microsoft.teams.app.plugins.plugin_activity_event import PluginActivityEvent
from microsoft.teams.common import Client, ClientOptions, ConsoleLogger, EventEmitter, LocalStorage

from .app_oauth import OauthHandlers
Expand All @@ -43,6 +45,7 @@
version = importlib.metadata.version("microsoft-teams-app")

F = TypeVar("F", bound=Callable[..., Any])
R = TypeVar("R")
load_dotenv(find_dotenv(usecwd=True))

USER_AGENT = f"teams.py[app]/{version}"
Expand Down Expand Up @@ -87,7 +90,7 @@ def __init__(self, options: Optional[AppOptions] = None):
self.http_client.clone(ClientOptions(token=lambda: self.tokens.bot)),
)

plugins: List[Plugin] = list(self.options.plugins or [])
plugins: List[Plugin[Any]] = list(self.options.plugins or [])

http_plugin = None
for i, plugin in enumerate(plugins):
Expand Down Expand Up @@ -321,7 +324,7 @@ async def handle_activity(self, input_activity: HttpActivityEvent) -> Dict[str,

try:
self._events.emit("activity", ActivityEvent(activity))
ctx = await self.build_context(activity, input_activity.token)
ctx = await self._build_context(activity, input_activity.token)
response = await self._activity_processor.process_activity(ctx)
await self.http.on_activity_response(
PluginActivityResponseEvent(
Expand Down Expand Up @@ -350,7 +353,7 @@ async def handle_activity(self, input_activity: HttpActivityEvent) -> Dict[str,
)
raise

async def build_context(self, activity: ActivityBase, token: TokenProtocol) -> ActivityContext[ActivityBase]:
async def _build_context(self, activity: Activity, token: TokenProtocol) -> ActivityContext[ActivityBase]:
"""Build the context object for activity processing.

Args:
Expand Down Expand Up @@ -389,6 +392,11 @@ async def build_context(self, activity: ActivityBase, token: TokenProtocol) -> A
# User token not available
pass

context_by_plugin: dict[Type[Plugin[Any]], Any] = {}
for plugin in self.plugins:
ctx = await plugin.on_activity(PluginActivityEvent(sender=self.http, token=token, activity=activity))
context_by_plugin[type(plugin)] = ctx

return ActivityContext(
activity,
self.id or "",
Expand All @@ -399,6 +407,7 @@ async def build_context(self, activity: ActivityBase, token: TokenProtocol) -> A
conversation_ref,
is_signed_in,
self.options.default_connection_name,
context_by_plugin,
)

@overload
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/microsoft/teams/app/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AppOptions:
# Infrastructure
logger: Optional[Logger] = None
storage: Optional[Storage[str, Any]] = None
plugins: List[Plugin] = field(default_factory=list[Plugin])
plugins: List[Plugin[Any]] = field(default_factory=list[Plugin[Any]])
enable_token_validation: bool = True

# Oauth
Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/microsoft/teams/app/plugins/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Licensed under the MIT License.
"""

from typing import Callable
from typing import Any, Callable, TypeVar

from microsoft.teams.api.clients.conversation import ActivityParams
from microsoft.teams.api.models import Resource
Expand All @@ -27,8 +27,10 @@
Emitted when the plugin receives an activity
"""

TExtraContext = TypeVar("TExtraContext", bound=dict[str, Any] | None)

class Plugin:

class Plugin[TExtraContext]:
"""The base plugin for Teams app plugins."""

async def on_init(self) -> None:
Expand All @@ -47,7 +49,7 @@ async def on_error(self, event: PluginErrorEvent) -> None:
"""Called by the App when an error occurs."""
...

async def on_activity(self, event: PluginActivityEvent) -> None:
async def on_activity(self, event: PluginActivityEvent) -> TExtraContext:
"""Called by the App when an activity is received."""
...

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
Licensed under the MIT License.
"""

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, NamedTuple

from microsoft.teams.api import Activity, TokenProtocol
from microsoft.teams.api.models.conversation import ConversationReference

if TYPE_CHECKING:
from .sender import Sender


class PluginActivityEvent(ConversationReference):
class PluginActivityEvent(NamedTuple):
"""Event emitted by a plugin when an activity is received."""

sender: "Sender"
Expand Down
12 changes: 11 additions & 1 deletion packages/app/src/microsoft/teams/app/routing/activity_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import json
from dataclasses import dataclass
from logging import Logger
from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar
from typing import Any, Awaitable, Callable, Generic, Optional, Type, TypeVar

from microsoft.teams.api import (
ActivityBase,
Expand All @@ -32,6 +32,8 @@
from microsoft.teams.cards import AdaptiveCard
from microsoft.teams.common import Storage

from ..plugins import Plugin

T = TypeVar("T", bound=ActivityBase, contravariant=True)

SendCallable = Callable[[str | ActivityParams | AdaptiveCard], Awaitable[Resource]]
Expand Down Expand Up @@ -65,6 +67,7 @@ def __init__(
conversation_ref: ConversationReference,
is_signed_in: bool,
connection_name: str,
context_by_plugin: dict[Type[Plugin[Any]], Any],
):
self.activity = activity
self.app_id = app_id
Expand All @@ -75,6 +78,7 @@ def __init__(
self.user_token = user_token
self.connection_name = connection_name
self.is_signed_in = is_signed_in
self._context_by_plugin = context_by_plugin

self._next_handler: Optional[Callable[[], Awaitable[None]]] = None

Expand Down Expand Up @@ -235,3 +239,9 @@ async def sign_out(self) -> None:
self.logger.debug(f"User {self.activity.from_.id} signed out successfully.")
except Exception as e:
self.logger.error(f"Failed to sign out user: {e}")

def get_plugin_context[R](self, cls: Type[Plugin[R]]) -> R:
context = self._context_by_plugin.get(cls)
if context is None:
raise ValueError(f"No context found for plugin class {cls}")
return context
Loading