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
3 changes: 1 addition & 2 deletions packages/apps/src/microsoft/teams/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from . import auth, contexts, events, plugins
from .app import App
from .app_tokens import AppTokens
from .auth import * # noqa: F403
from .contexts import * # noqa: F403
from .events import * # noqa: F401, F403
Expand All @@ -16,7 +15,7 @@
from .routing import ActivityContext

# Combine all exports from submodules
__all__: list[str] = ["App", "AppOptions", "HttpPlugin", "HttpStream", "ActivityContext", "AppTokens"]
__all__: list[str] = ["App", "AppOptions", "HttpPlugin", "HttpStream", "ActivityContext"]
__all__.extend(auth.__all__)
__all__.extend(events.__all__)
__all__.extend(plugins.__all__)
Expand Down
117 changes: 21 additions & 96 deletions packages/apps/src/microsoft/teams/apps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
ConversationAccount,
ConversationReference,
Credentials,
JsonWebToken,
MessageActivityInput,
TokenCredentials,
)
Expand All @@ -32,7 +31,6 @@
from .app_oauth import OauthHandlers
from .app_plugins import PluginProcessor
from .app_process import ActivityProcessor
from .app_tokens import AppTokens
from .auth import TokenValidator
from .auth.remote_function_jwt_middleware import remote_function_jwt_validation
from .container import Container
Expand All @@ -45,12 +43,12 @@
get_event_type_from_signature,
is_registered_event,
)
from .graph_token_manager import GraphTokenManager
from .http_plugin import HttpPlugin
from .options import AppOptions, InternalAppOptions
from .plugins import PluginBase, PluginStartEvent, get_metadata
from .routing import ActivityHandlerMixin, ActivityRouter
from .routing.activity_context import ActivityContext
from .token_manager import TokenManager

version = importlib.metadata.version("microsoft-teams-apps")

Expand Down Expand Up @@ -83,22 +81,26 @@ def __init__(self, **options: Unpack[AppOptions]):
self._events = EventEmitter[EventType]()
self._router = ActivityRouter()

self._tokens = AppTokens()
self.credentials = self._init_credentials()

self._token_manager = TokenManager(
http_client=self.http_client,
credentials=self.credentials,
logger=self.log,
default_connection_name=self.options.default_connection_name,
)

self.container = Container()
self.container.set_provider("id", providers.Object(self.id))
self.container.set_provider("name", providers.Object(self.name))
self.container.set_provider("credentials", providers.Object(self.credentials))
self.container.set_provider("bot_token", providers.Callable(lambda: self.tokens.bot))
self.container.set_provider("graph_token", providers.Callable(lambda: self.tokens.graph))
self.container.set_provider("bot_token", providers.Factory(lambda: self._get_or_get_bot_token))
self.container.set_provider("logger", providers.Object(self.log))
self.container.set_provider("storage", providers.Object(self.storage))
self.container.set_provider(self.http_client.__class__.__name__, providers.Factory(lambda: self.http_client))

self.api = ApiClient(
"https://smba.trafficmanager.net/teams",
self.http_client.clone(ClientOptions(token=lambda: self.tokens.bot)),
self.http_client.clone(ClientOptions(token=self._get_or_get_bot_token)),
)

plugins: List[PluginBase] = list(self.options.plugins)
Expand All @@ -125,20 +127,14 @@ def __init__(self, **options: Unpack[AppOptions]):
self._running = False

# initialize all event, activity, and plugin processors
self.graph_token_manager = GraphTokenManager(
api_client=self.api,
credentials=self.credentials,
logger=self.log,
)
self.activity_processor = ActivityProcessor(
self._router,
self.log,
self.id,
self.storage,
self.options.default_connection_name,
self.http_client,
self.tokens,
self.graph_token_manager,
self._token_manager,
)
self.event_manager = EventManager(self._events, self.activity_processor)
self.activity_processor.event_manager = self.event_manager
Expand Down Expand Up @@ -169,11 +165,6 @@ def is_running(self) -> bool:
"""Whether the app is currently running."""
return self._running

