Spaces:
Paused
Paused
| /** | |
| * 🏢 Microsoft Graph Email & SharePoint Harvester | |
| * Bruger Graph API til at søge i TDC Outlook og SharePoint | |
| */ | |
| import { logger } from '../../utils/logger.js'; | |
| export interface GraphSearchResult { | |
| id: string; | |
| title: string; | |
| url: string; | |
| summary: string; | |
| type: 'email' | 'document' | 'site' | 'list'; | |
| from?: string; | |
| received?: string; | |
| modified?: string; | |
| } | |
| export class MicrosoftGraphAdapter { | |
| private accessToken: string | null = null; | |
| private graphBase = 'https://graph.microsoft.com/v1.0'; | |
| constructor(accessToken?: string) { | |
| this.accessToken = accessToken || process.env.MS_GRAPH_TOKEN || null; | |
| } | |
| setAccessToken(token: string) { | |
| this.accessToken = token; | |
| logger.info('🔐 Microsoft Graph access token set'); | |
| } | |
| async isAvailable(): Promise<boolean> { | |
| if (!this.accessToken) return false; | |
| try { | |
| const response = await fetch(`${this.graphBase}/me`, { | |
| headers: { 'Authorization': `Bearer ${this.accessToken}` } | |
| }); | |
| return response.ok; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async searchEmails(query: string, limit = 25): Promise<GraphSearchResult[]> { | |
| if (!this.accessToken) { | |
| logger.warn('⚠️ No Graph access token - use setAccessToken()'); | |
| return []; | |
| } | |
| try { | |
| // Search messages | |
| const response = await fetch( | |
| `${this.graphBase}/me/messages?$search="${encodeURIComponent(query)}"&$top=${limit}&$select=id,subject,from,receivedDateTime,bodyPreview,webLink`, | |
| { | |
| headers: { | |
| 'Authorization': `Bearer ${this.accessToken}`, | |
| 'ConsistencyLevel': 'eventual' | |
| } | |
| } | |
| ); | |
| if (!response.ok) { | |
| logger.error(`Graph email search failed: ${response.status}`); | |
| return []; | |
| } | |
| const data = await response.json(); | |
| return (data.value || []).map((msg: any) => ({ | |
| id: msg.id, | |
| title: msg.subject || '(No subject)', | |
| url: msg.webLink || `https://outlook.office365.com/mail/deeplink/read/${msg.id}`, | |
| summary: msg.bodyPreview?.slice(0, 300) || '', | |
| type: 'email' as const, | |
| from: msg.from?.emailAddress?.address || 'Unknown', | |
| received: msg.receivedDateTime | |
| })); | |
| } catch (err) { | |
| logger.error('Graph email search error:', err); | |
| return []; | |
| } | |
| } | |
| async searchSharePoint(query: string, limit = 25): Promise<GraphSearchResult[]> { | |
| if (!this.accessToken) return []; | |
| try { | |
| const searchBody = { | |
| requests: [{ | |
| entityTypes: ['driveItem', 'listItem', 'site'], | |
| query: { queryString: query }, | |
| from: 0, | |
| size: limit | |
| }] | |
| }; | |
| const response = await fetch(`${this.graphBase}/search/query`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${this.accessToken}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(searchBody) | |
| }); | |
| if (!response.ok) { | |
| logger.error(`Graph SharePoint search failed: ${response.status}`); | |
| return []; | |
| } | |
| const data = await response.json(); | |
| const results: GraphSearchResult[] = []; | |
| for (const resultSet of data.value || []) { | |
| for (const container of resultSet.hitsContainers || []) { | |
| for (const hit of container.hits || []) { | |
| const resource = hit.resource || {}; | |
| results.push({ | |
| id: resource.id || hit.hitId, | |
| title: resource.name || resource.displayName || query, | |
| url: resource.webUrl || '', | |
| summary: hit.summary?.slice(0, 300) || '', | |
| type: this.detectType(resource['@odata.type']), | |
| modified: resource.lastModifiedDateTime | |
| }); | |
| } | |
| } | |
| } | |
| return results; | |
| } catch (err) { | |
| logger.error('Graph SharePoint search error:', err); | |
| return []; | |
| } | |
| } | |
| async searchAll(query: string, limit = 20): Promise<GraphSearchResult[]> { | |
| const [emails, docs] = await Promise.all([ | |
| this.searchEmails(query, limit), | |
| this.searchSharePoint(query, limit) | |
| ]); | |
| return [...emails, ...docs]; | |
| } | |
| async getSites(): Promise<{ id: string; name: string; url: string }[]> { | |
| if (!this.accessToken) return []; | |
| try { | |
| const response = await fetch( | |
| `${this.graphBase}/sites?search=*&$top=50`, | |
| { headers: { 'Authorization': `Bearer ${this.accessToken}` } } | |
| ); | |
| if (!response.ok) return []; | |
| const data = await response.json(); | |
| return (data.value || []).map((site: any) => ({ | |
| id: site.id, | |
| name: site.displayName, | |
| url: site.webUrl | |
| })); | |
| } catch { | |
| return []; | |
| } | |
| } | |
| private detectType(odataType: string): 'email' | 'document' | 'site' | 'list' { | |
| if (!odataType) return 'document'; | |
| if (odataType.includes('site')) return 'site'; | |
| if (odataType.includes('listItem')) return 'list'; | |
| return 'document'; | |
| } | |
| } | |
| // Singleton instance | |
| export const graphAdapter = new MicrosoftGraphAdapter(); | |