99from logging import Logger
1010from pathlib import Path
1111from types import SimpleNamespace
12- from typing import Annotated , Any , AsyncGenerator , Awaitable , Callable , Dict , Optional , cast
12+ from typing import (
13+ Annotated ,
14+ Any ,
15+ AsyncGenerator ,
16+ Awaitable ,
17+ Callable ,
18+ Dict ,
19+ Optional ,
20+ TypedDict ,
21+ Union ,
22+ Unpack ,
23+ cast ,
24+ )
1325
1426import uvicorn
1527from fastapi import FastAPI , Request , Response
2335 SentActivity ,
2436 TokenProtocol ,
2537)
38+ from microsoft .teams .api .auth .credentials import Credentials
2639from microsoft .teams .apps .http_stream import HttpStream
2740from microsoft .teams .common .http import Client , ClientOptions , Token
2841from microsoft .teams .common .logging import ConsoleLogger
4760version = importlib .metadata .version ("microsoft-teams-apps" )
4861
4962
63+ class HttpPluginOptions (TypedDict , total = False ):
64+ """Options for configuring the HTTP plugin."""
65+
66+ logger : Logger
67+ skip_auth : bool
68+ server_factory : Callable [[FastAPI ], uvicorn .Server ]
69+
70+
5071@Plugin (name = "http" , version = version , description = "the default plugin for sending/receiving activities" )
5172class HttpPlugin (Sender ):
5273 """
5374 Basic HTTP plugin that provides a FastAPI server for Teams activities.
5475 """
5576
5677 logger : Annotated [Logger , LoggerDependencyOptions ()]
78+ credentials : Annotated [Optional [Credentials ], DependencyMetadata (optional = True )]
5779
5880 on_error_event : Annotated [Callable [[ErrorEvent ], None ], EventMetadata (name = "error" )]
5981 on_activity_event : Annotated [Callable [[ActivityEvent ], None ], EventMetadata (name = "activity" )]
@@ -64,16 +86,9 @@ class HttpPlugin(Sender):
6486
6587 lifespans : list [Lifespan [Starlette ]] = []
6688
67- def __init__ (
68- self ,
69- app_id : Optional [str ],
70- logger : Optional [Logger ] = None ,
71- skip_auth : bool = False ,
72- server_factory : Optional [Callable [[FastAPI ], uvicorn .Server ]] = None ,
73- ):
89+ def __init__ (self , ** options : Unpack [HttpPluginOptions ]):
7490 """
7591 Args:
76- app_id: Optional Microsoft App ID.
7792 logger: Optional logger.
7893 skip_auth: Whether to skip JWT validation.
7994 server_factory: Optional function that takes an ASGI app
@@ -88,8 +103,9 @@ def custom_server_factory(app: FastAPI) -> uvicorn.Server:
88103 ```
89104 """
90105 super ().__init__ ()
91- self .logger = logger or ConsoleLogger ().create_logger ("@teams/http-plugin" )
106+ self .logger = options . get ( " logger" ) or ConsoleLogger ().create_logger ("@teams/http-plugin" )
92107 self ._port : Optional [int ] = None
108+ self ._skip_auth : bool = options .get ("skip_auth" , False )
93109 self ._server : Optional [uvicorn .Server ] = None
94110 self ._on_ready_callback : Optional [Callable [[], Awaitable [None ]]] = None
95111 self ._on_stopped_callback : Optional [Callable [[], Awaitable [None ]]] = None
@@ -122,20 +138,14 @@ async def combined_lifespan(app: Starlette):
122138 self .app = FastAPI (lifespan = combined_lifespan )
123139
124140 # Create uvicorn server if user provides custom factory method
141+ server_factory = options .get ("server_factory" )
125142 if server_factory :
126143 self ._server = server_factory (self .app )
127144 if self ._server .config .app is not self .app :
128145 raise ValueError (
129146 "server_factory must return a uvicorn.Server configured with the provided FastAPI app instance."
130147 )
131148
132- # Add JWT validation middleware
133- if app_id and not skip_auth :
134- jwt_middleware = create_jwt_validation_middleware (
135- app_id = app_id , logger = self .logger , paths = ["/api/messages" ]
136- )
137- self .app .middleware ("http" )(jwt_middleware )
138-
139149 # Expose FastAPI routing methods (like TypeScript exposes Express methods)
140150 self .get = self .app .get
141151 self .post = self .app .post
@@ -167,6 +177,20 @@ def on_stopped_callback(self, callback: Optional[Callable[[], Awaitable[None]]])
167177 """Set callback to call when HTTP server is stopped."""
168178 self ._on_stopped_callback = callback
169179
180+ async def on_init (self ) -> None :
181+ """
182+ Initialize the HTTP plugin when the app starts.
183+ This adds JWT validation middleware unless `skip_auth` is True.
184+ """
185+
186+ # Add JWT validation middleware
187+ app_id = getattr (self .credentials , "client_id" , None )
188+ if app_id and not self ._skip_auth :
189+ jwt_middleware = create_jwt_validation_middleware (
190+ app_id = app_id , logger = self .logger , paths = ["/api/messages" ]
191+ )
192+ self .app .middleware ("http" )(jwt_middleware )
193+
170194 async def on_start (self , event : PluginStartEvent ) -> None :
171195 """Start the HTTP server."""
172196 self ._port = event .port
@@ -337,38 +361,51 @@ async def _handle_activity_request(self, request: Request) -> Any:
337361
338362 return result
339363
364+ def _handle_activity_response (self , response : Response , result : Any ) -> Union [Response , Dict [str , object ]]:
365+ """
366+ Handle the activity response formatting.
367+
368+ Args:
369+ response: The FastAPI response object
370+ result: The result from activity processing
371+
372+ Returns:
373+ The formatted response
374+ """
375+ status_code : Optional [int ] = None
376+ body : Optional [Dict [str , Any ]] = None
377+ resp_dict : Optional [Dict [str , Any ]] = None
378+ if isinstance (result , dict ):
379+ resp_dict = cast (Dict [str , Any ], result )
380+ elif isinstance (result , BaseModel ):
381+ resp_dict = result .model_dump (exclude_none = True )
382+
383+ # if resp_dict has status set it
384+ if resp_dict and "status" in resp_dict :
385+ status_code = resp_dict .get ("status" )
386+
387+ if resp_dict and "body" in resp_dict :
388+ body = resp_dict .get ("body" , None )
389+
390+ if status_code is not None :
391+ response .status_code = status_code
392+
393+ if body is not None :
394+ self .logger .debug (f"Returning body { body } " )
395+ return body
396+ self .logger .debug ("Returning empty body" )
397+ return response
398+
399+ async def on_activity_request (self , request : Request , response : Response ) -> Any :
400+ """Handle incoming Teams activity."""
401+ # Process the activity (token validation handled by middleware)
402+ result = await self ._handle_activity_request (request )
403+ return self ._handle_activity_response (response , result )
404+
340405 def _setup_routes (self ) -> None :
341406 """Setup FastAPI routes."""
342407
343- async def on_activity_request (request : Request , response : Response ) -> Any :
344- """Handle incoming Teams activity."""
345- # Process the activity (token validation handled by middleware)
346- result = await self ._handle_activity_request (request )
347- status_code : Optional [int ] = None
348- body : Optional [Dict [str , Any ]] = None
349- resp_dict : Optional [Dict [str , Any ]] = None
350- if isinstance (result , dict ):
351- resp_dict = cast (Dict [str , Any ], result )
352- elif isinstance (result , BaseModel ):
353- resp_dict = result .model_dump (exclude_none = True )
354-
355- # if resp_dict has status set it
356- if resp_dict and "status" in resp_dict :
357- status_code = resp_dict .get ("status" )
358-
359- if resp_dict and "body" in resp_dict :
360- body = resp_dict .get ("body" , None )
361-
362- if status_code is not None :
363- response .status_code = status_code
364-
365- if body is not None :
366- self .logger .debug (f"Returning body { body } " )
367- return body
368- self .logger .debug ("Returning empty body" )
369- return response
370-
371- self .app .post ("/api/messages" )(on_activity_request )
408+ self .app .post ("/api/messages" )(self .on_activity_request )
372409
373410 async def health_check () -> Dict [str , Any ]:
374411 """Basic health check endpoint."""
0 commit comments