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,
}