matrix-bridge-teams 0.1.0

A bridge between Matrix and Microsoft Teams written in Rust
use anyhow::{Context, Result};
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};

use super::models::{TeamsChannel, TeamsChat, TeamsMessage, TeamsTeam, TeamsUser};
use super::oauth::OAuth2Client;

const GRAPH_API_BASE_URL: &str = "https://graph.microsoft.com/v1.0";

pub struct GraphApiClient {
    http_client: HttpClient,
    oauth_client: OAuth2Client,
}

impl GraphApiClient {
    pub fn new(oauth_client: OAuth2Client) -> Self {
        Self {
            http_client: HttpClient::new(),
            oauth_client,
        }
    }

    async fn make_request<T: for<'de> Deserialize<'de>>(
        &self,
        method: reqwest::Method,
        endpoint: &str,
    ) -> Result<T> {
        let access_token = self.oauth_client.get_access_token().await?;

        let url = format!("{}{}", GRAPH_API_BASE_URL, endpoint);

        let response = self
            .http_client
            .request(method, &url)
            .bearer_auth(&access_token)
            .send()
            .await
            .context("failed to make Graph API request")?;

        let status = response.status();
        if !status.is_success() {
            let error_text = response.text().await.unwrap_or_default();
            anyhow::bail!("Graph API request failed: {} - {}", status, error_text);
        }

        response.json().await.context("failed to parse response")
    }

    pub async fn get_me(&self) -> Result<TeamsUser> {
        let response: GraphUserResponse = self
            .make_request(reqwest::Method::GET, "/me")
            .await?;

        Ok(TeamsUser {
            id: response.id,
            display_name: response.display_name,
            user_principal_name: response.user_principal_name,
            mail: response.mail,
            avatar_url: None,
        })
    }

    pub async fn get_team(&self, team_id: &str) -> Result<TeamsTeam> {
        let endpoint = format!("/teams/{}", team_id);
        let response: GraphTeamResponse = self
            .make_request(reqwest::Method::GET, &endpoint)
            .await?;

        Ok(TeamsTeam {
            id: response.id,
            display_name: response.display_name,
            description: response.description,
            web_url: response.web_url,
        })
    }

    pub async fn get_channel(&self, team_id: &str, channel_id: &str) -> Result<TeamsChannel> {
        let endpoint = format!("/teams/{}/channels/{}", team_id, channel_id);
        let response: GraphChannelResponse = self
            .make_request(reqwest::Method::GET, &endpoint)
            .await?;

        Ok(TeamsChannel {
            id: response.id,
            team_id: team_id.to_string(),
            display_name: response.display_name,
            description: response.description,
            channel_type: response.channel_type,
            web_url: response.web_url,
        })
    }

    pub async fn list_channels(&self, team_id: &str) -> Result<Vec<TeamsChannel>> {
        let endpoint = format!("/teams/{}/channels", team_id);
        let response: GraphChannelListResponse = self
            .make_request(reqwest::Method::GET, &endpoint)
            .await?;

        Ok(response
            .value
            .into_iter()
            .map(|c| TeamsChannel {
                id: c.id,
                team_id: team_id.to_string(),
                display_name: c.display_name,
                description: c.description,
                channel_type: c.channel_type,
                web_url: c.web_url,
            })
            .collect())
    }

    pub async fn send_channel_message(
        &self,
        team_id: &str,
        channel_id: &str,
        content: &str,
    ) -> Result<TeamsMessage> {
        let endpoint = format!("/teams/{}/channels/{}/messages", team_id, channel_id);
        
        let message_body = SendMessageRequest {
            body: MessageBody {
                content: content.to_string(),
                content_type: "text".to_string(),
            },
        };

        let access_token = self.oauth_client.get_access_token().await?;
        let url = format!("{}{}", GRAPH_API_BASE_URL, endpoint);

        let response = self
            .http_client
            .post(&url)
            .bearer_auth(&access_token)
            .json(&message_body)
            .send()
            .await
            .context("failed to send channel message")?;

        let status = response.status();
        if !status.is_success() {
            let error_text = response.text().await.unwrap_or_default();
            anyhow::bail!("Failed to send message: {} - {}", status, error_text);
        }

        let message: GraphMessageResponse = response.json().await?;
        Ok(self.graph_message_to_teams_message(message))
    }

    fn graph_message_to_teams_message(&self, msg: GraphMessageResponse) -> TeamsMessage {
        TeamsMessage {
            id: msg.id,
            channel_id: None,
            chat_id: None,
            sender_id: msg.from.user.id,
            sender_name: msg.from.user.display_name,
            content: msg.body.content,
            content_type: msg.body.content_type,
            attachments: vec![],
            reply_to: None,
            created_at: msg.created_date_time,
            updated_at: msg.last_modified_date_time,
        }
    }
}

#[derive(Debug, Serialize)]
struct SendMessageRequest {
    body: MessageBody,
}

#[derive(Debug, Serialize)]
struct MessageBody {
    content: String,
    content_type: String,
}

#[derive(Debug, Deserialize)]
struct GraphUserResponse {
    id: String,
    display_name: String,
    user_principal_name: Option<String>,
    mail: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GraphTeamResponse {
    id: String,
    display_name: String,
    description: Option<String>,
    web_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GraphChannelResponse {
    id: String,
    display_name: String,
    description: Option<String>,
    channel_type: String,
    web_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GraphChannelListResponse {
    value: Vec<GraphChannelResponse>,
}

#[derive(Debug, Deserialize)]
struct GraphMessageResponse {
    id: String,
    from: GraphMessageFrom,
    body: GraphMessageBody,
    created_date_time: String,
    last_modified_date_time: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GraphMessageFrom {
    user: GraphMessageUser,
}

#[derive(Debug, Deserialize)]
struct GraphMessageUser {
    id: String,
    display_name: String,
}

#[derive(Debug, Deserialize)]
struct GraphMessageBody {
    content: String,
    content_type: String,
}