// src/lib/integrations/meta/FacebookClient.ts // Facebook Client für Page Management, Posts und Kommentare import { MetaBaseClient, MetaApiError } from './MetaBaseClient' import type { FacebookPage, FacebookPost, FacebookComment, FacebookConversation, FacebookMessage, FacebookPageInsight, MetaPaginatedResponse, } from '@/types/meta' // ============================================================================= // Types // ============================================================================= export interface FacebookPostsOptions { /** Maximale Anzahl Posts */ limit?: number /** Filter: published, scheduled, draft */ filter?: 'published' | 'scheduled' | 'draft' /** Nur Posts seit diesem Datum */ since?: Date /** Nur Posts bis zu diesem Datum */ until?: Date } export interface FacebookCommentsOptions { /** Maximale Anzahl Kommentare */ limit?: number /** Sortierung: time (älteste zuerst) oder reverse_time (neueste zuerst) */ order?: 'time' | 'reverse_time' /** Filter: toplevel (nur Top-Level), stream (alle inkl. Replies) */ filter?: 'toplevel' | 'stream' } export interface FacebookInsightsOptions { /** Metriken die abgefragt werden sollen */ metrics: string[] /** Zeitraum: day, week, days_28 */ period?: 'day' | 'week' | 'days_28' | 'lifetime' /** Startdatum */ since?: Date /** Enddatum */ until?: Date } // ============================================================================= // Facebook Client // ============================================================================= export class FacebookClient extends MetaBaseClient { private pageId: string private pageAccessToken: string constructor(options: { /** User Access Token (Long-lived) */ userAccessToken: string /** Page ID */ pageId: string /** Page Access Token (optional, wird sonst automatisch abgerufen) */ pageAccessToken?: string }) { super(options.userAccessToken) this.pageId = options.pageId this.pageAccessToken = options.pageAccessToken || options.userAccessToken } // =========================================================================== // Page Token Management // =========================================================================== /** * Holt den Page Access Token für die aktuelle Page */ async getPageAccessToken(): Promise { const response = await this.request<{ access_token: string }>(`${this.pageId}`, { params: { fields: 'access_token', }, }) this.pageAccessToken = response.access_token return response.access_token } /** * Setzt den Page Access Token für nachfolgende Requests */ setPageAccessToken(token: string): void { this.pageAccessToken = token } /** * Führt einen Request mit dem Page Access Token aus */ private async pageRequest( endpoint: string, options: { method?: 'GET' | 'POST' | 'DELETE' params?: Record body?: Record } = {} ): Promise { // Temporär den Access Token auf Page Token setzen const originalToken = this.accessToken this.accessToken = this.pageAccessToken try { return await this.request(endpoint, options) } finally { this.accessToken = originalToken } } // =========================================================================== // Posts // =========================================================================== /** * Holt alle Posts einer Page */ async getPosts(options: FacebookPostsOptions = {}): Promise { const { limit = 25, since, until } = options const params: Record = { fields: [ 'id', 'message', 'story', 'full_picture', 'permalink_url', 'created_time', 'updated_time', 'shares', 'reactions.summary(true)', 'comments.summary(true)', 'attachments{type,url,media}', ].join(','), limit, } if (since) { params.since = Math.floor(since.getTime() / 1000) } if (until) { params.until = Math.floor(until.getTime() / 1000) } const response = await this.pageRequest>( `${this.pageId}/posts`, { params } ) return response.data } /** * Holt einen einzelnen Post mit allen Details */ async getPost(postId: string): Promise { return this.pageRequest(postId, { params: { fields: [ 'id', 'message', 'story', 'full_picture', 'permalink_url', 'created_time', 'updated_time', 'shares', 'reactions.summary(true)', 'comments.summary(true)', 'attachments{type,url,media}', ].join(','), }, }) } /** * Holt alle Posts mit Pagination */ async getAllPosts(options: FacebookPostsOptions = {}): Promise { const { limit = 100, since, until } = options const params: Record = { fields: [ 'id', 'message', 'story', 'full_picture', 'permalink_url', 'created_time', 'updated_time', 'shares', 'reactions.summary(true)', 'comments.summary(true)', ].join(','), } if (since) { params.since = Math.floor(since.getTime() / 1000) } if (until) { params.until = Math.floor(until.getTime() / 1000) } // Temporär Token wechseln const originalToken = this.accessToken this.accessToken = this.pageAccessToken try { return await this.requestPaginated(`${this.pageId}/posts`, { params, limit, maxPages: 10, }) } finally { this.accessToken = originalToken } } // =========================================================================== // Comments // =========================================================================== /** * Holt alle Kommentare eines Posts */ async getPostComments( postId: string, options: FacebookCommentsOptions = {} ): Promise { const { limit = 50, order = 'reverse_time', filter = 'stream' } = options const response = await this.pageRequest>( `${postId}/comments`, { params: { fields: [ 'id', 'message', 'from{id,name}', 'created_time', 'like_count', 'comment_count', 'is_hidden', 'can_hide', 'can_reply_privately', 'parent{id}', 'attachment{type,url,media}', ].join(','), order, filter, limit, }, } ) return response.data } /** * Holt alle Kommentare eines Posts mit Pagination */ async getAllPostComments( postId: string, options: FacebookCommentsOptions = {} ): Promise { const { limit = 100, order = 'reverse_time', filter = 'stream' } = options const originalToken = this.accessToken this.accessToken = this.pageAccessToken try { return await this.requestPaginated(`${postId}/comments`, { params: { fields: [ 'id', 'message', 'from{id,name}', 'created_time', 'like_count', 'comment_count', 'is_hidden', 'can_hide', 'parent{id}', ].join(','), order, filter, }, limit, maxPages: 10, }) } finally { this.accessToken = originalToken } } /** * Holt einen einzelnen Kommentar mit Details */ async getComment(commentId: string): Promise { return this.pageRequest(commentId, { params: { fields: [ 'id', 'message', 'from{id,name}', 'created_time', 'like_count', 'comment_count', 'is_hidden', 'can_hide', 'can_reply_privately', 'parent{id}', 'attachment{type,url,media}', ].join(','), }, }) } /** * Antwortet auf einen Kommentar */ async replyToComment(commentId: string, message: string): Promise<{ id: string }> { return this.pageRequest<{ id: string }>(`${commentId}/comments`, { method: 'POST', body: { message }, }) } /** * Versteckt einen Kommentar */ async hideComment(commentId: string): Promise<{ success: boolean }> { return this.pageRequest<{ success: boolean }>(commentId, { method: 'POST', body: { is_hidden: true }, }) } /** * Zeigt einen versteckten Kommentar wieder an */ async unhideComment(commentId: string): Promise<{ success: boolean }> { return this.pageRequest<{ success: boolean }>(commentId, { method: 'POST', body: { is_hidden: false }, }) } /** * Löscht einen Kommentar */ async deleteComment(commentId: string): Promise<{ success: boolean }> { return this.pageRequest<{ success: boolean }>(commentId, { method: 'DELETE', }) } /** * Liked einen Kommentar (als Page) */ async likeComment(commentId: string): Promise<{ success: boolean }> { return this.pageRequest<{ success: boolean }>(`${commentId}/likes`, { method: 'POST', }) } /** * Entfernt Like von einem Kommentar */ async unlikeComment(commentId: string): Promise<{ success: boolean }> { return this.pageRequest<{ success: boolean }>(`${commentId}/likes`, { method: 'DELETE', }) } // =========================================================================== // Conversations (Messenger) // =========================================================================== /** * Holt alle Conversations der Page */ async getConversations(limit: number = 25): Promise { const response = await this.pageRequest>( `${this.pageId}/conversations`, { params: { fields: [ 'id', 'participants{id,name,email}', 'updated_time', 'message_count', 'unread_count', 'can_reply', ].join(','), limit, }, } ) return response.data } /** * Holt Nachrichten einer Conversation */ async getConversationMessages( conversationId: string, limit: number = 25 ): Promise { const response = await this.pageRequest>( `${conversationId}/messages`, { params: { fields: [ 'id', 'message', 'from{id,name,email}', 'to{id,name}', 'created_time', 'attachments{id,mime_type,name,file_url,image_data}', ].join(','), limit, }, } ) return response.data } /** * Sendet eine Nachricht in einer Conversation */ async sendMessage( recipientId: string, message: string ): Promise<{ recipient_id: string; message_id: string }> { return this.pageRequest<{ recipient_id: string; message_id: string }>( `${this.pageId}/messages`, { method: 'POST', body: { recipient: { id: recipientId }, message: { text: message }, }, } ) } // =========================================================================== // Page Insights // =========================================================================== /** * Holt Page Insights/Analytics */ async getPageInsights(options: FacebookInsightsOptions): Promise { const { metrics, period = 'day', since, until } = options const params: Record = { metric: metrics.join(','), period, } if (since) { params.since = Math.floor(since.getTime() / 1000) } if (until) { params.until = Math.floor(until.getTime() / 1000) } const response = await this.pageRequest<{ data: FacebookPageInsight[] }>( `${this.pageId}/insights`, { params } ) return response.data } /** * Holt Standard Page Insights (Impressions, Reach, Engagement) */ async getStandardInsights( since?: Date, until?: Date ): Promise<{ impressions: number reach: number engagedUsers: number newFans: number }> { const insights = await this.getPageInsights({ metrics: [ 'page_impressions', 'page_post_engagements', 'page_fan_adds', 'page_engaged_users', ], period: 'day', since, until, }) const getValue = (name: string): number => { const insight = insights.find((i) => i.name === name) if (!insight?.values?.[0]) return 0 const value = insight.values[0].value return typeof value === 'number' ? value : 0 } return { impressions: getValue('page_impressions'), reach: getValue('page_post_engagements'), engagedUsers: getValue('page_engaged_users'), newFans: getValue('page_fan_adds'), } } // =========================================================================== // Utility Methods // =========================================================================== /** * Prüft ob die Page verbunden ist und Zugriffsrechte hat */ async verifyPageAccess(): Promise<{ isValid: boolean pageName?: string permissions?: string[] }> { try { const page = await this.pageRequest(`${this.pageId}`, { params: { fields: 'id,name,tasks', }, }) return { isValid: true, pageName: page.name, permissions: page.tasks, } } catch (error) { if (error instanceof MetaApiError) { return { isValid: false, } } throw error } } /** * Holt alle ungelesenen Aktivitäten (Kommentare + Nachrichten) */ async getUnreadActivity(): Promise<{ unreadComments: number unreadMessages: number }> { // Ungelesene Conversations zählen const conversations = await this.getConversations(100) const unreadMessages = conversations.reduce((sum, conv) => sum + (conv.unread_count || 0), 0) // Für Kommentare gibt es keine direkte "unread" API // Dies müsste über die Datenbank/Sync getracked werden return { unreadComments: 0, // Muss über eigenen Sync-Status getracked werden unreadMessages, } } } export default FacebookClient