msteams-mcp-server
Version:
Microsoft Teams MCP Server - Complete Teams integration for Claude Desktop and MCP clients with secure OAuth2 authentication and comprehensive team management
1,448 lines • 64.6 kB
JavaScript
import { logger } from './api.js';
import { teamsAuth } from './teams-auth.js';
/**
* Microsoft Teams operations using Microsoft Graph API
*/
export class TeamsOperations {
graphClient = null;
constructor() {
// Initialize will be called when needed
}
/**
* Initialize Graph client with authentication
*/
async ensureGraphClient() {
if (!this.graphClient) {
logger.log('Initializing Teams Graph client...');
this.graphClient = await teamsAuth.getGraphClient();
logger.log('Teams Graph client initialized successfully');
}
else {
logger.log('Using existing Teams Graph client');
}
return this.graphClient;
}
/**
* Get all teams the user is a member of
*/
async getUserTeams(maxResults) {
try {
logger.log('getUserTeams() method called');
// Build API path with select minimal fields
// Note: $top is not supported by /me/joinedTeams endpoint
let apiPath = '/me/joinedTeams';
const params = [];
// Select only essential fields to reduce payload size
params.push('$select=id,displayName,description,visibility,webUrl,createdDateTime');
if (params.length > 0) {
apiPath += '?' + params.join('&');
}
logger.log(`Making API call to: ${apiPath}`);
// Use direct HTTP request instead of Graph SDK to avoid hanging
const token = await this.getAccessToken();
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Successfully retrieved ${response.value.length} teams`);
// Apply client-side limiting if maxResults is specified
let teams = response.value;
if (maxResults && teams.length > maxResults) {
teams = teams.slice(0, maxResults);
logger.log(`Limited results to ${maxResults} teams`);
}
return teams.map((team) => ({
id: team.id,
displayName: team.displayName,
description: team.description,
visibility: team.visibility,
webUrl: team.webUrl,
createdDateTime: team.createdDateTime,
memberSettings: team.memberSettings,
guestSettings: team.guestSettings,
messagingSettings: team.messagingSettings,
funSettings: team.funSettings
}));
}
catch (error) {
logger.error('Error getting user teams:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get teams: ${errorMessage}`);
}
}
/**
* Search for teams
*/
async searchTeams(options = {}) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Build query
let apiPath = '/me/joinedTeams';
const params = [];
if (options.maxResults) {
params.push(`$top=${options.maxResults}`);
}
if (params.length > 0) {
apiPath += '?' + params.join('&');
}
const response = await this.makeDirectGraphRequest(apiPath, token);
let teams = response.value;
// Client-side filtering for now (Graph API has limited search capabilities for teams)
if (options.query) {
const query = options.query.toLowerCase();
teams = teams.filter((team) => team.displayName.toLowerCase().includes(query) ||
(team.description && team.description.toLowerCase().includes(query)));
}
if (options.visibility) {
teams = teams.filter((team) => team.visibility === options.visibility);
}
logger.log(`Found ${teams.length} teams matching search criteria`);
return teams.map((team) => ({
id: team.id,
displayName: team.displayName,
description: team.description,
visibility: team.visibility,
webUrl: team.webUrl,
createdDateTime: team.createdDateTime
}));
}
catch (error) {
logger.error('Error searching teams:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to search teams: ${errorMessage}`);
}
}
/**
* Get a specific team by ID
*/
async getTeam(teamId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const response = await this.makeDirectGraphRequest(`/teams/${teamId}`, token);
logger.log(`Retrieved team: ${response.displayName}`);
return {
id: response.id,
displayName: response.displayName,
description: response.description,
visibility: response.visibility,
webUrl: response.webUrl,
createdDateTime: response.createdDateTime,
memberSettings: response.memberSettings,
guestSettings: response.guestSettings,
messagingSettings: response.messagingSettings,
funSettings: response.funSettings
};
}
catch (error) {
logger.error('Error getting team:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get team: ${errorMessage}`);
}
}
/**
* Create a new team
*/
async createTeam(request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const teamData = {
'template@odata.bind': "https://graph.microsoft.com/v1.0/teamsTemplates('standard')",
displayName: request.displayName,
description: request.description || '',
visibility: request.visibility
};
const response = await this.makeDirectGraphPostRequest('/teams', token, teamData);
logger.log(`Created team: ${request.displayName}`);
// Add members and owners if specified
if (request.owners && request.owners.length > 0) {
for (const ownerId of request.owners) {
await this.addTeamMember(response.id, ownerId, ['owner']);
}
}
if (request.members && request.members.length > 0) {
for (const memberId of request.members) {
await this.addTeamMember(response.id, memberId, ['member']);
}
}
return {
id: response.id,
displayName: response.displayName,
description: response.description,
visibility: response.visibility,
webUrl: response.webUrl,
createdDateTime: response.createdDateTime
};
}
catch (error) {
logger.error('Error creating team:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create team: ${errorMessage}`);
}
}
/**
* Get channels for a team
*/
async getTeamChannels(teamId) {
try {
// Use direct HTTP request to avoid Graph SDK issues
const token = await this.getAccessToken();
const apiPath = `/teams/${teamId}/channels`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} channels for team ${teamId}`);
return response.value.map((channel) => ({
id: channel.id,
displayName: channel.displayName,
description: channel.description,
webUrl: channel.webUrl,
membershipType: channel.membershipType,
createdDateTime: channel.createdDateTime
}));
}
catch (error) {
logger.error('Error getting team channels:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get channels: ${errorMessage}`);
}
}
/**
* Search channels in a team
*/
async searchChannels(options) {
try {
let channels = await this.getTeamChannels(options.teamId);
// Client-side filtering
if (options.query) {
const query = options.query.toLowerCase();
channels = channels.filter(channel => channel.displayName.toLowerCase().includes(query) ||
(channel.description && channel.description.toLowerCase().includes(query)));
}
if (options.membershipType) {
channels = channels.filter(channel => channel.membershipType === options.membershipType);
}
if (options.maxResults) {
channels = channels.slice(0, options.maxResults);
}
logger.log(`Found ${channels.length} channels matching search criteria`);
return channels;
}
catch (error) {
logger.error('Error searching channels:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to search channels: ${errorMessage}`);
}
}
/**
* Create a new channel in a team
*/
async createChannel(teamId, request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const channelData = {
displayName: request.displayName,
description: request.description || '',
membershipType: request.membershipType || 'standard'
};
const response = await this.makeDirectGraphPostRequest(`/teams/${teamId}/channels`, token, channelData);
logger.log(`Created channel: ${request.displayName} in team ${teamId}`);
return {
id: response.id,
displayName: response.displayName,
description: response.description,
webUrl: response.webUrl,
membershipType: response.membershipType,
createdDateTime: response.createdDateTime
};
}
catch (error) {
logger.error('Error creating channel:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create channel: ${errorMessage}`);
}
}
/**
* Send a message to a channel
*/
async sendChannelMessage(teamId, channelId, request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Format mentions for Microsoft Graph API
const mentions = request.mentions ? await Promise.all(request.mentions.map(async (mention, index) => {
// Try to find the user by display name if ID is not provided
let userId = mention.id;
if (!userId && mention.displayName) {
// Search for user by display name using direct HTTP
try {
const response = await this.makeDirectGraphRequest(`/users?$filter=displayName eq '${mention.displayName}'`, token);
if (response.value && response.value.length > 0) {
userId = response.value[0].id;
}
}
catch (error) {
logger.warn(`Failed to find user with display name '${mention.displayName}': ${error instanceof Error ? error.message : String(error)}`);
}
}
if (!userId) {
throw new Error(`User ID is required for mention. Could not find user with display name: ${mention.displayName}`);
}
return {
id: index,
mentionText: mention.displayName || userId,
mentioned: {
user: {
id: userId,
displayName: mention.displayName || userId
}
}
};
})) : [];
const messageData = {
body: {
content: request.content,
contentType: request.contentType || 'text'
},
attachments: request.attachments || [],
mentions: mentions
};
// Use direct HTTP POST request
const response = await this.makeDirectGraphPostRequest(`/teams/${teamId}/channels/${channelId}/messages`, token, messageData);
logger.log(`Sent message to channel ${channelId} in team ${teamId}`);
return {
id: response.id,
messageType: response.messageType,
createdDateTime: response.createdDateTime,
lastModifiedDateTime: response.lastModifiedDateTime,
body: response.body,
from: response.from,
attachments: response.attachments,
mentions: response.mentions,
reactions: response.reactions
};
}
catch (error) {
logger.error('Error sending channel message:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to send message: ${errorMessage}`);
}
}
/**
* Get messages from a channel
*/
async getChannelMessages(teamId, channelId, options = {}) {
try {
// Use direct HTTP request to avoid Graph SDK hanging issues
const token = await this.getAccessToken();
let apiPath = `/teams/${teamId}/channels/${channelId}/messages`;
if (options.maxResults) {
apiPath += `?$top=${options.maxResults}`;
}
logger.log(`Making direct HTTP request to: ${apiPath}`);
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} messages from channel ${channelId}`);
return response.value.map((message) => ({
id: message.id,
messageType: message.messageType,
createdDateTime: message.createdDateTime,
lastModifiedDateTime: message.lastModifiedDateTime,
body: message.body,
from: message.from,
attachments: message.attachments,
mentions: message.mentions,
reactions: message.reactions
}));
}
catch (error) {
logger.error('Error getting channel messages:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get messages: ${errorMessage}`);
}
}
/**
* Search messages in a channel
*/
async searchChannelMessages(options) {
try {
let messages = await this.getChannelMessages(options.teamId, options.channelId, {
maxResults: options.maxResults || 100
});
// Client-side filtering
if (options.query) {
const query = options.query.toLowerCase();
messages = messages.filter(message => message.body.content.toLowerCase().includes(query));
}
if (options.fromDate) {
const fromDate = new Date(options.fromDate);
messages = messages.filter(message => new Date(message.createdDateTime) >= fromDate);
}
if (options.toDate) {
const toDate = new Date(options.toDate);
messages = messages.filter(message => new Date(message.createdDateTime) <= toDate);
}
logger.log(`Found ${messages.length} messages matching search criteria`);
return messages;
}
catch (error) {
logger.error('Error searching channel messages:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to search messages: ${errorMessage}`);
}
}
/**
* Get team members
*/
async getTeamMembers(teamId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const apiPath = `/teams/${teamId}/members`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} members for team ${teamId}`);
return response.value.map((member) => ({
id: member.id,
displayName: member.displayName,
email: member.email,
roles: member.roles,
visibleHistoryStartDateTime: member.visibleHistoryStartDateTime
}));
}
catch (error) {
logger.error('Error getting team members:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get team members: ${errorMessage}`);
}
}
/**
* Add a member to a team
*/
async addTeamMember(teamId, userId, roles = ['member']) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const memberData = {
'@odata.type': '#microsoft.graph.aadUserConversationMember',
'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`,
roles: roles
};
await this.makeDirectGraphPostRequest(`/teams/${teamId}/members`, token, memberData);
logger.log(`Added member ${userId} to team ${teamId} with roles: ${roles.join(', ')}`);
}
catch (error) {
logger.error('Error adding team member:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to add team member: ${errorMessage}`);
}
}
/**
* Remove a member from a team
*/
async removeTeamMember(teamId, memberId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
await this.makeDirectGraphDeleteRequest(`/teams/${teamId}/members/${memberId}`, token);
logger.log(`Removed member ${memberId} from team ${teamId}`);
}
catch (error) {
logger.error('Error removing team member:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to remove team member: ${errorMessage}`);
}
}
/**
* Get current user information
*/
async getCurrentUser() {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const response = await this.makeDirectGraphRequest('/me', token);
return {
id: response.id,
displayName: response.displayName,
email: response.userPrincipalName || response.mail
};
}
catch (error) {
logger.error('Error getting current user:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get current user: ${errorMessage}`);
}
}
/**
* Search for users in the organization
*/
async searchUsers(query, maxResults = 10) {
try {
logger.log(`Searching for users with query: ${query}`);
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const searchQuery = `$search="displayName:${query}" OR "mail:${query}"&$top=${maxResults}`;
const apiPath = `/users?${searchQuery}`;
logger.log(`Making direct HTTP request to: ${apiPath}`);
const response = await this.makeDirectGraphRequest(apiPath, token, {
'ConsistencyLevel': 'eventual'
});
logger.log(`Found ${response.value.length} users matching query: ${query}`);
return response.value.map((user) => ({
id: user.id,
displayName: user.displayName,
email: user.userPrincipalName || user.mail
}));
}
catch (error) {
logger.error('Error searching users:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to search users: ${errorMessage}`);
}
}
/**
* Get or create a chat with a specific user
*/
async getOrCreateChatWithUser(userId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// First try to find existing chat with this user
logger.log(`Looking for existing chat with user ${userId}`);
const chatsResponse = await this.makeDirectGraphRequest('/me/chats', token);
for (const chat of chatsResponse.value) {
if (chat.chatType === 'oneOnOne') {
// Get members of this chat
const membersResponse = await this.makeDirectGraphRequest(`/chats/${chat.id}/members`, token);
const members = membersResponse.value;
// Check if this chat includes the target user
if (members.some((member) => member.userId === userId)) {
logger.log(`Found existing chat ${chat.id} with user ${userId}`);
return {
id: chat.id,
topic: chat.topic,
createdDateTime: chat.createdDateTime,
lastUpdatedDateTime: chat.lastUpdatedDateTime,
chatType: chat.chatType,
members: members.map((member) => ({
id: member.userId,
displayName: member.displayName,
email: member.email,
roles: member.roles || []
}))
};
}
}
}
// If no existing chat found, create a new one using POST request
logger.log(`No existing chat found, creating new chat with user ${userId}`);
const chatData = {
chatType: 'oneOnOne',
members: [
{
'@odata.type': '#microsoft.graph.aadUserConversationMember',
'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`,
roles: ['owner']
}
]
};
const newChatResponse = await this.makeDirectGraphPostRequest('/chats', token, chatData);
logger.log(`Created new chat ${newChatResponse.id} with user ${userId}`);
return {
id: newChatResponse.id,
topic: newChatResponse.topic,
createdDateTime: newChatResponse.createdDateTime,
lastUpdatedDateTime: newChatResponse.lastUpdatedDateTime,
chatType: newChatResponse.chatType,
members: [] // Will be populated when needed
};
}
catch (error) {
logger.error('Error getting or creating chat:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get or create chat: ${errorMessage}`);
}
}
/**
* Send a direct message to a user
*/
async sendDirectMessage(userId, request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Get or create chat with the user
const chat = await this.getOrCreateChatWithUser(userId);
// Format mentions for Microsoft Graph API (same as channel messages)
const mentions = request.mentions ? await Promise.all(request.mentions.map(async (mention, index) => {
let mentionUserId = mention.id;
if (!mentionUserId && mention.displayName) {
try {
// Use direct HTTP request for user search
const response = await this.makeDirectGraphRequest(`/users?$filter=displayName eq '${mention.displayName}'`, token);
if (response.value && response.value.length > 0) {
mentionUserId = response.value[0].id;
}
}
catch (error) {
logger.warn(`Failed to find user with display name '${mention.displayName}': ${error instanceof Error ? error.message : String(error)}`);
}
}
if (!mentionUserId) {
throw new Error(`User ID is required for mention. Could not find user with display name: ${mention.displayName}`);
}
return {
id: index,
mentionText: mention.displayName || mentionUserId,
mentioned: {
user: {
id: mentionUserId,
displayName: mention.displayName || mentionUserId
}
}
};
})) : [];
const messageData = {
body: {
content: request.content,
contentType: request.contentType || 'text'
},
mentions: mentions
};
// Use direct HTTP POST request
const response = await this.makeDirectGraphPostRequest(`/chats/${chat.id}/messages`, token, messageData);
logger.log(`Sent direct message to user ${userId} in chat ${chat.id}`);
return {
id: response.id,
messageType: response.messageType,
createdDateTime: response.createdDateTime,
lastModifiedDateTime: response.lastModifiedDateTime,
body: response.body,
from: response.from,
attachments: response.attachments,
mentions: response.mentions,
reactions: response.reactions
};
}
catch (error) {
logger.error('Error sending direct message:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to send direct message: ${errorMessage}`);
}
}
/**
* Get direct messages from a chat
*/
async getDirectMessages(userId, options = {}) {
try {
// Get chat with the user
const chat = await this.getOrCreateChatWithUser(userId);
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const query = options.maxResults ? `?$top=${options.maxResults}` : '';
const apiPath = `/chats/${chat.id}/messages${query}`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} messages from chat ${chat.id}`);
return response.value.map((message) => ({
id: message.id,
messageType: message.messageType,
createdDateTime: message.createdDateTime,
lastModifiedDateTime: message.lastModifiedDateTime,
body: message.body,
from: message.from,
attachments: message.attachments,
mentions: message.mentions,
reactions: message.reactions
}));
}
catch (error) {
logger.error('Error getting direct messages:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get direct messages: ${errorMessage}`);
}
}
/**
* Add a reaction to a channel message
*/
async addMessageReaction(teamId, channelId, messageId, reactionType) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Microsoft Graph uses specific reaction types
const validReactions = ['like', 'heart', 'laugh', 'surprised', 'sad', 'angry'];
if (!validReactions.includes(reactionType.toLowerCase())) {
throw new Error(`Invalid reaction type. Must be one of: ${validReactions.join(', ')}`);
}
const reactionData = {
reactionType: reactionType.toLowerCase()
};
await this.makeDirectGraphPostRequest(`/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions`, token, reactionData);
logger.log(`Added ${reactionType} reaction to message ${messageId}`);
}
catch (error) {
logger.error('Error adding message reaction:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to add reaction: ${errorMessage}`);
}
}
/**
* Remove a reaction from a channel message
*/
async removeMessageReaction(teamId, channelId, messageId, reactionType) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Get current user to identify which reaction to remove
const currentUser = await this.getCurrentUser();
await this.makeDirectGraphDeleteRequest(`/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions/${reactionType.toLowerCase()}/${currentUser.id}`, token);
logger.log(`Removed ${reactionType} reaction from message ${messageId}`);
}
catch (error) {
logger.error('Error removing message reaction:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to remove reaction: ${errorMessage}`);
}
}
/**
* Add a reaction to a chat message (direct message)
*/
async addChatMessageReaction(chatId, messageId, reactionType) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const validReactions = ['like', 'heart', 'laugh', 'surprised', 'sad', 'angry'];
if (!validReactions.includes(reactionType.toLowerCase())) {
throw new Error(`Invalid reaction type. Must be one of: ${validReactions.join(', ')}`);
}
const reactionData = {
reactionType: reactionType.toLowerCase()
};
await this.makeDirectGraphPostRequest(`/chats/${chatId}/messages/${messageId}/reactions`, token, reactionData);
logger.log(`Added ${reactionType} reaction to chat message ${messageId}`);
}
catch (error) {
logger.error('Error adding chat message reaction:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to add chat reaction: ${errorMessage}`);
}
}
/**
* Remove a reaction from a chat message
*/
async removeChatMessageReaction(chatId, messageId, reactionType) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const currentUser = await this.getCurrentUser();
await this.makeDirectGraphDeleteRequest(`/chats/${chatId}/messages/${messageId}/reactions/${reactionType.toLowerCase()}/${currentUser.id}`, token);
logger.log(`Removed ${reactionType} reaction from chat message ${messageId}`);
}
catch (error) {
logger.error('Error removing chat message reaction:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to remove chat reaction: ${errorMessage}`);
}
}
/**
* Upload a file to a team's SharePoint drive
*/
async uploadFileToTeam(teamId, request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Get the team's drive
const driveResponse = await this.makeDirectGraphRequest(`/groups/${teamId}/drive`, token);
const driveId = driveResponse.id;
// Upload the file to the drive's root folder
let fileContent;
if (Buffer.isBuffer(request.fileContent)) {
fileContent = request.fileContent;
}
else {
fileContent = Buffer.from(request.fileContent);
}
const uploadUrl = `/drives/${driveId}/root:/${encodeURIComponent(request.fileName)}:/content`;
const response = await this.makeDirectGraphPutRequest(uploadUrl, token, fileContent, {
'Content-Type': 'application/octet-stream'
});
logger.log(`Uploaded file ${request.fileName} to team ${teamId}`);
return {
id: response.id,
name: response.name,
size: response.size,
createdDateTime: response.createdDateTime,
lastModifiedDateTime: response.lastModifiedDateTime,
downloadUrl: response['@microsoft.graph.downloadUrl'],
webUrl: response.webUrl,
folder: response.folder,
file: response.file,
createdBy: response.createdBy
};
}
catch (error) {
logger.error('Error uploading file to team:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to upload file: ${errorMessage}`);
}
}
/**
* Upload a file to a channel's files tab
*/
async uploadFileToChannel(teamId, channelId, request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Get the channel's drive folder
const channelResponse = await this.makeDirectGraphRequest(`/teams/${teamId}/channels/${channelId}/filesFolder`, token);
const driveId = channelResponse.parentReference.driveId;
const folderId = channelResponse.id;
// Upload the file to the channel folder
let fileContent;
if (Buffer.isBuffer(request.fileContent)) {
fileContent = request.fileContent;
}
else {
fileContent = Buffer.from(request.fileContent);
}
const uploadUrl = `/drives/${driveId}/items/${folderId}:/${encodeURIComponent(request.fileName)}:/content`;
const response = await this.makeDirectGraphPutRequest(uploadUrl, token, fileContent, {
'Content-Type': 'application/octet-stream'
});
logger.log(`Uploaded file ${request.fileName} to channel ${channelId} in team ${teamId}`);
return {
id: response.id,
name: response.name,
size: response.size,
createdDateTime: response.createdDateTime,
lastModifiedDateTime: response.lastModifiedDateTime,
downloadUrl: response['@microsoft.graph.downloadUrl'],
webUrl: response.webUrl,
folder: response.folder,
file: response.file,
createdBy: response.createdBy
};
}
catch (error) {
logger.error('Error uploading file to channel:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to upload file to channel: ${errorMessage}`);
}
}
/**
* List files in a team's drive
*/
async getTeamFiles(teamId, maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const queryParams = maxResults ? `?$top=${maxResults}` : '';
const apiPath = `/groups/${teamId}/drive/root/children${queryParams}`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} files from team ${teamId}`);
return response.value.map((item) => ({
id: item.id,
name: item.name,
size: item.size || 0,
createdDateTime: item.createdDateTime,
lastModifiedDateTime: item.lastModifiedDateTime,
downloadUrl: item['@microsoft.graph.downloadUrl'],
webUrl: item.webUrl,
folder: item.folder,
file: item.file,
createdBy: item.createdBy
}));
}
catch (error) {
logger.error('Error getting team files:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get team files: ${errorMessage}`);
}
}
/**
* List files in a channel's files tab
*/
async getChannelFiles(teamId, channelId, maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Get the channel's drive folder
const channelResponse = await this.makeDirectGraphRequest(`/teams/${teamId}/channels/${channelId}/filesFolder`, token);
const driveId = channelResponse.parentReference.driveId;
const folderId = channelResponse.id;
const queryParams = maxResults ? `?$top=${maxResults}` : '';
const response = await this.makeDirectGraphRequest(`/drives/${driveId}/items/${folderId}/children${queryParams}`, token);
logger.log(`Retrieved ${response.value.length} files from channel ${channelId}`);
return response.value.map((item) => ({
id: item.id,
name: item.name,
size: item.size || 0,
createdDateTime: item.createdDateTime,
lastModifiedDateTime: item.lastModifiedDateTime,
downloadUrl: item['@microsoft.graph.downloadUrl'],
webUrl: item.webUrl,
folder: item.folder,
file: item.file,
createdBy: item.createdBy
}));
}
catch (error) {
logger.error('Error getting channel files:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get channel files: ${errorMessage}`);
}
}
/**
* Download a file from a team's drive
*/
async downloadFile(driveId, itemId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
// Get file metadata
const fileResponse = await this.makeDirectGraphRequest(`/drives/${driveId}/items/${itemId}`, token);
// Download file content
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
const content = Buffer.from(await response.arrayBuffer());
logger.log(`Downloaded file ${fileResponse.name} (${content.length} bytes)`);
return {
fileName: fileResponse.name,
content: content,
mimeType: fileResponse.file?.mimeType || 'application/octet-stream'
};
}
catch (error) {
logger.error('Error downloading file:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to download file: ${errorMessage}`);
}
}
/**
* Reply to a message in a channel
*/
async replyToChannelMessage(teamId, channelId, messageId, content, contentType = 'text') {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const replyData = {
body: {
content: content,
contentType: contentType
}
};
// Use direct HTTP POST request
const response = await this.makeDirectGraphPostRequest(`/teams/${teamId}/channels/${channelId}/messages/${messageId}/replies`, token, replyData);
logger.log(`Replied to message ${messageId} in channel ${channelId}`);
return {
id: response.id,
messageType: response.messageType,
createdDateTime: response.createdDateTime,
lastModifiedDateTime: response.lastModifiedDateTime,
body: response.body,
from: response.from,
replyToId: messageId
};
}
catch (error) {
logger.error('Error replying to channel message:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to reply to message: ${errorMessage}`);
}
}
/**
* Get replies to a message in a channel
*/
async getChannelMessageReplies(teamId, channelId, messageId, maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const queryParams = maxResults ? `?$top=${maxResults}` : '';
const apiPath = `/teams/${teamId}/channels/${channelId}/messages/${messageId}/replies${queryParams}`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} replies to message ${messageId}`);
return response.value.map((reply) => ({
id: reply.id,
messageType: reply.messageType,
createdDateTime: reply.createdDateTime,
lastModifiedDateTime: reply.lastModifiedDateTime,
body: reply.body,
from: reply.from,
replyToId: messageId
}));
}
catch (error) {
logger.error('Error getting message replies:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get message replies: ${errorMessage}`);
}
}
/**
* Reply to a message in a chat (direct message)
*/
async replyToChatMessage(chatId, messageId, content, contentType = 'text') {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const replyData = {
body: {
content: content,
contentType: contentType
}
};
// Use direct HTTP POST request
const response = await this.makeDirectGraphPostRequest(`/chats/${chatId}/messages/${messageId}/replies`, token, replyData);
logger.log(`Replied to chat message ${messageId} in chat ${chatId}`);
return {
id: response.id,
messageType: response.messageType,
createdDateTime: response.createdDateTime,
lastModifiedDateTime: response.lastModifiedDateTime,
body: response.body,
from: response.from,
replyToId: messageId
};
}
catch (error) {
logger.error('Error replying to chat message:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to reply to chat message: ${errorMessage}`);
}
}
/**
* Get replies to a message in a chat
*/
async getChatMessageReplies(chatId, messageId, maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const queryParams = maxResults ? `?$top=${maxResults}` : '';
const apiPath = `/chats/${chatId}/messages/${messageId}/replies${queryParams}`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} replies to chat message ${messageId}`);
return response.value.map((reply) => ({
id: reply.id,
messageType: reply.messageType,
createdDateTime: reply.createdDateTime,
lastModifiedDateTime: reply.lastModifiedDateTime,
body: reply.body,
from: reply.from,
replyToId: messageId
}));
}
catch (error) {
logger.error('Error getting chat message replies:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get chat message replies: ${errorMessage}`);
}
}
/**
* Create a group chat
*/
async createGroupChat(request) {
try {
// Format members for Microsoft Graph API
const members = await Promise.all(request.members.map(async (userId) => ({
'@odata.type': '#microsoft.graph.aadUserConversationMember',
'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`,
roles: ['owner']
})));
const chatData = {
chatType: 'group',
topic: request.topic,
members: members
};
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const response = await this.makeDirectGraphPostRequest('/chats', token, chatData);
logger.log(`Created group chat with ${request.members.length} members`);
return {
id: response.id,
topic: response.topic,
createdDateTime: response.createdDateTime,
lastUpdatedDateTime: response.lastUpdatedDateTime,
chatType: response.chatType,
members: [], // Will be populated when needed
webUrl: response.webUrl
};
}
catch (error) {
logger.error('Error creating group chat:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create group chat: ${errorMessage}`);
}
}
/**
* Get all group chats for the current user
*/
async getGroupChats(maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const queryParams = `?$filter=chatType eq 'group'&$top=${maxResults}`;
const apiPath = `/me/chats${queryParams}`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} group chats`);
return response.value.map((chat) => ({
id: chat.id,
topic: chat.topic,
createdDateTime: chat.createdDateTime,
lastUpdatedDateTime: chat.lastUpdatedDateTime,
chatType: chat.chatType,
members: [], // Members can be fetched separately if needed
webUrl: chat.webUrl
}));
}
catch (error) {
logger.error('Error getting group chats:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get group chats: ${errorMessage}`);
}
}
/**
* Get members of a group chat
*/
async getGroupChatMembers(chatId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const apiPath = `/chats/${chatId}/members`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} members from group chat ${chatId}`);
return response.value.map((member) => ({
id: member.userId,
displayName: member.displayName,
email: member.email,
roles: member.roles || []
}));
}
catch (error) {
logger.error('Error getting group chat members:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get group chat members: ${errorMessage}`);
}
}
/**
* Add members to a group chat
*/
async addGroupChatMembers(chatId, userIds) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
for (const userId of userIds) {
const memberData = {
'@odata.type': '#microsoft.graph.aadUserConversationMember',
'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`,
roles: ['owner']
};
await this.makeDirectGraphPostRequest(`/chats/${chatId}/members`, token, memberData);
logger.log(`Added user ${userId} to group chat ${chatId}`);
}
}
catch (error) {
logger.error('Error adding group chat members:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to add group chat members: ${errorMessage}`);
}
}
/**
* Remove a member from a group chat
*/
async removeGroupChatMember(chatId, membershipId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
await this.makeDirectGraphDeleteRequest(`/chats/${chatId}/members/${membershipId}`, token);
logger.log(`Removed member ${membershipId} from group chat ${chatId}`);
}
catch (error) {
logger.error('Error removing group chat member:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to remove group chat member: ${errorMessage}`);
}
}
/**
* Create an online meeting
*/
async createOnlineMeeting(request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const meetingData = {
subject: request.subject,
startDateTime: request.startDateTime,
endDateTime: request.endDateTime,
participants: request.attendees ? {
attendees: request.attendees.map(email => ({
identity: {
user: {
email: email
}
}
}))
} : undefined,
allowMeetingChat: true,
allowTeamworkReactions: true,
isRecordingEnabled: request.isRecordingEnabled || false
};
const response = await this.makeDirectGraphPostRequest('/me/onlineMeetings', token, meetingData);
logger.log(`Created online meeting: ${request.subject}`);
return {
id: response.id,
subject: response.subject,
startDateTime: response.startDateTime,
endDateTime: response.endDateTime,
joinWebUrl: response.joinWebUrl,
organizer: response.organizer,
participants: response.participants,
isRecordingEnabled: response.isRecordingEnabled,
allowMeetingChat: response.allowMeetingChat,
allowTeamworkReactions: response.allowTeamworkReactions
};
}
catch (error) {
logger.error('Error creating online meeting:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create online meeting: ${errorMessage}`);
}
}
/**
* Get user's online meetings
*/
async getOnlineMeetings(maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK issues
const token = await this.getAccessToken();
const apiPath = '/me/onlineMeetings';
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} online meetings`);
// Apply client-side limiting if maxResults is specified
let meetings = response.value;
if (maxResults && meetings.length > maxResults) {
meetings = meetings.slice(0, maxResults);
logger.log(`Limited results to ${maxResults} meetings`);
}
return meetings.map((meeting) => ({
id: meeting.id,
subject: meeting.subject,
startDateTime: meeting.startDateTime,
endDateTime: meeting.endDateTime,
joinWebUrl: meeting.joinWebUrl,
organizer: meeting.organizer,
participants: meeting.participants,
isRecordingEnabled: meeting.isRecordingEnabled,
allowMeetingChat: meeting.allowMeetingChat,
allowTeamworkReactions: meeting.allowTeamworkReactions
}));
}
catch (error) {
logger.error('Error getting online meetings:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get online meetings: ${errorMessage}`);
}
}
/**
* Get calendar events
*/
async getCalendarEvents(startTime, endTime, maxResults = 20) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
let queryParams = `?$top=${maxResults}&$orderby=start/dateTime`;
if (startTime && endTime) {
queryParams += `&$filter=start/dateTime ge '${startTime}' and end/dateTime le '${endTime}'`;
}
const apiPath = `/me/calendar/events${queryParams}`;
const response = await this.makeDirectGraphRequest(apiPath, token);
logger.log(`Retrieved ${response.value.length} calendar events`);
return response.value.map((event) => ({
id: event.id,
subject: event.subject,
start: event.start,
end: event.end,
location: event.location,
organizer: event.organizer,
attendees: event.attendees,
isOnlineMeeting: event.isOnlineMeeting,
onlineMeeting: event.onlineMeeting
}));
}
catch (error) {
logger.error('Error getting calendar events:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get calendar events: ${errorMessage}`);
}
}
/**
* Get user presence
*/
async getUserPresence(userId) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const endpoint = userId ? `/users/${userId}/presence` : '/me/presence';
const response = await this.makeDirectGraphRequest(endpoint, token);
logger.log(`Retrieved presence for user ${userId || 'current user'}`);
return {
id: response.id,
availability: response.availability,
activity: response.activity,
statusMessage: response.statusMessage
};
}
catch (error) {
logger.error('Error getting user presence:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get user presence: ${errorMessage}`);
}
}
/**
* Set user presence
*/
async setUserPresence(request) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const presenceData = {
availability: request.availability,
activity: request.activity,
expirationDuration: request.expirationDuration || 'PT1H' // Default to 1 hour
};
await this.makeDirectGraphPostRequest('/me/presence/setPresence', token, presenceData);
logger.log(`Set presence to ${request.availability}/${request.activity}`);
}
catch (error) {
logger.error('Error setting user presence:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to set user presence: ${errorMessage}`);
}
}
/**
* Get presence for multiple users
*/
async getBulkPresence(userIds) {
try {
// Use direct HTTP request to avoid Graph SDK timeout issues
const token = await this.getAccessToken();
const requestBody = {
ids: userIds
};
const response = await this.makeDirectGraphPostRequest('/communications/getPresencesByUserId', token, requestBody);
logger.log(`Retrieved presence for ${response.value.length} users`);
return response.value.map((presence) => ({
id: presence.id,
availability: presence.availability,
activity: presence.activity,
statusMessage: presence.statusMessage
}));
}
catch (error) {
logger.error('Error getting bulk user presence:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get bulk user presence: ${errorMessage}`);
}
}
/**
* Get access token directly
*/
async getAccessToken() {
const token = await teamsAuth.authenticate();
return token.accessToken;
}
/**
* Make direct HTTP request to Graph API
*/
async makeDirectGraphRequest(path, token, additionalHeaders) {
const url = `https://graph.microsoft.com/v1.0${path}`;
logger.log(`Making direct HTTP request to: ${url}`);
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...additionalHeaders
};
const response = await fetch(url, {
method: 'GET',
headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
/**
* Make direct HTTP POST request to Graph API
*/
async makeDirectGraphPostRequest(path, token, body, additionalHeaders) {
const url = `https://graph.microsoft.com/v1.0${path}`;
logger.log(`Making direct HTTP POST request to: ${url}`);
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...additionalHeaders
};
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
return await response.json();
}
/**
* Make direct HTTP DELETE request to Graph API
*/
async makeDirectGraphDeleteRequest(path, token, additionalHeaders) {
const url = `https://graph.microsoft.com/v1.0${path}`;
logger.log(`Making direct HTTP DELETE request to: ${url}`);
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...additionalHeaders
};
const response = await fetch(url, {
method: 'DELETE',
headers
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
}
/**
* Make direct HTTP PUT request to Graph API
*/
async makeDirectGraphPutRequest(path, token, body, additionalHeaders) {
const url = `https://graph.microsoft.com/v1.0${path}`;
logger.log(`Making direct HTTP PUT request to: ${url}`);
const headers = {
'Authorization': `Bearer ${token}`,
...additionalHeaders
};
const response = await fetch(url, {
method: 'PUT',
headers,
body: body
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
return await response.json();
}
/**
* Reset the Graph client (useful for re-authentication)
*/
resetClient() {
this.graphClient = null;
logger.log('Teams Graph client reset');
}
}
// Export singleton instance
export const teamsOperations = new TeamsOperations();