@property
def tokens(self) -> AppTokens:
"""Current authentication tokens."""
return self._tokens

@property
def logger(self) -> Logger:
"""The logger instance used by the app."""
Expand All @@ -191,17 +182,10 @@ def router(self) -> ActivityRouter:

@property
def id(self) -> Optional[str]:
"""The app's ID from tokens."""
return (
self._tokens.bot.app_id if self._tokens.bot else self._tokens.graph.app_id if self._tokens.graph else None
)

@property
def name(self) -> Optional[str]:
"""The app's name from tokens."""
return getattr(self._tokens.bot, "app_display_name", None) or getattr(
self._tokens.graph, "app_display_name", None
)
"""The app's ID from credentials."""
if not self.credentials:
return None
return self.credentials.client_id

async def start(self, port: Optional[int] = None) -> None:
"""
Expand All @@ -220,9 +204,6 @@ async def start(self, port: Optional[int] = None) -> None:
self._port = port or int(os.getenv("PORT", "3978"))

try:
await self._refresh_tokens(force=True)
self._running = True

for plugin in self.plugins:
# Inject the dependencies
self._plugin_processor.inject(plugin)
Expand All @@ -234,6 +215,7 @@ async def on_http_ready() -> None:
self.log.info("Teams app started successfully")
assert self._port is not None, "Port must be set before emitting start event"
self._events.emit("start", StartEvent(port=self._port))
self._running = True

self.http.on_ready_callback = on_http_ready

Expand Down Expand Up @@ -280,13 +262,13 @@ async def on_http_stopped() -> None:
async def send(self, conversation_id: str, activity: str | ActivityParams | AdaptiveCard):
"""Send an activity proactively."""

if self.id is None or self.name is None:
if self.id is None:
raise ValueError("app not started")

conversation_ref = ConversationReference(
channel_id="msteams",
service_url=self.api.service_url,
bot=Account(id=self.id, name=self.name, role="bot"),
bot=Account(id=self.id, role="bot"),
conversation=ConversationAccount(id=conversation_id, conversation_type="personal"),
)

Expand Down Expand Up @@ -326,65 +308,6 @@ def _init_credentials(self) -> Optional[Credentials]:

return None

async def _refresh_tokens(self, force: bool = False) -> None:
"""Refresh bot and graph tokens."""
await asyncio.gather(self._refresh_bot_token(force), self._refresh_graph_token(force), return_exceptions=True)

async def _refresh_bot_token(self, force: bool = False) -> None:
"""Refresh the bot authentication token."""
if not self.credentials:
self.log.warning("No credentials provided, skipping bot token refresh")
return

if not force and self._tokens.bot and not self._tokens.bot.is_expired():
return

if self._tokens.bot:
self.log.debug("Refreshing bot token")

try:
token_response = await self.api.bots.token.get(self.credentials)
self._tokens.bot = JsonWebToken(token_response.access_token)
self.log.debug("Bot token refreshed successfully")

except Exception as error:
self.log.error(f"Failed to refresh bot token: {error}")

self._events.emit("error", ErrorEvent(error, context={"method": "_refresh_bot_token"}))
raise

async def _refresh_graph_token(self, force: bool = False) -> None:
"""Refresh the Graph API token."""
if not self.credentials:
self.log.warning("No credentials provided, skipping graph token refresh")
return

if not force and self._tokens.graph and not self._tokens.graph.is_expired():
return

if self._tokens.graph:
self.log.debug("Refreshing graph token")

try:
# Use GraphTokenManager for tenant-aware token management
tenant_id = self.credentials.tenant_id if self.credentials else None
token = await self.graph_token_manager.get_token(tenant_id)

if token:
self._tokens.graph = token
self.log.debug("Graph token refreshed successfully")

# Emit token acquired event
self._events.emit("token", {"type": "graph", "token": self._tokens.graph})
else:
self.log.debug("Failed to get graph token from GraphTokenManager")

except Exception as error:
self.log.error(f"Failed to refresh graph token: {error}")

self._events.emit("error", ErrorEvent(error, context={"method": "_refresh_graph_token"}))
raise

@overload
def event(self, func_or_event_type: F) -> F:
"""Register event handler with auto-detected type from function signature."""
Expand Down Expand Up @@ -521,7 +444,6 @@ async def endpoint(req: Request):
async def call_next(r: Request) -> Any:
ctx = FunctionContext(
id=self.id,
name=self.name,
api=self.api,
http=self.http,
log=self.log,
Expand All @@ -541,3 +463,6 @@ async def call_next(r: Request) -> Any:

# Named decoration: @app.func("name")
return decorator

async def _get_or_get_bot_token(self):
return await self._token_manager.get_bot_token()
33 changes: 12 additions & 21 deletions packages/apps/src/microsoft/teams/apps/app_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,22 @@
ActivityParams,
ApiClient,
ConversationReference,
GetUserTokenParams,
InvokeResponse,
SentActivity,
TokenProtocol,
is_invoke_response,
)
from microsoft.teams.api.clients.user.params import GetUserTokenParams
from microsoft.teams.cards import AdaptiveCard
from microsoft.teams.common import Client, ClientOptions, LocalStorage, Storage

if TYPE_CHECKING:
from .app_events import EventManager
from .app_tokens import AppTokens
from .events import ActivityEvent, ActivityResponseEvent, ActivitySentEvent
from .graph_token_manager import GraphTokenManager
from .plugins import PluginActivityEvent, PluginBase, Sender
from .routing.activity_context import ActivityContext
from .routing.router import ActivityHandler, ActivityRouter
from .token_manager import TokenManager
from .utils import extract_tenant_id


Expand All @@ -42,30 +41,20 @@ def __init__(
storage: Union[Storage[str, Any], LocalStorage[Any]],
default_connection_name: str,
http_client: Client,
token: AppTokens,
graph_token_manager: GraphTokenManager,
token_manager: TokenManager,
) -> None:
self.router = router
self.logger = logger
self.id = id
self.storage = storage
self.default_connection_name = default_connection_name
self.http_client = http_client
self.tokens = token
self._graph_token_manager = graph_token_manager
self.token_manager = token_manager

# This will be set after the EventManager is initialized due to
# a circular dependency
self.event_manager: Optional["EventManager"] = None

async def _get_or_refresh_graph_token(self, tenant_id: Optional[str] = None) -> Optional[TokenProtocol]:
"""Get the current graph token or refresh it if needed."""
try:
return await self._graph_token_manager.get_token(tenant_id)
except Exception as e:
self.logger.error(f"Failed to get graph token via manager: {e}")
return self.tokens.graph

async def _build_context(
self,
activity: ActivityBase,
Expand All @@ -92,29 +81,31 @@ async def _build_context(
locale=activity.locale,
user=activity.from_,
)
api_client = ApiClient(service_url, self.http_client.clone(ClientOptions(token=self.tokens.bot)))
api_client = ApiClient(
service_url, self.http_client.clone(ClientOptions(token=self.token_manager.get_bot_token))
)

# Check if user is signed in
is_signed_in = False
user_token: Optional[str] = None
try:
user_token_res = await api_client.users.token.get(
GetUserTokenParams(
connection_name=self.default_connection_name,
user_id=activity.from_.id,
channel_id=activity.channel_id,
user_id=activity.from_.id,
connection_name=self.default_connection_name,
)
)

user_token = user_token_res.token
is_signed_in = True
except Exception:
# User token not available
self.logger.debug("No user token available")
pass

tenant_id = extract_tenant_id(activity)

graph_token = await self._get_or_refresh_graph_token(tenant_id)

activityCtx = ActivityContext(
activity,
self.id or "",
Expand All @@ -126,7 +117,7 @@ async def _build_context(
is_signed_in,
self.default_connection_name,
sender,
app_token=graph_token,
app_token=lambda: self.token_manager.get_graph_token(tenant_id),
)

send = activityCtx.send
Expand Down
17 changes: 0 additions & 17 deletions packages/apps/src/microsoft/teams/apps/app_tokens.py

This file was deleted.

Loading