use axum::Router;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::header::AUTHORIZATION;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use systemprompt_config::SecretsBootstrap;
use systemprompt_identifiers::{TeamsConversationId, TeamsTenantId};
use systemprompt_loader::ConfigLoader;
use systemprompt_models::services::TeamsAppConfig;
use systemprompt_runtime::AppContext;
use systemprompt_security::authz::EntityRef;
use systemprompt_teams::activities::Activity;
use systemprompt_teams::auth::ActivityTokenVerifier;
use systemprompt_teams::client::TeamsClient;
use crate::routes::messaging::{
DispatchOutcome, MessagingInbound, ReplyTarget, dispatch_messaging, http_client,
};
const ISSUER: &str = "https://api.botframework.com";
pub fn teams_router() -> Router<AppContext> {
Router::new().route("/messages", post(handle_messages))
}
async fn handle_messages(
State(ctx): State<AppContext>,
headers: HeaderMap,
body: Bytes,
) -> Response {
let Ok(activity) = serde_json::from_slice::<Activity>(&body) else {
return StatusCode::BAD_REQUEST.into_response();
};
let Ok(normalized) = activity.normalize() else {
return StatusCode::OK.into_response();
};
let Some(app) = resolve_app(&normalized.tenant_id) else {
return StatusCode::OK.into_response();
};
let Some(token) = bearer(&headers) else {
return StatusCode::UNAUTHORIZED.into_response();
};
let verifier = activity_verifier(&app.app_id);
if verifier
.verify(
token,
&normalized.service_url,
chrono::Utc::now().timestamp(),
)
.await
.is_err()
{
return StatusCode::UNAUTHORIZED.into_response();
}
let Some(agent) = app.agent_for(&normalized.routing_key).cloned() else {
return StatusCode::OK.into_response();
};
let Some(app_password) = app_password(&app) else {
tracing::warn!(tenant = %normalized.tenant_id.as_str(), "no teams app password configured");
return StatusCode::OK.into_response();
};
let inbound = MessagingInbound {
platform: "teams",
issuer: ISSUER.to_owned(),
org_id: normalized.tenant_id.as_str().to_owned(),
channel_id: normalized.conversation_id.as_str().to_owned(),
external_user_id: normalized.teams_user_id.as_str().to_owned(),
text: normalized.text,
agent_name: agent,
entity: EntityRef::TeamsTenant(normalized.tenant_id),
reply: ReplyTarget::Channel {
id: normalized.conversation_id.as_str().to_owned(),
},
};
let reply = TeamsReply {
service_url: normalized.service_url,
conversation_id: normalized.conversation_id,
app_id: app.app_id,
app_password,
};
spawn_reply(ctx, inbound, reply);
StatusCode::OK.into_response()
}
struct TeamsReply {
service_url: String,
conversation_id: TeamsConversationId,
app_id: String,
app_password: String,
}
fn spawn_reply(ctx: AppContext, inbound: MessagingInbound, reply: TeamsReply) {
tokio::spawn(async move {
let text = match dispatch_messaging(&ctx, inbound).await {
Ok(DispatchOutcome::Replied(reply)) => non_empty(reply),
Ok(DispatchOutcome::Denied(reason)) => format!("⛔ {reason}"),
Err(err) => {
tracing::error!(error = %err, "teams dispatch failed");
"Sorry — something went wrong handling that.".to_owned()
},
};
let attachments = systemprompt_teams::cards::render_card(&text);
let client = teams_client(reply.app_id, reply.app_password);
if let Err(err) = client
.reply(
&reply.service_url,
&reply.conversation_id,
attachments,
chrono::Utc::now().timestamp(),
)
.await
{
tracing::error!(error = %err, "failed to post teams reply");
}
});
}
fn non_empty(text: String) -> String {
if text.trim().is_empty() {
"(no response)".to_owned()
} else {
text
}
}
fn resolve_app(tenant_id: &TeamsTenantId) -> Option<TeamsAppConfig> {
let config = ConfigLoader::load().ok()?;
config
.teams_apps
.into_values()
.find(|app| app.enabled && app.tenant_id == *tenant_id)
}
fn app_password(app: &TeamsAppConfig) -> Option<String> {
SecretsBootstrap::get()
.ok()?
.get(app.app_password_ref.as_str())
.cloned()
}
fn bearer(headers: &HeaderMap) -> Option<&str> {
headers
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
}
fn activity_verifier(app_id: &str) -> ActivityTokenVerifier {
#[cfg(feature = "test-api")]
if let Ok(openid_url) = std::env::var("SYSTEMPROMPT_TEST_TEAMS_OPENID_URL") {
return ActivityTokenVerifier::with_openid_url(
http_client(),
app_id.to_owned(),
openid_url,
);
}
ActivityTokenVerifier::new(http_client(), app_id.to_owned())
}
fn teams_client(app_id: String, app_password: String) -> TeamsClient {
#[cfg(feature = "test-api")]
if let Ok(token_url) = std::env::var("SYSTEMPROMPT_TEST_TEAMS_TOKEN_URL") {
return TeamsClient::with_endpoints(http_client(), app_id, app_password, token_url);
}
TeamsClient::new(http_client(), app_id, app_password)
}