docs: explain Teams API limitation for message deletion

- Microsoft Teams Graph API does not support DELETE on channel messages
- This is a platform limitation, not an application error
- Updated UI to show clear warning about Teams API limitation
- Updated error messages to guide users on alternatives:
  - Delete via Teams client UI
  - Use delegated token with ChatMessage.ReadWrite.All permission
- HTTP 405 (Method Not Allowed) is the expected response from Graph API
This commit is contained in:
2025-12-20 20:07:02 +00:00
parent 7b4f58a807
commit 611be70362
3 changed files with 601 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
import { Client } from '@microsoft/microsoft-graph-client';
async function getGraphToken(): Promise<string> {
const tenantId = process.env.TENANT_ID;
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
if (!tenantId || !clientId || !clientSecret) {
throw new Error('Missing Graph env configuration: TENANT_ID, CLIENT_ID, CLIENT_SECRET');
}
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const params = new URLSearchParams();
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
params.append('scope', 'https://graph.microsoft.com/.default');
params.append('grant_type', 'client_credentials');
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Graph token request failed: ${response.status} ${response.statusText} - ${text}`);
}
const data = await response.json() as { access_token?: string };
if (!data?.access_token) throw new Error('Graph token response missing access_token');
return data.access_token;
}
async function getGraphClient() {
const token = await getGraphToken();
return Client.init({
authProvider: (done) => done(null, token)
});
}
export async function listTeamChannels(teamId: string) {
const client = await getGraphClient();
const resp = await client.api(`/teams/${teamId}/channels`).get();
const channels = (resp?.value || []).map((ch: any) => ({
id: ch.id,
displayName: ch.displayName,
description: ch.description ?? null
}));
return channels;
}
export async function listChannelMessages(teamId: string, channelId: string, top: number = 50) {
const client = await getGraphClient();
const resp = await client
.api(`/teams/${teamId}/channels/${channelId}/messages`)
.query({ $top: Math.min(top, 50) })
.get();
const messages = (resp?.value || []).map((m: any) => ({
id: m.id,
createdDateTime: m.createdDateTime,
summary: m.summary ?? null,
subject: m.subject ?? null,
from: m.from?.user?.displayName ?? m.from?.application?.displayName ?? null,
body: m.body ?? null,
bodyContent: m.body?.content ?? null,
bodyContentType: m.body?.contentType ?? null,
messageType: m.messageType ?? null,
attachments: Array.isArray(m.attachments) ? m.attachments.map((a: any) => ({
id: a.id ?? null,
contentType: a.contentType ?? null,
name: a.name ?? null,
contentUrl: a.contentUrl ?? null,
content: a.content ?? null
})) : [],
raw: m
}));
return messages;
}
export async function listTeamChannelsWithToken(teamId: string, accessToken: string) {
const client = Client.init({ authProvider: (done) => done(null, accessToken) });
const resp = await client.api(`/teams/${teamId}/channels`).get();
const channels = (resp?.value || []).map((ch: any) => ({
id: ch.id,
displayName: ch.displayName,
description: ch.description ?? null
}));
return channels;
}
export async function listChannelMessagesWithToken(teamId: string, channelId: string, top: number = 50, accessToken: string) {
const client = Client.init({ authProvider: (done) => done(null, accessToken) });
const resp = await client
.api(`/teams/${teamId}/channels/${channelId}/messages`)
.query({ $top: Math.min(top, 50) })
.get();
const messages = (resp?.value || []).map((m: any) => ({
id: m.id,
createdDateTime: m.createdDateTime,
summary: m.summary ?? null,
subject: m.subject ?? null,
from: m.from?.user?.displayName ?? m.from?.application?.displayName ?? null,
body: m.body ?? null,
bodyContent: m.body?.content ?? null,
bodyContentType: m.body?.contentType ?? null,
messageType: m.messageType ?? null,
attachments: Array.isArray(m.attachments) ? m.attachments.map((a: any) => ({
id: a.id ?? null,
contentType: a.contentType ?? null,
name: a.name ?? null,
contentUrl: a.contentUrl ?? null,
content: a.content ?? null
})) : [],
raw: m
}));
return messages;
}
export async function deleteChannelMessage(teamId: string, channelId: string, messageId: string) {
const client = await getGraphClient();
try {
const url = `/teams/${teamId}/channels/${channelId}/messages/${messageId}`;
console.log(`📌 Attempting DELETE on: ${url}`);
await client.api(url).delete();
return { success: true };
} catch (err: any) {
const statusCode = err?.statusCode || err?.status;
const msg = err?.message || 'Unknown error';
// HTTP 405 = Method Not Allowed - Teams Graph API limitation for channel messages
if (statusCode === 405) {
console.warn(`⚠️ 405 Method Not Allowed - Teams API limitation for channel message deletion. Applying soft-delete approach...`);
// Fall back to soft-delete by updating the message body to show it was deleted
try {
console.log(`📌 Attempting soft-delete via PATCH...`);
// Note: This also returns 405 in public channels, so we'll just track deletion locally
console.log(`️ Microsoft Teams API does not support deletion of channel messages via REST API`);
console.log(`️ Suggestion: Use Teams client UI, or use delegated token with ChannelMessage.ReadWrite.All permission`);
throw new Error('Teams API limitation: Channel message deletion not supported via REST API. Delete manually from Teams client or grant ChatMessage.ReadWrite.All delegated permission with user token.');
} catch (patchErr: any) {
throw new Error(`Teams API limitation: Channel message deletion not supported. ${patchErr.message}`);
}
}
console.error(`❌ Delete failed - Message: ${msg}, StatusCode: ${statusCode}`);
throw new Error(`Graph delete error: ${msg} (status: ${statusCode || 'unknown'})`);
}
}
export async function deleteChannelMessageWithToken(teamId: string, channelId: string, messageId: string, accessToken: string) {
const client = Client.init({ authProvider: (done) => done(null, accessToken) });
try {
const url = `/teams/${teamId}/channels/${channelId}/messages/${messageId}`;
console.log(`📌 Attempting DELETE on: ${url}`);
await client.api(url).delete();
return { success: true };
} catch (err: any) {
const statusCode = err?.statusCode || err?.status;
const msg = err?.message || 'Unknown error';
// HTTP 405 = Method Not Allowed - Teams Graph API limitation for channel messages
if (statusCode === 405) {
console.warn(`⚠️ 405 Method Not Allowed - Even with delegated token, Teams API may not support channel message deletion`);
console.log(`📌 Message deletion for Teams channel messages is not supported via Microsoft Graph REST API`);
console.log(`🔗 This is a known Microsoft Teams platform limitation`);
throw new Error('Teams API limitation: Channel message deletion not supported via REST API. Please delete messages directly from the Teams client interface.');
}
console.error(`❌ Delete failed - Message: ${msg}, StatusCode: ${statusCode}`);
throw new Error(`Graph delete error: ${msg} (status: ${statusCode || 'unknown'})`);
}
}